# Model resolution matrix (/docs/model-resolution-matrix)



Every call into the runtime resolves to a concrete model through a
`(ProviderFamily × CallClass)` matrix. The **family** is locked at
session start (it picks tokenizer, prompt-cache key, tool-call dialect,
and refusal pattern); the **call class** is declared per-call by the
seam that's invoking the LLM.

This page is the reference table for the routing cluster. The
three concepts — [CallClass](/docs/call-classes),
[Seam](/docs/seams), [family-lock](/docs/family-lock) — that decide
which model fires for which kind of call are framed in
[Family-lock → the routing cluster](/docs/family-lock#the-routing-cluster).

## The matrix [#the-matrix]

|              | `anthropic`           | `openai`       | `google`                 | `deepseek`          | `moonshot`               | `mistral` ‡              | `xai`                          |
| ------------ | --------------------- | -------------- | ------------------------ | ------------------- | ------------------------ | ------------------------ | ------------------------------ |
| `synthesize` | `claude-sonnet-4-6`   | `gpt-5.4`      | `gemini-3-pro`           | `deepseek-v4-pro`   | `moonshotai/kimi-k2.5`   | `mistral-large-latest`   | `grok-4.3`                     |
| `reasoning`  | `claude-sonnet-4-6` † | `gpt-5.4-mini` | `gemini-3-pro` †         | `deepseek-v4-pro` † | `moonshotai/kimi-k2.5` † | `mistral-large-latest` † | `grok-4.20-0309-reasoning`     |
| `converse`   | `claude-haiku-4-5`    | `gpt-5.4-mini` | `gemini-3-flash-preview` | `deepseek-v4-flash` | `moonshotai/kimi-k2.5` † | `mistral-small-latest`   | `grok-4.20-0309-non-reasoning` |
| `utility`    | `claude-haiku-4-5`    | `gpt-5.4-nano` | `gemini-3-flash-preview` | `deepseek-v4-flash` | `moonshotai/kimi-k2.5` † | `mistral-small-latest`   | `grok-4.20-0309-non-reasoning` |

† Aliased up to that family's `synthesize` rung because no distinct
mid-rung model exists. Moonshot is single-rung today — every call
class resolves to the same model id.

‡ `mistral` is &#x2A;*not currently exposed for routing.** Its matrix
entries are placeholder model ids (`mistral-small-latest` /
`mistral-large-latest`) that don't resolve at any transport, so the
family is excluded from `EXPOSED_FAMILIES` and a session cannot lock
to it today. The row is shown for matrix completeness only.

<Callout type="info">
  The live matrix is the source of truth — it lives in `@pleach/core`
  and is the only place edited when a model is added or rotated. A
  future build of this page will regenerate the table directly from
  the published matrix; until then, treat this rendering as a
  snapshot suitable for orientation, not for procurement decisions.
</Callout>

## Call classes [#call-classes]

| Class        | When the runtime picks it                                                       | Why it has its own row                                                                       |
| ------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `synthesize` | The single final answer for a turn.                                             | Highest-quality model per family; exactly one of these fires per turn.                       |
| `reasoning`  | An intermediate generator whose output feeds the next call.                     | Quality matters, but it's not user-visible prose — a smaller model is acceptable.            |
| `converse`   | Short user-facing prose (clarifying questions, refusal hints, retry narration). | Latency-sensitive and conversational — typically the cheapest model that can hold the voice. |
| `utility`    | Internal classification or extraction (intent detection, refusal repair).       | Pure machine-consumed; cheapest model the family ships.                                      |

The classes are a downgrade ladder: `synthesize → reasoning → converse →
utility` is the cost-quality direction.

## Transports [#transports]

Family choice fixes the tokenizer; transport choice fixes how the
request gets to the provider. Both lock at session start.

| Family      | Available transports                                                        |
| ----------- | --------------------------------------------------------------------------- |
| `anthropic` | `native` · `openrouter` · `byok-native` · `byok-openrouter`                 |
| `openai`    | `native` · `openrouter` · `byok-native` · `byok-openrouter`                 |
| `google`    | `openrouter` · `byok-openrouter` (native pending)                           |
| `deepseek`  | `openrouter` · `byok-openrouter` (native pending)                           |
| `moonshot`  | `openrouter` · `byok-openrouter`                                            |
| `mistral` ‡ | not exposed for routing — placeholder model ids (see the matrix note above) |
| `xai`       | `openrouter` · `byok-openrouter` (native pending)                           |

Families without a native provider collapse `native` and `byok-native`
requests to their OpenRouter equivalents at resolve time, rather than
dispatching to a transport that doesn't exist. The resulting
`transport` is recorded on the audit row, so the collapse is visible
after the fact.

`native` reaches the provider directly. `openrouter` routes through
OpenRouter — useful for evaluation and for families without a native
transport. `byok-*` variants delegate credentials to the end user's
BYOK key rather than the deployment's. Transport is session-scoped, so
mid-session escalation across transports doesn't silently happen.
A session that starts on `anthropic` + `byok-native` and exhausts the
user's key surfaces the exhaustion through the ledger as a
`providerCascade` row with `outcome.status: "exhausted"`, not a
silent flip to the deployment's own credentials.

## Cross-family fallback policy [#cross-family-fallback-policy]

The runtime does **not** silently widen across families when the
session's locked family is exhausted. Once every in-family rung is
tried, the runtime surfaces a `family-exhausted` state to the host
and lets the user pick another family explicitly. The pivot
exists because cross-family widening produced a measurable regression
class: a session that began on `anthropic`'s tool-call dialect would
silently cross to `openai`'s on cascade, and tools whose argument
schemas relied on Anthropic's `input_schema` shape would stop being
called correctly — the same agent code, the same session id, two
incompatible tool dialects within one conversation. The family lock
makes that crossing visible to the host instead of invisible to the
runtime.

A narrow legacy carve-out preserves cross-family fallback for models
that don't resolve through the matrix at all — BYOK rigs, modal-LLM
endpoints, and unrecognized model slugs — because those sessions never
had a family lock to honor in the first place.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Providers" href="/docs/providers" description="The family-strict cascade and the permittedFamilies constraint." />

  <Card title="Fingerprint" href="/docs/fingerprint" description="Family, model, and call class as inputs to the cache key." />

  <Card title="Auditable call row" href="/docs/auditable-call-row" description="Which family and model actually fired, recorded per call." />

  <Card title="Env vars" href="/docs/env-vars" description="Provider credentials per family, including OpenRouter." />
</Cards>
