Skip to content

Fallback models & failover

runtime.fallbacks is the canonical ordered list of backup routes. Each entry selects a model and, optionally, its exact reasoning effort. The list is not artificially capped: the router walks it in authored order until one route succeeds or every eligible route is exhausted. Failover history is reported in results and traces, and a run that leaves its configured route says so in the transcript — see Who sees a failover. mono-agent never silently swaps providers.

{
"runtime": {
"model": "openai-codex:gpt-5.6-terra",
"effort": "high",
"fallbacks": [
{ "model": "openai-codex:gpt-5.6-sol", "effort": "xhigh" },
{ "model": "ollama:gemma4:31b" }
]
}
}

A local fallback such as ollama:gemma4:31b still needs its provider declared — "providers": { "ollama": {} } — or the route loads and then fails at turn time; see Providers.

The primary uses runtime.effort. Every canonical fallback owns its effort:

  • An explicit effort is forwarded only to that route and must be supported by its known model metadata.
  • Omitted effort means the provider/model default. It does not inherit the primary’s runtime.effort.
  • Each fallback infers its own execution from its model reference; it does not inherit settings from the primary.

The canonical environment form is JSON:

Terminal window
export MONO_AGENT_FALLBACKS_JSON='[
{"model":"openai-codex:gpt-5.6-sol","effort":"xhigh"},
{"model":"ollama:gemma4:31b"}
]'

For non-interactive scaffolding, repeat --fallback and put an optional --fallback-effort immediately after the route it configures:

Terminal window
mono-agent init \
--model openai-codex:gpt-5.6-terra --effort high \
--fallback openai-codex:gpt-5.6-sol --fallback-effort xhigh \
--fallback ollama:gemma4:31b --fallback-effort provider-default

runtime.fallbackModels and MONO_AGENT_FALLBACK_MODELS were retired in 0.21.0. They are rejected at load with the replacement named for the surface each was set on — the JSON key points at runtime.fallbacks, the environment variable points at MONO_AGENT_FALLBACKS_JSON — so an agent still carrying one does not start. The CLI CSV flag --fallback-models was removed earlier; repeat --fallback <ref> instead. See the canonical deprecation tracker.

Convert by hand, with the agent stopped, then run mono-agent validate before restarting it:

{
"runtime": {
"model": "openai-codex:gpt-5.6-terra",
"effort": "high",
"fallbacks": [{ "model": "ollama:gemma4:31b" }]
}
}

Before the chain advances, a route can retry itself. runtime.retry.primaryAttempts sets the total attempts on runtime.model including the first (default 2, so the primary gets one retry); each runtime.fallbacks[] entry takes its own optional attempts and stays single-shot when omitted. Set primaryAttempts to 1 to turn same-model retries off entirely.

{
"runtime": {
"model": "openai-codex:gpt-5.6-terra",
"retry": { "primaryAttempts": 2, "backoffMs": 1000, "maxBackoffMs": 15000 },
"fallbacks": [
{ "model": "anthropic:claude-sonnet-4-6", "effort": "high", "attempts": 2 },
{ "model": "ollama:gemma4:31b" }
]
}
}

A retry re-runs the whole logical turn on the same route after backoffMs, doubling on each further retry and capped by maxBackoffMs. Only transient failures retry — overloaded, rate_limited, timeout, network, server_error, retryable_request, and terminated streams. Two retryable kinds deliberately advance instead:

  • context_limit is deterministic against the same window, so a second identical request is a guaranteed second failure. It needs a route with a larger window.
  • provider_auth cannot change mid-turn, but a different provider’s credentials may work.

Cancellation and mid-turn sandbox/safety failures never retry, and a failed attempt resolver advances immediately rather than burning the route’s budget. An attempt that already ran a tool or consumed a live input never retries either; see No failover after a tool ran.

A same-model retry drops the provider session (the failed attempt already appended to it), emits provider_retry_started rather than a failover event, and appends its own failoverHistory entry carrying retryIndex — each retry keeps its own request id and failure subkind.

