> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ziro-agent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Providers and models

> Choose an LLM provider and model globally, per agent, or per thread, including custom OpenAI- and Anthropic-compatible endpoints.

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

| Provider     | Adapter wraps                       | Key env var          | Model env var      |
| ------------ | ----------------------------------- | -------------------- | ------------------ |
| `openrouter` | OpenRouter chat completions         | `OPENROUTER_API_KEY` | `OPENROUTER_MODEL` |
| `anthropic`  | `langchain_anthropic.ChatAnthropic` | `ANTHROPIC_API_KEY`  | `ANTHROPIC_MODEL`  |
| `openai`     | `langchain_openai.ChatOpenAI`       | `OPENAI_API_KEY`     | `OPENAI_MODEL`     |

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.

<Warning>
  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.
</Warning>

## 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`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
ZIRO_LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_MODEL=claude-sonnet-4-20250514
```

An unrecognised value here degrades to `openrouter` rather than crashing at
launch. Cross-provider knobs use the `ZIRO_` prefix:

| Variable                | Effect                   |
| ----------------------- | ------------------------ |
| `ZIRO_LLM_PROVIDER`     | Default provider         |
| `ZIRO_REASONING_EFFORT` | Default reasoning effort |
| `ZIRO_MAX_RETRIES`      | Transport retry attempts |
| `ZIRO_RETRY_BASE_DELAY` | Retry backoff base       |

Each has an `OPENROUTER_*` back-compat fallback for older configurations.

## Per-agent pinning

An agent pins both axes in its `meta.yaml`:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Researcher
description: General research assistant - factual, source-driven, concise.
enabled: true
model: google/gemini-2.5-flash
provider: openrouter
```

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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
python -m app.cli.manage_agents set-model researcher anthropic/claude-sonnet-4
python -m app.cli.manage_agents set-model researcher none   # clear the pin
```

## 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:

<Steps>
  <Step title="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`.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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.

<Note>
  `/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.
</Note>

## 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`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "my-corp": {
    "style": "openai",
    "base_url": "https://llm.corp.example/v1",
    "headers": { "X-Org": "platform" },
    "models": ["corp-large", "corp-fast"]
  }
}
```

<ParamField path="style" type="&#x22;openai&#x22; | &#x22;anthropic&#x22;" required>
  Which native adapter to reuse. This is the wire protocol your endpoint speaks,
  not the vendor behind it.
</ParamField>

<ParamField path="base_url" type="string" required>
  Your endpoint's base URL.
</ParamField>

<ParamField path="headers" type="object">
  Extra headers sent on every request.
</ParamField>

<ParamField path="models" type="string[]">
  A typed fallback model list, used when the endpoint's live model list cannot be
  fetched.
</ParamField>

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:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
MY_CORP_API_KEY=sk-...
MY_CORP_MODEL=corp-large
```

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.

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.

<Tip>
  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`.
</Tip>

## 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:

| Tier             | Env var pattern                   | Typical use                      |
| ---------------- | --------------------------------- | -------------------------------- |
| `planning`       | `<PROVIDER>_PLANNING_MODEL`       | Decomposition, plan construction |
| `execution`      | `<PROVIDER>_EXECUTION_MODEL`      | The main working model           |
| `fast_execution` | `<PROVIDER>_FAST_EXECUTION_MODEL` | Cheap high-volume subtasks       |

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
OPENROUTER_PLANNING_MODEL=anthropic/claude-opus-4
OPENROUTER_EXECUTION_MODEL=anthropic/claude-sonnet-4
OPENROUTER_FAST_EXECUTION_MODEL=google/gemini-2.5-flash
```

A tier that is unset collapses to `<PROVIDER>_MODEL`, so partial configuration is
fine.

### 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.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# in a subagent's *.agent.md frontmatter, or an agent's meta.yaml
model: ${EXECUTION_MODEL}
```

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](/concepts/subagents) inherits the session's **active** provider. It
is never hardcoded to a global one. Its model candidates resolve in order:

<Steps>
  <Step title="The subagent's own pin">
    A `model:` in its `*.agent.md` frontmatter.
  </Step>

  <Step title="The parent's per-child default">
    `model_defaults[<agent_id>][<provider>]` in the parent's `subagent_policy.yaml`.
  </Step>

  <Step title="The parent's generic default">
    `model_defaults["default"][<provider>]`.
  </Step>

  <Step title="The tier fallback">
    `${EXECUTION_MODEL}` for the active provider.
  </Step>
</Steps>

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.

## Switching in chat

| Command                           | Effect                                                           |
| --------------------------------- | ---------------------------------------------------------------- |
| `/model`                          | List the active provider's models and switch; rebuilds the graph |
| `/settings`                       | Edit provider, API key, model, tier models, Langfuse, database   |
| `/think <high\|medium\|low\|off>` | Re-bind at a new reasoning effort                                |

`/think` warns rather than blocks when the current model does not advertise
reasoning support, so you are told without being stopped.

## Next steps

<Columns cols={3}>
  <Card title="Environment variables" icon="key" href="/reference/environment">
    Every provider and tier variable in one place.
  </Card>

  <Card title="Configuration" icon="settings" href="/guides/configuration">
    How `meta.yaml` resolves across config layers.
  </Card>

  <Card title="Compaction" icon="layers" href="/concepts/compaction">
    How model context windows drive the compaction budget.
  </Card>
</Columns>
