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

Route by caller, time, or budget

Use request metadata, the clock, or live budget state to choose a tier without inspecting the prompt.

How-to guideLast updated

On this page

Two identical requests may need different models because of who sent them, when they arrived, or how much budget remains. You can route on that context without inspecting the prompt.

Before you start, configure at least two tiers and a valid routing policy; Turn routing on covers that setup. After copying an example, validate it and explain one request in shadow mode:

$ tokentriage validate --config tokentriage.yaml
$ tokentriage policy explain --req-id <request-id>

shadow records the choice without changing live traffic. Each example uses an ordered rules list: TokenTriage checks from top to bottom and applies the first match. A tier names a model configuration, such as cheap or frontier. Checking a rule adds no model calls; a refuse action stops the request before any model is called.

Choose the context you have

Start with the row that matches your goal. Each section below stands alone.

Goal Read this input
give a caller, agent, or requested model its own tier dim.*, trace.*, or request.*
move scheduled batch work to a cheaper tier ctx.time.*
change behavior as one named budget runs low ctx.budget.<id>.*
apply one rule to every caller’s budget ctx.principal.*

Route by caller, agent, or requested model

Request metadata tells you which customer, workflow step, or model name is involved. Use trace.* for agent activity, dim.* for business dimensions such as tenant or plan, and request.* for fields the client sent.

Business dimensions are off by default. Set ledger.dimensions.enabled: true before routing on dim.*; trace.* and request.* do not need that setting.

routing:
  default_tier: frontier
  policy:
    version: 1
    root:
      rules:
        - id: vip-tenant-frontier
          when: 'dim.tenant = "acme" or dim.tag.plan = "enterprise"'
          to: { tier: frontier }
        - id: batch-agents-cheap
          when: 'trace.agent in ("summarizer", "labeler") and trace.step_class != "tool_loop"'
          to: { tier: cheap }
        - id: haiku-family-pin
          when: 'request.model ~ "claude-haiku-*"'
          to: { tier: cheap }
      default: { tier: frontier }

The VIP rule comes first, so it wins when a request is both enterprise traffic and a batch-agent step. Move the rules only when you intend to change that priority. trace.step_class is user_turn, tool_loop, or provider_state; the second rule keeps tool loops on the default tier.

Explain a recorded request and check that the expected rule matched. The read lines show the values used in the decision:

  • rules[batch-agents-cheap]    matched
      read trace.agent = summarizer
      read trace.step_class = user_turn
Matching syntax and missing values

Write membership with parentheses: in ("a", "b"). Glob matching with ~ is case-sensitive. * matches a run of characters but does not cross /, and ? matches one non-/ character. That means claude-haiku-* matches neither Claude-Haiku-4-5 nor the provider-qualified anthropic/claude-haiku-4-5.

When a client omits a business-dimension header, that dim.* value is missing and its comparison is false. With ledger.dimensions.enabled: false, every dim.* value is missing regardless of which headers the client sends. Routing inputs lists every field and when it may be missing.

Use a quality floor instead when the requirement is “whichever model is good enough” rather than “this caller gets this tier.”

Route by the clock

Batch work that runs overnight can accept a slower, cheaper model that daytime traffic cannot.

routing:
  default_tier: frontier
  policy:
    version: 1
    root:
      rules:
        - id: off-peak-batch-cheap
          when: 'trace.agent = "nightly-batch" and (ctx.time.hour_utc >= 22 or ctx.time.hour_utc < 6)'
          to: { tier: cheap }
      default: { tier: frontier }

The example matches from 22:00 through 05:59 UTC and only for nightly-batch. Keep the agent condition unless you intend to move every request during that window.

Time inputs are always present. ctx.time.hour_utc runs from 0 to 23, ctx.time.weekday uses 0 for Sunday, and ctx.time.unix is the arrival time in epoch seconds. The main trap is timezone conversion: the policy always compares UTC, not local time.

Explain a request near the edge of the window and check the recorded hour. Use a budget rule instead when the goal is to control spend rather than schedule work.

Why a replayed decision gets the same answer

TokenTriage records the request’s arrival time and uses that value during evaluation. Replaying the row under the same policy therefore follows the original time branch, not the branch that matches the current wall clock.

Degrade, then decline, as a budget runs out

When a budget is close to exhausted, the useful behaviour is usually to keep serving on something cheaper, and only stop once there is nothing left.

routing:
  default_tier: frontier
  policy:
    version: 1
    root:
      rules:
        - id: budget-hard-stop
          when: "ctx.budget.default.exhausted"
          to: { refuse: { status: 429, reason: "budget_exhausted" } }
        - id: budget-degrade
          when: "ctx.budget.default.remaining_frac < 0.20"
          to: { tier: cheap }
      default: { tier: frontier }

ctx.budget.<id>.* provides three values for a configured budget: remaining_frac, remaining_usd, and exhausted. The example stops exhausted requests first, then moves requests to cheap after less than 20% remains.

Change the budget id, the 20% threshold, or the refusal response to fit your policy. The id must name a configured budget; an unknown id fails validation.

TokenTriage reads an already-computed budget snapshot, so this rule does not call another service for each request. If budget state is unavailable or suspended, the values are missing, the conditions do not match, and the request uses default instead of being declined.

A declined request returns your status, an X-TT-Refused-By-Policy header, and a body containing the reason. It also writes decision.refused_by_policy to the ledger:

{
  "error": {
    "type": "policy_refused",
    "message": "request refused by routing policy: budget_exhausted",
    "refused_by_policy": "budget_exhausted"
  }
}

This policy declines requests only in route mode. In ledger and shadow, TokenTriage records the plan and still serves the request. Use governance when a spending limit must be enforced in every mode.

Route by the caller’s own budget

The previous section names one budget. This rule reads all budgets that apply to the current caller, so the same condition works for every virtual key.

routing:
  default_tier: frontier
  policy:
    version: 1
    root:
      rules:
        - id: budget-aware
          when: "ctx.principal.budget_remaining_frac < 0.10"
          to: { tier: cheap }
      default: { tier: frontier }

ctx.principal.budget_remaining_frac is the smallest remaining fraction across the caller’s key, user, team, organisation, and global budgets. A key that has spent 95% of its budget reads 0.05 and routes to cheap. This input requires governance.enabled: true.

Anonymous or unresolved traffic has no ctx.principal.* values, so the condition does not match and the request uses default.

Do not route on remaining request-rate capacity yet

ctx.principal.rpm_remaining_frac exists in the policy language, but this build always reports it as fully available. A low-remaining condition on that field does not fire. Use budget_remaining_frac for routing, or use governance to enforce request-rate limits.

Reading ctx.principal.* steers the request in route mode. Use governance when the request must be admitted or declined regardless of the routing mode.

Avoid negating a value that may be missing

Most comparisons are false when a context value is missing, but not can turn a missing value into true. For example, not ctx.budget.default.exhausted matches when budget state is unavailable. Prefer a positive numeric condition such as ctx.budget.default.remaining_frac > 0.10, which is false when the value is missing.

How a missing input behaves has the complete truth table.