Interactive parent turns and direct runtime calls keep the following legacy policy by default. Children use the outage profile below on supported adapters unless subagents.providerRetryBudgetMs is zero.

Router attempts compose with provider-transport retries inside each Pi attempt. providers.piNative.piMaxRetries (0–8, default 2) sizes SDK retries before a response starts; zero disables budgeted retries. A connection lost before response headers may be re-sent even if the provider received the request: this is standard SDK behavior and may leave usage unreported.

The shared HTTP retry policy handles connection errors, HTTP 408/409/429/5xx, and provider x-should-retry, Retry-After and retry-after-ms headers. Provider-requested delays above maxRetryDelayMs fail rather than sleeping past the cap. With this shared policy, each assistant request has at most N + 1 pre-response requests per router attempt: 3 with the default, or 6 across primaryAttempts: 2 before advancing the chain. Codex differs: openai-codex uses its own retry classification and exponential backoff, and can fall back from websocket to SSE independently of this retry budget. The shared policy’s classification and request-count bound do not cover that fallback. Codex websocket turns can also re-send once when the server rejects a cached continuation with previous_response_not_found; that rejection carries no generated output. Bedrock differs too: amazon-bedrock builds its AWS client without this budget, so the AWS SDK’s own retries apply to every Bedrock request: three attempts by default, or AWS_MAX_ATTEMPTS / the profile’s max_attempts.

A response that fails after streaming starts is not repeated by the SDK layer. The native driver retries that one assistant request instead, but only when the provider had already started the response (emitted its first stream event) and the failure is transient: websocket error/close, connection reset, terminated or prematurely closed stream, timeout, overloaded, 5xx or 429. Codex WebSocket error, WebSocket closed … and WebSocket stream closed before response.completed are the common cases. It makes up to three further attempts with 1 s, 2 s and 4 s backoff (capped by maxRetryDelayMs) and emits a provider_request_retry runtime warning per retry; the run reports the total as diagnostics.pi_request_retries. Auth, quota/usage limits, context limits, safety and validation failures are never retried, and cancellation during the backoff ends the turn immediately. piMaxRetries: 0 disables this layer too.

A failed assistant message never executes its tool calls, and live input is only consumed between requests, so the retry repeats no tool or input. It re-sends the same request: earlier assistant messages and tool results stay; the failed partial does not. That partial stays in the native journal as an error message, so its usage is billed and its error is traceable. The reply excludes it. When every attempt fails, the turn keeps its usual provider_unavailable classification and the error names the attempt count, for example WebSocket error (failed after 4 provider request attempts). After any Codex websocket failure, pi-ai pins that session to SSE, so the retry and later requests in the session use SSE.

The two layers never act on the same failure: SDK retries handle failures before a response starts; the driver handles failures after it started. Each assistant request in one router attempt therefore makes at most four started responses and 4 × (N + 1) provider requests: 12 with the default N = 2. Codex can add its websocket-to-SSE fallback and continuation re-send to each of those requests before a response starts. Router re-attempts multiply the whole turn: primaryAttempts: 2 allows 24 requests and 8 started responses per assistant request before the chain advances. Durable native Web turns are primary-only, without router fallback or re-attempt, so their bound is 12 requests and 4 started responses. These retries never replay tools or whole turns; a turn with several assistant requests can exceed a per-request bound.

Native Agent children (foreground and background, persistent and stateless) default to subagents.providerRetryBudgetMs: 120000 on Anthropic messages, OpenAI Responses and OpenAI Codex. This integer accepts 0–180000 milliseconds; zero restores the legacy profile. Direct runtime callers can opt in with piRequestRetryBudgetMs (default zero). Other adapters retain legacy behavior; Bedrock always keeps AWS SDK retry ownership. An inherited providers.piNative.piMaxRetries: 0 disables both short and outage retries, even when the child window is positive.

The outage profile passes zero retries to the adapter. The native driver owns transient connection/server failures both before and after generation starts, within one recovery window per assistant request. An opaque Connection error. or fetch failed, failed body read, websocket loss, timeout or transient 5xx/529 can recover. DNS/TLS failures that surface as fetch failed can also consume this whole window in children, even when retrying cannot repair the underlying configuration. Auth, quota, context, validation and safety failures never enter this window. x-should-retry: false vetoes resends.

