Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Routing selectors and signal sources

Look up each built-in routing selector and signal source, including its configuration, missing-data behavior, and decision output.

ReferenceLast updated

On this page

Use this reference when you need to configure a select node or understand the values in a routing decision. A tier is a model option you configured. A candidate is a tier still being considered for the current request.

A select node makes its choice in three steps:

  1. from builds the candidate list.
  2. where removes candidates that do not meet your conditions.
  3. by names the selector that chooses one remaining candidate.

This example keeps healthy candidates, then chooses the one with the lowest recently observed latency:

select:
  from: { tiers: all }
  where: "candidate.healthy"
  by: { selector: least_latency, params: { stat: p50 } }
  on_empty: { tier: frontier }

The selector never sees candidates removed by where. If it returns a choice that is missing from its score table, TokenTriage treats the result as a routing fault.

If you are still deciding between rules, selection, splits, and cascades, start with Choose a routing strategy. Use the policy language for the surrounding YAML grammar.

Choose a selector

TokenTriage includes six selectors. Start from the job you need the router to do.

Selector Use it when How it chooses
first_eligible candidates have a deliberate priority order takes the first candidate
weighted you want a fixed traffic distribution draws in proportion to pool weights
least_latency recent response time matters most takes the lowest observed latency
cheapest_meeting price matters after required conditions are met takes the lowest estimated cost
weighted_objective you need to balance cost, latency, and quality combines the axes with weights you set
epsilon_greedy you have graded outcomes and can reserve traffic for learning usually takes the best learned tier and sometimes explores

The selectors compare existing candidate data before the provider request is sent. They do not add another model call.

weighted and epsilon_greedy use seeded randomness. The other four selectors are deterministic for the same candidate data.

When selector data is missing

Missing data does not have one universal meaning. Check this behavior before you enable a selector on live traffic.

Selector What happens
first_eligible reads no selector-specific measurements, so it excludes nothing
weighted treats a missing weight or weight: 0 as 1
least_latency marks an unmeasured candidate cold_start and keeps it eligible
cheapest_meeting excludes an unpriced candidate; a quality floor also excludes missing or insufficient quality
weighted_objective excludes a candidate missing any axis whose weight is above zero
epsilon_greedy keeps an unlearned tier available only for exploration; a quality floor can exclude it

Selector reference

Each entry shows the smallest valid by block, the data the selector needs, and what to inspect on a recorded decision. Open the scoring details when you need formulas, terms, or tie-breaking.

first_eligible

Use first_eligible when the candidate order expresses your preference. It takes the first candidate that passed where.

by: { selector: first_eligible }

Parameters. None. Adding a parameter prevents the configuration from loading.

Data and missing values. The selector reads only the order of the candidates it receives. It does not exclude candidates or depend on runtime measurements.

Check the decision. Look for selector=first_eligible and a position term beside each candidate. The selected candidate should be the first one that was not filtered.

Use another mechanism when you want to retry after an upstream request fails. first_eligible chooses a tier before the request is sent; it is not a retry policy.

Scoring and state

For candidate i in a list of n candidates, the score is (n − i) / n. Higher wins. The scores exist so the recorded decision shows the comparison.

This selector stamps no ctx_ver because it reads no changing runtime state.

weighted

Use weighted to distribute traffic across a pool in fixed proportions.

by: { selector: weighted }

Parameters. None.

Data and missing values. The selector reads each pool member’s weight. A missing weight or an explicit weight: 0 becomes 1. Candidates built from a tier list have no candidate.pool_weight value.

Check the decision. Look for selector=weighted, then compare each candidate’s weight and probability terms with the pool configuration.

Use first_eligible for a priority order or least_latency when measurements should decide the winner.

Scoring and randomness

Each candidate’s score is weight / total. Higher probability means a larger share of requests, not a guarantee about a small sample.

The selector makes one seeded draw, keyed to the trace id, and records it on the decision. It stamps no ctx_ver because pool weights do not change while a request is being evaluated.

least_latency

