@molecule/api-ai-decisions-jev

Provider bond · ai-decisions · API (Node) · v1.0.0 · Apache-2.0

Jev decisions provider for molecule.dev — typed choice/score/yes-no answers from TypeSafe's hosted Jev System One API

npm install @molecule/api-ai-decisions-jev

npm · Source on GitHub · Implements @molecule/api-ai-decisions

How it works

@molecule/api-ai-decisions-jev is a provider bond on the API (Node) side: it implements the ai-decisions core interface (@molecule/api-ai-decisions) with a concrete vendor or library behind it.

Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.

import { setProvider, requireProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-jev'

setProvider(provider) // reads TYPESAFE_API_KEY on first use

const { answers } = await requireProvider().decide({
  state: { from: 'ana@example.com', body: 'Your app deleted my notes!!' },
  questions: {
    severity: {
      type: 'score',
      instructions: 'How severe is the problem?',
      criteria: ['cosmetic', 'annoying', 'blocking', 'data loss'],
    },
    churnRisk: { type: 'yesNo', instructions: 'The customer is likely to cancel.' },
  },
})
answers.severity.level // 3
answers.churnRisk.probability // 0.81

Works with: @molecule/api-ai-decisions, @molecule/api-secrets

Secrets: TYPESAFE_API_KEY

Reference

Jev decisions provider for molecule.dev — typed decisions from TypeSafe AI's hosted Jev "System One" model.

Jev answers choice, score and yes/no questions about text or JSON with a probability distribution instead of generated text. This bond calls POST https://api.typesafe.ai/v1/systemone with your TYPESAFE_API_KEY. The open-weights Laya server speaks the same protocol, so @molecule/api-ai-decisions-laya is a drop-in self-hosted swap.

Quick Start

import { setProvider, requireProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-jev'

setProvider(provider) // reads TYPESAFE_API_KEY on first use

const { answers } = await requireProvider().decide({
  state: { from: 'ana@example.com', body: 'Your app deleted my notes!!' },
  questions: {
    severity: {
      type: 'score',
      instructions: 'How severe is the problem?',
      criteria: ['cosmetic', 'annoying', 'blocking', 'data loss'],
    },
    churnRisk: { type: 'yesNo', instructions: 'The customer is likely to cancel.' },
  },
})
answers.severity.level // 3
answers.churnRisk.probability // 0.81

Type

provider

Installation

npm install @molecule/api-ai-decisions-jev @molecule/api-ai-decisions @molecule/api-secrets

API

Interfaces

JevConfig

Configuration for the Jev decisions provider.

interface JevConfig {
  /** TypeSafe API key. Defaults to the `TYPESAFE_API_KEY` env var (the name TypeSafe's own SDKs read). */
  apiKey?: string
  /**
   * API base URL. Defaults to `TYPESAFE_BASE_URL`, then `https://api.typesafe.ai`.
   * Point it at a gateway that relays `/v1/systemone` (or at a `laya-serve`
   * host — same protocol) without changing code.
   */
  baseUrl?: string
  /** Default model id. Defaults to `'jev-latest'`. */
  model?: string
}

PostOptions

Options for postSystemOne.

interface PostOptions {
  /** Full endpoint URL. */
  url: string
  /** Bearer token, if any. */
  apiKey?: string
  /** Request body. */
  body: Record<string, unknown>
  /** Abort signal. */
  signal?: AbortSignal
  /** Label for error messages (`'Laya'`, `'Jev'`). */
  label: string
  /** Max retries on a retryable status (default 3). */
  maxRetries?: number
}

WireAnswer

One answer as received on the wire (only the fields we read).

interface WireAnswer {
  type?: string
  choice?: string
  score?: number
  noul?: number
  probabilities?: Record<string, number>
}

WireQuestion

One question as sent on the wire.

interface WireQuestion {
  type: 'choice' | 'score' | 'noul'
  instructions: string
  criteria?: Record<string, string> | string[]
}

WireResponse

The response body (only the fields we read).

interface WireResponse {
  model?: string
  answers?: Record<string, WireAnswer>
  usage?: { input_tokens?: number; output_tokens?: number }
}

Functions

createProvider(config)

Creates a Jev decisions provider.

function createProvider(config?: JevConfig): AIDecisionsProvider
  • config — API key, base URL and default model.

Returns: An AIDecisionsProvider backed by TypeSafe's Jev API.

fromWireAnswer(id, question, wire, minConfidence)

Converts one wire answer into the core's answer for question.

function fromWireAnswer(
  id: string,
  question: DecisionQuestion,
  wire: WireAnswer | undefined,
  minConfidence?: number,
): DecisionAnswer
  • id — The question id (for error messages).
  • question — The question that was asked.
  • wire — The wire answer.
  • minConfidence — Optional low-confidence threshold.

Returns: The typed answer.

fromWireResponse(questions, body, minConfidence)

Converts a whole wire response into the core's result.

function fromWireResponse(questions: Q, body: WireResponse, minConfidence?: number): DecideResult<Q>
  • questions — The questions that were asked.
  • body — The parsed response body.
  • minConfidence — Optional low-confidence threshold.

Returns: The typed result.

postSystemOne(opts)

POSTs a /v1/systemone request with retry on 429/5xx-busy, honouring Retry-After. Throws an Error carrying status on a non-2xx response.

function postSystemOne(opts: PostOptions): Promise<WireResponse>
  • opts — Request options.

Returns: The parsed response body.

toWireQuestions(questions)

Converts the core's questions into wire questions (yesNo → noul).

function toWireQuestions(questions: Record<string, DecisionQuestion>): Record<string, WireQuestion>
  • questions — The questions keyed by id.

Returns: The wire questions object.

Constants

aiDecisionsJevSecretDefinitions

Secret definitions required by the Jev decisions bond.

const aiDecisionsJevSecretDefinitions: SecretDefinition[]

DEFAULT_JEV_MODEL

TypeSafe's flagship model id.

const DEFAULT_JEV_MODEL: 'jev-latest'

DEFAULT_JEV_URL

TypeSafe's API host.

const DEFAULT_JEV_URL: 'https://api.typesafe.ai'

provider

The provider implementation — lazy, so env vars are read on first use.

const provider: AIDecisionsProvider

Core Interface

Implements @molecule/api-ai-decisions interface.

Bond Wiring

Setup function to register this provider with the core interface:

import { setProvider } from '@molecule/api-ai-decisions'
import { provider } from '@molecule/api-ai-decisions-jev'

export function setupAiDecisionsJev(): void {
  setProvider(provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-ai-decisions ^1.0.0
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • TYPESAFE_API_KEY (required) — TypeSafe API key
    • Setup: Create an API key in your TypeSafe AI account; it is sent as a bearer token to the Jev API.
    • Get it here: https://docs.typesafe.ai/api
    • Example: ts-...

Runtime Dependencies

  • @molecule/api-ai-decisions

  • @molecule/api-secrets

  • Config: TYPESAFE_API_KEY (required — sent as Authorization: Bearer …), TYPESAFE_BASE_URL (optional; a gateway, or a laya-serve host), and createProvider({ model }) (default 'jev-latest', or pass model per call).

  • Limits (TypeSafe docs): up to 255 options per choice, 2–10 levels per score. 429 (rate limit) and 529 (overloaded) are retried up to 3 times with backoff; 401 and 422 throw immediately with the API's message and a status property.

  • English-first. TypeSafe describes English as Jev's primary training language; for other languages evaluate carefully or use Laya's multilingual checkpoint.

  • Your data leaves your servers (TypeSafe processes the state). For data that must stay in your environment, bond the Laya provider instead.

  • confidence in the answers is the probability of the reported answer, NOT Jev's own confidence field (see the core's remarks).

  • Use the core's setProvider, not bond('ai-decisions', …) directly.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • Each flow that makes a decision (routing, triage, moderation, a guardrail) runs it from the real UI, and the answer DRIVES what happens next (the item lands in the chosen queue, the badge shows, the action is blocked) — not just printed.
  • Both directions: a clearly-billing input routes to billing AND a clearly-technical one routes elsewhere. One label for every input is a broken integration.
  • A low-confidence answer takes the app's fallback path (human review, "unsure" state) instead of being acted on.
  • Provider errors (service down, bad key) show a visible, recoverable state — never a blank screen or an unhandled rejection.
  • The call runs server-side: no provider request or key in the browser's Network tab.