API Providers

Every provider stella speaks to — the matrix, the auto-detection order, the credential chain, and the differences that matter for each one.

stella is BYOK (bring your own key) and works with any vendor. There's no proxy in the middle, no markup, and each provider's native wire dialect is spoken directly. Switching vendors is just a --model flag.

Anthropic
The strongest coding and agentic models in the catalog, with first-class prompt caching.
id
anthropic
Env var
ANTHROPIC_API_KEY
Default
claude-fable-5
Dialect
Anthropic Messages
OpenAI
A strong worker and the usual second family for cross-family judging.
id
openai
Env var
OPENAI_API_KEY
Default
gpt-5.5
Dialect
OpenAI Responses
Google Gemini
Very large context windows at a low price — the long-document worker.
id
gemini
Env var
GEMINI_API_KEY (GOOGLE_API_KEY)
Default
gemini-3-pro
Dialect
Gemini generateContent
Google Vertex AI
The same Gemini models billed through your GCP project, for enterprises that require it.
id
vertex
Env var
VERTEX_ACCESS_TOKEN
Default
gemini-3-pro
Dialect
Gemini via Vertex
Amazon Bedrock
Claude and friends inside your AWS account, on your existing IAM and billing.
id
bedrock
Env var
AWS_ACCESS_KEY_ID
Default
us.anthropic.claude-sonnet-4-5-20250929-v1:0
Dialect
Bedrock Converse
xAI
Grok, over the OpenAI-compatible dialect.
id
xai
Env var
XAI_API_KEY
Default
grok-4.3
Dialect
OpenAI-compatible
DeepSeek
Very cheap per token — the reference budget worker.
id
deepseek
Env var
DEEPSEEK_API_KEY
Default
deepseek-chat
Dialect
OpenAI-compatible
Z.ai
GLM models, and a flat-rate coding plan that decouples cost from token count.
id
zai
Env var
ZAI_API_KEY
Default
glm-5.2
Dialect
OpenAI-compatible
OpenRouter
One key, hundreds of models — the gateway when you would rather not manage keys.
id
openrouter
Env var
OPENROUTER_API_KEY
Default
moonshotai/kimi-k3
Dialect
OpenAI-compatible
Local server
Ollama, llama.cpp, vLLM, LM Studio — anything that serves the OpenAI shape. No key, no egress.
id
local
Env var
none (optional LOCAL_API_KEY)
Default
you choose
Dialect
OpenAI-compatible
# The same task, three vendors — no config change, no restart.
stella --model anthropic/claude-fable-5  run "add a retry to the upload path"
stella --model openai/gpt-5.5            run "add a retry to the upload path"
stella --model deepseek/deepseek-chat    run "add a retry to the upload path"

The matrix

Detection rank is the order auto-detection tries providers in. The default model is what you get when you name none.

#ProviderCredential env varDefault modelWire dialect
1openrouterOPENROUTER_API_KEYmoonshotai/kimi-k3OpenAI-compatible
2zaiZAI_API_KEYglm-5.2OpenAI-compatible
3anthropicANTHROPIC_API_KEYclaude-fable-5Anthropic Messages
4openaiOPENAI_API_KEYgpt-5.5OpenAI Responses
5xaiXAI_API_KEYgrok-4.3OpenAI-compatible
6deepseekDEEPSEEK_API_KEYdeepseek-chatOpenAI-compatible
7geminiGEMINI_API_KEY (alias GOOGLE_API_KEY)gemini-3-proGemini generateContent
8vertexVERTEX_ACCESS_TOKENgemini-3-proGemini generateContent
9bedrockAWS_ACCESS_KEY_IDus.anthropic.claude-sonnet-4-5-20250929-v1:0Bedrock Converse
localLOCAL_API_KEY (optional)noneOpenAI-compatible

Catalog pricing

List prices come from the built-in seed catalog, in USD per million tokens. stella models refresh re-syncs them from models.dev.