Ordinary 429 throttling and HTTP 408/409 keep short SDK-policy handling through pi-ai’s native retryProviderRequest helper at a request-local fetch boundary: the existing piMaxRetries count, header precedence and abortable sleeps remain. A final short-policy failure never enters the long driver loop. A 200 response stream or Codex websocket that reports throttling after generation starts gets only the legacy three short driver retries (1/2/4 seconds, capped by maxRetryDelayMs and the remaining window), never the long outage schedule. A server delay that cannot fit that short schedule stops recovery instead. Only replayable request bodies (strings, byte arrays or cloned Requests) receive those short resends; other bodies return the first response unchanged. Successful bodies are not buffered, and backpressure/cancellation are preserved.

Driver delays start at 1/2/4/8/16 seconds, then cap at 20 seconds, with up to 25% downward jitter and the existing maxRetryDelayMs ceiling. A provider-requested delay remains a lower bound; exceeding the delay cap fails instead. The recovery clock starts at the first eligible failure and never resets between phases. No new driver attempt starts at or after expiry. An independent limit permits at most 16 additional attempts (17 total); without jitter the default window normally fits nine resends at about 1,3,7,15,31,51,71,91,111 seconds and a final shortened wait toward expiry (subject to provider delay lower bounds). Cancellation interrupts either kind of backoff promptly; child/job deadlines still win.

This is not a hard request or turn timeout. Worst-case wall time in each router attempt is healthy work before the first failure, plus the recovery window, plus the last in-flight request/transport duration. That final duration is not bounded by this setting: an attempt started before expiry may finish later, including its short HTTP-policy waits. Codex can additionally spend its 15-second websocket connect timeout or perform its native pre-event SSE fallback and definite continuation corrections. Router re-attempts multiply independent windows; a multi-request turn has a separate window per assistant request. Router restrictions after tools/live input are unchanged: no tools or whole tool-bearing turns are replayed. Unknown-outcome request resends can cause duplicate provider billing, including usage the failed response never reports.

Child request failures, including non-retried terminal failures, append a bounded diagnostic suffix: generation phase, observed HTTP phase, driver attempts, observed HTTP sends (fetch invocations, not proof of provider receipt), elapsed milliseconds, allowlisted attributed cause code (or unknown), and exhaustion reason. The elapsed value measures that assistant request, not the entire child turn. Generation pre-response means no provider start event, not necessarily no headers: HTTP error responses already have headers, and Codex websocket start waits for its first mapped event. HTTP phase is reported separately as pre-response, post-response or unknown; websocket handshakes/sends are not invented from fetch observations. Process-wide undici correlation is not an attributed cause. Warnings distinguish before/after generation start. Failed partials remain journaled and billed but absent from the resend context.

Provider checks, compaction summaries and handoff summaries force SSE with zero retries and no mid-stream retry, avoiding Codex websocket-to-SSE re-sends and keeping each generation single-request on every provider except Bedrock, whose AWS SDK retries still apply (set AWS_MAX_ATTEMPTS=1 to make Bedrock single-request). The harness’s one-shot session-resume retry is disjoint: it fires on session_not_found / session_busy, which never retry at the router.

The router advances after a route exhausts its attempts on retryable provider errors (transport failures, rate limits, transient server failures), provider-auth failures, and a classified context_limit after the active bridge’s compaction recovery is exhausted. A fallback may have a larger usable window even when the primary cannot reduce its request further. Quota, output-token, and max-turn failures remain usage_limit and do not become context failover. Successful but undesired output does not trigger failover. Mid-turn sandbox/safety failures are terminal because retrying them on another provider could weaken the established contract.

The primary’s first attempt owns the provider session. Retries and failovers run stateless with canonical replay and bounded transcript-tail context. With coordinated durable session history, any answer from a retry or backup retires the primary epoch. The next turn cold-reseeds from canonical history; after a primary first-attempt success, subsequent turns resume the new session and are eligible for provider caching.

