Routing explanation fields
Look up every line emitted by policy explain, including selector scores, filtered candidates, random draws, overrides, and cascade attempts.
On this page
Every policy-language decision stores an explanation on its decision-ledger row. tokentriage policy explain renders that block as terminal text. This page defines the fields in the order they
appear.
For the diagnostic workflow and prerequisite request id, use Explain a routing decision.
Summary fields
The first lines identify the row, its result, and the policy walk that produced it.
| Field | Meaning |
|---|---|
req_id |
request id used to find the ledger row |
policy_rev |
short fingerprint of the policy revision that decided this row |
decision |
rule, tier, upstream, model rewrite, policy refusal, and fail-safe markers that apply to the result |
path |
policy nodes visited in order, ending at the node that produced the result |
trace |
one entry for each node that recorded an event during the walk |
read |
values that were present and consumed by the node’s condition, in first-read order |
policy_rev may be absent on a row written by an older or non-policy router. When it is present,
compare it with the revision printed in the first comment from tokentriage policy migrate --config <path> before debugging against a config file.
The decision line reflects the mode that wrote the row. A matched ledger rule can show
tier=— because ledger mode records the policy evaluation without applying a routing target.
If the row has no explanation block, the renderer prints (no explanation block — a v1/foreign policy decided this row) and stops. No path or trace follows because the row did not store
them.
Rule traces and absent inputs
Rule entries use matched when their condition selected the rule and skipped when it did not.
why=when=false means the condition evaluated to false.
Conditions stop as soon as their result is known. Later terms in an and expression are not read
after an earlier term is false; later terms in an or expression are not read after an earlier
term is true. Only the values consulted before that point appear under read.
An input with no value also makes its comparison false, but rule read lines contain
present values only. The missing input is therefore absent from read. This differs from a
selector’s filtered reason, which can name an absent candidate fact because it explains why that
candidate was removed.
Select nodes
A select node separates candidates removed before scoring from candidates that reached the
selector.
$ tokentriage policy explain --req-id tt_select --log ~/.tokentriage/decisions.jsonl
req_id: tt_select
policy_rev: 8067f04
decision: rule=default tier=mid upstream=openai
path: root → select[tiers]
trace:
• select[tiers] selected
selector=cheapest_meeting
score mid 0.0032 {input_cost=0.0009, output_cost=0.00225}
score frontier 0.0189 {input_cost=0.0054, output_cost=0.0135}
filtered cheap (candidate.latency.p95_ms=3400)
Read the select-specific lines as follows.
| Line | Meaning |
|---|---|
selector |
selector used to rank the candidates that passed where |
score |
candidate that reached the selector, its final score, and the named terms behind that score |
filtered |
candidate removed by where, followed by the input or predicate that rejected it |
ctx_ver |
version string for the runtime-state providers consulted by this node |
In the sample, cheap was removed before scoring because its p95 latency was 3,400 ms. mid and
frontier reached the selector, and mid won because its estimated input and output costs produced
the lower total score.
filtered reasons preserve the condition’s read order. The final part is the predicate that
rejected the candidate. A condition that finished early does not list later predicates.
ctx_ver appears when the selector depends on runtime state or its where clause reads calibrated
quality. It can contain several provider versions joined with semicolons. Two rows with the same
ctx_ver saw the same versions of those providers; rows with different values may have made their
decisions from different state.
The complete score-term vocabulary is in Selectors and scorers. Two common terms are:
| Term | Meaning |
|---|---|
unpriced |
the price catalog could not price the candidate |
excluded |
a configured floor or required input removed the candidate from consideration |
Random draws
Traffic splits and selectors that explore between candidates record each random draw.
path: root → split → arm:canary
trace:
• split split (arm:canary)
roll split#1 scope=trace key=51bcc7b value=0.462589 → arm:canary
The draw line has five parts.
| Part | Meaning |
|---|---|
split#1 |
stable identity of the node that made the draw |
scope |
whether the draw used the trace id or request id as its key |
key |
short hash of the key; the original id is not printed |
value |
deterministic value produced from the policy seed, scope key, and node identity |
| outcome | selected split arm or exploration result |
scope=request under a policy configured with sticky: trace means the request had no trace id,
so the draw fell back to request scope. The recorded inputs and the seed in the config are enough
to recompute the assignment.
Overrides and fail-safes
Lines after the node trace record changes or terminal conditions that happened around the policy walk.
| Line | Meaning |
|---|---|
overridden by=x-tt-tier A→B |
the request header replaced the policy’s tier after evaluation |
overridden by=affinity_pin A→B |
cache-affinity state replaced the policy’s tier after evaluation |
fail_safe_why |
reason the decision used its disclosed fail-safe result |
REFUSED=<reason> |
policy produced a typed refusal instead of a route |
FAIL_SAFE |
decision line marker showing that a fail-safe result was used |
An override can suppress a staged plan. The JSON row keeps that fact even when the terminal text only needs to show the source and tier change.
Cascade attempts
A cascade records every provider attempt, including stages whose response was not accepted.
path: root → rules[tools-cascade] → cascade
trace:
• rules[tools-cascade] matched
read request.has_tools = true
• cascade[try-cheap] stage (to:cheap)
• cascade[final] stage (to:frontier)
cascade: attempts=2 final=frontier
stage try-cheap tier=cheap model=gpt-4o-mini accepted=false cost=$0.000381
stage final tier=frontier model=gpt-4o accepted=true cost=$0.011902
The cascade summary and stage lines have these meanings.
| Field | Meaning |
|---|---|
attempts |
number of provider calls made for this request |
final |
tier whose response was ultimately returned |
stage |
configured stage id |
tier / model |
target used for that provider attempt |
accepted |
whether this stage supplied the returned response |
cost |
cost recorded for this individual attempt |
In the sample, the request made two billable provider calls. The accepted stage’s cost is the cost on the main row. The rejected stage cost is additional cascade spend and is not included in that top-level row cost.
Storage and other surfaces
The explanation is stored inline on the decision-ledger row. These surfaces read that stored block, although they format it differently:
tokentriage policy explain --req-id <id>- the dashboard request drawer on the admin plane
GET /tt/api/v1/requests/{req_id}
There is no separate explain endpoint. POST /tt/api/v1/replay serves a different purpose: it
re-evaluates a log under a draft policy, like policy preview.