You cannot measure a misroute until you have written down what a correct route is. That sounds obvious, and it is the step most teams skip. Without a definition, every analyst counts differently, the numbers move whenever a new model is added, and the whole exercise loses the argument with finance.
Start with a routing policy table. For each intent your product recognizes, name the tier you want it to reach (cheap, mid, frontier, or human), the fallback tier if the first call fails or returns low quality, and the acceptance criterion that decides whether the answer counted as good. Write this as data, not prose, so the ledger query can join against it.
| Intent | Expected tier | Allowed fallback | Acceptance criterion |
|---|---|---|---|
| Order status | cheap | mid | Answer matches order lookup, no escalation |
| Refund request | mid | human queue | Refund amount and policy correct |
| Contract clause reading | frontier | none | Reviewer marks cited clause accurate |
| Chit-chat | cheap | cheap | No tool call attempted |
The expected tier is the anchor. Every log row gets a chosen_tier and an expected_tier; a row where they differ is a routing error. Note the difference between a routing error and a quality failure: a request that reached the right tier and still produced a bad answer is a prompt or model problem, and it belongs in a different ledger. Mixing them inflates the routing cost and sends engineers to fix the wrong thing.
Two practical points. First, the policy table should be versioned. If you change which tier handles refunds, past rows must be scored against the policy that was live when they ran, or your historical baseline shifts under you. Second, keep the intent label separate from the free-text user message. Classifiers drift, and you want to be able to replay labeling later without re-reading prompts.



