# Providers

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

Section: Guides · HTML: https://jevascript.org/docs/providers · Markdown: https://jevascript.org/docs/providers.md

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.

```ts
// 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](https://console.typesafe.ai/keys).

### Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `apiKey` | `string` | `process.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. |
| `model` | `string` | `"jev-1.13.0"` | A pinned model version. `JEV_MODEL` in the environment overrides the default; this option overrides both. |
| `baseUrl` | `string` | `"https://api.typesafe.ai/v1"` | API endpoint. `JEV_BASE_URL` overrides the default. Trailing slashes are stripped. |
| `fetch` | `typeof fetch` | `globalThis.fetch` | Swap in an instrumented or proxied `fetch`. |
| `maxRetries` | `number` | `3` | 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`:

```ts
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:

```ts
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

```ts
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

| Kind | Question | Answer |
|---|---|---|
| `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:

```ts
// 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](https://jevascript.org/docs/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](https://jevascript.org/docs/testing).
