jevascript
esc

    Type to search · ↑↓ to move · ↵ to open

    Get started

    Guides

    Providers

    The provider interface, the built-in Jev provider and its options, retries and timeouts, and how to write a provider of your own.

    The public API never names a vendor. is, score and choose talk to a SemanticProvider, and jev() is one implementation of it. The vocabulary the runtime uses (truth, choice, level) is deliberately its own, so a second provider needs no changes at any call site.

    The Jev provider#

    Jev is a decision model: it scores propositions and returns calibrated probabilities. It generates no text.

    lib/semantic.ts
    import { createSemantic, jev } from "jevascript"
    
    export const semantic = createSemantic({
      provider: jev({ model: "jev-1.13.0" }),
    })

    With JEV_API_KEY in the environment, jev() with no arguments is what the zero-config path builds for you. Jev API keys are issued by TypeSafe at console.typesafe.ai/keys.

    Options#

    OptionTypeDefaultDescription
    apiKeystringprocess.env.JEV_API_KEY

    Resolved per request, not at construction, so a module that creates the provider can be imported in tests and builds that have no key.

    modelstring"jev-1.13.0"

    A pinned model version. JEV_MODEL in the environment overrides the default; this option overrides both.

    baseUrlstring"https://api.typesafe.ai/v1"

    API endpoint. JEV_BASE_URL overrides the default. Trailing slashes are stripped.

    fetchtypeof fetchglobalThis.fetch

    Swap in an instrumented or proxied fetch.

    maxRetriesnumber3

    Attempts on retryable failures, including the first.

    Precedence: explicit option, then environment variable, then built-in default.

    Why the model is pinned#

    jev-latest would be an alias that moves. A silent move invalidates cached answers, shifts production behaviour, and detaches dataset evaluations from the model they were measured against. The default is a concrete version, and a provider's model property must always be one.

    Retries and timeouts#

    • Retried statuses: 408, 409, 429, 500, 502, 503, 504, 529. Network failures are retried too.
    • Backoff is exponential from 250 ms with jitter. A retry-after header is honoured.
    • The request timeout is timeoutMs from the call, the instance defaults, or 10 s. It is enforced with AbortSignal.timeout, combined with any signal the runtime passes.
    • After the last attempt, a ProviderError is thrown with the HTTP status when there was one.

    Capabilities#

    Every provider declares what it can express, so impossible requests fail before the network with UnsupportedByProviderError:

    import { JEV_CAPABILITIES } from "jevascript"
    
    JEV_CAPABILITIES
    // { maxChoiceOptions: 255, levelRange: [2, 10], maxStateTokens: 32_000,
    //   maxTotalTokens: 64_000, generatesText: false, batching: true }

    Using two providers#

    A question can name its own provider. Questions to different providers in the same turn go out as one request each:

    const careful = jev({ model: "jev-1.13.0", maxRetries: 5 })
    
    const t = await semantic(ticket).batch({
      urgency: score("A human needs to act on this urgently."),
      fraud: score("This order is fraudulent.", { asProposition: true, provider: careful }),
    })

    Or create two instances with createSemantic(), each with its own provider and cache.

    Write a provider#

    A provider is an object with a name, a pinned model, a capabilities declaration and one method. It is multi-question by construction: a request carries one state and any number of questions, because a single-question interface would make batching impossible to express.

    The contract#

    interface SemanticProvider {
      readonly name: string
      readonly model: string                    // a concrete version, never a moving alias
      readonly capabilities: ProviderCapabilities
      evaluate(request: SemanticRequest): Promise<SemanticProviderResponse>
    }
    
    interface SemanticRequest {
      readonly state: State                                     // string | string[] | object
      readonly questions: Record<string, SemanticQuestion>      // keyed; answer under the same keys
      readonly signal?: AbortSignal
      readonly timeoutMs?: number
    }
    
    interface SemanticProviderResponse {
      readonly answers: Record<string, SemanticAnswer>
      readonly usage?: { inputTokens?: number; outputTokens?: number }
      readonly model?: string
    }

    The three question kinds#

    KindQuestionAnswer
    truth{ kind, instructions, trueWhen?, falseWhen? }{ kind: "truth", probability } with probability in [0, 1]
    choice{ kind, instructions, options: Record<string, string | ChoiceOptionSpec> }{ kind: "choice", choice, probabilities, confidence } where probabilities sums to 1
    level{ kind, instructions, levels: (string | LevelSpec)[] }{ kind: "level", level, probabilities, confidence } with level 0-indexed and possibly fractional

    is() sends a truth question. score() sends truth unless levels is given, then level. choose() sends choice. find() sends a choice plus a truth existence check in one request.

    A minimal provider#

    This one wraps any HTTP service that returns a probability per question:

    providers/mine.ts
    import type { SemanticProvider, SemanticRequest, SemanticAnswer } from "jevascript"
    import { ProviderError } from "jevascript"
    
    export function mine(options: { apiKey: string; endpoint: string }): SemanticProvider {
      return {
        name: "mine",
        model: "mine-2026-09-01",
        capabilities: {
          maxChoiceOptions: 32,
          levelRange: [2, 5],
          generatesText: false,
          batching: true,
        },
    
        async evaluate(request: SemanticRequest) {
          const response = await fetch(options.endpoint, {
            method: "POST",
            headers: { authorization: `Bearer ${options.apiKey}`, "content-type": "application/json" },
            body: JSON.stringify({ state: request.state, questions: request.questions }),
            signal: request.signal,
          })
          if (!response.ok) throw new ProviderError(`mine returned ${response.status}`, response.status)
    
          const raw = (await response.json()) as Record<string, { p?: number; pick?: string; dist?: Record<string, number> }>
          const answers: Record<string, SemanticAnswer> = {}
    
          for (const [key, question] of Object.entries(request.questions)) {
            const r = raw[key]
            if (!r) throw new ProviderError(`mine returned no answer for "${key}"`)
            if (question.kind === "truth") {
              answers[key] = { kind: "truth", probability: r.p ?? 0.5 }
            } else if (question.kind === "choice") {
              const probabilities = r.dist ?? { [r.pick!]: 1 }
              answers[key] = { kind: "choice", choice: r.pick!, probabilities, confidence: Math.max(...Object.values(probabilities)) }
            } else {
              const probabilities = Object.values(r.dist ?? {})
              answers[key] = { kind: "level", level: r.p ?? 0, probabilities, confidence: Math.max(...probabilities) }
            }
          }
          return { answers, model: "mine-2026-09-01" }
        },
      }
    }

    Rules a provider must follow#

    1. Answer every key in request.questions, under the same key. A missing answer is an error.
    2. Honour signal. The runtime combines the caller's signal with the timeout. Pass it to fetch.
    3. Declare capabilities honestly. The runtime checks a request against them with assertSupported() before calling evaluate, so the limits you declare are the ones users see as UnsupportedByProviderError.
    4. Pin model. The cache key includes it. A moving alias silently serves stale answers.
    5. Throw ProviderError for transport and upstream failures, with the status when there is one, so application code can distinguish "the model was unavailable" from everything else.
    6. Return usage if you can. It surfaces in observability events for cost accounting.

    Wrapping a text-generating model#

    A chat model can be made into a provider by asking it to return a JSON object of probabilities and parsing the result. Two cautions: set generatesText: true so future features can rely on it, and expect worse calibration than a purpose-built decision model. The probabilities a generative model prints are not measured; a 0.9 from it is a style, not a frequency. Use minConfidence with samples to measure stability if you go this route.

    An OpenAI-compatible provider and a Vercel AI SDK bridge are on the roadmap but not built. Track progress in the changelog.

    Testing a provider#

    jevascript/testing ships a provider that answers from a lookup table. It is also a good reference implementation of the contract: read its source for the exact answer shapes. See Testing.