Model routing & fallback
When a request names a model, ModelRouter turns that into a concrete provider adapter and an ordered execution plan. This is where policy, auto selection, budgets and fallback live.
#Resolution order
- Explicit model — the alias you asked for.
- Default — if the request names none, the gateway-wide
Gateway:DefaultModel. There is no per-workspace default. No model and no default →400 model_not_found. - Alias remap —
Gateway:ModelAliases:{requested}rewrites the name (see below). auto/tierstrategies — chosen from the catalog by price and latency (see below).- Lookup — the alias must exist and be enabled, with an enabled provider; otherwise
404 model_not_found.
Before executing, the router also enforces the per-key model allow-list (403 model_not_allowed), the workspace budget hard-stop (402 budget_exceeded) and, when the request names an end user, that user's spend limit (402 end_user_budget_exceeded). Model aliases are resolved globally by name: a workspace's own copy of an alias wins over a global one of the same name.
#Dynamic aliases
Gateway:ModelAliases:{requested} remaps a requested name to another at runtime — handy for redirecting gpt-4 → gpt-4o org-wide without touching apps or the catalog.
#auto strategies
Pass one of these as the model to let Relay choose among enabled models whose provider is enabled, narrowed to the key's allow-list:
| Model value | Picks |
|---|---|
auto, auto:cheap (also auto:cheapest, tier:cheap) | The cheapest model by catalog input + output price per 1M tokens, ties broken by latency. Bare auto means cheap. |
auto:fast (also auto:fastest, tier:fast) | The lowest rolling-latency model, ties broken by price. |
auto:balanced (also tier:balanced) | The average of normalised price and normalised latency. A model with no latency samples yet scores neutral. |
Selection uses the catalog's pricing and the per-model EWMA latency (alpha 0.3) from ModelMetricsRegistry, updated from live outcomes. The registry also tracks a success ratio, shown at GET /v1/models/metrics, but selection does not use it. Models on a degraded provider are passed over while a healthy candidate exists. There is no capability filter: an auto request can land on any enabled chat model in the key's view, so keep realtime-only or embedding-only models off a key that uses auto by giving it an allow-list.
#Resilience
GatewayChatService wraps every call:
- Retry — on a non-streaming call, transient upstream failures (
ProviderException.IsTransient: 5xx, 429, timeout, connection error) are retried with exponential backoff (Gateway:MaxRetries, default 2;Gateway:RetryBaseDelayMs, default 250, doubling per attempt). A streaming call is not retried: a transient failure before the first token moves straight to the next fallback target. - Circuit breaking —
ProviderHealthRegistrymarks a provider degraded after 3 consecutive transient failures; it stays degraded for a 30-second cooldown, after which one request is let through. The router avoids degraded providers forautoand for fallback targets. A successful active probe clears the state. - Fallback — try another model when this one fails transiently. See below.
- Active probing —
HealthProbeService(opt-in,Gateway:HealthProbe:*) sends a token-free GET to each provider's base URL on an interval; any HTTP response counts as reachable. Results feed the circuit breaker and surface atGET /v1/providers/health, and a provider crossing the failure threshold — or recovering — sends an alert through the webhook / e-mail channel.
#Fallback
A model can name another model to be retried against when it fails. Set it per model in Models → Edit → Fallback model, which stores fallback_model_id on the row — no redeploy, no config change.
#Precedence
The router builds one fallback chain per request, first source that yields a target wins:
- The model's own
Fallback model, followed transitively —gpt-4o→gpt-4o-mini→claude-sonnet. Gateway:FallbackModel— the global default, appended when the primary declares no chain of its own (and it is not the primary itself). This is the original behaviour and still works; a per-model link simply overrides it.- The
autorunner-up — forauto/tierrequests only, when neither of the above produced a target, second place in the ranking becomes the fallback.
A chain is capped at 3 targets including the primary. Each hop costs a full retry cycle before it is abandoned, so an unbounded chain would let one slow upstream stretch a single request across every model's retries.
#When it applies
| Path | Falls back | Why |
|---|---|---|
POST /v1/chat/completions, /v1/responses | Yes | |
Agents (/v1/agents/{id}/chat/completions) | Yes | Runs through GatewayChatService. |
Workflow llm.* and Agent nodes | Yes | Same pipeline. |
| Batch jobs | Yes | Applied per row, so a rate limit that clears mid-job lets later rows return to the requested model. |
| Evals / A-B runs | No | An eval measures a named model. A substituted answer would attribute a score to a model that never produced it, and an A/B comparison would be against a mixture. A failed case is honest; a substituted one is not. (The judge model of a judge-scored run or a live evaluator goes through the normal pipeline and can fall back — it is not the thing being measured.) |
| Embeddings and RAG | No | Two embedding models produce vectors in different spaces. Answering with a different one returns results that are wrong rather than degraded. |
| Realtime sessions | No | A voice call is stateful; switching models mid-call would discard the audio context and change the voice. |
#When it triggers
Fallback is not a general retry. It engages only when all of these hold:
- The failure is transient — upstream 5xx, 429, timeout, or connection error. A 400, 401 or 404 fails immediately, because a malformed or unauthorised request fails identically on any provider and retrying elsewhere only adds latency.
- The primary's own retries are exhausted first (non-streaming; a stream has none).
- For streaming, no token has reached the client yet. Once SSE output has begun it cannot be taken back, so a mid-stream failure ends the response with an error event.
- The next target's provider is not degraded. Degraded providers are skipped as fallbacks; the explicitly requested primary is always attempted regardless.
#Edge cases
- Loops are safe.
A → B → Ais a legitimate "each covers the other" setup and is accepted. The router tracks which models it has already tried and ends the chain rather than re-attempting one. - A model cannot be its own fallback — rejected on save, since it would be a silent no-op.
- Dead links are skipped, not fatal. If the fallback is deleted, disabled, or its provider is disabled, the per-model chain ends there — a broken backup never stops a healthy primary from serving. If that leaves no fallback at all,
Gateway:FallbackModel(when set) still applies. The panel shows such a link as(deleted model). - Cost and telemetry are recorded against whichever model actually served the request. The generation is flagged
fallback_used, androuted_providerrecords the provider it was routed away from, so fallback traffic can be told apart from direct traffic in the trace explorer and over OTLP (relay.fallback_used).
#Catalog cache
The model + provider catalog is cached in-process for Gateway:CatalogCacheSeconds (default 10) and shared across requests. The panel runs in a separate process and does not push edits to the gateway, so a fallback link or model change you just saved takes effect when the cache next expires — within that window.