Cascade escalation
Try the cheap tier; escalate only when the cheap answer looks bad. The confidence gate is the whole point: escalating on every call is strictly worse than starting at the expensive tier, because you pay for both.
The loop
Section titled “The loop”After a successful provider answer, the policy scores it from structural signals only — no second LLM call:
| Signal | Default score | Note |
|---|---|---|
| empty response | 0.0 | always escalates |
| schema validation failure | 0.1 | after one in-place repair attempt |
| provider failure | — | trigger, not a score |
truncated (stop_reason: max_tokens) |
0.55 | deliberately above the threshold: truncation usually means the caller under-budgeted max_tokens, and a bigger model hits the same cap — escalating pays twice for the same truncated answer |
| short response | disabled | see below |
| anything else | 1.0 |
Answers below confidence_threshold (default 0.5) escalate one rung up the
tier’s own ladder, re-entering the failover ring with the next tier’s
candidates. max_escalations (default 1) bounds the walk; roles can opt out
(escalate: false), and callers can per-call ("escalate": false).
Why the short-response check ships disabled
Section titled “Why the short-response check ships disabled”An earlier build scored answers under 8 characters as low-confidence.
“Paris” — a correct one-word answer to a one-word question — scored 0.3,
escalated to a bigger model, and made cost-per-resolved-task worse while
cost-per-call looked unchanged. Emptiness is evidence an answer is bad;
brevity is not. The check exists (min_response_chars) for task classes
whose answers are known to be long, and ships at 0.
The same reasoning keeps hedge_markers empty by default: a marker list is
a guess about one task class, and a wrong guess escalates good answers.
What you see
Section titled “What you see”Escalations are visible everywhere the call is: the response’s
router_decision carries escalations and the full tier_path
(["TINY", "LARGE"]), attempted records the reason
("escalated: empty_response"), the ledger row counts it, and the OTel span
carries the confidence score and escalation reason.
Tuning lives in routing.yaml’s escalation: block — thresholds, triggers,
and per-signal scores are all data.