FifeRouter

1 September 2026 · routing constraints

Fallback chains that cannot reach past a filter

A critical request that exhausts its candidates is refused, not handed to the next model down. The chain is the survivor list, which is why.

Models fail. They time out, they refuse, they return an overflow because the context was longer than advertised. A router has to try the next one.

That is a fallback chain, and it is the third feature in this system that wants to select a model. Ranking and stickiness were the other two. All three are holes in the constraint system if they are built the obvious way.

The obvious way

Rank the candidates, try the first, and on failure walk down the list. The list is the policy row:

moneypath.verify:
  objective: quality
  candidates: [infra-large, vendor-frontier]

infra-large errors, so try vendor-frontier. Reasonable, available, and it has just served a money-path request from a vendor-hosted model — the exact thing residency: in_infra exists to prevent.

The policy row lists vendor-frontier because for a non-critical request that happens to classify as moneypath.verify, it is a fine second choice. The row is a preference order, not a permission list.

The chain is the survivor list

The selection order and the fallback chain are the same object, and it is produced after filtering:

pool  →  hard filters  →  survivors  →  rank  →  chain

For a critical money-path request the survivor set is [infra-large]. That is the whole chain. When infra-large fails there is no next element — not because a rule forbids proceeding, but because the list has ended.

So the request is refused. Not downgraded, not quietly served by something else. CON-hard-filters-outrank-preference holds because a fallback that could reach past a filter would be a hole straight through it, and the first acceptance criterion for the fallback feature tests exactly that.

Refusing is a feature

This is the part that feels wrong the first time. An available model was right there and we returned an error instead.

But consider the alternative from the caller's side. They marked a request critical because it touches money. The system agreed, eliminated the vendor models on residency, tried the compliant one, and it failed. Serving the vendor model at that point means the compliance decision held right up until it was inconvenient — and the caller cannot tell, because the response looks identical.

A refusal is legible. SpendCapExceeded and a residency refusal are both typed errors that say what happened, and a caller can decide whether to retry, wait, or route the work somewhere else. Silence about a downgrade takes that decision away from the person who made the constraint in the first place.

The same reasoning applies to the spend cap, which "refuses rather than downgrades" for a reason stated in the code: a budget control that silently swaps in a cheaper model is an invisible quality change, and the caller cannot tell the difference between "we saved you money" and "your results got worse".

What is logged

Every attempt, including the failed ones:

{"model": "infra-large", "outcome": "error",   "fallback_from": null}
{"model": "vendor-fast", "outcome": "success", "fallback_from": "infra-small"}

BR-fallback-is-logged and BR-log-every-decision between them mean the record shows the attempt that failed, not just the one that worked. So a model that fails a third of the time and is silently covered by its fallback is visible as a rising error rate rather than as an unexplained cost increase.

That matters for a specific case we are living with: content.summarize ranks infra-small first, and there is no in-infrastructure backend, so every one of those requests fails over to vendor-fast. The fallback rate is high and it is configuration, not incident — which is only knowable because the log says which model was tried first and why the second one was used.


← All posts