On an ordinary warm turn whose primary fails, an eligible retry or backup gets canonical earlier conversation messages, the bounded tool-history projection, and the current input, just as on a cold turn. A bounded failed-attempt snapshot may accompany that replay. The host loads history lazily under conversation ownership, only after any required durable detach acknowledgement; it reuses one frozen replay across detached attempts and never doubles a cold-turn prefix. Durable, custom and in-memory history stores are supported. If warm canonical history is absent or cannot be loaded, no retry or backup starts: the primary failure returns with detached_context_unavailable. A tool-projection failure alone keeps canonical replay and emits tool_history_projection_degraded. The next turn still reseeds from canonical history.

Configuring any fallback route counts as consent for that route to receive the conversation history, including when a local primary falls back to a cloud provider. Replay neutralises instruction markup but is not secret redaction. The replay loader and its output are not returned in results, failover history or router telemetry. Persistent subagents remain excluded: they have no host canonical replay loader, and detached answers still lose session continuity. Direct router callers without a loader retain their existing behaviour; private memory completion-only runtimes ignore the loader. Prepared native dispatch remains primary-only, with no routed retry or backup.

Only the primary’s first attempt carries the conversation’s provider attribution id. Every retry and backup gets a fresh attribution id of its own, so it never reaches the provider as the primary’s session (for OpenCode, its x-opencode-session header differs from the primary’s). The router withholds the resumable result id and recovery receipt from every retry and backup result — successful, cancelled, failed or exhausted — so the harness cannot synchronize or recover an untouched or failed primary transcript from it. Fresh stateless Pi calls use a private in-memory repository; they do not create a second durable session.

Tools are never re-run. A retry or backup replays the whole logical turn, so once a failed attempt started a tool or consumed a mid-turn (live) input, the router starts no same-model retry and no backup. This applies to every conversation and to backups too: a backup that ran a tool and then failed ends the run. The run ends with that attempt’s own failure (for example provider_unavailable, not provider_unavailable_exhausted) plus a provider_failover_blocked runtime warning whose reason is tool_already_executed or live_input_consumed. The warning is streamed as a runtime_warning event and recorded in the result’s runtimeWarnings, so the host reports the failure and waits for the next message instead of answering from a different model.

Pi results also carry monotonic dispatchProgress evidence, armed only after historical replay seeding. Tool admission and yielded live input block another ordinary attempt even when no corresponding event reached the router. Other backends retain the normalized-event fallback. Output alone does not change ordinary fallback policy. Prepared native dispatch remains primary-only.

The router decides from the attempt’s normalized events: a tool_use or tool_result block, or Pi’s tool_execution_start, whether streamed or in the attempt result. Pi emits tool_use at its conservative execution-start boundary, which can precede argument validation or hook blocking. A model that merely requested a tool before either admission boundary still fails over. Text or reasoning output alone also still fails over. On Pi, a live input counts as taken as soon as its iterator yields it, before steering or provider consumption. Even if it is later removed from the provider queue unconsumed, Pi starts no retry or backup; only never-yielded inputs can transfer. Tool admission is conservative too: a call reaching before_tool counts even when another hook blocks that call before execution. For marker-less custom runtimes, consumed or uncertain input events still block failover; a removed, unconsumed input remains mailbox-replay-safe. A custom runtime emitting neither progress markers nor these events cannot be gated and keeps the earlier behaviour.

The router checks this evidence when the attempt fails and again immediately before it admits the next attempt — after the retry backoff and after the route resolver returns — so a late tool event or live-input acknowledgement still blocks the retry or backup. Evidence that arrives after the next attempt was admitted cannot be honoured. A custom route runtime (the private resolveAttempt seam) must therefore emit its tool events and settle every live-input lease (acknowledge, mark uncertain, or reject) before its run() returns; the built-in Pi runtime does this. When no further attempt could run anyway, the chain reports provider_unavailable_exhausted as before, without the warning.