Provider / modelContextInOutCached in
zai/glm-5.2200k0.602.200.11
anthropic/claude-fable-51M10.0050.001.00
anthropic/claude-opus-51M5.0025.000.50
anthropic/claude-sonnet-51M3.0015.000.30
openai/gpt-5.5400k1.2510.000.125
xai/grok-4.31M1.252.500.20
xai/grok-4256k3.0015.000.75
deepseek/deepseek-chat128k0.271.100.07
gemini/gemini-3-pro1M1.2510.000.31
vertex/gemini-3-pro1M1.2510.000.31
bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0200k3.0015.000.30
openrouter/moonshotai/kimi-k31M3.0015.000.30
local/*server's00

The model guide covers which model fits which seat.

Reasoning and effort support

reasoning and effort are set per agent in agent_engine_config. What actually reaches the wire differs by provider. If a provider can't express a parameter, stella drops it instead of failing the request:

ProviderreasoningeffortNotes
anthropicthinking: {type: adaptive}output_config.effort, all five tiers 1:1Sampling params are never sent on Claude 4.6+ and the 5 family
openaireasoning.effortlow/medium/high; xhigh and max collapse to highAlso honours verbosity and service_tier
gemini / vertexthinkingConfig.thinkingLevellow; everything else maps to highreasoning: "off" gives the minimum level, not zero
zaithinking: {type: enabled|disabled}droppedTo make a GLM verifier different, use prompt, not effort
xaireasoning_effortlow/medium/high; xhigh/max collapseGated off entirely on grok-4 and its dated snapshots — not on the grok-4.3 default
deepseeknot sentdroppedShape it with prompt and params.max_tokens instead
openrouterreasoning objectall five tiers 1:1The gateway translates per routed vendor; an unadvertised tier is normalized, not rejected
bedrocklegacy budget_tokens shapedroppedConverse inferenceConfig only has slots for max_tokens, temperature, top_p
localnot sentdroppedNo reasoning field works across every OpenAI-compatible server

Claude 4.6 and the 5 family answer any sampling parameter with an HTTP 400, so temperature, top_p, and top_k are always left out on anthropic. Turning reasoning off does not bring temperature control back. Claude 4.5 and earlier use the thinking: {type: enabled, budget_tokens: N} shape and do accept sampling. stella checks the model id and sends whichever shape that generation needs.

Auto-detection

With no --model and no configured default, stella picks the first provider in the matrix above that has a usable credential.

Both ends of that rank order come from what having a credential actually means.

OpenRouter sits first because OPENROUTER_API_KEY is specific to that gateway. Nobody has one exported by accident, and it reaches every other vendor in the matrix. An instance holding one has already decided how it wants to route, so stella gives it a whole default engine posture, not just a default model.

Vertex and Bedrock sit last for the opposite reason: they key off generic cloud credentials (VERTEX_ACCESS_TOKEN, AWS_ACCESS_KEY_ID) that are often exported for unrelated reasons. Generic cloud credentials shouldn't hijack detection — pin them with --model vertex/… or --model bedrock/…. The local pseudo-provider is never auto-detected.

A settings-configured default_model sits between the --model flag and auto-detection: an explicit flag wins, but a configured default beats "first provider with a key."

~/.stella/settings.json
{
  "agent_engine_config": {
    "default_model": "anthropic/claude-fable-5"
  }
}

The credential chain

For the selected provider, the API key resolves in this order — first hit wins:

The credential chain1--api-key flag (needs an explicit --model)2the provider's env var, plus its aliases3settings.json — providers.<id>.api_key4~/.stella/credentials.toml5interactive prompt — saved, so it asks oncekey resolvedfirst hit wins — the rest are skippednothing below the first hit is ever read
  1. --api-key
  2. The provider's env var, plus aliases
  3. settings.json providers.<id>.api_key
  4. ~/.stella/credentials.toml
  5. An interactive prompt on a TTY — the entered key is saved, so you are prompted once

stella models prints which link answered for each provider (env:ANTHROPIC_API_KEY, a dotenv filename, credentials.toml, or settings.json).

Two settings.json fields reroute steps 2 and 3:

  • providers.<id>.api_key_env renames the primary credential var; the original demotes to an alias and keeps working.
  • providers.<id>.api_key embeds the key literal inline.
~/.stella/settings.json
{
  "providers": {
    "anthropic": { "api_key_env": "MY_TEAM_ANTHROPIC_KEY" }
  }
}

In a project's .stella/settings.json, the credential-routing fields — base_url, api_key, api_key_env — are ignored (with a stderr notice) unless STELLA_TRUST_PROJECT=1. A cloned repo must not redirect where your key is sent. User scope always applies. See the project trust boundary. Prefer an env var or credentials.toml over --api-key, which is visible in shell history and ps.

Custom base URLs

Every built-in provider accepts a base_url override for a gateway or proxy. The wire dialect does not change with it.

~/.stella/settings.json
{
  "providers": {
    "anthropic": { "base_url": "https://llm-gateway.internal.example.com" }
  }
}

--base-url (env form STELLA_BASE_URL) does the same for one run and outranks the settings value.

Per-provider differences

Everything above is shared. These are the differences worth knowing.

Anthropic

Prompt caching works fully here: stella meters both cache reads and cache writes, so Observatory figures reflect the true bill. Cached input bills at $0.30/Mtok against a $3.00 list price, a 10x discount on the stable prefix the context engine maintains.

The cache window is the one setting here you may want to change. Anthropic holds a written prefix for 5 minutes by default and offers a 1-hour window per request. stella picks based on the surface: stella chat and stella resume ask for the hour, every headless run keeps the 5-minute default. The reason is cost — a 1-hour write bills at 2x the input rate against the short window's 1.25x, and that premium only pays off when turns are far enough apart to lose the prefix. An eviction is the expensive event: it re-bills the whole prefix at the write rate and loses the 10x read discount on it. Interactive turns sit minutes apart, so the wider window is close to free insurance; a headless run's calls are seconds apart and would pay the premium for a prefix that was never going to expire. Override either default with providers.anthropic.cache_ttl ("5m" or "1h").

Server-side context editing is off, and there is no setting for it. The Messages API can drop old tool results and thinking blocks from a chat before the model reads it. stella can ask for that, and does not: the trigger that decides when clearing starts has never been measured, and each edit throws away the cached prefix from the point of the edit. A cache read bills at $0.30/Mtok against a write at $3.75/Mtok, so a trigger picked by taste moves the bill more than tenfold in the direction you did not want. The decision is recorded in ADR 0026, and it holds until a panel measures the number.

params.max_tokens defaults to 32000 with thinking on, 4096 with it off. Adaptive thinking spends from the same output allowance as the answer, so without this, a max-effort verifier could get cut off mid-verdict. A value you set is used exactly as given.

OpenAI

Two parameters only this provider puts on the wire:

ParamWire fieldValues
verbositytext.verbositylow, medium, high
service_tierservice_tierauto, default, flex, priority

Every other adapter drops both silently, so they're only worth setting on an OpenAI-routed agent. The built-in openai dialect is fixed to OpenAI Responses, so a custom base_url must speak it too (Azure's /openai/v1 surface does). For a gateway that only speaks the older chat completions format, define a custom provider id instead.

Google Gemini

The 1M-token window is the reason to reach for it: whole-repo comprehension, long-log analysis, giant-diff review. Give it a task that needs that much room:

stella --model gemini/gemini-3-pro run \
  "read every module under src/ingest and write a design note on where retries are handled inconsistently"

Google Vertex AI

Same models, same dialect as Gemini. The difference is governance: inference runs inside your GCP project, billed to your account, under your own IAM and data-residency controls.

export VERTEX_ACCESS_TOKEN=$(gcloud auth print-access-token)
export VERTEX_PROJECT_ID=my-gcp-project        # or GOOGLE_CLOUD_PROJECT
export VERTEX_LOCATION=global                  # optional; this is the default

Access tokens expire after about an hour. Wrap the export so a fresh one is a keystroke away:

# ~/.zshrc or ~/.bashrc
stella-vertex() {
  VERTEX_ACCESS_TOKEN="$(gcloud auth print-access-token)" stella "$@"
}

Amazon Bedrock

export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...        # STS/SSO-derived credentials only
export AWS_REGION=us-east-1         # or AWS_DEFAULT_REGION; default us-east-1

stella reads only these environment variables — it does not walk the AWS credential chain. AWS_PROFILE, ~/.aws/credentials, SSO caches, and IMDS/instance roles are never checked. On a profile or SSO setup, turn those into env vars first: eval "$(aws configure export-credentials --profile dev --format env)".

The default model is a cross-region inference profile, not a bare model id: Bedrock rejects on-demand calls to newer Anthropic models without one. The real host is region-scoped (bedrock-runtime.<AWS_REGION>.amazonaws.com) and built per request.

xAI

The seeded default is grok-4.3: xAI's flagship on its published model list, with the largest context in the current lineup and its lowest price.

grok-4.3 prices in two tiers — the catalog carries the below-200k-input figures, so a turn that crosses that boundary is costed low rather than high. grok-4.6 is a newer model for coding and agentic work, at roughly 1.6x the input price and 2.4x the output price; pin it under providers.xai.default_model if that trade is worth it to you.

A providers.xai.default_model pinned to grok-4 still resolves — the catalog keeps the row so an existing config does not hard-error — but that model isn't served by the vendor, and its seeded prices are stale. It is also the one xAI model stella never sends effort to, because it answers the parameter with a 400. Repin it to a current model.

DeepSeek

The lowest price in the catalog among hosted models, and a natural choice for triage in any lineup. deepseek-chat is the non-thinking model; deepseek-reasoner is the reasoning one.

Z.ai

A GLM coding plan puts your quota on a dedicated endpoint — https://api.z.ai/api/coding/paas/v4 instead of the pay-per-token https://api.z.ai/api/paas/v4. Same key, same models, a flat subscription instead of a meter.

export ZAI_API_KEY="..."
export ZAI_GLM_CODING_PLAN=1
stella config    # Base URL must read https://api.z.ai/api/coding/paas/v4

Or set it permanently in user scope:

~/.stella/settings.json
{
  "providers": {
    "zai": { "base_url": "https://api.z.ai/api/coding/paas/v4" }
  }
}

Endpoint precedence, highest first: --base-url / STELLA_BASE_URL > ZAI_GLM_CODING_PLAN=1 > settings.json base_url > the built-in default. A stray STELLA_BASE_URL routes you off the plan without warning, and a leftover ZAI_GLM_CODING_PLAN=1 overrides the settings file. Don't set both. In a project's .stella/settings.json, base_url needs STELLA_TRUST_PROJECT=1 — a subscription is personal, so put it in user scope instead.

On a coding plan the $ figures in stella stats are notional — stella prices tokens at catalog list rates, but you pay the flat subscription. Token counts, step counts, and cross-model $/resolved comparisons stay meaningful; absolute dollars for zai/* do not. --spend-limit also caps against those notional prices.

OpenRouter

One key, every vendor. Gateway slugs are vendor/model and pass through unseeded: any slug the gateway serves works exactly as written, and a typo fails with OpenRouter's own 400/404 error instead of a stella error. The default model is moonshotai/kimi-k3.

The default posture

One key reaches every vendor, so on OpenRouter the interesting question isn't which model, but which model for which role, since each role wants genuinely different things. With no configuration at all, an OpenRouter instance runs:

RoleModelWhy
defaultmoonshotai/kimi-k3long-context agentic driver, at xhigh effort with thinking on
verifieranthropic/claude-opus-5a different vendor's strongest reasoner — not the worker grading itself
triagez-ai/glm-5.2fast and cheap, and it runs on every turn

Note the verifier is a different family from the driver. stella already treats cross-family judging as the goal (see auto_mode) — this default just does that without asking you to configure anything.

This is a starting point, not a lock. It sits underneath the whole agent_engine_config scope chain, so any field you set is yours — set default_model and the session model is whatever you said, while the rest of the posture stays. It also applies only when OpenRouter is the provider that actually got picked: run --model anthropic/… with an OpenRouter key present and you get an Anthropic session, with no part of this routed through the gateway.

A router that picks a different backing model per request is the one thing a coding agent can't handle well: prompt-cache prefixes stop hitting between turns, tool-call dialect drifts mid-session, and no two turns are comparable when something breaks. That's why the default here is a fixed model rather than the gateway's own meta-router, openrouter/auto. It still works if you pin it directly.

Pinning the upstream

A gateway is a router, not a vendor: OpenRouter picks the upstream that actually serves a request based on app identity, and it is free to choose a different one between two calls to the same slug. In one test, a probe carrying stella's own attribution asked for anthropic/claude-sonnet-5 and was served by provider: Amazon Bedrock. This is invisible in the ordinary case: you asked for a model family, and you got it. But it means the model provider behind an OpenRouter-routed run is not something a bare --model pins.

--upstream-pin <vendor> (env STELLA_UPSTREAM_PIN, repeatable or comma-separated) fixes it: the request carries provider: {order: [...], allow_fallbacks: false}, so OpenRouter either serves it from the named vendor, in the order given, or the call fails rather than silently routing elsewhere.

stella run "…" --model openrouter/anthropic/claude-fable-5 \
  --upstream-pin z-ai --upstream-pin anthropic

Use this whenever two runs need to be the same experiment — a before/after comparison, a head-to-head against another agent, or anything where "which vendor actually answered" needs to stay fixed. See the terminal-bench head-to-head guide for an example. This does nothing on any provider that is not a gateway — a direct anthropic/… or openai/… session has no routing step to pin.

--upstream-pin is a flag, not a settings.json field, on purpose: a benchmark harness runs with settings isolation (STELLA_NO_SETTINGS), so a settings-defined pin would never reach a measured trial. A command-line argument is the only thing that survives that isolation — the same reason --base-url is a flag.

Selecting models

stella --model openrouter/moonshotai/kimi-k3       run "tidy the changelog"
stella --model openrouter/openai/gpt-5.5           run "review this diff as a strict verifier"
stella --model openrouter/anthropic/claude-fable-5 run "refactor the scheduler"
stella --model openrouter/openrouter/auto          run "let the gateway pick per request"

The doubled prefix on the openrouter/openrouter/auto line is correct. --model splits on the first /: everything before is the provider id, everything after is the wire slug sent exactly as written. Every OpenRouter model id is vendor/model, and the meta-router's own id is openrouter/auto — so the full spec is openrouter + / + openrouter/auto. Write --model openrouter/auto instead, and the wire slug becomes a bare auto, which OpenRouter does not serve; stella warns on stderr that it is missing the vendor namespace, but the warning is informational only, and the run still fails at the gateway.

Two routing forms:

FormWritten asSent on the wire
Flat model fieldopenrouter/<vendor>/<slug><vendor>/<slug>
Per-agent pinprovider: openrouter · model: <vendor>/<slug><vendor>/<slug>

With provider set, model goes to that provider exactly as written, with no splitting — which is what lets an OpenRouter slug contain a / of its own.

Verifier-diversity logic does not treat "openrouter" as one family: an OpenRouter spec's family is the routed slug's vendor prefix, so openrouter/openai/gpt-5.5 counts as OpenAI, and cross-family preference works through the gateway too.

Cost telemetry comes from the gateway, not the catalog. Gateway slugs are unseeded, so there is no local list price. stella requests OpenRouter's usage accounting on every call, sends app-attribution headers, and treats the gateway's reported per-call cost as the source of truth. If a call's final usage frame carries no cost, that call falls back to $0; treat the OpenRouter dashboard as the record to reconcile against. You also add a hop this way: traffic goes through OpenRouter's servers rather than to each vendor directly.

Local servers

Any OpenAI-compatible endpoint you run yourself: Ollama, vLLM, LM Studio, llama.cpp. Never auto-detected — --base-url (or STELLA_BASE_URL) is required every time.

stella --model local/llama3.3      --base-url http://localhost:11434/v1 chat  # Ollama
stella --model local/qwen2.5-coder --base-url http://localhost:8000/v1 run "…"  # vLLM

A bearer token is always sent (the placeholder local when LOCAL_API_KEY is unset), so key-checking servers fail loudly instead of mysteriously. Local slugs skip catalog validation: any model name your server knows is accepted.

The id local is reserved and cannot be redefined in settings.json, which is why --base-url is always required with it. To bake an endpoint into config instead, define your own provider under a non-reserved id:

~/.stella/settings.json
{
  "providers": {
    "ollama": {
      "base_url": "http://localhost:11434/v1",
      "default_model": "llama3.3",
      "api_key": "local"
    }
  }
}
  • dialect defaults to openai-compatible. anthropic, gemini, and openai-responses are also available; vertex and bedrock are rejected for custom ids, because those dialects need credential resolution a settings entry cannot express.
  • A config-defined provider reads its key from a derived env var: the id uppercased with anything outside [A-Za-z0-9] folded to _, plus _API_KEY (my-gateway reads MY_GATEWAY_API_KEY). api_key_env names a different one.
  • It auto-detects (after every built-in) only when it has a default_model.

Local models have no catalog prices, so stella stats dollar columns read zero. Tokens, steps, and resolve-rate comparisons still work; --spend-limit has nothing to meter against.

Next