LLM Providers
AgentOS abstracts every LLM behind a single IProvider interface. Thirteen providers are wired in directly: eleven reached with an API key or a base URL, and two local CLI bridges that ride an existing Claude Max or Google account subscription. Three of the thirteen are gateways: OpenRouter and Requesty route to models from many vendors, and LiteLLM is a proxy you host. Every provider speaks the same streaming protocol, supports the same tool-call shape (with the documented exceptions below), and reports each call's token usage in the same shape; Prices and Cost Reporting says which providers also report the call's cost. The fallback chain is auto-built from whichever keys are set in the environment and is overridable per agent.
Table of Contents
- Overview
- Provider Matrix
- Quick Start
- Auto-Detection Order
- Provider Configuration
- Fallback Behavior
- Prices and Cost Reporting
- Provider Details
- Programmatic Configuration
- Adding a Custom Provider
- Provider Capabilities Detail
- Related Documentation
Overview
AgentOS abstracts LLM access behind a unified IProvider interface. You configure providers via environment variables, and AgentOS handles model selection, streaming, tool calling, retries, and fallback routing.
Key features:
- 13 providers supported out of the box (11 by API key or base URL, 2 CLI-based)
- CLI providers: Use your Claude Max or Google account subscription via local CLI — no API key needed
- Auto-detection: Set an API key or install a CLI and the provider is available
- Fallback: Automatic retry with alternate providers on failure (
fallbackProviders) - Cost-aware caps:
agency()checkscontrols.maxCostUSDand the other run limits after each run;agent()appliescontrols.maxTotalTokensandcontrols.maxDurationMsto each model call; abudgetongenerateText(),streamText(),generateObject(),embedText()oragent()checks each provider call against a spend cap before it is sent; a custom router sends requests to cheaper models - Streaming: All providers support streaming with a unified async iterator
- Tool calling: Unified function/tool calling across providers that support it
Provider Matrix
| Provider | Env Var | Default Model | Streaming | Tool Calling | Vision | Embedding | Default model's price (input / output, USD per 1M tokens) |
|---|---|---|---|---|---|---|---|
| OpenAI | OPENAI_API_KEY | gpt-4o | Yes | Yes | Yes | Yes | $2.50 / $10.00 |
| Anthropic | ANTHROPIC_API_KEY | claude-sonnet-4-6 | Yes | Yes | Yes | No | $3.00 / $15.00 |
| Gemini | GEMINI_API_KEY | gemini-2.5-flash | Yes | Yes | Yes | Yes | $0.30 / $2.50 |
| Groq | GROQ_API_KEY | llama-3.3-70b-versatile | Yes | Yes | No | No | Enterprise contracts only |
| Together | TOGETHER_API_KEY | meta-llama/Llama-3.3-70B-Instruct-Turbo | Yes | Yes | No | No | $1.04 / $1.04 |
| Mistral | MISTRAL_API_KEY | mistral-large-latest | Yes | Yes | No | Yes | $0.50 / $1.50 |
| xAI | XAI_API_KEY | grok-2 | Yes | Yes | Yes | No | Not on xAI's model list |
| OpenRouter | OPENROUTER_API_KEY | openai/gpt-4o | Yes | Yes | Yes* | Yes* | $2.50 / $10.00 |
| Requesty | REQUESTY_API_KEY | openai/gpt-4o | Yes | Yes | Yes* | Yes* | $2.50 / $10.00 |
| LiteLLM | (none read; pass apiKey) | (none; pass model) | Yes | Yes* | Yes* | Yes* | Your proxy's upstream prices |
| Ollama | OLLAMA_BASE_URL | llama3.2 | Yes | Partial | Model-dep. | Yes | Free |
| Claude Code CLI | (PATH detection) | claude-sonnet-4-6 | Yes | Yes | Yes | No | Free* |
| Gemini CLI | (PATH detection) | gemini-3.5-flash | Yes | Partial** | Yes | No | Free* |
Prices are the vendors' list prices, read on 2026-10-12; Prices and Cost Reporting links each source.
*CLI providers use your existing subscription — $0 per token. **Gemini CLI tool calling uses XML prompt-based parsing (less reliable than native API tool calling).
Gemini CLI ToS Warning: Google's Gemini CLI ToS may prohibit third-party subprocess invocation with OAuth auth. Use
geminiwith API key for production. See CLI Providers for details.
*OpenRouter, Requesty and LiteLLM capabilities depend on the underlying model selected.
Quick Start
Option 1: Environment Variable (Simplest)
Set one API key and start using AgentOS:
export OPENAI_API_KEY=sk-...
import { agent } from '@framers/agentos';
const myAgent = agent({}); // Auto-detects from env (OpenAI here)
const result = await myAgent.generate('Hello, world!');
console.log(result.text);
Option 2: Programmatic
import { agent } from '@framers/agentos';
const myAgent = agent({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
});
The agent() factory is synchronous — it does not return a Promise. The first network call happens on generate() / stream() / session().send().
Auto-Detection Order
When neither provider nor model is set, AgentOS checks for API keys in this
order and uses the first one found:
OPENROUTER_API_KEY→ OpenRouterOPENAI_API_KEY→ OpenAIANTHROPIC_API_KEY→ AnthropicGEMINI_API_KEY→ Google GeminiGROQ_API_KEY→ GroqTOGETHER_API_KEY→ Together AIMISTRAL_API_KEY→ MistralXAI_API_KEY→ xAIREQUESTY_API_KEY→ Requestywhich claude→ Claude Code CLI (PATH detection — no API key, uses Max subscription)which gemini→ Gemini CLI (PATH detection — no API key, uses Google account)OLLAMA_BASE_URL→ Ollama
An OLLAMA_BASE_URL that is empty or whitespace alone is unset, as the call reads it, so detection passes over Ollama.
LiteLLM is never auto-detected: name it with provider: 'litellm' (LiteLLM).
You can override auto-detection in four ways, highest priority first:
- Inline —
agent({ provider: '...', apiKey: '...' })on a single call. - Module-level default —
setDefaultProvider({ provider, apiKey })once at boot. Every subsequent call inherits it; inline opts still win when supplied. Useful when credentials live in a secrets manager rather than.env. - Reorder the auto-detect chain —
setProviderPriority(['anthropic', 'openai', ...])to change which env-var keys are preferred when multiple are set, without forcing a single provider. Empty array disables auto-detect entirely. - CLI flag — for the Wunderland CLI, pass
--provider <name>.
import { setDefaultProvider, generateText, agent } from '@framers/agentos';
setDefaultProvider({
provider: 'openai',
apiKey: process.env.MY_OWN_KEY,
// optional: model: 'gpt-4o-mini', baseUrl: '...'
});
// No env vars, no inline opts — just works:
const { text } = await generateText({ prompt: 'hello' });
const bot = agent({ instructions: '...' });
// Inline still wins:
generateText({ apiKey: 'sk-tenant-scoped', prompt: 'isolated call' });
Provider Configuration
Each provider is configured via environment variables. You can set them in
your shell or .env file:
# .env
# Primary provider
OPENAI_API_KEY=sk-...
# Fallback provider
OPENROUTER_API_KEY=sk-or-...
# Local provider (no API key needed)
OLLAMA_BASE_URL=http://localhost:11434
For a provider that rotates keys, the key variable (or the apiKey you pass) may hold a comma-separated list: a key pool. The LLM providers move a request to the next usable key when one is revoked, unpaid or throttled, probe a failed key before it returns, and persist the keys' standing when given a store; AGENTOS_KEY_POOL_STRATEGY_ANTHROPIC=ordered uses Anthropic's keys in the order given (credited keys first, the paid key last), and the same variable with another provider's name (_OPENAI, _GEMINI, _OPENROUTER, _REQUESTY) orders that provider's; AGENTOS_KEY_POOL_STRATEGY=ordered orders every provider's keys, so a list kept for load spreading on another provider becomes ordered too. API Key Rotation has the strategies, the failure kinds, and the providers that need a single key.
Per-Agent Override
Individual agents pick their provider/model directly in the agent({ ... }) config:
import { agent } from '@framers/agentos';
const writer = agent({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
apiKey: process.env.ANTHROPIC_API_KEY, // optional override
});
Fallback Behavior
AgentOS supports automatic fallback when a provider request fails on a
retryable error: HTTP 401, 402, 403, 429, 500, 502, 503, 504 or 529, a
network failure or request timeout, a primary provider that cannot initialize
(for example a revoked key that its model listing rejects), a request larger
than the model's context window, or a content-policy refusal
(isRetryableError()).
Fallback is on by default with an auto-built chain — to disable it, pass
an empty array.
The chain serves generateText() and streamText(), and agent() and
agency() through them. The GMIs of the full runtime (processRequest())
call their provider without a fallback chain; a GMI built with a completion
gateway moves to the next hop when a hop cannot start or an attempt fails
with a retryable error before any output
(Model calls through a completion gateway).
An agent created with runtime: 'gmi' builds that gateway from its own
fallbackProviders (unset, the same auto-built chain), policyTier, router
and onFallback, so its sessions fall back as a gateway does: before a step's
first output, and not after it
(GMIs from agent()).
A failover never repeats work the caller already received or that had side
effects. A stream that has delivered text or tool activity is not restarted
on a fallback provider; it ends with an error part, so the consumer never
receives a partial answer followed by a second one. A generateText call
that already ran native tool rounds continues on the fallback provider from
those rounds (the fallback sees each tool call and its result) instead of
starting over and running the tools again. A call whose prompt-emulated
tools (toolMode: 'prompt') ran throws the error instead of failing over.
Each leg receives the caller's customModelParams minus the fields only
another vendor accepts: OpenRouter's routing controls (provider, models,
route, transforms) reach only OpenRouter, and Gemini's request fields
(thinkingConfig, topK, safetySettings and the rest of
GEMINI_ONLY_PARAM_KEYS) reach only Gemini.
The structured-output fields follow the same rule: OpenAI's response_format
does not reach Anthropic or Gemini, and Anthropic's output_config does not
reach OpenAI or Gemini, so an OpenAI call that fails over to Claude (or the
reverse) sends the leg only fields its API defines; the leg's own structured
output is built for it by its provider.
With planning on, the planning call carries the caller's messages, so it
takes OpenRouter's routing controls and plugins from customModelParams: a
provider.data_collection: 'deny' or a context-compression plugin set for
the turn holds for it too. The other keys, such as max_tokens or
response_format, go to the turn's requests alone.
Primary provider (e.g., Anthropic)
↓ fails with a retryable error
OpenAI gpt-5.6-sol (if OPENAI_API_KEY is set)
↓ fails
OpenRouter openai/gpt-5.6-sol (if OPENROUTER_API_KEY is set)
↓ fails
Gemini gemini-3.1-pro-preview (if GEMINI_API_KEY is set)
↓ fails
Error returned to caller
The auto-built chain (buildFallbackChain())
takes these legs in this order, each when its key is set, and leaves out the
primary's own provider: OpenAI gpt-5.6-sol, Anthropic claude-sonnet-5-5
(effort low, 1024 tokens of output headroom), OpenRouter
openai/gpt-5.6-sol, and Gemini gemini-3.1-pro-preview (effort low, 1024
tokens of output headroom). Every leg runs with prompt caching off
(cache: false). Ollama is not in the auto-built chain; list it in
fallbackProviders to use it. For a call whose policy tier is mature or
private-adult, the chain starts with the policy catalog's fallback ladder for
that tier when OPENROUTER_API_KEY is set
(buildPolicyAwareFallbackChain()).
Configuring Fallback
import { agent } from '@framers/agentos';
const myAgent = agent({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
// Ordered fallback chain — each entry can override the model.
fallbackProviders: [
{ provider: 'openrouter', model: 'anthropic/claude-sonnet-4-5-20250929' },
{ provider: 'ollama', model: 'llama3.2' },
],
onFallback: (err, next) => {
console.warn(`Falling back to ${next}: ${err.message}`);
},
});
// Disable fallback entirely:
const strict = agent({ provider: 'anthropic', fallbackProviders: [] });
OpenRouter as Universal Fallback
Setting OPENROUTER_API_KEY automatically enables it as a fallback for any
primary provider in the auto-built chain. OpenRouter routes to 200+ models
across all major providers.
# Primary: Anthropic. Fallback: OpenRouter (automatic)
export ANTHROPIC_API_KEY=sk-ant-...
export OPENROUTER_API_KEY=sk-or-...
Prices and Cost Reporting
Every provider reports each call's token usage. Whether the result also carries the call's cost, usage.costUSD, depends on the provider:
| Provider | usage.costUSD |
|---|---|
| OpenAI, Anthropic, Gemini | Priced from the provider's own table of the vendor's list prices (openaiPricing.ts, anthropicCatalog.ts, GeminiProvider.ts). A model the table has no row for reports no cost. |
| OpenRouter, Requesty | The cost the gateway returns with the response |
| Ollama, Claude Code CLI, Gemini CLI | 0 |
| Groq, Together, Mistral, xAI, LiteLLM, MiniMax | These providers send their calls through the OpenAI provider (MiniMax extends it), which prices a call from OpenAI's table (openaiPricing.ts) by the model id the response names, exact or with a trailing -YYYY-MM-DD removed; an id the table has no row for reports no cost. The table lists only OpenAI's models, so a call to the default model of Groq, Together, Mistral, xAI or MiniMax reports no cost, and a LiteLLM call whose response names an OpenAI model, such as gpt-4o-mini, is priced at OpenAI's list price. A spend budget charges such a call at the row a host registered for the provider with registerModelPricing(), where it registered one, instead of the cost reported. |
Prices for a host: openAIModelPricing(modelId) and anthropicModelPricing(modelId), exported from @framers/agentos/safety/runtime and from the package root with the types OpenAIModelPrice and AnthropicModelPrice, give a spend guard a call's list price in USD per 1K tokens before the call is made, without loading a provider: { input, output, cachedInput? } for OpenAI, cachedInput being the cached-input rate OpenAI's pricing page lists for the model (absent where it lists none, for the pro models, or no longer lists the model; such a model's cache reads are billed at input), and { input, output, cacheRead, cacheWrite, cacheWriteOneHour, longContext? } for Claude, cacheWrite and cacheWriteOneHour being the 5-minute and 1-hour prompt-cache writes at 1.25x and 2x the input rate. modelPricing(providerId, modelId) reads both tables, and a table a host registers for any other provider with registerModelPricing(), in one shape in USD per million tokens with every tier a request can reach; estimateMaxCallCostUSD() bounds a call's cost with it (Pricing a call before it is sent). anthropicModelPricing resolves an exact catalog id, a dated snapshot of a bare id (claude-sonnet-5-5-20261001) and the bare alias of a dated row (claude-opus-4-5), and answers undefined for any other id (claude-opus-4, claude-sonnet-4, claude), never a prefix guess; a retired snapshot (claude-opus-4-20250514) prices by its exact id alone. longContext is present on a model priced by prompt length (Claude Haiku 5.5: { fromTokens: 100000, input, output, cacheRead, cacheWrite, cacheWriteOneHour }) and names the prompt size above which its long input, output and cacheRead rates apply; estimateAnthropicCostUSD bills a response whose prompt, cached tokens included, is over that size at the same long rates.
The list prices of the default text models, in USD per 1M tokens, as each vendor's page gave them on 2026-10-12. Each vendor's other models cost more or less; the linked pages list them.
| Provider | Default text model | Input | Output | Source |
|---|---|---|---|---|
| Ollama | llama3.2 | 0 | 0 | Runs on your machine |
| Claude Code CLI | claude-sonnet-4-6 | 0 | 0 | Uses your existing subscription |
| Gemini CLI | gemini-3.5-flash | 0 | 0 | Uses your existing subscription |
| Gemini | gemini-2.5-flash | $0.30 ($1.00 for audio) | $2.50, thinking included | Gemini API pricing |
| Mistral | mistral-large-latest (Mistral Large 3) | $0.50 | $1.50 | Mistral Large 3 |
| Together | meta-llama/Llama-3.3-70B-Instruct-Turbo | $1.04 | $1.04 | Together AI pricing |
| OpenAI | gpt-4o | $2.50 | $10.00 | OpenAI pricing |
| OpenRouter | openai/gpt-4o | $2.50 | $10.00 | OpenRouter: GPT-4o |
| Requesty | openai/gpt-4o | $2.50 | $10.00 | Requesty model list |
| Anthropic | claude-sonnet-4-6 | $3.00 | $15.00 | Anthropic pricing |
| Groq | llama-3.3-70b-versatile | On request | On request | Groq models: enterprise contracts only (Groq) |
| xAI | grok-2 | Not listed | Not listed | xAI models (xAI) |
| LiteLLM | none; pass model | Your proxy's upstream | Your proxy's upstream |
Cost-Aware Caps
Run limits live on controls, and agency() is the surface that checks all of them:
import { agency } from '@framers/agentos';
const team = agency({
provider: 'anthropic',
agents: {
researcher: { instructions: 'Find the facts.' },
writer: { instructions: 'Write the summary.' },
},
controls: {
maxCostUSD: 0.05, // total cost of a run
maxTotalTokens: 50_000, // prompt + completion tokens of a run
maxDurationMs: 30_000, // wall-clock time of a run
maxAgentCalls: 10, // agent invocations in a run
onLimitReached: 'error', // 'stop' | 'warn' | 'error'
},
on: {
limitReached: (e) => console.warn(`${e.metric}: ${e.value} > ${e.limit}`),
},
});
agency() compares a run against these limits when the run ends, and compares
the agency's running token and cost totals against them before each new run
starts. A breach calls on.limitReached; with onLimitReached: 'error' it
throws an AgencyConfigError instead. 'stop' and 'warn' both report
through the callback, and no limit interrupts a run that is in progress.
agent() reads two of the fields, per model call: controls.maxTotalTokens
becomes the call's completion-token cap (maxTokens) when the agent sets no
maxTokens, and controls.maxDurationMs becomes the request timeout. It does
not read maxCostUSD, maxAgentCalls or onLimitReached. To cap an agent's
spend, give it a budget (agent({ budget: { maxCostUSD: 0.25 } })): each
provider call of its generate(), stream() and sessions is checked against
what is left before it is sent, and by default a call whose estimated cost
would pass the cap is refused with CostCapExceededError. The budget holds its
spending in a
CostGuard
(Cost Optimization).
For cheap-first routing across multiple models, attach a custom IModelRouter
via agent({ router }) — the router decides which provider/model to call per
request. See Cost Optimization for the full guide.
Provider Details
OpenAI
export OPENAI_API_KEY=sk-...
| Model | Context | Vision | Tool Calling | Notes |
|---|---|---|---|---|
gpt-6-astra | 1.05M | Yes | Yes | Most capable GPT ($10/$50 per MTok) |
gpt-6-sol | 1.05M | Yes | Yes | GPT-6 at $2/$10 per MTok |
gpt-6-luna | 1.05M | Yes | Yes | Cheapest GPT-6 ($0.10/$0.50 per MTok) |
gpt-5.6-sol | 1.05M | Yes | Yes | GPT-5.6 flagship ($4/$20 per MTok) |
gpt-4o | 128K | Yes | Yes | Accepts temperature |
gpt-4o-mini | 128K | Yes | Yes | Fast, cheap |
gpt-image-2.5-flare | n/a | n/a | n/a | Image generation only: the generateImage() default |
gpt-image-2.5-sunburst | n/a | n/a | n/a | Image editing: the editImage(), variateImage() and transferStyle() default |
The GPT-5, GPT-6 and o-series models reject temperature and top_p and require max_completion_tokens; the provider handles both. Streamed and non-streamed calls follow one routing rule. The provider sends a call to the Responses API when it goes to a Responses-only model (the -pro models, the codex models, gpt-5.6-cyber and the deep-research models), when it is a GPT-6 call carrying function tools, or when it is a GPT-5 call carrying function tools and an effort; everything else goes to Chat Completions. OpenAI serves GPT-6 tool calls only through Responses (Chat Completions accepts them from Sol and Luna only with reasoning_effort: "none"). On the Responses path, responseFormat travels as text.format, images in user turns and tool results travel as input_image, and a streamed call reports its usage on the final chunk. A result marked failed throws RESPONSES_FAILED, streamed or not, whatever text it holds, a non-streamed one with the usage it reported in details.usage; a non-streamed one whose error code is retryable (server_error, rate_limit_exceeded, vector_store_timeout) comes with a 200 and is sent again while the call has attempts left, as a 5xx is, and the provider reports that attempt as the success its status was; the usage it reported, priced as its own request, is added to the result the call returns, or to the error it ends with in details.usage (an answer with no output items among them, whose INVALID_RESPONSE carries its body with usage in the provider's form), so two requests each below the long-context threshold are never priced as one above it. One marked failed with rate_limit_exceeded rests its pooled key as a 429 does, and the retry goes out with the key the pool draws next: a usable one, or, when every key rests, the one whose rest ends soonest (Key rotation). A message part Responses cannot carry, such as audio, fails a call that has no Chat Completions route with RESPONSES_UNMAPPABLE_CONTENT. OpenAI retires o1, o1-pro, o3-mini and o4-mini on 2026-10-23, and o3 and o3-pro on 2026-12-11.
usage.costUSD prices the input as the prompt-caching guide does: cached_tokens at the model's cached-input rate (gpt-6-sol $0.20 against $2 input per MTok), and on GPT-5.6 and later cache_write_tokens at 1.25x input for the 30-minute lifetime and 2.5x for the 12-hour one, by the Responses API's cache_write_tokens_details split (a usage without the split is priced as 30-minute writes, the lifetime a request with no cache setting writes with); the rest of prompt_tokens at the input rate. Over 272,000 prompt tokens on a model with long-context pricing, every input rate doubles. The 12-hour count reaches the usage as cacheCreationExtendedInputTokens.
OAuth support: Use your ChatGPT subscription instead of an API key via the device code flow. See OAuth Auth for details.
Anthropic
export ANTHROPIC_API_KEY=sk-ant-...
| Model | Context | Vision | Tool Calling | Notes |
|---|---|---|---|---|
claude-opus-5-5 | 1M | Yes | Yes | Recommended starting model ($4/$20 per MTok) |
claude-fable-5-1 | 1M | Yes | Yes | Most capable ($10/$50 per MTok) |
claude-sonnet-5-5 | 1M | Yes | Yes | Current Sonnet ($2/$10 per MTok) |
claude-sonnet-5 | 1M | Yes | Yes | Near-Opus coding and agentic work ($2/$10 per MTok) |
claude-opus-5 | 1M | Yes | Yes | Previous Opus ($5/$25 per MTok) |
claude-sonnet-4-6 | 1M | Yes | Yes | Best value |
claude-haiku-5-5 | 1M | Yes | Yes | Current Haiku ($0.10/$0.50 per MTok up to a 100K-token prompt) |
claude-haiku-4-5-20251001 | 200K | Yes | Yes | Previous Haiku ($1/$5 per MTok) |
claude-opus-4-5-20251101 | 200K | Yes | Yes | Legacy Opus, 64K output ($5/$25 per MTok) |
Reasoning-default models (claude-opus-5-5, claude-fable-5-1, claude-sonnet-5-5, claude-opus-5, claude-fable-5, claude-sonnet-5, claude-opus-4-8, claude-opus-4-7) reject temperature and top_p with HTTP 400; the provider drops both automatically for these models and sends adaptive thinking when a thinking budget is requested. claude-haiku-5-5 rejects any non-default temperature, top_p or top_k too, so the provider drops temperature and top_p for it as well; it thinks adaptively by default, so a thinking budget sends no thinking block there. The same fields passed through customModelParams are dropped too (top_k as well on Claude Haiku 5.5, whose migration guide says to remove it), with a one-time warning. A customModelParams.model replaces the model on the wire, and the provider shapes the request (sampling, thinking, effort, max_tokens and caching) for that model rather than the one given. Claude Opus 5.5, Claude Sonnet 5.5, Claude Mythos 5.1 and the Fable models also reject a forced tool_choice, so the provider sends auto for them; a schema request on these models takes the native JSON output format (output_config.format, see Tool Calling Support below) when the schema fits the format's limits, and the provider's own schema block in the system prompt when it does not. The provider sends a forced tool_choice to every other Claude model, so a schema the format cannot carry goes to it as a forced tool call (Claude Haiku 5.5 among them, probed on 2026-10-19). Anthropic retired claude-opus-4-20250514 and claude-sonnet-4-20250514 on 2026-06-15 (the API answers 404 for both; the catalog keeps them as deprecated); a schema request on those two ids takes the forced tool.
Opus 5 and later, Sonnet 5 and later, Haiku 5.5, Fable and Mythos think by default. Pass thinking: false to turn thinking off: the provider sends { type: 'between_tools' } on claude-sonnet-5-5 and { type: 'disabled' } on claude-opus-5, claude-sonnet-5 and claude-haiku-5-5, and lowers effort to high on Sonnet 5.5, Opus 5 and Haiku 5.5, which turn thinking off only at that level or below; an effort passed through customModelParams.output_config is lowered the same way, and the rest of that output_config is sent as given. claude-opus-5-5, the Fable models and Mythos always think; thinking: false sends nothing there and logs a warning once. Older models think only when asked, so thinking: false leaves the field out. effort follows each model's ladder: claude-opus-4-5 takes low, medium and high, claude-opus-4-6 and claude-sonnet-4-6 add max, and later models (claude-haiku-5-5 among them) take all five levels; a level a model does not take is sent as high. claude-haiku-4-5 takes no effort, so the provider leaves it out there. generateObject and streamObject take the same thinking and effort options and forward them unchanged; a structured call on a model that thinks by default needs thinking: false or room in maxTokens for the thinking.
usage.costUSD prices cache reads at 0.1x the input price, except Claude Opus 5.5 and Claude Sonnet 5.5 (0.05x) and Claude Fable 5.1 (0.025x), and cache writes at 1.25x for the 5-minute TTL and 2x for the 1-hour TTL, read from the response's cache_creation split; a response without the split is priced as 5-minute writes, the TTL a cache_control without ttl writes with. The 1-hour count reaches the call's usage as cacheCreationExtendedInputTokens (cacheCreationExtendedTokens on a generateText() or streamText() result), so a spend budget that prices the tokens itself (a model the catalog lacks, priced at a host's row) prices the two TTLs apart.
A call on the anthropic provider is sent to ANTHROPIC_BASE_URL when it is set (https://api.anthropic.com form, without /v1, as Anthropic's own SDKs read it), after the call's own baseUrl and the global default's, as OPENAI_BASE_URL routes the openai provider. A value with surrounding whitespace is read trimmed, trailing slashes are dropped before /v1/messages is appended, and an empty or whitespace-only value is unset.
When Claude declines a request (stop_reason: "refusal"), the provider throws an AnthropicProviderError with code content_filter, whether the refusal came before any output or partway through it. The error carries stop_details and any partial text in details, and no tool call from the refused turn runs. generateText and streamText treat it like other content-policy errors: the fallback chain fires, and with fallbackProviders: [] the call fails. A streamText refusal that arrives after text has streamed ends the stream with an error part and finish reason error instead of falling back, because the refused text has already reached the consumer. Refusals do not count toward the provider-health breaker.
Google Gemini
export GEMINI_API_KEY=AIza...
The key travels in the x-goog-api-key header, never in the request URL, so proxy and access logs do not record it. A comma-separated list of keys is a key pool: a key that answers HTTP 429, or the 400 that names an invalid key, is taken out and the request moves to the next usable key at once; a streaming request marks the key and the next request draws another.
| Model | Context | Vision | Tool Calling | Notes |
|---|---|---|---|---|
gemini-3.1-pro-preview | 1M | Yes | Yes | Most capable ($2/$12 per MTok, $4/$18 once the prompt passes 200K tokens) |
gemini-3.8-flash | 1M | Yes | Yes | Newest Flash ($0.75/$3.75 per MTok through 2026-12-31, then $1.50/$7.50) |
gemini-3.5-flash-lite | 1M | Yes | Yes | Low-cost Gemini 3 |
gemini-2.5-pro | 1M | Yes | Yes | Previous-generation Pro |
gemini-2.5-flash | 1M | Yes | Yes | Fast, large context |
gemini-2.5-flash-lite | 1M | Yes | Yes | Cheapest ($0.10/$0.40 per MTok) |
Google has retired gemini-2.0-flash, gemini-2.0-flash-lite and gemini-1.5-pro, and requests for them return HTTP 404. A request for gemini-3.1-pro-preview that returns 404 is retried once on gemini-pro-latest, within the call's maxAttempts: the alias gets the attempts the pinned request left, and with maxAttempts: 1 the 404 ends the call. On gemini-3.1-pro-preview, effort sets the thinking level (low, medium or high; xhigh and max send high), and other models keep the API default. A thinkingConfig object passed through customModelParams takes precedence: thinkingBudget on Gemini 2.5, thinkingLevel on Gemini 3. With thinkingConfig.includeThoughts, the model's thought summaries come back on the provider's message as reasoningText (streamed as reasoningTextDelta chunks), apart from the answer; no provider sends them back. The shared thinking option is Anthropic's and Gemini ignores it. Gemini 3 requires its thought signature back on every tool-call turn; the provider captures and replays it, and sends Google's placeholder when a turn has none, such as a call made by another provider in a fallback chain.
Images go to Gemini in two ways. A data URL is sent inline, read as WHATWG Fetch reads it, and a request carries at most 100 MB of inline data counted in base64 (Google's file limits). An https URL is sent as fileData, and Gemini fetches it itself, up to 15 MB a file (file input methods); the provider fetches nothing. An http URL, a malformed data URL and too much inline data are refused before the request, with HTTP status 400, so they count as the caller's error and not against Gemini's health. A prompt Gemini blocks (promptFeedback.blockReason) and an answer its filters stop with no content (SAFETY, RECITATION, BLOCKLIST, PROHIBITED_CONTENT, SPII and the IMAGE_ reasons) throw a content_filter error that names the reason.
Groq
export GROQ_API_KEY=gsk_...
provider: 'groq' without a model uses llama-3.3-70b-versatile. Groq shut that model and llama-3.1-8b-instant down for its free and developer plans on 2026-08-16 and serves them only under enterprise contracts, and it shut mixtral-8x7b-32768 down on 2025-03-20 (Groq deprecations, read 2026-10-12). On other plans, name a model from Groq's model list, such as openai/gpt-oss-120b, which Groq recommends in place of Llama 3.3 70B ($0.15 input and $0.60 output per 1M tokens on 2026-10-12):
import { generateText } from '@framers/agentos';
const { text } = await generateText({
provider: 'groq',
model: 'openai/gpt-oss-120b',
prompt: 'Hello',
});
Groq runs its models on its own LPU hardware; its model list gives each model's speed in tokens per second.
Together AI
export TOGETHER_API_KEY=...
| Model | Context | Vision | Tool Calling | Notes |
|---|---|---|---|---|
meta-llama/Llama-3.3-70B-Instruct-Turbo | 128K | No | Yes | Default |
openai/gpt-oss-120b | 128K | No | Yes | Always reasons |
zai-org/GLM-5.3-Flash | 1M | No | Yes | Thinking on by default |
Together serverless offers no embedding models.
Mistral AI
export MISTRAL_API_KEY=...
| Model | Context | Vision | Tool Calling | Notes |
|---|---|---|---|---|
mistral-large-latest | 256K | No | Yes | Mistral Large 3 (mistral-large-2512); the default |
codestral-latest | 128K | No | Yes | Codestral (codestral-2508), for code |
mistral-small-latest | 256K | No | Yes | Mistral Small 4 (mistral-small-2603) |
Each alias's model and context window are from its page in Mistral's model list, read 2026-10-12.
xAI (Grok)
export XAI_API_KEY=xai-...
provider: 'xai' without a model uses grok-2. xAI's model list names neither grok-2 nor grok-2-mini on 2026-10-12, so name a model from that list.
OpenRouter
export OPENROUTER_API_KEY=sk-or-...
OpenRouter is a multi-provider proxy that routes to 200+ models. Specify the
model using the provider/model format:
import { agent } from '@framers/agentos';
const myAgent = agent({
provider: 'openrouter',
model: 'anthropic/claude-sonnet-4-5-20250929',
});
Popular OpenRouter models:
openai/gpt-4oanthropic/claude-sonnet-4-5-20250929google/gemini-2.5-flashmeta-llama/llama-3.3-70b-instruct
Requesty
export REQUESTY_API_KEY=...
Requesty is an OpenAI-compatible gateway at https://router.requesty.ai/v1
(REQUESTY_BASE_URL overrides it). Name models as vendor/model:
import { agent } from '@framers/agentos';
const myAgent = agent({
provider: 'requesty',
model: 'anthropic/claude-sonnet-4-5-20250929',
});
provider: 'requesty' without a model uses openai/gpt-4o.
LiteLLM
LiteLLM is a proxy you host; it exposes the models
you configure on it through an OpenAI-compatible API. AgentOS reaches it with
provider: 'litellm' and three explicit fields, because it reads no
environment variable for this provider and has no default model for it:
import { generateText } from '@framers/agentos';
const { text } = await generateText({
provider: 'litellm',
model: 'anthropic/claude-sonnet-4-6', // a model name your proxy serves
apiKey: process.env.LITELLM_API_KEY, // the proxy's master or virtual key
baseUrl: 'http://localhost:4000/v1', // the default when omitted
prompt: 'Hello',
});
provider: 'litellm' without model throws Unknown provider "litellm", and
without apiKey it throws No API key for litellm. The provider class has a
default model of its own, gpt-4o-mini, used only when the class is driven
directly without a model id; generateText(), streamText() and agent()
never reach it. LiteLLM is not part of
auto-detection or of the auto-built fallback chain; add it to
fallbackProviders with its model, apiKey and baseUrl to use it as a
fallback.
Ollama
export OLLAMA_BASE_URL=http://localhost:11434
Run any open model locally. No API key, no cost, full privacy.
# Pull models manually
ollama pull llama3.2
ollama pull codellama
ollama pull dolphin-mixtral
| Model | Parameters | Context | Tool Calling | Notes |
|---|---|---|---|---|
llama3.2 | 3B/8B | 128K | Partial | General-purpose |
codellama | 7B/13B/34B | 16K | No | Code-optimized |
dolphin-mixtral | 8x7B | 32K | No | Uncensored |
mistral | 7B | 32K | Partial | Fast |
phi3 | 3.8B | 128K | No | Small, fast |
Programmatic Configuration
Provider + Model + Auth
The agent factory accepts provider, model, apiKey, and baseUrl
directly. There is no separate LLMProviderConfig type — these fields live
on AgentOptions (and on BaseAgentConfig, so every sub-agent in an
agency() roster takes the same fields).
import { agent } from '@framers/agentos';
const myAgent = agent({
provider: 'anthropic',
model: 'claude-sonnet-4-5-20250929',
apiKey: process.env.ANTHROPIC_API_KEY, // optional override
baseUrl: undefined, // optional custom base URL
});
Per-Call Overrides
generate() and stream() accept the same provider/model fields as a
per-call override on top of the agent's base config — useful for sending one
specific question through a different provider:
const result = await myAgent.generate(
'Run this complex analysis as a one-off.',
{
provider: 'openai',
model: 'gpt-4o',
},
);
Operation id across retries
OpenAIProvider, AnthropicProvider and OpenRouterProvider can send each call's operation id in a header you name. A server in front of the provider, such as a proxy that meters spend, reads it to tell a retry of a call from a new call.
# Every request from these providers carries x-operation-id: <uuid>
export AGENTOS_OPERATION_ID_HEADER=x-operation-id
import { AnthropicProvider } from '@framers/agentos/core/llm/providers/implementations/AnthropicProvider';
const anthropic = new AnthropicProvider();
await anthropic.initialize({
apiKey: process.env.ANTHROPIC_API_KEY!,
baseURL: 'https://proxy.example.com',
operationIdHeader: 'x-operation-id', // wins over AGENTOS_OPERATION_ID_HEADER
});
- The provider makes the id, a UUID, once before a call's first attempt, unless the call names its own in the
operationIdoption. Every retry of that call sends the same value: a retry after a 429, a 5xx or a network failure, an Anthropic stream that ended before its last event, and a hop to another key of the pool. - The next call gets a new id. So does a request sent again with a changed body: the request
generateObjectsends again with a raised token budget after a truncated answer, and OpenRouter's schema request sent again without its schema, which keeps a call's ownoperationIdwhen it names one. - On a
SpendBudgetwith a spend meter, the generation helpers and a launched team's seats pass each provider call the operation id its reservation is under, so the header carries the id the meter holds the call by (Cost Optimization).embedText()sends its requests itself and puts that id on each of them, in the headerAGENTOS_OPERATION_ID_HEADERnames. - Streamed calls, embeddings (OpenAI, OpenRouter), the key probe's requests and OpenRouter's health check carry the header too; each probe request has an id of its own. An embeddings call that names an
operationIdsends it in place of the id the provider makes. GeminiProvidersends the header on embeddings only, and only when the call names anoperationId, which it sends on every batch and retry, under itsoperationIdHeaderor else the nameAGENTOS_OPERATION_ID_HEADERgives. Its completions send none.operationIdHeaderin the provider's config names the header; when it is unset the provider readsAGENTOS_OPERATION_ID_HEADERat each call. A blank value counts as unset. With neither, no header is sent.- On OpenAI, the id replaces a
customHeadersentry of the same name in any case (X-Operation-Idforx-operation-id), so each request carries one value. The other custom headers are sent as before. - A name that is not an HTTP header name, or one the provider sets itself (
Authorization,x-api-key,x-goog-api-key,Content-Type,anthropic-version,anthropic-beta,OpenAI-Organization,HTTP-Referer,X-Title,User-Agent), is refused with aRangeErrorbefore anything is sent: by the provider'sinitialize(), by each call, and byprobeApiKey()and soprobeProviderKey(). A key probe answers for the key; a refused name is the host's error, so the probe rejects rather than answeringunknown.
Adding a Custom Provider
AIModelProviderManager builds providers from a fixed list of ids, skips
an id it does not know, and has no method that registers another
IProvider class. There are three ways to reach a model the list
does not cover:
-
An OpenAI-compatible endpoint. Point the
openaiprovider at it withbaseUrl(orOPENAI_BASE_URL):import { generateText } from '@framers/agentos';const { text } = await generateText({provider: 'openai',baseUrl: 'https://llm.internal.example.com/v1',apiKey: process.env.INTERNAL_LLM_KEY,model: 'my-model',prompt: 'Hello',});The endpoint must answer
GET /models: the provider lists the models when it initializes, and a listing that fails fails the call. AbaseUrlthat is empty or whitespace alone is unset:OPENAI_BASE_URLapplies, and with neither the request goes tohttps://api.openai.com/v1; no request is built on a blank base. -
A gateway. OpenRouter, Requesty and a self-hosted LiteLLM proxy each put many vendors behind one provider id.
-
A new provider in AgentOS. Implement
IProvider(providerId,isInitialized,initialize(),generateCompletion(),generateCompletionStream(),generateEmbeddings(),listAvailableModels(),getModelInfo(),checkHealth(),shutdown()) next to the classes undersrc/core/llm/providers/implementations/, add its id to the manager and toprovider-defaults.ts, and open a pull request. Adding an LLM Provider lists what a provider pull request must include.
The interface is importable for typing as
import type { IProvider } from '@framers/agentos/core/llm/providers/IProvider'.
Provider Capabilities Detail
Tool Calling Support
The Structured Output column is the provider-side enforcement that
generateObject() and a session's responseSchema request. generateObject()
also writes the schema into the system prompt on every provider. A session's
responseSchema, on either agent runtime, writes it there only when the
provider-side format carries no schema: on a provider whose entry is "None",
on an Anthropic model whose schema neither the JSON output format nor a forced
tool carries (a record schema on a model that refuses a forced tool choice), and
in JSON-object mode. Each fallback provider is checked for its
own format, and so is the provider a call is sent to: a session send that
agent({ router }) moves to another provider or model is sent the format of
the provider and model it goes to, and the schema in its system prompt when
that format carries none, so a send moved from Anthropic to an OpenRouter
model goes out with OpenRouter's json_schema or JSON-object format rather
than an Anthropic payload OpenRouter does not forward. A call on Anthropic
with no Anthropic key or base URL goes to OpenRouter, whether the caller names
Anthropic, a router picks it or a fallback leg names it, and is sent
OpenRouter's format there, with the key and base URL a call that names
OpenRouter is sent with (a setDefaultProvider() default that names
OpenRouter, else OPENROUTER_API_KEY and OPENROUTER_BASE_URL). A fallback entry whose model id carries
another provider's prefix ({ provider: 'anthropic', model: 'openrouter:anthropic/claude-sonnet-4.5' }) is sent the format of the provider
the prefix names, and onFallback, the llm.fallback_fired line (its
fallbackProvider and fallbackModel) and the leg's hop in
result.fallback.hops name that provider and the model id after the prefix
(openrouter, anthropic/claude-sonnet-4.5). An Anthropic entry the
credential layer sends to OpenRouter ({ provider: 'anthropic', model: 'claude-sonnet-4-5' } with no Anthropic key or base URL and
OPENROUTER_API_KEY set) is named the same way, openrouter and
anthropic/claude-sonnet-4-5, in both generateText() and streamText():
in onFallback, the llm.fallback_fired, llm.fallback_succeeded and
llm.fallback_failed_after_output lines and the hops; its breaker read is
OpenRouter's, its capability check judges anthropic/claude-sonnet-4-5, and
an llm.fallback_skipped line names the provider and model
(fallbackProvider, fallbackModel) the entry would have been sent as. An
excluded model (routerParams.excludedModelIds, a bare id or a
provider:model id) skips every entry that names it as written, as the model
resolves (the provider's default for an entry with no model, the id after an
anthropic: or anthropic/ prefix) or as it is sent (anthropic/<model> on
OpenRouter), in generateText(), streamText() and the completion gateway.
When a leg's own walk serves on a later entry, the outer
llm.fallback_succeeded and llm.fallback_failed_after_output lines name the
provider that served, in both walkers. The completion gateway's
onHopSkipped report names the entry as written (provider, model) and the
provider and model it would have been sent to (sentAsProvider,
sentAsModel), the names its onFallback and onHopFailure reports use. A send that goes to the agent's own provider and
model keeps the agent's format. streamObject()
sends no provider-side format on any provider: its
schema travels in the system prompt only. All three validate the reply against
the caller's Zod schema, and send() throws an ObjectGenerationError when the
reply is not matching JSON.
On Anthropic, anthropicSchemaRoute(modelId, schema)
(model-strict-tool-use.ts)
picks one of three routes for the model and the schema. The native JSON output
format (output_config.format with type: json_schema) goes to every model
that has it (Opus 4.5 and later, Sonnet 4.5 and later, Haiku 4.5 and later,
Fable, Mythos) when the schema fits the vendor's limits: an object root with
properties, additionalProperties: false on every object (stamped on the wire
copy), no $ref, oneOf, not or record, at most 16 union-typed and 24
optional properties. minLength, maxLength and pattern go out as written;
minItems above 1, maxItems, numeric bounds and a format outside the
vendor's list are stripped from the wire copy, and the caller's Zod schema
re-validates the answer with every constraint. The model answers JSON in its
text block; the format rides with adaptive thinking and beside a direct
caller's tools, with no beta header, and the vendor's union-typed and optional
property limits count a direct caller's strict: true tools with the format's
schema. A forced tool call whose input is the schema, sent without strict and
with tool_choice forced on it, carries a schema the format cannot carry (a
record, say) on every model that accepts a forced tool choice, Claude Sonnet
4.6 (the provider's default text model), Opus 4.5 to 4.8, Opus 5, Sonnet 4.5,
Sonnet 5, Haiku 4.5 and Haiku 5.5 among them. It carries every schema on Claude
Opus 4 and Claude Sonnet 4 (the May 2025 ids, retired on 2026-06-15), which have
no JSON output format. The call's input is the answer: it becomes the message's
JSON text and leaves tool_calls, so generateText()'s tool loop never runs it
as a tool. On a streamed call the text is the input as the model wrote it
(the streamed input deltas joined), so a key the model repeated reaches
generateObject()'s parse, which refuses it and asks again. A single-shot call
(streamCompletions: false) receives the input already parsed by the API, which
has kept only a repeated key's last value, and its text is that input written
out with JSON.stringify. A call to the tool a request forces by name (a
caller's toolChoice: { type: 'function', function: { name } }, as
StructuredOutputManager's function_calling strategy sends) keeps its
arguments as the stream delivered them, for the same reason; every other call's
arguments are its parsed input written out.
A passthrough customModelParams.tools array replaces the schema
tool, so it must hold a tool of the schema's name for the forced choice to go
out; without one the schema rides the prompt. A passthrough tool_choice keeps
precedence too: one under which the model need not call the schema tool
(auto, none, another tool's name) sends the schema in the prompt, and any
keeps the forced tool, since under any the model calls a tool and writes no
text: a call to the schema tool is the answer, and a call to another tool stays
a tool call. The format and prompt routes send no schema tool,
so a passthrough tool_choice they leave with no tool on the wire is dropped.
When neither route carries the schema (such a schema on Claude Opus 5.5, Sonnet
5.5, Fable or Mythos 5.1, which refuse a forced tool choice), and when a request
on the forced-tool route turns thinking on, which a forced tool choice cannot
ride beside, the provider puts the schema into the system prompt itself as one
block after the caller's ("Answer with JSON only, matching this schema: ...")
and warns once per model and reason. The schema tool is never sent clamped to
auto, and thinking moves only the forced-tool route: a request on the format
route keeps the format with thinking on. A request that enables citations
leaves the format, which the API refuses beside citations, for the forced tool,
or for the prompt on a model that refuses a forced choice. Every completion and
stream chunk of a schema request names the route it took (schemaRoute), and
one whose schema rode the prompt says schemaInPayload: false, so a completion
gateway lifts the answer from where the request put it. A cut-off answer
(max_tokens) keeps finishReason: 'length', even when its JSON parses. The
API sends none of the input of a forced call the limit stops (the single-shot
response gives it {}, the stream one empty input delta; probed on 2026-10-19
on Claude Sonnet 4.6 and Haiku 5.5), so the provider answers that call with no
text, never a {} that would pass for an answer under a schema whose fields
are all optional, and answers a forced call cut mid-JSON with its raw input as
the text, in one request.
generateObject() raises its output budget and asks again when the answer does
not parse or validate, and returns one that does with finishReason: 'length';
a completion gateway lifts nothing from a cut-off, and a session's send() on
the GMI runtime rejects naming the cut-off, whatever the JSON. Two cautions
from the vendor's page: a schema property that asks for the model's reasoning
may draw a refusal, and the capitalization of enum and const values is not
guaranteed, so generateObject() repairs an enum or literal value that differs
from the schema's only by case once before it spends a retry.
The Tool Choice column is what the provider does with the toolChoice option
('auto', 'none', 'required', or { type: 'function', function: { name } }).
| Provider | Structured Output | Tool Choice |
|---|---|---|
| OpenAI | json_schema with strict: true when the schema fits OpenAI's strict rules, otherwise JSON-object mode | Sent as tool_choice |
| Anthropic | The native JSON output format (output_config.format) on every model that has it and whose schema fits its limits; a non-strict forced tool call whose input is the schema for a schema the format cannot carry on a model that accepts a forced choice, and for every schema on Claude Opus 4 and Sonnet 4; the provider's own schema block in the system prompt when neither carries the schema, and on the forced-tool route when the request turns thinking on, a passthrough tools drops the schema tool, or a passthrough tool_choice of auto, none or another tool's name lets the model skip it (the format route keeps the format with thinking on) | 'required' is sent as any and a named function as tool; 'none' is sent as auto. A model that rejects a forced choice gets auto |
| Gemini | responseSchema with JSON output | Not sent; the API default applies |
| OpenRouter | Strict json_schema when the schema fits, otherwise JSON-object mode. When OpenRouter finds no endpoint that serves the strict schema (HTTP 404, "No endpoints found"), the request is sent once more in JSON-object mode with the schema written into its system prompt, unless the system prompt states a schema already | Sent as tool_choice; the routed model decides |
| Requesty | None | Sent as tool_choice; the routed model decides |
| Groq, Together, Mistral, xAI, LiteLLM | None | Sent as tool_choice; the vendor decides which values it accepts |
| Ollama | None | Not sent |
| Claude Code CLI, Gemini CLI | None | Written into the prompt as an instruction; 'none' leaves the tool schemas out |
Embedding Support
| Provider | Models | Dimensions | Batch Size |
|---|---|---|---|
| OpenAI | text-embedding-3-small, text-embedding-3-large | 256–3072 | 2048 |
| Gemini | gemini-embedding-001, gemini-embedding-2 | 3072 | 100 |
| Mistral | mistral-embed | 1024 | 512 |
| Ollama | nomic-embed-text, mxbai-embed-large | 768–1024 | 512 |
embedText() posts an OpenAI, OpenRouter, Mistral or Requesty call to that provider's own /embeddings endpoint, or to the base URL the caller set, so a key never goes to another provider's host. It refuses Anthropic, Groq, Together, xAI, MiniMax, the image providers and the CLIs before their key is read, and a provider id agentos does not know needs baseUrl. embedText() lists each provider's endpoint and the documentation it comes from.
Related Documentation
- Getting Started — Initial setup and configuration
- Cost Optimization — Budget management and routing
- Architecture — System architecture overview
- Structured Output — JSON schema enforcement per provider
- API Key Rotation: comma-separated key lists as a pool, the strategies, failover and probes, and which providers need a single key
- CLI Error Codes: the errors the Claude Code and Gemini CLI providers raise
Prompt caching
Provider-side prompt caching (Anthropic explicit breakpoints, OpenAI and Gemini automatic, OpenRouter forwarding) is on by default with per-call overrides — see Prompt Caching.