Skip to content

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.

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.

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.