The built-in providers
Each provider reads its own live source of truth for capabilities (OpenRouter’s
/models, Anthropic’s /v1/models, OpenAI’s /v1/models), cached on disk with
a 24-hour TTL. So context windows, max output tokens, and reasoning support stay
current without a Ziro release. A catalog miss stays conservative rather than
guessing wrong.
Global default
ZIRO_LLM_PROVIDER sets the provider an agent falls back to when it has no pin
of its own. It defaults to openrouter.
openrouter rather than crashing at
launch. Cross-provider knobs use the ZIRO_ prefix:
Each has an
OPENROUTER_* back-compat fallback for older configurations.
Per-agent pinning
An agent pins both axes in itsmeta.yaml:
provider to use ZIRO_LLM_PROVIDER. Omit model (or set it to null)
to use that provider’s env default model.
Change a pin without editing YAML:
The coupling rule
Provider and model are one coupled pair, never two independent axes. Treating them independently is what produces a provider switch that 404s: you keep the old provider’s model id and point it at the new provider’s endpoint. Resolution works like this:1
An explicit model choice wins, and carries its provider
If you pick a model directly, its provider comes with it. A heuristic
backstop infers the provider from the id shape when needed: an id containing
/ implies openrouter, a claude-* id implies anthropic.2
Otherwise, provider first
The provider decides. The agent’s pinned model is honoured only if it is
compatible with that provider; otherwise the provider’s default model is
used.
/model or /settings overrides
the agent’s pinned model for that thread, persists to threads.json, and is
restored when you resume the thread.
/model lists the active provider’s catalog, not a merged list across all
providers. Switch provider first if you want to see another provider’s models.Custom providers
Any OpenAI- or Anthropic-compatible endpoint becomes a first-class provider by adding a label to~/.ziro/custom_providers.json. Each label reuses a native
adapter, pointed at your base_url.
The file lives in the current home: with ZIRO_HOME set, Ziro reads
$ZIRO_HOME/custom_providers.json and ignores ~/.ziro/custom_providers.json, so
copy an existing file into that home to keep its labels (and the matching
<LABEL>_API_KEY into that home’s .env).
Which native adapter to reuse. This is the wire protocol your endpoint speaks,
not the vendor behind it.
Extra headers sent on every request. A value holding
{thread_id} (or
{session_id}) is filled per request with the raw thread id, not a hash, so it
can carry local names such as job::<user>::... or a subagent’s agent id.meta.yaml’s provider:,
as ZIRO_LLM_PROVIDER, and in the provider picker. Ziro fetches the endpoint’s
live model list (/models for openai-style, /v1/models for anthropic-style),
caches it for 24 hours, and falls back to your models list if unreachable.
Other capabilities stay permissive: your endpoint decides.
A local endpoint that needs no key (Ollama, vLLM) still needs a placeholder such
as OLLAMA_API_KEY=unused: a subagent route reaches a custom
label only while its <LABEL>_API_KEY is set.
The whole mechanism is tolerant by design. A missing or malformed file, a bad
entry, or a label that collides with a built-in name is skipped with a warning.
It is never fatal.
Model tiers
Different jobs deserve different models. Rather than pinning one model per agent and living with it, Ziro defines three role-based tiers, each resolved per provider:models.yaml file in any config layer (your
ZIRO_HOME or a project’s .ziro/). Layers merge per provider and tier, so a
project can pin one tier and inherit the rest:
- the env var
<PROVIDER>_<TIER>_MODEL; - the most specific
models.yamltiers:pin; <PROVIDER>_MODEL, so partial configuration is fine.
_: openrouter-team reads
OPENROUTER_TEAM_PLANNING_MODEL. The older name that kept the label as is
(OPENROUTER-TEAM_PLANNING_MODEL) is not read; ziro logs a warning naming the
one to use. /model info shows each tier’s model and
where it came from (env, config:<layer> or default).
A project you have not trusted cannot pin tiers: its tiers: section is ignored
(and ziro config doctor warns), so a cloned repo cannot point a tier at any
model.
The ${TIER_MODEL} token
Anywhere a model: field is accepted, you can write a tier token instead of a
literal model id. It must be the whole value, not embedded in a longer string.
/settings → Planning / Execution /
Fast Execution Model, prefilled from your env or picked from the live provider
catalog.
Subagent model resolution
A subagent inherits the session’s active provider. It is never hardcoded to a global one. Its model candidates resolve in order:1
The subagent's own pin
A
model: in its *.agent.md frontmatter.2
The parent's per-child default
model_defaults[<agent_id>][<provider>] in the parent’s subagents section.3
The parent's generic default
model_defaults["default"][<provider>].4
The tier fallback
${EXECUTION_MODEL} for the active provider.Subagent routes
A parent can choose where a child runs with one value,route="instance:tier",
plus an optional effort:
- instance is a provider label configured in the current home whose API key
is set: a built-in, or a custom provider (see the placeholder key above for
keyless endpoints). A bare tier (
route="planning") means that tier on the parent’s own provider. - tier is one of
planning,execution,fast_execution. The model is always the tier’s model on that instance (see Model tiers), so provider and model resolve as one pair. A model id where a tier belongs, or a third segment, is refused. - effort is one of
high,medium,low,off,xhigh,max. It is refused on a model known not to support reasoning, unless it isoff.
? when unknown):
requires:, the
model supports tools, and the effort is valid. A failure returns a refusal naming
the check and listing the legal routes; a route is never rewritten silently.
Routing changes the model only, never the child’s tools or permissions.
Limit routes per parent in the subagents section (subagent_policy.yaml):
requires: in its *.agent.md
frontmatter (vision, reasoning, long_context). Routes whose model lacks one
are hidden from the menu and refused, and the child’s default model is checked
too. An unknown capability never satisfies a requirement.
A literal model id in a subagent
model: field or in model_defaults still
runs, but logs a deprecation warning and shows in ziro doctor. Use a tier
token and pin the tier in models.yaml instead. Per-provider model_defaults
keys stay readable and are deprecated.Prompt cache routing
OpenAI requests carryprompt_cache_key and OpenRouter requests x-session-id,
so the provider routes a thread’s calls to the same cache. The value is a salted
hash of the thread (a fork shares its origin’s), never the raw id. Turn both off
in models.yaml:
routing_hints: true turns
the hints back on over your user-level false. An untrusted project’s cache:
section is ignored. {thread_id} header templates (see Custom
providers) are separate: they send the raw id and this switch
does not turn them off.
Switching in chat
/think warns rather than blocks when the current model does not advertise
reasoning support, so you are told without being stopped.
Next steps
Environment variables
Every provider and tier variable in one place.
Configuration
How
meta.yaml resolves across config layers.Compaction
How model context windows drive the compaction budget.