Routing inputs
Look up every routing input, where you can use it, its type, and when it may be absent.
On this page
Use this page when you are writing or debugging a routing condition. It lists each supported input, the clauses where you can use it, and the cases where TokenTriage has no value for it.
An input with no value is absent. Absence is different from 0 and false, which are both
present values. Most comparisons with an absent input evaluate to false, but != and not are
important exceptions. How a routing decision is made
shows the complete behavior.
If tiers, upstreams, or routing clauses are new to you, start with the Routing overview. A tier is a named model-and-upstream target; an upstream is the configured provider connection that serves it.
Scope decides what exists
Routing conditions run at different stages of a request. The stage determines which inputs are available.
Read left to right along one request. Request-scope inputs exist as soon as the request
arrives and stay readable everywhere afterwards. Candidate-scope inputs exist only while the
router is comparing tiers, so they are readable only inside a select … where. Outcome-scope
inputs exist only after an attempt has returned, so they are readable only inside a
cascade … accept_when.
Use the clause you are writing to choose the matching scope:
| Clause | Inputs you can read |
|---|---|
rule when or predicate |
request.*, trace.*, dim.*, signal.*, ctx.* |
select … where |
all request-scope inputs, plus candidate.* |
cascade … accept_when |
all request-scope inputs, plus outcome.* |
Using an input outside its scope is different from reading an absent value. An out-of-scope input makes validation fail and identifies the invalid path. An absent input is valid, but it changes how the condition evaluates.
For example, this rule condition reads one request-scope input:
when: 'dim.tenant exists and dim.tenant = "acme"'
If business dimensions are disabled or the request has no x-tt-tenant header, dim.tenant exists
is false. Evaluation stops there and the rule does not match.
How absent values affect conditions
X exists is true only when X has a value. X = false is true only when X is present and
false. If X is absent, both X != false and not X are true. Test exists first when absence
must behave differently from a present false.
Request scope
Request-scope inputs describe the incoming request and the runtime state available before a tier is
chosen. You can read them in a rule’s when, in a predicate body, and inside where and
accept_when.
request.* — what the client asked for
These inputs describe the request body after TokenTriage has parsed it.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
request.model |
string | model name from the request | the request names no model |
request.dialect |
string | parsed request format | not expected; the parser normally sets it |
request.endpoint |
string | classified API endpoint | the endpoint cannot be classified |
request.stream |
bool | whether the client requested streaming | never; false is present |
request.prompt_bytes |
number | combined prompt length in bytes | never |
request.messages |
number | message count | never |
request.system_bytes |
number | system-prompt length in bytes | never |
request.has_tools |
bool | whether the request declares tools | never |
request.tool_count |
number | declared tool count | never |
request.tool_result_count |
number | tool-result message count | never |
request.max_tokens |
number | requested output cap; 0 when unset |
never |
request.reasoning_effort |
string | requested reasoning level | the request sets none |
Numbers and booleans here are present even at zero or false. Only the string fields go absent, and only when they are empty.
trace.* — the agent and the step
Trace inputs connect a request to an agent, conversation, and step. Each value comes from an
x-tt-* header unless the table says otherwise.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
trace.agent |
string | x-tt-agent value |
the header was not sent |
trace.step_class |
string | user_turn, tool_loop, or provider_state |
never; derived when the header is missing |
trace.step_class_source |
string | header or derived |
never |
trace.trace_id |
string | conversation ID | the header was not sent |
trace.step_id |
string | current step ID | the header was not sent |
trace.parent_id |
string | parent step ID | the header was not sent |
trace.w3c |
string | raw traceparent header |
the header was not sent |
dim.* — your business dimensions
Business dimensions are optional labels supplied by your application. They are disabled by
default. Set ledger.dimensions.enabled to true before sending their headers; otherwise every
dim.* input is absent.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
dim.user |
string | x-tt-user value |
dimensions are disabled or the header is missing |
dim.tenant |
string | x-tt-tenant value |
dimensions are disabled or the header is missing |
dim.feature |
string | x-tt-feature value |
dimensions are disabled or the header is missing |
dim.tag.<key> |
string | matching x-tt-tag-<key> value |
dimensions are disabled or the header is missing |
dim.tag.<key> is a family: any single-segment key is accepted, so dim.tag.env works without
being declared anywhere. A key containing a dot is not accepted.
signal.* — the classifier and your scorers
The built-in classifier runs on every request. It always emits these numeric inputs:
| Input | Type | What it holds | Absent when |
|---|---|---|---|
signal.complexity |
number | combined complexity score in [0,1] |
never |
signal.token_est |
number | estimated prompt tokens, calculated as characters divided by four | never |
signal.code |
number | code-vocabulary score | never |
signal.reasoning_markers |
number | reasoning-phrase score | never |
signal.reasoning_marker_count |
number | phrase count behind the reasoning score | never |
signal.technical |
number | technical-vocabulary score | never |
signal.simple |
number | simple-phrase score, which lowers complexity | never |
signal.multi_step |
number | multi-step-structure score | never |
signal.question |
number | question-density score | never |
signal.has_tools |
number | 1 when tools are declared; otherwise 0 |
never |
signal.tool_count |
number | declared tool count | never |
signal.tool_result_count |
number | tool-result message count | never |
signal.message_count |
number | message count | never |
Declaring a scorer adds more: semantic emits signal.semantic.<topic> per topic, and
est_cost emits signal.est_cost. Both are described in
selectors and scorers.
ctx.* — clock and governance state
The clock is always available. Budget, limit, and principal inputs expose governance state to a routing condition. They steer a request but do not decide whether it is admitted.
| Input | Type | Available when | Absent when |
|---|---|---|---|
ctx.time.hour_utc |
number | always | never; 0–23, UTC |
ctx.time.weekday |
number | always | never; 0 is Sunday |
ctx.time.unix |
number | always | never; request arrival, epoch seconds |
ctx.budget.<id>.remaining_frac |
number | that budget is configured | budget state is unavailable or suspended |
ctx.budget.<id>.remaining_usd |
number | that budget is configured | budget state is unavailable or suspended |
ctx.budget.<id>.exhausted |
bool | that budget is configured | budget state is unavailable or suspended |
ctx.limit.<id>.remaining_frac |
number | a global or node-pinned limit with that ID is configured | limit state is unavailable or that limit is suspended |
ctx.limit.<id>.exhausted |
bool | a global or node-pinned limit with that ID is configured | limit state is unavailable or that limit is suspended |
ctx.limit.<id>.suspended |
bool | a global or node-pinned limit with that ID is configured | limit state is unavailable |
ctx.principal.budget_remaining_frac |
number | governance is enabled | anonymous or unresolved traffic |
ctx.principal.rpm_remaining_frac |
number | governance is enabled | anonymous or unresolved traffic; otherwise present as 1 |
ctx.principal.exhausted |
bool | governance is enabled | anonymous or unresolved traffic |
Naming an unconfigured budget or limit ID is a validation error, not an absent read. Admission is a separate governance gate that runs in every mode regardless of the routing policy.
Candidate scope
Candidate-scope inputs describe one tier while a select node filters its choices. They are
readable only inside select … where.
Identity and eligibility
Use these inputs to identify a candidate and check whether it can serve the request.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
candidate.tier |
string | tier name | never |
candidate.upstream |
string | configured upstream name | never |
candidate.model |
string | model assigned to the tier | never |
candidate.rank |
number | position in the declared tiers: list |
never |
candidate.pool_weight |
number | configured pool weight | the candidate set came from a tier list rather than a pool |
candidate.healthy |
bool | whether cooldown and request-format checks both pass | never |
Cost, latency, and rewards
These inputs support cost, performance, and learned-outcome comparisons.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
candidate.price.input_per_tok |
number | catalog input price in dollars per token | the catalog cannot price that tier |
candidate.price.output_per_tok |
number | catalog output price in dollars per token | the catalog cannot price that tier |
candidate.latency.p50_ms |
number | median observed latency in milliseconds | that upstream has no observations |
candidate.latency.p95_ms |
number | 95th-percentile observed latency in milliseconds | that upstream has no observations |
candidate.reward_mean |
number | graded pass rate in [0,1] |
that tier has no graded outcomes |
candidate.reward_count |
number | number of graded outcomes behind the pass rate | that tier has no graded outcomes |
Calibrated quality
These six inputs come from the same calibration result. If no result is available for the candidate, all six are absent.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
candidate.quality.lcb |
number | lower confidence bound used for quality floors | no calibration result is available |
candidate.quality.n |
number | sample count stored with the bound | no calibration result is available |
candidate.quality.calibrated |
number | 1, marking the value as calibrated |
no calibration result is available |
candidate.quality.confidence |
number | nominal confidence stored with the result | no calibration result is available |
candidate.quality.method |
string | calibration method that produced the bound | no calibration result is available |
candidate.quality.estimand |
string | type of guarantee represented by the bound | no calibration result is available |
Inputs that are not available
Two candidate names pass validation but currently have no value. A positive comparison against
either one evaluates to false. The != and not exceptions described earlier still apply.
| Input | Type | What happens | Use instead |
|---|---|---|---|
candidate.provider |
string | always absent | candidate.upstream |
candidate.max_context |
number | always absent | request.prompt_bytes with a threshold you set for each tier |
candidate.quality.mean is different: it is not a readable routing input. Using it in a when,
where, accept_when, predicate body, or selector dependency produces a validation error. Route on
the calibrated lower confidence bound, candidate.quality.lcb, instead.
Outcome scope
Outcome-scope inputs describe one completed cascade attempt. They are readable only inside that
stage’s accept_when. The optional judge is a model-based quality check requested by the cascade
stage.
| Input | Type | What it holds | Absent when |
|---|---|---|---|
outcome.status |
number | HTTP response status | never |
outcome.response_bytes |
number | response size in bytes | never |
outcome.latency_ms |
number | attempt latency in milliseconds | never |
outcome.output_tokens |
number | measured output-token count | usage cannot be measured for that attempt |
outcome.finish_reason |
string | provider finish reason | the provider sends none or the value is unrecognized |
outcome.judge.available |
bool | whether the optional judge returned a result | the stage did not set judge: true |
outcome.judge.ok |
bool | optional judge’s pass-or-fail result | the stage did not set judge: true |
outcome.refusal_markers |
number | distinct decline patterns in the first 8 KiB of the response | never |
What this page does not cover
This page lists values; it does not teach the syntax that reads them or the components that produce them. See the policy language for operators and grammar, selectors and scorers for the producers and consumers of routing inputs, and ledger fields for the recorded result.