Use least_latency when recent response time matters more than a fixed priority or traffic share. An upstream is a configured connection to a model provider.

by: { selector: least_latency, params: { stat: p50 } }
Parameter Type Default Notes
stat string p50 p50 or p95
window string none must parse as a duration; currently has no effect

Data and missing values. TokenTriage records response-time observations after returning a response to your application. A candidate with no observations stays eligible and carries a cold_start term. If every candidate is cold, the first one wins.

Warm every candidate with representative traffic before relying on this selector. One practical path is to start with weighted, wait until every upstream has observations, and then switch to least_latency.

Check the decision. Look for latency_p50_ms or latency_p95_ms beside every measured candidate and cold_start beside candidates without observations.

Use another selector when price or answer quality matters. least_latency considers neither.

Measurement, scoring, and state

The selector uses nearest-rank latency over the last 256 observations for each upstream. The raw percentile in milliseconds is the score, so lower wins.

A p95 selector sees candidates measured only at p50 as cold. Ties go to the lowest tier rank, then declaration order.

The decision’s ctx_ver identifies the latency snapshot used for the choice.

cheapest_meeting

Use cheapest_meeting to choose the least expensive candidate among those that passed where.

by: { selector: cheapest_meeting }
Parameter Type Default Notes
default_output_tokens number mirrors the prompt estimate must be above zero when set
min_quality_lcb number none in [0,1]; a calibrated floor
quality_metric string none metric read by the quality floor; requires min_quality_lcb

LCB means lower confidence bound: a conservative quality estimate produced by calibration.

Data and missing values. The selector needs input and output prices for each candidate. It excludes an unpriced candidate and records unpriced. A tier you declared free has a genuine $0 estimate, remains eligible, and can win.

Prompt tokens come from signal.token_est. When that value is missing or not positive, the selector estimates 1000 prompt tokens. The output estimate mirrors the prompt estimate unless you set default_output_tokens.

When every candidate is unpriced, the selector chooses nothing and the node follows on_empty. A configured quality floor also excludes candidates whose measured lower bound is missing or below the floor.

Check the decision. Look for input_cost and output_cost on every priced candidate, plus quality_lcb when you configured a floor.

Use weighted_objective when cost is one part of a trade-off rather than the sole score.

Cost formula, terms, and ties

The score is the estimated cost in dollars, so lower wins:

score = prompt_tokens × price.input_per_tok + est_output × price.output_per_tok

The score terms are input_cost and output_cost, plus quality_lcb when a floor is configured. Ties go to the lowest tier rank, then declaration order.

The selector stamps a ctx_ver only when it reads a calibrated quality floor.

weighted_objective

Use weighted_objective when no single measurement should decide the route. It combines up to three axes—cost, latency, and quality—with weights you set.

by:
  selector: weighted_objective
  params:
    weights: { cost: 0.5, quality: 0.5 }
    quality_prior: { cheap: 0.6, frontier: 0.9 }
Parameter Type Default Notes
weights map required any of cost, latency, or quality; at least one must be above zero
quality_prior map none a quality value you supply for each tier
quality_source string prior prior or calibrated; calibrated cannot be combined with quality_prior
quality_metric string none metric read when quality_source is calibrated
normalize string minmax the only accepted value

How it chooses. The selector converts each active axis to a value from 0 to 1. It reverses cost and latency so that a better result is always higher, applies your weights, and adds the contributions. The highest total wins.

Data and missing values. The cost axis needs prices and a prompt-token estimate. The latency axis reads candidate.latency.p95_ms. Quality comes from your quality_prior or the calibrated candidate.quality.lcb value.

A candidate missing any axis with a weight above zero is excluded. A weight of zero removes that axis completely, including as a reason to exclude a candidate.

Check the decision. Look for cost, latency, and quality terms. They are the weighted contributions, so the visible terms add up to the final score.

Use cheapest_meeting when price alone should decide. It avoids supplying and maintaining another measurement.

Normalization, exclusions, and ties

The score is:

score = Σ weight(axis) × normalized(axis)

