Skip to content
pleach
Architecture

Model resolution matrix

Which model fires for which call class, resolved per provider family at session start.

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, Seam, family-lock — that decide which model fires for which kind of call are framed in Family-lock → the routing cluster.

The matrix

anthropicopenaigoogledeepseekmoonshotmistral ‡qwenthinkingmachinesxai
synthesizeclaude-sonnet-4-6openai/gpt-5.6-luna-progoogle/gemini-3.6-flashdeepseek/deepseek-v4-flash-0731moonshotai/kimi-k2.5mistral-large-latestqwen/qwen3.7-flashthinkingmachines/inklingx-ai/grok-4.5
reasoningclaude-sonnet-4-6 †openai/gpt-5.6-lunagoogle/gemini-3.5-flash-litedeepseek/deepseek-v4-flash-0731 †moonshotai/kimi-k2.5 †mistral-large-latest †qwen/qwen3.7-flash †thinkingmachines/inkling-smallx-ai/grok-4.5 †
converseclaude-haiku-4-5openai/gpt-5.6-lunagoogle/gemini-3.5-flash-litedeepseek/deepseek-v4-flash-0731 †moonshotai/kimi-k2.5 †mistral-small-latestqwen/qwen3.7-flash †thinkingmachines/inkling-smallx-ai/grok-4.5 †
utilityclaude-haiku-4-5openai/gpt-5.6-lunagoogle/gemini-3.5-flash-litedeepseek/deepseek-v4-flash-0731 †moonshotai/kimi-k2.5 †mistral-small-latestqwen/qwen3.7-flash †thinkingmachines/inkling-smallx-ai/grok-4.5 †

† Resolves to the same model id as that family's synthesize rung — either an explicit alias-up (anthropic reasoning: no distinct mid-rung model exists) or a single-rung family where every call class shares one model (moonshot, deepseek, qwen, xai). openai (luna → luna-pro), google (flash-lite → 3.6-flash), and thinkingmachines (inkling-small → inkling) carry a real two-rung ladder.

‡ mistral is 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.

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.

Call classes

ClassWhen the runtime picks itWhy it has its own row
synthesizeThe single final answer for a turn.Highest-quality model per family; exactly one of these fires per turn.
reasoningAn intermediate generator whose output feeds the next call.Quality matters, but it's not user-visible prose — a smaller model is acceptable.
converseShort user-facing prose (clarifying questions, refusal hints, retry narration).Latency-sensitive and conversational — typically the cheapest model that can hold the voice.
utilityInternal 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

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

FamilyAvailable transports
anthropicnative · openrouter · byok-native · byok-openrouter
openainative · openrouter · byok-native · byok-openrouter
googleopenrouter · byok-openrouter (native pending)
deepseekopenrouter · byok-openrouter (native pending)
moonshotopenrouter · byok-openrouter
mistral ‡not exposed for routing — placeholder model ids (see the matrix note above)
xaiopenrouter · 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.

Per-call-class overrides

SessionRuntimeConfig.perCallClassModelRouting replaces the matrix rung for named call classes. It's a partial map from call class to model id, read once at construction. Classes you leave out keep their matrix rung.

const runtime = new SessionRuntime({
  // ...
  // anthropic session: serve converse calls from the synthesize rung
  perCallClassModelRouting: { converse: "claude-sonnet-4-6" },
});

@pleach/coding-agent/policy ships createPerCallClassRouting({ routes }), which stamps the same field from a policy (Multi-synthesize). An absent, empty, or all-blank map leaves the runtime on the unrouted path.

Which adapters honour it. The runtime passes the routed id to adapter.resolveModel as the call's modelOverride. Core's createProviderAdapter and the quickstart standard adapter (what createPleachRuntime wires) resolve to it and serve it. An adapter that ignores the override keeps its own model, and the decision is recorded as not-honored rather than applied.

Cross-family routes are refused. The routed id's family is derived and compared with the session's locked family. The route is refused when the families differ (cross-family), when no family can be derived from the id (family-unknown), or when the lock isn't observable yet (lock-unobservable). A family that was only defaulted counts as unobservable, not as a lock. A refused call resolves as if unrouted. The refusal is disclosed by a [PerCallClassRouting] override-refused probe and by the decision stamped on auditMetadata.perCallClassRouting (requestedModelId, status, reason, and the requested and locked families). Change families by re-locking the session, not by routing.

Pricing follows the model served. A route to another rung of the same family is priced at that rung's per-model rates. A route to the class's own rung is priced exactly like an unrouted call. A route to a model that sits in no rung of the family is left unpriced: costUsd is omitted rather than filled at the call class's default rate.

Custom quickstart streams must dispatch to ctx.resolvedModel. A providerStream you pass to the quickstart receives a context whose resolvedModel is set on the seam path only when a route was honoured for that call. When it's present, send the request to that model: the call is already priced, cached, and audited as it. When it's absent, pick your model as before.

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