Model providers
Anthropic, OpenAI, and Google behind one interface — and how model strings resolve.
Jonaki normalizes three providers to one Provider interface. Messages, tool
calls, finish reasons, and usage all come back in the same shape.
Resolution order
- An explicit
provideron the run options, then on the agent. - The model prefix.
- Whichever API key is in the environment — Anthropic, then OpenAI, then Google.
If none of those identify a provider, you get NO_PROVIDER naming the exact
variables to set.
Model strings
Pass the provider's own model id.
| Provider | Prefix | Examples | Env var |
|---|---|---|---|
| Anthropic | claude-* | claude-sonnet-4-5, claude-opus-4-1, claude-haiku-4-5 | ANTHROPIC_API_KEY |
| OpenAI | gpt-*, o* | gpt-4.1, gpt-4.1-mini, o3, o4-mini | OPENAI_API_KEY |
gemini-* | gemini-2.5-flash, gemini-2.5-pro | GOOGLE_API_KEY |
import { Agent } from "@rohhitrz/jonaki";
new Agent({ name: "A", model: "claude-sonnet-4-5" });
new Agent({ name: "A", model: "gpt-4.1-mini" });
new Agent({ name: "A", model: "gemini-2.5-flash" });Not valid
"anthropic/claude-sonnet-4-5", "claude", and "gpt4" do not resolve.
Use the provider's own id.
Fallback ladders
An array is tried in order, and can cross providers:
import { Agent } from "@rohhitrz/jonaki";
const agent = new Agent({
name: "Resilient",
model: ["claude-sonnet-4-5", "gpt-4.1", "gemini-2.5-flash"],
});Each rung resolves its own provider, so an Anthropic outage falls through to OpenAI. The trace records which model actually answered:
const call = result.steps.find((step) => step.type === "model");
console.log(call?.provider, call?.model);Explicit providers
import {
Agent,
createAnthropicProvider,
createGoogleProvider,
createOpenAIProvider,
} from "@rohhitrz/jonaki";
const agent = new Agent({
name: "A",
provider: createAnthropicProvider({ apiKey: process.env.MY_KEY }),
});Options for each: apiKey, and client to inject a pre-configured SDK client.
Anthropic and OpenAI also accept baseURL:
import { createOpenAIProvider } from "@rohhitrz/jonaki";
const local = createOpenAIProvider({
apiKey: "not-needed",
baseURL: "http://localhost:11434/v1", // any OpenAI-compatible server
});Cost
Usage is priced per model and accumulated on the run:
result.usage; // { inputTokens, outputTokens, costUSD }Unknown models price at 0, so costUSD is a floor rather than a guarantee.
Check usage.inputTokens if you need certainty that a call happened.
Resolving manually
import { resolveProvider } from "@rohhitrz/jonaki";
const { provider, model } = resolveProvider({ model: "claude-sonnet-4-5" });
console.log(provider.id, model); // "anthropic" claude-sonnet-4-5Writing your own
Provider is small. Implement it and Jonaki treats it like any built-in — this
is exactly how replay and recording work.
import { Agent } from "@rohhitrz/jonaki";
import type { ModelResponse, Provider, ProviderChunk } from "@rohhitrz/jonaki";
const echo: Provider = {
id: "anthropic",
async generate(model, request) {
const last = request.messages.at(-1)?.content ?? "";
return {
text: `echo: ${last}`,
toolCalls: [],
usage: { inputTokens: 0, outputTokens: 0, costUSD: 0 },
finishReason: "stop",
raw: null,
} satisfies ModelResponse;
},
async *stream(model, request): AsyncIterable<ProviderChunk> {
const response = await this.generate(model, request);
yield { type: "text", delta: response.text };
yield { type: "done", response };
},
};
const agent = new Agent({ name: "Offline", provider: echo });That is the whole contract: id, generate, stream.