The routing table is a configuration file, not application code. Each row maps a task type and difficulty band to an ordered list of models, with a quality threshold and a latency budget per row.
routes:
- task: "ticket_classification"
difficulty: "low" # score < 0.4
quality_bar: 0.92 # agreement with gold labels
latency_budget_ms: 800 # p95 end to end
chain: ["small-fast", "mid-general", "frontier"]
- task: "contract_summary"
difficulty: "high" # score >= 0.7
quality_bar: 0.88
latency_budget_ms: 6000
chain: ["mid-general", "frontier"]
The decision rule is one sentence: for each request, walk the chain in order and take the first model whose measured quality on that task type meets the row's bar and whose recent p95 latency fits the budget. If the head of the chain is degraded, the chain handles it. If the head fails outright, fall back to the next entry.
Two distinctions matter here. Error fallback and latency fallback solve different problems: error fallback moves after an explicit failure, while a time-to-first-token fallback moves when a model has not begun responding within a threshold. The latter protects responsiveness but can create simultaneous or sequential spend, so define the timeout from your application budget and the primary route's normal p95 per Inworld. Record total time across attempts rather than reporting only the successful model.
Also note that routing is not failover. Routing picks the right model for the task. Failover keeps the same task alive when a provider dies. A provider fallback for the same model reduces behavior change, while a model fallback improves availability at the cost of greater output variance per the same guide. Keep those two policies in separate config sections so an outage does not silently rewrite your quality policy.
The cost trap in cheap-first routing is real. If the cheap model consistently fails the quality bar, you pay for the first attempt and the fallback, adding latency and sometimes increasing total cost per Merge's routing overview. Set an alarm on your fallback rate and treat a sustained rise as a broken routing row, not a busy day.
Retry budgets and circuit breakers
Give every request a retry budget in wall-clock time, not attempts. If the budget is 2.5 seconds and the first model has already burned 1.9, the fallback gets 600 milliseconds and the router should skip any model whose recent p95 exceeds that. This single rule prevents the worst failure mode: a slow primary plus two retries that add up to a timeout three times longer than any user will wait.
Circuit breakers sit per model, not per route. Track a rolling window of errors and timeouts; when the failure rate crosses a threshold, open the breaker and send that route's traffic straight to the next chain entry. Close it after a cooldown and a probe request. Keep the breaker state in shared storage so every instance agrees, otherwise half your fleet keeps hammering a dead provider.