jonaki

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

  1. An explicit provider on the run options, then on the agent.
  2. The model prefix.
  3. 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.

ProviderPrefixExamplesEnv var
Anthropicclaude-*claude-sonnet-4-5, claude-opus-4-1, claude-haiku-4-5ANTHROPIC_API_KEY
OpenAIgpt-*, o*gpt-4.1, gpt-4.1-mini, o3, o4-miniOPENAI_API_KEY
Googlegemini-*gemini-2.5-flash, gemini-2.5-proGOOGLE_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-5

Writing 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.

On this page