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.
Configure canonical routes
Section titled “Configure canonical routes”{ "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
effortis forwarded only to that route and must be supported by its known model metadata. - Omitted
effortmeans the provider/model default. It does not inherit the primary’sruntime.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:
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:
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-defaultRetired: fallbackModels
Section titled “Retired: fallbackModels”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" }] }}Same-model retries
Section titled “Same-model retries”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_limitis deterministic against the same window, so a second identical request is a guaranteed second failure. It needs a route with a larger window.provider_authcannot 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.
Legacy request retries
Section titled “Legacy request retries”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.
Child provider outage window
Section titled “Child provider outage window”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.
What failover does
Section titled “What failover does”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.
No failover after a tool ran
Section titled “No failover after a tool ran”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.
Who sees a failover
Section titled “Who sees a failover”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.
Guided readiness
Section titled “Guided readiness”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.
Ordering and cost
Section titled “Ordering and cost”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.
Related
Section titled “Related”- Multi-model fallback chain
- Pi runtime & model references
- Execution, effort & permissions
- Sandbox
- Sessions & concurrency
Context fit and provider schema dialects
Section titled “Context fit and provider schema dialects”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.