# Providers and models

> How Fermix picks which AI provider and model to use, switches to a backup when one fails, and lets you set the model, how hard it thinks, and per-route overrides.

Fermix can talk to seven AI providers (the companies that host the language models, such as OpenAI and Anthropic). One is marked primary; the rest form an automatic fallback chain, so if the primary fails Fermix tries the next one. All provider details live in a single built-in list (`Providers.Descriptor`) so routing, setup, readiness, doctor, and telemetry (Fermix's internal metrics) never maintain separate provider lists.

## Providers

The canonical provider order is also the fallback order. Ollama is always last as the local last-resort hop.

| Provider atom | Label | Auth mode(s) | Adapter | Reasoning effort |
|---|---|---|---|---|
| `openai_codex` | OpenAI Codex (ChatGPT) | OAuth only | `OpenAI.Codex` | yes |
| `openai` | OpenAI | API key | `:routed` (Responses or ChatCompletions) | yes |
| `anthropic` | Anthropic | API key or OAuth | `Anthropic.Messages` | yes |
| `xai` | SpaceXAI | API key or OAuth | `XAI.Responses` | yes |
| `openrouter` | OpenRouter | API key | `OpenAI.ChatCompletions` | no |
| `mistral` | Mistral | API key | `OpenAI.ChatCompletions` | no |
| `ollama` | Ollama | None (keyless) | `OpenAI.ChatCompletions` | no |

**OpenAI (`:routed`):** the model slug and base URL determine whether the Responses or ChatCompletions adapter is used. You do not configure this split directly.

**Anthropic OAuth:** connects via a Claude subscription. Log in with `fermix auth login --provider anthropic`, passing one of `--setup-token TOKEN` (a token from `claude setup-token`), `--import-claude-code` (reuse an existing Claude Code login), or the `CLAUDE_CODE_OAUTH_TOKEN` environment variable. A stored token is inert until `auth_mode = "oauth"` is set in the provider's config block — and logging in (CLI or web setup) sets that for you, while `fermix auth logout` reverts it to `api_key`. The change applies after a daemon restart.

**xAI OAuth:** connects via a Grok Build subscription. Activate with `fermix auth login --provider xai` (add `--no-browser` to print the URL instead of opening a browser). Logging in sets `auth_mode = "oauth"` the same way. A 403 on the OAuth route means the Grok plan lacks API access — a plan limitation, not a stale token, so Fermix reports it instead of retrying or failing over. xAI is displayed as SpaceXAI in Fermix's user-facing labels; the provider atom (`xai`), `XAI_API_KEY`, the api.x.ai host, and the `grok-*` model ids are unchanged.

**OpenRouter:** uses vendor-prefixed model ids (e.g. `anthropic/claude-sonnet-4.6`, `openai/gpt-5.5`). The setup page checks OpenRouter's live model list, filtered to models that support tool calls; the model field accepts any slug (a model's short text id). Calls carry static attribution headers, so they show up labeled as Fermix in your OpenRouter dashboard.

**Mistral:** rolling `-latest` aliases (`mistral-large-latest`, `mistral-medium-latest`, `mistral-small-latest`) always resolve to the current build. No reasoning-effort field is sent.

**Ollama:** keyless (it runs models on your own machine, so no API key is needed). Counts as configured when a `base_url` is present in the provider block (default `http://localhost:11434/v1`). When the server is reachable, the setup page lists your locally installed models to pick from. The doctor probe checks the served `num_ctx` (the context window Ollama is actually serving) against the catalog window via the native `/api/show` endpoint. A 300-second receive timeout applies to local inference.

Credential setup is handled by `fermix setup` or the web setup page at `http://127.0.0.1:4030/setup`. An OAuth token left idle until it is well past expiry is flagged before its next use: the setup provider page shows a "Reconnect needed" badge, and `fermix doctor` runs an offline `auth tokens` check that names the stale profiles. See [auth and secrets](/docs/auth-and-secrets) and [configuration](/docs/configuration).

## OpenAI Codex OAuth

