Wiring up multiple model providers takes an afternoon. Keeping the product alive when one of them degrades takes considerably longer. In production, failures rarely look like "the model answered wrong." They look like this:
an upstream provider returns 429 in the middle of your peak hour;
the connection opens but the first byte never arrives;
a streamed response dies halfway and the client is left holding truncated JSON;
one regional endpoint is unavailable while the global status page stays green;
a model is deprecated, or a preview endpoint is shut down on a published date;
a content filter refuses before any output is generated — and you are still billed;
output hits the token ceiling and returns a truncated result with finish_reason: length.
The order of operations that actually works is: classify, then retry; put a budget on retries; break circuits above that; fail over in defined levels; and cover the whole thing with deprecation management and observability.
1. Classify the error before deciding to retry
| | | |
|---|
| | | Back off 1–5s, then alternate provider |
| | | Back off, then alternate provider |
| reset, first-byte timeout, mid-stream drop | | Check for side effects before resending |
| | | Fix the payload; retrying reproduces the bug |
| | | Alert; a key or scope problem |
| | | Trim, chunk, or route to a longer-window model |
| | | Rewrite the prompt or escalate to a human |
| | | Raise the cap or demand shorter output |
The most common waste is retrying errors that cannot succeed. Retrying a 400 once triples your cost and your latency and changes your success rate by exactly zero.
2. Retries need a budget, not just a policy
At most two retries per request (three attempts total).
Exponential backoff with full jitter: sleep random(0, base * 2^n) rather than a fixed delay, so a fleet of clients does not retry in lockstep.
A global retry rate ceiling — for example 5%. Above it, stop retrying and treat it as an incident. A retry storm is self-inflicted: a client that retries everything twice multiplies load by 3x at exactly the moment the upstream is weakest.
Idempotency guards. Tool calls with side effects — placing an order, sending mail, writing to a database — must carry a dedupe key, or must not be retried at all. On timeout, query state instead of resending.
3. Timeouts come in layers
One global timeout is not a timeout strategy; it is a way to pay for work you throw away.
| |
|---|
| |
| 10–30s for standard models, 30–60s for reasoning models |
| 60s with no delta arrives → treat as dead |
| derived from max_tokens, not a global 300s |
Ordering matters: upstream timeout < your gateway timeout < client timeout. Reverse that and you will kill connections while the upstream is still generating — and generate a bill for output nobody reads.
4. Break circuits at the right granularity
Granularity: provider x model x region. A global "model is down" breaker will take healthy traffic offline with the sick traffic.
Trigger: error rate above 50% within a window, with at least 20 samples. Small samples produce false alarms; require volume.
Cooldown: 30–60s before allowing probe traffic.
Half-open probe: admit 1–5 real requests; restore on success, keep the breaker open on failure.
Make breaker state observable. Silent degradation is how you end up with a week of low-quality answers and no incident ticket.
5. Failover in four levels, each with different requirements
| | | |
|---|
| Same model, different provider or region | | |
| Equivalent model at the same tier | | Verified schema, tool format, context window |
| | | Tagged in logs, ideally surfaced in UI |
| Cached or templated response | Explicitly not a model answer | Honest disclosure to the user, never disguised |
The compatibility checklist for L2 — this is where most failovers quietly break:
Structured output: strict JSON-schema enforcement versus prompt-based JSON hinting.
Tool calling: parallel calls, argument validation, tool-name length limits.
System role: some models have no separate system role and fold it into the first user turn.
Multimodal input: image formats, size caps, whether PDFs are accepted natively.
Streaming: delta formats and end-event semantics differ between vendors.
Stop reasons: length versus max_tokens versus stop — your parser must handle all of them.
Context window: a request that is fine on a 1M-token model returns 400 on a 128K model.
6. Deprecation is planned failure
Treat vendor deprecation notices as scheduled outages with a known date.
Track the deprecation announcements and put shutdown dates on a calendar with at least a 30-day migration buffer.
Pin snapshot versions in production; do not put a preview suffix in a path that carries an SLA.
Run shadow traffic: send 5–10% of requests to the successor model and compare output properties, not exact strings.
Recalculate the bill, not just the code. Published examples are blunt about this: when a small audio model was retired, the recommended successor cost roughly three times as much per audio token, and a price-equivalent option existed only behind a different API surface — meaning a rewrite, not a config change. Preview image endpoints have also been shut down on fixed dates, after which requests simply fail.
7. Failed requests still cost money
Three mechanisms worth budgeting for:
- Some providers bill certain pre-output refusals at standard rates.
- When you time out and disconnect, the upstream may have already generated and billed the output.
- Every failed retry attempt is billed like the one that finally succeeded.
So failure rate is a cost metric, not just a quality metric. The single most honest number to put on a dashboard is cost per successful task: total spend divided by completed tasks. It captures retries, failures, and failover overhead in one figure.
8. Log these twelve fields
request_id, upstream request id, provider, model, attempt count, failover level, TTFT, total duration, input tokens, output tokens, finish_reason, cache hit, estimated cost.
Four golden signals, reviewed weekly: success rate, p95 latency, failover rate, cost per successful task.
9. Rehearse the failures
Once a quarter, deliberately break things in staging:
- Inject 429 for 10 minutes; confirm backoff and provider rotation work.
- Inject first-byte delays past your TTFT threshold; confirm the breaker opens.
- Disable the primary provider entirely; confirm L2 failover passes your schema validation.
- Return a truncated
finish_reason: length payload; confirm the client does not treat it as success.
- Change one field name in a structured-output response; confirm validation catches it instead of silently writing bad data.
- Take the primary key out of scope; confirm you get an alert, not a retry loop.
The takeaway
Reliability in a multi-model system is not about preventing every error. It is about making sure that when an error happens, the fallback path was written, tested, and priced before it was needed.
Billing behaviour and deprecation dates referenced above are illustrative examples of public announcements and change over time. Confirm current policy with each provider.