/** * Model failover for a STREAMING turn. * * `/model-resolution`'s `runWithModelFailover` already owns the policy: walk a * chain, classify a resolved-or-thrown signal, re-throw a non-outage failure * immediately, and carry the attempt trail. This module does NOT re-implement * any of that — it composes it. What it adds is the one thing a whole-call * primitive cannot express, because a stream fails PARTWAY: * * **A turn may only fail over before its first client-visible byte.** * * Once a text delta, tool call, or ask has reached the browser (and the * persisted transcript), restarting on another model would duplicate the * answer. So each attempt is probed: open the stream, pull events into a small * buffer, and decide at the first meaningful event whether this model is * serving. Committing replays the buffer and hands the live iterator through; * abandoning discards the buffer (those events describe the dead model's * session — including its `step-finish` usage, which must never be billed) and * lets `runWithModelFailover` walk to the next model. * * The classification itself is `isUpstreamUnavailable` verbatim, so this path * inherits the measured facts from the 2026-07-25 outage — above all that an * outage is NOT always a thrown error: the sandbox RESOLVES a terminal `error` * event carrying `{ errorCode: 'provider_inference_unavailable' }`, which a * classifier inspecting only `catch` misses entirely. That resolved shape is * the whole reason the breakage went unnoticed, so it is classified here first. * * Conservative by construction: * - A terminal failure that is NOT an outage (400, bad schema, content filter) * COMMITS rather than failing over — it surfaces to the user exactly as it * does today. Those fail identically on every model; walking the chain would * only multiply latency and spend to reach the same error. * - A clean stream that produced nothing NEVER walks the chain. An empty answer * is not evidence of a dead upstream, and a silent re-roll on another model is * precisely the unattributable downgrade this work exists to prevent. Opt-in * `emptyTurnRetries` re-runs the SAME model instead, which leaves attribution * untouched — measured on production 2026-07-27, an empty turn is a transient * platform flake that a same-model re-run recovers (8 hard cases: 7/8 * delivered on the first pass, 8/8 with one re-run, at a cost of 1 extra turn * in 9). Default `0`, so the behavior is unchanged unless a product asks. * - A responsive chain of length 1 costs no extra call or latency and remains * byte-identical to no failover. A silent chain is now deliberately bounded. */ import { ModelFailoverExhaustedError, type ModelFailoverAttempt } from '../model-resolution/failover'; /** * True when `event` puts content in front of the user (or in the persisted * transcript), making a restart on another model unsafe. * * Deliberately an allow-list of KNOWN-INERT types rather than a deny-list: an * unrecognized event commits. Getting this wrong in the safe direction costs a * missed failover; getting it wrong the other way duplicates a user's answer. */ export declare function isCommittingSandboxEvent(event: unknown): boolean; /** * Condense an abandoned attempt's raw failure text into something safe to show * a customer in the transcript. * * Measured on the live router 2026-07-27 19:40 UTC: an edge 5xx arrives as * Cloudflare's full HTML error PAGE, so the verbatim reason began * `\n