Skip to main content
Ziro talks to LLMs through a provider registry. Three providers ship live, any OpenAI- or Anthropic-compatible endpoint can be registered as a custom provider, and provider and model always resolve together as one coupled pair.

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.
An unknown provider name fails fast with an error. There is no silent fallback to a default provider, because a silent fallback produces a confusing 404 from the wrong endpoint instead of a clear message.

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.
An unrecognised value here degrades to 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 its meta.yaml:
Omit 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.
In the TUI, switching provider mid-thread via /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).
"openai" | "anthropic"
required
Which native adapter to reuse. This is the wire protocol your endpoint speaks, not the vendor behind it.
string
required
Your endpoint’s base URL.
object
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.
string[]
A typed fallback model list, used when the endpoint’s live model list cannot be fetched.
The API key lives in the env file, never in the JSON. The variable name is derived from the label, uppercased with non-alphanumeric runs collapsed to underscores:
A custom label works everywhere a built-in does: in 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.
You do not have to write the JSON by hand. The setup wizard has an add custom provider flow that validates your input, writes the file, and persists the API key for you. Run ziro setup.

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:
Tiers can also be pinned in a 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:
A tier resolves in this order:
  1. the env var <PROVIDER>_<TIER>_MODEL;
  2. the most specific models.yaml tiers: pin;
  3. <PROVIDER>_MODEL, so partial configuration is fine.
For a custom provider label the env var stem is the label uppercased with every run of other characters turned into _: 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.
The token resolves against whichever provider is active at build time. That is what makes a subagent portable: the same definition runs on OpenRouter for you and on Anthropic for a colleague, each picking up their own tier configuration. Tiers are also reachable in the TUI under /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.
That produces a candidate list, not a single choice. If a call fails, Ziro fails over to the next candidate on a fresh thread. Recursion-limit errors are excluded from failover: a child stuck in a loop is a bug to surface, not a model to swap.

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 is off.
The spawn tool description lists the legal routes, with each model’s context size and whether it supports reasoning and vision (? when unknown):
Every route is re-checked at spawn, in order: it parses, the instance is credentialed now, policy allows it, the model meets the child’s 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):
A child can demand capabilities with 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 carry prompt_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:
The most specific layer wins, so a trusted project’s 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.