Codex uses ChatGPT OAuth (the same ChatGPT account used by the Codex CLI, but Fermix runs its own authorization grant to avoid colliding with the CLI's refresh chain).

- `fermix auth login` (or `fermix auth login --provider openai_codex`) opens a browser to `https://auth.openai.com/oauth/authorize` with PKCE (a standard way to keep the login exchange secure).
- A loopback listener (a small local-only web server reachable only from your own machine) at `127.0.0.1:1455` captures the auth code.
- Tokens land in `~/.fermix/auth.json` (mode `0600` enforced).
- `Auth.TokenManager` schedules a proactive refresh a few minutes before each token expires — one timer per token, so even an idle daemon keeps its token warm — and always refreshes from the latest token persisted on disk, so a refresh token rotated by another process is never reused. It runs whenever Codex is routable — as the primary or as a configured fallback. Anthropic and xAI OAuth profiles get their own per-profile token managers, started on demand.
- `fast = true` in `[fermix_core.providers.openai_codex]` sends requests at the priority service tier (faster scheduling on ChatGPT plans that include it). This knob exists only for Codex.

Realtime voice always uses the OpenAI API key even when the text provider is `openai_codex`. See [realtime voice](/docs/realtime-voice).

Image generation can also run on the Codex subscription instead of an image API key; see [configuration](/docs/configuration).

## Model catalog

The catalog is static and ships with each release. The first entry per provider is the setup default. Custom slugs not in the catalog are accepted; the doctor probe validates them at boot.

**OpenAI (API key)**

Within the GPT-5.6 generation, Sol is the frontier model, Terra is balanced, and Luna is the fast, cheaper option.

| Model | Context window | Default |
|---|---|---|
| `gpt-5.6-sol` | 272,000 | yes |
| `gpt-5.6-terra` | 272,000 | |
| `gpt-5.6-luna` | 272,000 | |
| `gpt-5.5` | 1,050,000 | |
| `gpt-5.4` | 1,050,000 | |
| `gpt-5.4-mini` | 400,000 | |

**OpenAI Codex (OAuth)**

Same model slugs as OpenAI direct. The GPT-5.6 models serve the same 272,000-token window on both access paths; the older models are capped at 400,000 tokens on the Codex path.

| Model | Context window | Default |
|---|---|---|
| `gpt-5.6-sol` | 272,000 | yes |
| `gpt-5.6-terra` | 272,000 | |
| `gpt-5.6-luna` | 272,000 | |
| `gpt-5.5` | 400,000 | |
| `gpt-5.4` | 400,000 | |
| `gpt-5.4-mini` | 400,000 | |

**Anthropic**

| Model | Context window | Default |
|---|---|---|
| `claude-sonnet-4-6` | 1,000,000 | yes |
| `claude-fable-5` | 1,000,000 | |
| `claude-opus-4-8` | 1,000,000 | |
| `claude-haiku-4-5` | 200,000 | |

**xAI**

| Model | Context window | Default |
|---|---|---|
| `grok-4.5` | 1,000,000 | yes |
| `grok-4.3` | 1,000,000 | |
| `grok-4.20-0309-reasoning` | 256,000 | |
| `grok-4.20-0309-non-reasoning` | 256,000 | |
| `grok-code-fast-1` | 256,000 | |

**OpenRouter** (curated defaults; live catalog available in setup)

| Model | Context window | Default |
|---|---|---|
| `anthropic/claude-sonnet-4.6` | 1,000,000 | yes |
| `anthropic/claude-fable-5` | 1,000,000 | |
| `anthropic/claude-opus-4.8` | 1,000,000 | |
| `openai/gpt-5.5` | 1,050,000 | |
| `x-ai/grok-4.3` | 1,000,000 | |

**Mistral**

| Model | Context window | Default |
|---|---|---|
| `mistral-large-latest` | 128,000 | yes |
| `mistral-medium-latest` | 128,000 | |
| `mistral-small-latest` | 128,000 | |

**Ollama** (catalog defaults; served window depends on local `num_ctx`)

| Model | Context window | Default |
|---|---|---|
| `qwen3:32b` | 128,000 | yes |
| `gpt-oss:20b` | 128,000 | |
| `llama3.3:70b` | 128,000 | |

Models not in the catalog default to a context window of 100,000 tokens. A `[:fermix, :model_catalog, :unknown_model]` telemetry event is emitted on each such lookup.

## Vision and image input

Inbound images (from media-capable channels) and tool results that return images (e.g. a `browser` screenshot action) are forwarded to the model as image content blocks. Provider support:

| Provider | Vision support |
|---|---|
| `openai` / `openai_codex` | Yes (Responses/Codex wire) |
| `anthropic` | Yes |
| `xai` | Yes (Grok) |
| `openrouter` | Model-dependent |
| `ollama` | Model-dependent |
| `mistral` | Model-dependent |

Each provider encodes image bytes at its own edge from a common neutral content part. The text-only request shape is unchanged on text-only turns, so prompt caching is unaffected. A turn that routes an image-carrying message to a non-vision route fails loud rather than silently dropping the image — and that error is failover-eligible, so the fallback chain can move on to a vision-capable route.

## Descriptor registry

`Providers.Descriptor` is the single source of truth for every provider. It carries each provider's label, adapter module, default base URL, supported auth modes, secret keys, config block keys, setup fields, and whether it supports reasoning effort. Routing, selection, config validation, the setup wizard, the web setup page, doctor, readiness, and health all derive from this registry. Adding a provider is a registry entry plus an adapter.

## Provider selection and fallback

Each `[fermix_core.providers.<name>]` block carries a `primary = true|false` flag, and exactly one provider may be primary. If more than one block sets `primary = true`, Fermix refuses to route and asks you to mark exactly one provider primary rather than silently picking. During initial setup the first provider you configure is promoted to primary automatically; after that, newly configured providers join as fallbacks, and promotion is an explicit choice — the "Set primary" button on a provider card in web setup, or picking the provider in `fermix setup` — which flips the flag without re-entering credentials (restart to apply).

`Providers.Selection` builds the ordered route list as `[primary | configured fallbacks]`, where fallbacks appear in `Descriptor` order (the canonical fallback order). A provider is "configured" when its selected auth mode has usable credentials:

- `:api_key` providers: non-empty `api_key` in the provider block.
- `:oauth` providers: a usable access token in the auth store that is not in `reauthorization_required` state.
- `:none` (Ollama): a non-empty `base_url` in the provider block.

If the primary is not configured but at least one fallback is, Fermix logs a warning and routes to the fallbacks instead of failing the turn. If there are no fallbacks, the primary is kept so the turn fails with a clear auth error that readiness points to.

Failover between providers in the chain is handled by `Providers.Failover.run_chain/3`. Failover applies only to the turn's initial model call and only for transient errors: timeout, transport failure, 5xx responses, rate-limit, quota exhaustion, or a residual OAuth auth failure after the adapter's own refresh and retry. API-key auth failures, context-length errors, tool errors, mid-tool-loop (`continue`) errors, and mid-stream failures after visible streamed content are not retried on another provider. A `[:fermix, :provider, :failover]` telemetry event is emitted on each transition.

### Same-provider transient retry

The initial model call also gets a **bounded same-provider retry**: up to 3 retries per route on top of the initial call (4 attempts total), with short exponential backoff (250/500/1000 ms), classified by `Providers.Transient`. It fires in exactly two cases:

- **`connection_unavailable`** (the pool-checkout / wake-from-sleep race): no connection could be obtained at all, which is true for every provider at once — so this error always retries on the same route and never fails over (sweeping the chain would only burn doomed attempts).
- **Any other retryable transient error** (transport timeout/close/network, provider timeout or 5xx) when the failing route is the **last one in the chain** — including the common single-provider setup, which has nothing to fall over to. While fallback routes remain, these errors fail over immediately instead of retrying in place.

Rate-limit and quota errors are deliberately never retried on the same provider — an immediate retry would just hit the same limit again. Scheduled jobs opt out of this inner retry and keep their own coarser, deadline-bounded backoff (so a slow provider `:timeout` can never push a run past its configured job timeout).

Continuation calls — the ones that carry tool results back to the model inside the tool loop — get their own, narrower retry. Fermix re-issues the failed call on the same route up to twice (2s then 4s backoff), and only for a timeout the adapter measured as having received zero response bytes. Nothing is replayed: no tool runs a second time and no provider switch happens. Every other continuation error surfaces on the first failure. Unlike the initial-call retry above, this one applies to scheduled runs too.

### Friendly usage-limit reply

A genuine rate-limit / quota error whose body carries a reset time (`resets_at` / `plan_type`) surfaces a friendly "usage limit — try again in ~N min" reply instead of a raw error tuple.

### When routes are resolved

Routes for main-agent turns are **snapshotted at boot** — changing a provider or model in `config.toml` requires a daemon restart to take effect. Scheduled jobs without an explicit provider pin resolve the chain at execution time instead. This asymmetry is intentional: an interactive turn runs against a stable, known route, while a recurring job picks up the current default each time it fires.

## Route resolution

`RouteResolver.resolve!/1` translates a provider atom plus options into a `{route_key, adapter_opts}` pair. The route key is:

```elixir
%{
  provider: :openai,
  model:    "gpt-5.5",
  auth_mode: :api_key,
  base_url: "https://api.openai.com/v1"
}
```

`Adapter.for_route/1` dispatches to the correct adapter module using this key. Resolution order for each field is: explicit call option, then the provider's `config.toml` block, then the descriptor default.

Default base URLs:

| Provider | Default base URL |
|---|---|
| `openai_codex` | `https://chatgpt.com/backend-api/codex/responses` |
| `openai` | `https://api.openai.com/v1` |
| `anthropic` | `https://api.anthropic.com/v1` |
| `xai` | `https://api.x.ai/v1` |
| `openrouter` | `https://openrouter.ai/api/v1` |
| `mistral` | `https://api.mistral.ai/v1` |
| `ollama` | `http://localhost:11434/v1` |

The `base_url` can be overridden in the provider config block for proxies or self-hosted endpoints.

## Reasoning effort

Reasoning effort tells the model how hard to think before answering: higher effort means more thorough (and slower, more expensive) responses. Four providers accept this hint: `openai`, `openai_codex`, `anthropic`, and `xai`. The canonical levels, ordered from lowest to highest, are:

```
none | low | medium | high | xhigh | max
```

Provider support:

| Provider | Supported levels |
|---|---|
| `openai` | `none`, `low`, `medium`, `high`, `xhigh`, `max` |
| `openai_codex` | `none`, `low`, `medium`, `high`, `xhigh`, `max` |
| `anthropic` | `low`, `medium`, `high`, `xhigh`, `max` |
| `xai` | `none`, `low`, `medium`, `high` |

A level above a provider's ceiling clamps to the ceiling (e.g. `:max` on xAI becomes `high`). On OpenAI and Codex, `max` is a GPT-5.6-family capability: each model also carries its own effort ceiling in the catalog — `gpt-5.5`, `gpt-5.4`, and `gpt-5.4-mini` top out at `xhigh` — and an over-reaching config or routing override self-heals down to the model's ceiling at route resolution (e.g. `max` on `gpt-5.5` runs as `xhigh`) instead of failing at the provider. Setup only offers the effort levels a model accepts. Anthropic has no `none` level; its floor is `low` and its API-side default is `high`. On Anthropic the effort level calibrates how much the model deliberates but does not switch deliberation on: Fermix sends adaptive thinking automatically on the models that accept it (Sonnet 4.6, Opus 4.8, and Fable 5), while Claude Haiku 4.5 runs without it. OpenRouter and Mistral do not expose a reasoning-effort field; Ollama does not either.

Set the default effort per provider in `config.toml`:

```toml
[fermix_core.providers.openai]
reasoning_effort = "high"
```

Valid values are the lowercase level strings above. A hand-edited unrecognized value is dropped at config load and the provider's default applies — setup only ever writes valid values, and the removed `minimal` level migrates to `low` automatically. Putting a `reasoning_effort` key in an OpenRouter, Mistral, or Ollama block does stop the daemon from booting: those providers don't allow the key, and unknown keys in a provider block always fail loud. `none` omits the effort field from the request, deferring to the provider's server-side default.

## Routing overrides for sub-agents and scheduled jobs

`[fermix_core.routing]` lets you pin sub-agent workers (helper agents the main agent spins up to handle a piece of work) and unpinned scheduled jobs to a smaller or cheaper model without changing the main agent's route.

```toml
[fermix_core.routing]
subagent_provider        = "openai"
subagent_model           = "gpt-5.4-mini"
subagent_reasoning_effort = "low"

cron_provider            = "mistral"
cron_model               = "mistral-small-latest"
```

Unset keys inherit the main agent's current primary. The failover chain is preserved. Effort is overlaid onto each route in the chain and clamped per provider, so a lower effort never changes the model or drops a fallback.

The `subagents` tool also accepts one-shot `model`, `provider`, and `reasoning_effort` arguments for a single call. A model given without an explicit provider runs on the primary provider (only when no primary is configured does Fermix fall back to the model's catalog owner); to run on a non-primary provider, pass `provider` explicitly or set `subagent_provider`. Per-call args take precedence over the config defaults.

`RoutingOverrides` validates provider and effort values at config-read time and raises a clear `ArgumentError` for unknown providers or levels, naming the offending config key. It also enforces a **pairing guard**: an explicit provider paired with a model the catalog knows under a *different* provider (e.g. an Ollama slug under `openrouter`) is rejected, and `model_routing_config` validates the merged routing on every set — so no automated path can persist a mis-pairing. Only a hand-edited `config.toml` could hold one, and even then it fails loud at spawn rather than running silently.

Surface these settings at runtime with the `model_routing_config` tool, or inspect them via `fermix doctor`. See [configuration](/docs/configuration) for the full config reference and [the agent loop](/docs/the-agent-loop) for how sub-agents are spawned.

## Normalized provider response

Every adapter returns the same response shape to `AgentLoop`:

```elixir
%{
  content:        String.t(),
  tool_calls:     [%{call_id: id, name: name, arguments: args}],
  usage:          %{prompt_tokens: n, completion_tokens: n, total_tokens: n},
  model:          String.t(),
  provider_state: term()   # opaque; fed back to continue/3
}
```

Agent code depends only on this map. Provider-specific response bodies do not cross the boundary.

## Telemetry

| Event | Emitted when |
|---|---|
| `[:fermix, :provider, :call]` | Every `chat` or `continue` call, with provider, model, auth mode, duration, and usage |
| `[:fermix, :provider, :failover]` | Each provider transition in the fallback chain |
| `[:fermix, :agent, :iteration]` | Each agent loop iteration, with iteration count and whether tool calls were present |
| `[:fermix, :model_catalog, :unknown_model]` | Context window lookup for a model not in the catalog |

See [traces and telemetry](/docs/traces-and-telemetry) for the full telemetry contract.
