Providers API
Import vendor factories, mockProvider, resilience helpers and record/replay from @nola-lang/providers. Their shared types, including LanguageModel and ProviderRequest, are exported from @nola-lang/runtime.
For the Nola platform, use model: "nola" and a URL in telemetry. If you need options, import nola from @nola-lang/runtime and call nola.infer(...) or nola.tracer(...).
import { defineConfig } from "@nola-lang/runtime";import { anthropic, google, openai, mockProvider, withRetry, exponential, fallback, record, replay } from "@nola-lang/providers";
export default defineConfig({ model: { default: withRetry(anthropic("claude-sonnet-4-5"), exponential({ maxRetries: 3 })), fast: fallback([google("gemini-2.5-flash"), openai("gpt-5-mini")]), test: replay("./nola.replay.jsonl"), // record(...) once, replay offline forever mock: mockProvider([{ ok: true }]), },});Factories
Section titled “Factories”openai(options), anthropic(options), google(options), typesafe(options) — each returns a LanguageModel. A bare model string is shorthand for { model }.
| Option | Type | openai | anthropic | typesafe | |
|---|---|---|---|---|---|
model |
string |
required | required | required | default jev-latest |
apiKey |
string |
✓ | ✓ | ✓ | ✓ |
apiKeyEnv |
string — default |
OPENAI_API_KEY |
ANTHROPIC_API_KEY |
GEMINI_API_KEY |
TYPESAFE_API_KEY |
baseUrl |
string |
✓ (any Chat-Completions dialect) | ✓ | ✓ | ✓ |
fetch |
typeof fetch |
✓ | ✓ | ✓ | ✓ |
maxOutputTokens |
number |
— | ✓ | — | — |
threshold |
number — plain boolean is true strictly above it |
— | — | — | default 0.5 |
Keys are read lazily at the first request. The option interfaces are exported as OpenAiOptions, AnthropicOptions, GoogleOptions, TypesafeOptions. A providers constant maps each factory by its provider name: providers.openai, providers.anthropic, providers.google, providers.typesafe, providers.mock.
typesafe
Section titled “typesafe”typesafe() connects to typesafe.ai’s System One API, using the Jev model. It answers typed questions about JSON data and returns calibrated probabilities. Use it for decision types.
It receives the ask’s structured form. Contextual parameters and bindings become the request’s state, and each output property becomes a question. The property’s JSDoc comment supplies its instructions, with the ask’s instruction included as shared context.
Supported output types:
Choice<{ … }>— a choice question with the descriptions as criteria; the answer object comes back as isScale<[ … ]>— a score question over the ordered levels;score, the distribution re-keyed to an array,levels,confidenceProb/Prob<{ true; false }>— a yes/no question; the probability of yes- a string literal union or enum (
"billing" | "shipping") — a choice question, the label alone - a number literal union (
1 | 2 | 3) — a choice question, decoded back to the number boolean— a yes/no question,truewhen the probability of yes is strictly abovethreshold(default0.5)- a flat object whose properties are all of the above — one request, one question per property
Other output types, including free-form strings, Date, unbounded numbers, arrays, nested objects and nullable unions, fail before a network request is made. The definitive error names the unsupported property. Use fallback with a general model to handle those asks, or route decision asks to a named model:
import { defineConfig } from "@nola-lang/runtime";import { fallback, openai, typesafe } from "@nola-lang/providers";
export default defineConfig({ model: { default: openai("gpt-5-mini"), triage: fallback([typesafe(), openai("gpt-5-mini")]), // reads TYPESAFE_API_KEY },});If no contextual value is in scope, the instruction text becomes the state. The API has no conversation history, so a correction attempt sends the same state again.
Use .withParams({ providerOptions: { threshold: … } }) to override the threshold for a plain boolean answer. This provider ignores other per-intent params.
The nola namespace is exported from @nola-lang/runtime. It has two factories: infer for the model setting and tracer for telemetry. Call one of these methods; nola itself is not callable.
Set model: "nola" to use the platform defaults. Use nola.infer(model?) when you need to pass options:
import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: "nola" }); // the platform serves inference — reads NOLA_API_KEY"nola"is shorthand fornola.infer(). Other strings in themodelsetting produceNOLA3003.nola.infer()lets the platform choose the model. Trial keys are limited to the models allowed by the service. Requests contain the structured inference model. If anask with <name>has no matching local model, its name is sent as aprofilefor the platform to resolve.nola.infer("<provider>/<model>")selects an upstream model explicitly.nola.infer({ baseUrl?, apiKey?, apiKeyEnv?, fetch?, retry?, model? })accepts connection and retry options. The URL defaults toNOLA_API_URLorhttps://api.nola.sh, and the key is read fromNOLA_API_KEYat the first request unless overridden. Retries default to two extra attempts, with a 500 ms delay doubled between attempts and capped at 10 s. They honourRetry-After. Setretry: falseto disable them.
The platform model must be the bare model value or the map’s default entry. withRetry, fallback and roundRobin reject it; use its retry option for request failures, and platform routing for failover. You can wrap it in record and replay the resulting ledger with replay.
Server errors become NolaProviderError instances with code and details fields. Codes include quota_exceeded, insufficient_credits, unauthorized, forbidden, model_not_allowed, request_too_large and rate_limited. All HTTP 4xx errors except 408 and 429 are definitive.
quota_exceededincludes{ runsUsed, runsLimit }.insufficient_creditsmeans a paid key’s balance reached zero. It has HTTP status 402 and includes{ balanceMicro }.
For a trial key with five or fewer runs left, the client prints the remaining usage to stderr once per count. The notice includes a reminder to run nola account to add credits.
To send traces, also configure telemetry. Using the platform for inference does not enable tracing automatically. See Telemetry.
// nola.config.ts — a self-hosted console serves inference and tracingimport { openai } from "@nola-lang/providers";import { defineConfig, nola, terminalTrace } from "@nola-lang/runtime";
export default defineConfig({ model: { default: nola.infer({ baseUrl: "http://localhost:8787" }), fast: openai("gpt-5-nano") }, telemetry: ["http://localhost:8787", terminalTrace()],});tracer
Section titled “tracer”Set telemetry to a server URL, such as "http://127.0.0.1:4141", or add that URL to the observer list. This is shorthand for nola.tracer(url).
nola.tracer(target?) returns a NolaTelemetry observer that posts redacted events to the server’s /v1/ingest endpoint without waiting for a response. With no target, it uses NOLA_API_URL or https://api.nola.sh. Pass { baseUrl?, apiKey?, apiKeyEnv?, fetch? } for other connection settings. See Telemetry for delivery behaviour.
mockProvider
Section titled “mockProvider”mockProvider(source: unknown[] | ((req: MockRequest) => unknown)): LanguageModel
Pass a queue of values or a function that receives the request. A queue serves one value per provider call, including correction and retry attempts, and throws when it runs out. A function receives the request: req.intent is the ask as data, and req.prompt is its default rendering ({ system, messages }), rendered only when read.
mockProvider makes no network requests.
Combinators
Section titled “Combinators”| Export | Signature | Meaning |
|---|---|---|
withRetry |
(model: LanguageModel, policy: RetryPolicy) => LanguageModel |
retries the provider request with backoff; stops on definitive errors; honours Retry-After up to maxDelayMs |
constant |
({ maxRetries, delayMs? }) => RetryPolicy |
flat delay (default 0) |
exponential |
({ maxRetries, delayMs?, multiplier?, maxDelayMs? }) => RetryPolicy |
defaults 200, 2, 10_000 |
fallback |
(providers: LanguageModel[]) => LanguageModel |
first success in order; throws when all fail |
roundRobin |
(providers: LanguageModel[]) => LanguageModel |
rotating start, then the rest in order |
isDefinitiveProviderError |
(error: unknown) => boolean |
true for a NolaProviderError flagged definitive or with an HTTP 4xx status other than 408/429 |
RetryPolicy is { maxRetries: number; delayMs: number; multiplier: number; maxDelayMs: number }. An empty provider array is a config error (NOLA3003). Details and the three retry layers: Resilience.
record / replay
Section titled “record / replay”| Export | Signature | Meaning |
|---|---|---|
record |
(inner: LanguageModel, ledgerPath: string) => LanguageModel |
calls inner and appends each exchange to the JSONL ledger |
replay |
(ledgerPath: string) => LanguageModel |
serves answers from the ledger; a miss is NOLA3008, an unreadable ledger NOLA3007 |
See Record and replay.
Custom providers
Section titled “Custom providers”A model is { name, infer(request) }. Every model, built-in or yours, receives the same request: the ask as data. Render it however your backend needs, or call renderPrompt from @nola-lang/providers for the default text, then return the raw reply and, when you sent text, what you sent.
// not-checked — simplified types; ProviderParams is not re-exportedinterface LanguageModel { name: string; infer(req: InferRequest): Promise<InferResult>;}
interface InferRequest { intent: InferenceModel; // scopes, inputs, the task, the output schema; the correction on a retry params?: ProviderParams; // temperature, maxOutputTokens, providerOptions profile?: string; // the `ask with <name>` name under the platform signal?: AbortSignal; // the invocation's timeout trace?: { askId: string; invocationId: string; spanPath: readonly string[] };}
interface InferResult { text: string; // the raw reply; Nola parses and validates it durationMs?: number; usage?: { inputTokens?: number; outputTokens?: number }; sent?: { system: string; messages: { role: "user" | "assistant"; content: string }[] }; // what you sent — shown as the receipt's effectivePrompt}A minimal text provider using fetch:
import type { InferRequest, LanguageModel } from "@nola-lang/runtime";import { renderPrompt } from "@nola-lang/providers";
export function myProvider(endpoint: string): LanguageModel { return { name: "my-provider", async infer(req: InferRequest) { const started = Date.now(); const sent = renderPrompt(req.intent); const schema = req.intent.output.syntax === "json" ? req.intent.output.schema : undefined; const res = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ system: sent.system, messages: sent.messages, schema }), signal: req.signal, }); const { text } = (await res.json()) as { text: string }; return { text, durationMs: Date.now() - started, sent }; }, };}Return the raw reply in text. The runtime parses and validates it. If validation fails, it calls infer again with intent.correction set to the failed reply and the issues; renderPrompt renders that as the assistant turn plus a <correction> block.
The built-in openai, anthropic and google providers accept structuredOutputs: false for a server that cannot enforce a schema: nothing is sent on the wire and the schema is rendered into the system turn as a <schema> block. DEFAULT_SYSTEM is the default system text, exported for composing your own.
For provider failures, throw NolaProviderError from @nola-lang/runtime. Set status, definitive and retryAfterMs as appropriate so retry helpers can decide whether and when to try again.
Next: Config schema