# Routing explanation fields

Look up every line emitted by policy explain, including selector scores, filtered candidates, random draws, overrides, and cascade attempts.

Source: https://tokentriage.com/docs/build/routing-explanation-fields

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';

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](/docs/build/routing-explain).

## 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.

```console
$ 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](/docs/build/routing-selectors#read-the-decision-fields). 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.

```console
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.

```console
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.

<Callout type="warning">

The terminal renderer prints an unpriced cascade stage as `$0.000000`. That does not mean the call
was free. Check the JSON row or dashboard drawer: the stage has an unpriced flag, and the cascade
block counts unpriced stages.

</Callout>

## 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`.

<NextSteps items={[
  { title: 'Explain a routing decision', href: '/docs/build/routing-explain', description: 'Use these fields to diagnose one request from its id.' },
  { title: 'Preview a routing policy change', href: '/docs/build/routing-preview', description: 'Re-evaluate a log under a draft and read the changed-route and cost summaries.' },
  { title: 'Selectors, scorers, and context providers', href: '/docs/build/routing-selectors', description: 'Look up every selector, score term, dependency, and missing-input behavior.' },
  { title: 'Decision ledger fields', href: '/docs/reference/ledger-fields', description: 'Inspect the JSON representation behind the terminal output.' }
]} />
