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.
# 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.
| # | Provider | Credential env var | Default model | Wire dialect |
|---|---|---|---|---|
| 1 | openrouter | OPENROUTER_API_KEY | moonshotai/kimi-k3 | OpenAI-compatible |
| 2 | zai | ZAI_API_KEY | glm-5.2 | OpenAI-compatible |
| 3 | anthropic | ANTHROPIC_API_KEY | claude-fable-5 | Anthropic Messages |
| 4 | openai | OPENAI_API_KEY | gpt-5.5 | OpenAI Responses |
| 5 | xai | XAI_API_KEY | grok-4.3 | OpenAI-compatible |
| 6 | deepseek | DEEPSEEK_API_KEY | deepseek-chat | OpenAI-compatible |
| 7 | gemini | GEMINI_API_KEY (alias GOOGLE_API_KEY) | gemini-3-pro | Gemini generateContent |
| 8 | vertex | VERTEX_ACCESS_TOKEN | gemini-3-pro | Gemini generateContent |
| 9 | bedrock | AWS_ACCESS_KEY_ID | us.anthropic.claude-sonnet-4-5-20250929-v1:0 | Bedrock Converse |
| — | local | LOCAL_API_KEY (optional) | none | OpenAI-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 / model | Context | In | Out | Cached in |
|---|---|---|---|---|
zai/glm-5.2 | 200k | 0.60 | 2.20 | 0.11 |
anthropic/claude-fable-5 | 1M | 10.00 | 50.00 | 1.00 |
anthropic/claude-opus-5 | 1M | 5.00 | 25.00 | 0.50 |
anthropic/claude-sonnet-5 | 1M | 3.00 | 15.00 | 0.30 |
openai/gpt-5.5 | 400k | 1.25 | 10.00 | 0.125 |
xai/grok-4.3 | 1M | 1.25 | 2.50 | 0.20 |
xai/grok-4 | 256k | 3.00 | 15.00 | 0.75 |
deepseek/deepseek-chat | 128k | 0.27 | 1.10 | 0.07 |
gemini/gemini-3-pro | 1M | 1.25 | 10.00 | 0.31 |
vertex/gemini-3-pro | 1M | 1.25 | 10.00 | 0.31 |
bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0 | 200k | 3.00 | 15.00 | 0.30 |
openrouter/moonshotai/kimi-k3 | 1M | 3.00 | 15.00 | 0.30 |
local/* | server's | 0 | 0 | — |
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:
| Provider | reasoning | effort | Notes |
|---|---|---|---|
anthropic | thinking: {type: adaptive} | output_config.effort, all five tiers 1:1 | Sampling params are never sent on Claude 4.6+ and the 5 family |
openai | reasoning.effort | low/medium/high; xhigh and max collapse to high | Also honours verbosity and service_tier |
gemini / vertex | thinkingConfig.thinkingLevel | low; everything else maps to high | reasoning: "off" gives the minimum level, not zero |
zai | thinking: {type: enabled|disabled} | dropped | To make a GLM verifier different, use prompt, not effort |
xai | reasoning_effort | low/medium/high; xhigh/max collapse | Gated off entirely on grok-4 and its dated snapshots — not on the grok-4.3 default |
deepseek | not sent | dropped | Shape it with prompt and params.max_tokens instead |
openrouter | reasoning object | all five tiers 1:1 | The gateway translates per routed vendor; an unadvertised tier is normalized, not rejected |
bedrock | legacy budget_tokens shape | dropped | Converse inferenceConfig only has slots for max_tokens, temperature, top_p |
local | not sent | dropped | No 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."
{
"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:
--api-key- The provider's env var, plus aliases
settings.jsonproviders.<id>.api_key~/.stella/credentials.toml- 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_envrenames the primary credential var; the original demotes to an alias and keeps working.providers.<id>.api_keyembeds the key literal inline.
{
"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.
{
"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:
| Param | Wire field | Values |
|---|---|---|
verbosity | text.verbosity | low, medium, high |
service_tier | service_tier | auto, 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 defaultAccess 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-1stella 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/v4Or set it permanently in user scope:
{
"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:
| Role | Model | Why |
|---|---|---|
| default | moonshotai/kimi-k3 | long-context agentic driver, at xhigh effort with thinking on |
| verifier | anthropic/claude-opus-5 | a different vendor's strongest reasoner — not the worker grading itself |
| triage | z-ai/glm-5.2 | fast 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 anthropicUse 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:
| Form | Written as | Sent on the wire |
|---|---|---|
Flat model field | openrouter/<vendor>/<slug> | <vendor>/<slug> |
| Per-agent pin | provider: 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 "…" # vLLMA 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:
{
"providers": {
"ollama": {
"base_url": "http://localhost:11434/v1",
"default_model": "llama3.3",
"api_key": "local"
}
}
}dialectdefaults toopenai-compatible.anthropic,gemini, andopenai-responsesare also available;vertexandbedrockare 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-gatewayreadsMY_GATEWAY_API_KEY).api_key_envnames 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
- Model guide — which model fits which seat
- Examples — ready-made lineups from max-quality to dirt-cheap
- Agent engine config — per-agent model, provider, and effort