Fallback
Try backup models when an attempt fails or produces unusable output.
fallback() wraps at least two models or routing wrappers and tries them in order.
import { fallback } from "@use-crux/core/routing";
export const resilientModel = fallback(
[gpt5, sonnet, { model: gemini, maxTokens: 2000 }],
{
id: "launch-day-fallback",
on: ["rate_limit", "timeout", "server_error", "connection_error"],
timeout: { attempt: 10_000, firstToken: 3_000 },
onFallback: async ({ from, to, attempt, error }) => {
await alertOps({ from, to, attempt, error });
},
},
);Use fallback for provider outages, rate limits, slow starts, validation exhaustion, or response-aware rejection.
Error Categories
| Category | Typical source |
|---|---|
rate_limit | HTTP 429 |
timeout | attempt, first-token, or total deadline expiry |
server_error | HTTP 500-599 |
connection_error | DNS, network, connection failures |
auth_error | HTTP 401/403 |
invalid_response | structured validation exhaustion or when(result) rejection |
Use on to restrict which failures move to the next model.
fallback([primary, backup], { on: ["rate_limit", "timeout"] });Response-Aware Fallback
Use when(result) when a successful provider response is unusable for your app.
const model = fallback([fast, careful], {
id: "answer-quality-fallback",
when: (result) => {
const confidence =
typeof result === "object" && result && "confidence" in result
? result.confidence
: 0;
return Number(confidence) < 0.7;
},
});Predicate and callback errors are logged as routing.hook_error events and do not control recovery.
Exhaustion
When every option fails, Crux throws FallbackExhaustedError.
import { FallbackExhaustedError } from "@use-crux/core/routing";
try {
await generate(prompt, { model: resilientModel, input });
} catch (error) {
if (error instanceof FallbackExhaustedError) {
console.log(error.attempts);
console.log(error.routing);
}
}Receipts
Every fallback resolution includes a receipt step, even when the first option succeeds.
result.routing?.trace;
// [{ kind: 'fallback', id: 'launch-day-fallback', attempts: [{ model: 'gpt-5', status: 'ok', durationMs: 412 }] }]Failed attempts record their error category and bounded error text in the same receipt. Run Detail uses those fields to explain why it moved to the next model without exposing unbounded provider payloads.
Streaming
Fallback can retry stream attempts until the first token reaches the caller. timeout.firstToken is fallback-eligible because no token has been delivered yet.
After token 1, Crux does not restart the stream on another model. The stream errors and the completion error carries routing.trace[*].midStreamFailure = true; routing.firstTokenAt remains the elapsed TTFT measurement.
const streamResult = await stream(chatPrompt, {
model: fallback([primary, backup], { timeout: { firstToken: 2_000 } }),
input,
});
const completion = await streamResult.completion;
console.log(completion.routing?.model);