Cost uses the same estimate as cheapest_meeting, except the output estimate always mirrors the prompt estimate. Latency uses p95 only.

An excluded candidate carries excluded and a <axis>_absent term naming the first missing axis in alphabetical order. If every candidate has the same value for an axis, that axis normalizes to 1 for everyone and does not break the tie.

Ties go to the lowest tier rank, then declaration order. The decision stamps a ctx_ver when the selector reads latency or calibrated quality.

epsilon_greedy

Use epsilon_greedy when you have graded outcomes and want the router to keep learning. An arm is one eligible tier. The selector usually chooses the learned tier with the highest pass rate and reserves some requests for exploration.

by:
  selector: epsilon_greedy
  params: { epsilon: 0.05, min_samples: 50, reward: shadow_grade }
Parameter Type Default Notes
epsilon number 0.1 in [0,1]; the exploration share
min_samples number 1 graded observations required before a tier can be exploited
reward string none accepts shadow_grade only; does not change scoring in this version
min_quality_lcb number none in [0,1]; optional calibrated admission floor
quality_metric string none metric read by the quality floor; requires min_quality_lcb

How it chooses. An arm is learned when it has a mean and its count reaches min_samples. If at least one arm is learned, a seeded draw against epsilon decides whether to exploit or explore. Exploitation takes the highest learned pass rate. Exploration draws uniformly from all eligible arms.

If no arm is learned, every request explores regardless of epsilon.

Data and missing values. Reward means come from graded shadow-duplication rows for each tier. A tier with no mean, or too few samples, is marked cold and remains available only through exploration. An absent reward_count reads as zero. A configured quality floor can exclude a tier before this choice.

Check the decision. Look for reward_mean and count on each tier. The chosen row carries exploit or explore, plus the reason exploration happened.

Use a fixed selector when you cannot tolerate exploratory traffic or do not yet have a grading pipeline.

Randomness, score terms, and state

The score is candidate.reward_mean, a pass rate from 0 to 1. Higher wins among learned arms.

The selector makes up to two seeded draws with separate salts: one for the exploration decision on every request and another for the arm only when the request explores. Each draw is recorded.

The decision stamps the reward snapshot version as ctx_ver. The reward parameter currently accepts only shadow_grade and does not change the calculation.

Read the decision fields

Run policy explain with a request id to see the selector, every candidate score, and the reason a candidate was filtered or excluded.

$ tokentriage policy explain --req-id tt_route

The selector entries above name the fields that matter for a first check. Open the complete table when you need to interpret a less common term.

All selector score-table fields
Term Value From Meaning
position index first_eligible position in the candidate list
weight the weight weighted the pool weight used, defaulted to 1
probability share weighted that candidate’s chance of being drawn
latency_p50_ms / latency_p95_ms ms least_latency the observed percentile
cold_start 1 least_latency no observations for that upstream
input_cost / output_cost dollars cheapest_meeting the two halves of the estimate
unpriced 1 cheapest_meeting the catalog cannot price that tier
cost / latency / quality contribution weighted_objective the weighted, normalised contribution
cost_absent / latency_absent / quality_absent 1 weighted_objective which axis had no value
reward_mean / count rate, count epsilon_greedy the graded pass rate and its evidence
cold 1 epsilon_greedy not learned: no mean, or below min_samples
exploit / explore 1 epsilon_greedy how this pick was made
explore_epsilon / explore_under_min_samples 1 epsilon_greedy why it explored
excluded 1 weighted_objective or a quality floor not a candidate for the choice
quality_lcb the bound any with a quality floor the admitted lower bound
quality_below_min_lcb the floor any with a quality floor measured, and short of it
quality_absent:<reason> 1 any with a quality floor not measurable, with the reason

quality_absent and quality_below_min_lcb are separate because they describe different problems: no measurement was available, or a measurement was available and fell below the floor.

Request scorers

Selectors choose a candidate. Request scorers do a different job: they inspect the request before routing and emit signal.* values that rules and filters can read. Configure them under routing.policy.scorers. TokenTriage includes semantic and est_cost; any other name prevents the configuration from loading.

