On this page
- Retry, provider fallback, and model fallback are different
- Retry
- Provider fallback
- Model fallback
- Start with a failure taxonomy
- Route on capabilities before price or speed
- Represent the request contract explicitly
- A model name is not enough to describe an endpoint
- Health is not binary
- Backoff before fallback can be useful—but not always
- Never retry indefinitely
- Streaming changes the fallback boundary
- Tool calls are the hard safety boundary
- Fallback after a tool result can also change behavior
- Replay from the original user request
- Continue using the existing tool result
- Model switching can invalidate the context budget
- Reasoning settings are not portable enums
- Prompt caching and routing pull in opposite directions
- Privacy constraints are hard filters, not score bonuses
- User choice should survive routing
- Exact mode
- Provider fallback mode
- Model fallback mode
- Automatic routing mode
- A practical routing algorithm
- What OpenRouter demonstrates
- Measure the router, not just the model
- A fallback checklist
- Where BYOKchat fits
- Further reading
A multi-provider AI app can survive outages by trying another provider or model, but reliable fallback is not the same as retrying the same HTTP request somewhere else.
The second destination may use a different model family, tokenizer, context limit, tool protocol, safety behavior, reasoning format, prompt-caching state, or data-retention policy. If the first attempt already streamed text or executed a tool, replaying it can also duplicate user-visible output or side effects.
A production routing system therefore needs to answer three questions before every fallback:
- Why did the first attempt fail?
- Is the next candidate semantically compatible with this request?
- Is replaying the request still safe?
The routing algorithm comes after those questions, not before them.
Retry, provider fallback, and model fallback are different
These terms are often mixed together.
Retry
Send essentially the same request to the same logical destination again.
Examples:
- retry a transient network failure;
- retry after a short
429delay; - reconnect to the same provider region.
Provider fallback
Keep the logical model/capability target but use a different serving provider or endpoint.
This is common in gateways where the same model may be hosted by several inference providers.
Model fallback
Change the model itself.
For example, a request intended for one reasoning model is sent to a different model because the preferred model is unavailable.
Those operations have different risk levels:
same model + same provider retry
↓
same model + different provider
↓
different model + same family
↓
different model + different provider/family
The farther down the list you go, the more assumptions you must revalidate.
Start with a failure taxonomy
Routing code should not treat every error as “try the next one.”
A useful classification is:
| Failure | Usually retry? | Usually fallback? | Notes |
|---|---|---|---|
| DNS / connect timeout | Yes | Yes | No model work may have started |
Provider 5xx | Often | Often | Use bounded retries/backoff |
429 rate limit | Often later | Often | Respect retry hints where provided |
Authentication 401 | No | Maybe | Another configured provider may still work |
Authorization 403 | Usually no | Maybe | Could reflect account/model policy |
Invalid request 400 | No | Rarely | Fix request; another model may reject it too |
| Context too long | Not unchanged | Maybe | Requires smaller context or larger-window model |
| Unsupported tool/parameter | No | Yes, if candidate supports it | Capability routing problem |
| Safety refusal | No automatic retry | Policy-dependent | Do not route merely to evade safeguards |
| Mid-stream disconnect | Dangerous | Dangerous | Partial output may already be visible |
| Tool side effect already executed | Dangerous | Dangerous | Requires idempotency/explicit recovery |
The classification should be based on both HTTP status and provider-specific error semantics.
See AI API Error 401 vs 403 vs 429 for the underlying error categories.
Route on capabilities before price or speed
A candidate is not a fallback if it cannot satisfy the request contract.
Before considering latency or cost, filter by hard requirements such as:
- required input modalities;
- minimum context window;
- structured-output support;
- tool/function calling;
- parallel tool calls;
- reasoning controls;
- streaming support;
- maximum output size;
- data-retention constraints;
- regional or enterprise policy;
- compatible attachment formats.
Only then rank the remaining candidates.
Conceptually:
This avoids a common failure mode: selecting the cheapest model first and discovering during execution that it cannot call the required tool.
Represent the request contract explicitly
Do not infer requirements by inspecting random fields deep inside provider JSON every time you route.
Create a provider-neutral contract:
type RequestRequirements = {
streaming: boolean;
minContextTokens?: number;
modalities: Set<"text" | "image" | "audio">;
tools: boolean;
structuredOutput: boolean;
reasoning: "none" | "optional" | "required";
dataPolicy?: "standard" | "zdr-required";
};
Each model/provider endpoint can advertise a capability profile. Routing becomes a compatibility operation between requirements and capabilities.
This is more robust than provider-name conditionals scattered through the chat code.
A model name is not enough to describe an endpoint
Even the “same model” can behave differently across serving providers.
Differences may include:
- available context length;
- supported request parameters;
- tool support;
- quantization or serving configuration;
- throughput and queueing;
- regional location;
- logging/data policy;
- prompt caching;
- rate limits;
- release/version lag.
A router should therefore model an endpoint approximately as:
(model identity, provider identity, capability profile, policy profile, health)
not just:
model = "some-model"
Gateways such as OpenRouter expose provider-routing controls for exactly this reason: callers can constrain providers, require parameter support, prioritize latency/throughput/price, and allow or disable fallback.
Health is not binary
An endpoint can be technically online while performing badly.
Useful health signals include:
- connection-error rate;
5xxrate;429rate;- p50/p90/p99 time to first output;
- output throughput;
- recent tool-call failures;
- recent malformed-stream rate;
- current provider status signals.
Do not permanently eject a provider after one failure. Use a rolling window or circuit breaker.
A simple circuit-breaker lifecycle is:
The router should also distinguish endpoint health from request incompatibility. A context-length error does not prove the provider is unhealthy.
Backoff before fallback can be useful—but not always
For a transient same-provider failure, bounded retry with exponential backoff and jitter can prevent unnecessary model switching.
A typical delay shape is:
where:
- is the initial delay;
- is the retry attempt;
- caps the delay;
- is random jitter.
But interactive chat has a latency budget. Waiting 8 seconds to retry a saturated provider may be worse than immediately falling back to a healthy equivalent endpoint.
So routing policy should know the product goal:
batch job → tolerate longer backoff
interactive chat → prefer faster failover
high-value reasoning task → preserve preferred model if practical
Never retry indefinitely
Retries multiply load during outages. Every retry should have:
- a maximum attempt count;
- a total wall-clock budget;
- cancellation support;
- an exclusion set so the router does not bounce between the same failed endpoints.
For example:
type AttemptContext = {
attemptedEndpointIDs: Set<string>;
startedAt: number;
deadline: number;
maxAttempts: number;
};
The final error should retain the attempt history for diagnostics without leaking secrets.
Streaming changes the fallback boundary
The safest fallback occurs before semantic output has been emitted.
If Provider A fails while connecting, Provider B can usually receive the same request without the user noticing.
If Provider A already streamed:
The most likely cause is the lockfi...
and Provider B starts over, the new answer may diverge:
This looks more like a Node runtime mismat...
A client cannot splice those streams together as though they were one model response.
Use a generation state such as:
no output yet → transparent fallback may be safe
partial text → mark interrupted; explicit regenerate/continue
reasoning only → provider-specific policy; visible output may still be empty
completed tool call→ replay safety must be checked
For the full streaming state model, see How AI Streaming Works.
Tool calls are the hard safety boundary
Suppose a model calls:
send_invoice(customer_id=42)
The tool executes successfully. Before the model emits its final response, the stream drops.
A naive fallback replays the original prompt at another model. That model may decide to call send_invoice again.
This is not a theoretical edge case. Agentic routing needs idempotency.
For mutating tools, use one or more of:
- idempotency keys;
- durable tool-call IDs;
- application-level deduplication;
- explicit “already executed” state in continuation context;
- human confirmation before replay;
- no automatic fallback after irreversible side effects.
A router should know whether execution has crossed a side-effect boundary.
Fallback after a tool result can also change behavior
Even a read-only tool call changes the model trajectory.
Model A may have requested search_docs(query="cache invalidation"). After receiving the result, it fails.
You have two broad options:
Replay from the original user request
Model B chooses its own tool strategy.
Pros:
- clean model-native reasoning path.
Cons:
- repeats retrieval/tool work;
- may increase cost/latency;
- mutating tools may be unsafe.
Continue using the existing tool result
Provide Model B with the known result and enough neutral context to continue.
Pros:
- avoids repeated work.
Cons:
- requires a portable tool transcript;
- provider-specific reasoning state may not transfer;
- Model B did not choose the original call.
There is no universal answer. The application must distinguish portable evidence from provider-internal state.
Model switching can invalidate the context budget
A fallback model may have a smaller context window.
If the original request was constructed for a model with capacity and the fallback has capacity where , the same request can fail immediately.
Routing should either:
- exclude candidates that cannot fit the prepared context; or
- rebuild context using the fallback model’s budget before sending.
The second option can involve summarization or dropping lower-priority history, which changes semantics. That should be an explicit context-management decision, not a hidden router side effect.
Reasoning settings are not portable enums
Providers expose reasoning differently. Even when two APIs both have a setting called “effort” or “thinking,” their levels and semantics are not guaranteed to match.
Do not assume:
provider A: high == provider B: high
A provider-neutral client can model user intent at a higher level:
fast / balanced / deep
and let each adapter map that intent to supported provider controls.
If a fallback candidate lacks a required reasoning feature, either exclude it or clearly degrade according to policy.
Prompt caching and routing pull in opposite directions
Routing to the currently fastest provider can throw away a warm prompt cache at the previous provider. Staying sticky to one provider can improve cache reuse but hurt availability if that endpoint becomes unhealthy.
A mature router balances:
cache locality
availability
latency
throughput
cost
privacy
user preference
There is no single optimal ordering for every product.
Some gateways expose sticky routing or per-provider preference controls. If you build routing yourself, record whether a candidate has likely cache locality, but never prefer a broken endpoint merely to preserve a cache hit.
See AI Prompt Caching Explained.
Privacy constraints are hard filters, not score bonuses
Suppose the user requires a Zero Data Retention endpoint.
A router must not say:
Provider B is 100 ms faster, so use it even though it does not satisfy the required data policy.
Policy constraints should eliminate candidates before scoring.
The same applies to:
- allowed regions;
- approved providers;
- direct-BYOK-only requirements;
- enterprise endpoints;
- local-only conversations;
- disallowed data collection.
Availability does not justify silently crossing a privacy boundary.
User choice should survive routing
If a user explicitly chose a provider/model, automatic routing should respect what that choice means.
There are several product modes:
Exact mode
Use exactly this model/provider or fail.
Useful when reproducibility, billing ownership, or privacy is more important than uptime.
Provider fallback mode
Keep the logical model but allow another approved endpoint/provider.
Model fallback mode
Allow an ordered list of compatible alternatives.
Automatic routing mode
Let policy choose from a capability-compatible pool.
Do not present an exact model selector while secretly switching models without surfacing it somewhere in the result metadata.
A practical routing algorithm
A clear router can be expressed in stages:
function chooseCandidates(request, endpoints) {
return endpoints
.filter((x) => satisfiesCapabilities(x, request.requirements))
.filter((x) => satisfiesPolicy(x, request.policy))
.filter((x) => !request.attempts.has(x.id))
.filter((x) => !isCircuitOpen(x))
.sort((a, b) => score(b, request) - score(a, request));
}
Then the execution loop handles errors according to classification:
for (const endpoint of chooseCandidates(request, endpoints)) {
const result = await attempt(endpoint, request);
if (result.ok) return result;
if (!isReplaySafe(result, request)) throw result.error;
if (!isFallbackEligible(result.error)) throw result.error;
}
throw new Error("No compatible provider succeeded");
The important functions are not sort() and score(). They are satisfiesPolicy, isReplaySafe, and isFallbackEligible.
What OpenRouter demonstrates
OpenRouter is a useful real-world example because its routing API makes several of these dimensions explicit.
Current routing controls can include concepts such as:
- provider ordering;
- whether fallbacks are allowed;
- requiring support for request parameters;
- restricting providers;
- privacy/ZDR constraints;
- sorting by price, throughput, or latency;
- model fallback lists.
That does not mean every client should reproduce OpenRouter’s router. It demonstrates that production routing is a policy system, not one try/catch block.
Measure the router, not just the model
Useful routing telemetry includes:
requested model/provider
selected endpoint
fallback depth
failure category per attempt
TTFT per attempt
final TTFT
completion duration
cache read/write usage
final model/provider
whether tools had executed before fallback
user-visible interruption rate
Then ask questions such as:
- How often does the primary fail?
- Which fallback actually recovers requests?
- How much latency does failed first-attempt work add?
- Are
429s concentrated on one account/key? - Do fallbacks increase tool failures?
- How often does a model switch change output quality?
A router that “succeeds” 99.9% of the time can still be poor if 20% of requests take an extra 8 seconds because it always waits on an unhealthy primary first.
A fallback checklist
Before enabling automatic fallback, verify that:
- errors are classified rather than treated uniformly;
- capability and policy requirements are explicit;
- context fits the fallback model;
- provider-specific reasoning state is not assumed portable;
- partial streams are not silently spliced together;
- mutating tools have idempotency/replay protection;
- retries are bounded by attempt and time budgets;
- provider health uses rolling signals;
- privacy requirements are hard constraints;
- the final model/provider is recorded;
- users can choose exact/no-fallback behavior when it matters.
Where BYOKchat fits
A BYOK client already has an important routing primitive: explicit provider connections owned by the user. That makes it possible to keep routing policy above provider adapters instead of burying fallback behavior inside one SDK.
The key architectural goal is that switching providers does not require changing the persisted conversation model, while the client still respects differences in capabilities, context, tools, credentials, and policy.