The runtime result includes failoverHistory. An exhausted chain reports provider_unavailable_exhausted with per-attempt models, failure kinds/subkinds. Run summaries and local failover attributes preserve normalized failover details; the events JSONL preserves the per-attempt provider_retry_started, provider_failover_started, and provider_failover_completed events. There is no separate per-route safety record: every route is Pi-native, so there is no cross-route contract to reconcile.

A run that answers from a fallback behaves differently from one that answers from the primary — different schema adherence, tool-calling conventions, cost, and sometimes capabilities. Operator surfaces show that from runtime-owned route evidence rather than comparing their own model-selector state.

While the run is in flight, the transition joins the activity log:

⏳ Retrying openai-codex:gpt-5.6-sol — attempt 2 (overloaded)
⚠️ Failed over: openai-codex:gpt-5.6-sol → opencode-go:kimi-k2.7-code (overloaded)

Chat channels (Slack, Telegram) render these alongside tool activity; the web console shows them as inline warning notices. Both respect the channel’s activity-hint setting, so a channel with hints turned off shows neither.

On the answer, a run that did not execute on its configured route appends one line:

⚠️ Answered by opencode-go:kimi-k2.7-code, not the configured openai-codex:gpt-5.6-sol (overloaded).

This is attached to the output it explains, so it survives on every surface the answer reaches — including a cron or webhook notify payload, where nobody is watching activity lines. A same-model retry that recovered on the configured route produces no note: the run’s identity did not change. A turn whose answer is NOTHING_TO_REPORT also produces no note, so notification suppression is unaffected.

The web console additionally persists structured attribution per run. The assistant message and conversation header distinguish the requested route, the current or last attempt, and the route that actually answered. Known classified transition reasons and same-route retries are available in a bounded disclosure; a terminal model mismatch without a transition is still marked as a fallback with “reason not reported.” An exhausted chain never claims an answering model. Configured subagents carry the same evidence inside their own delegation card.

Pi also reports the effective thinking level used for each provider request. That makes normalizations such as non-reasoning to off, unsupported max to xhigh, and the current ultra compatibility mapping to low visible without reimplementing Pi’s rules in the browser.

Bare interactive mono-agent init makes one real, sequential no-tool call for every selected route. Each route receives its exact configured effort (or provider default) and its own 90-second cloud / 240-second local deadline. Escape or Ctrl-C interrupts safely. The recovery menu can:

  • resume only routes already verified under the exact non-secret plan fingerprint;
  • restart all route checks while retaining successful auth and managed SRT;
  • edit model choices; or
  • cancel without writing.

Changing routes, effort, or provider configuration, durable non-secret environment, secret names, Pi auth path, or timeout invalidates the resume fingerprint. Authentication repair also invalidates every route proof, even when the non-secret plan is unchanged, because credential bytes may have changed. A detected credential is not enough: a route becomes verified only after its exact live check succeeds.

Order routes by preference because the first success wins. A common production shape is capable cloud primary, lower-cost cloud fallback, then local fallback. The creation review prints every selected route and effort, the number of real readiness calls, and how many may be billed before it asks whether to create the agent.

After a context_limit, each backup is checked against its known window using its actual detached replay and resume snapshot, not the failed native transcript. The estimate includes system/tool overhead plus 4,096 safety and 1,024 answer reserve tokens. Too-small entries are recorded as skipped_context_window in failoverHistory with a fallback_context_window_skipped warning. Unknown windows stay eligible for normal admission. If no backup fits, the real context overflow is preserved rather than hidden as provider unavailability.

OpenAI-completions and Google tool declarations receive a per-request schema clone that supplies missing derivable enum/const and structural types. Shared MCP definitions, unions, constraints and explicit types are unchanged. This does not enable strict sampling or disable the provider’s compatibility flags. Recognized Moonshot missing-type and Google function-declaration dialect rejections can advance to another route, but are never retried on the same model. Generic invalid requests remain terminal. Tool/live-input side-effect fences still prevent replay, including effects discovered during preparation.

Reported effective effort comes from the final provider-shaped request, including provider alias mappings. Budget-only thinking or provider-controlled reasoning without an effort label is not reported as the configured effort.