semantic

Use semantic to compare a prompt with example phrases for topics you define. It emits one signal.semantic.<topic> value from 0 to 1 for each topic.

scorers:
  semantic:
    budget_ms: 200
    topics:
      legal:
        exemplars: ["draft a contract clause", "review this NDA"]
Key Default Notes
topics.<name>.exemplars required at least one topic with at least one phrase containing scorable words
budget_ms 1 per-request time budget for the whole scorer

Put the threshold in the routing condition, not in the scorer configuration. For example, signal.semantic.legal >= 0.7.

Check the decision. Confirm that the expected signal.semantic.<topic> values are present, then compare them with the threshold in the matched rule.

How semantic matching works

For each topic, the scorer takes the best cosine similarity between the prompt and any configured exemplar. It uses term frequency–inverse document frequency (TF-IDF), a local text comparison that gives less weight to common words. Common English words are removed first.

Words are split on ASCII word characters, so non-ASCII letters act as separators. This can reduce matching quality for prompts and exemplars written in other scripts.

est_cost

Use est_cost when a routing rule needs an approximate request cost before choosing a tier. It emits signal.est_cost in dollars for the reference tier you name.

scorers:
  est_cost:
    reference_tier: frontier
Key Default Notes
reference_tier required must name a configured tier
est_output_tokens 256 the assumed response length
chars_per_token 4 divisor used to estimate prompt tokens from prompt size

The signal is absent when TokenTriage cannot price the reference tier. A reference_tier that does not name a configured tier prevents the configuration from loading.

Check the decision. Look for signal.est_cost and confirm that the reference tier’s catalog entry matches the provider and region you expect.

Estimated-cost formula and pricing lookup
signal.est_cost = (prompt_bytes / chars_per_token) × input_rate
                + est_output_tokens × output_rate

Rates resolve through the reference tier’s catalog prefix and region, not its wire dialect. An Azure or Bedrock upstream therefore uses its own pricing entry.

Cascade outcome scorer

An outcome scorer inspects a model response inside a cascade. TokenTriage supplies outcome.refusal_markers, which is available only in a stage’s accept_when condition.

Use = 0 when a stage should accept only responses with no recognized decline phrase:

accept_when: "outcome.status = 200 and outcome.refusal_markers = 0"

The value counts distinct matching patterns, not the number of refusals. A condition such as outcome.refusal_markers >= 2 means that at least two different patterns matched; one strongly worded response can satisfy it.

How refusal markers are counted

The scorer checks the first 8 KiB of the response case-insensitively against a fixed phrase list, including forms such as “I cannot”, “unable to assist”, and “I must decline”. Curly apostrophes are normalized, so “I can’t” and “I can’t” both match.

The list omits openers such as “as an AI” because they can begin helpful responses. An empty response produces 0 rather than an absent value.

Context providers

Context providers supply ctx.* values about the current request, time, budget, or governance state. TokenTriage registers them automatically when the required feature is configured.

Provider Supplies Registered when
clock ctx.time.hour_utc, .weekday, .unix always
budgetstate ctx.budget.<id>.* per configured budget you configure at least one budget
governstate ctx.limit.<id>.* per global or node-pinned limit governance is enabled and such a limit exists
principalstate ctx.principal.* for this request’s own chain governance is enabled

If budget or governance state is unavailable, the corresponding provider emits no values. A routing condition that depends on an absent value evaluates false. clock reads the request’s arrival time and is always available.

Clock replay and state versions

The clock is derived from the request’s recorded arrival time. Replaying a logged decision therefore reproduces the time-based branch it originally took.

The clock is omitted from the decision’s state stamp. Including a per-request timestamp would change that stamp on every request.

What this page does not cover

This page covers selection mechanisms and the signal producers shipped with routing. Use Routing inputs for every value a policy can read and its absence conditions. Use the policy language for node grammar and terminal targets.

The calibration process that produces candidate.quality.lcb is covered under Calibration and evals.