Configuration reference
The full TokenTriage configuration schema — every top-level block and its keys, with defaults and meaning, from the annotated example config. Secrets are always env-var names, never values.
Last updated
On this page
The complete schema, from configs/tokentriage.example.yaml. Validation is strict — an
unknown field is an error, not a silent ignore. Every credential field is an environment-variable
name (*_env), never a literal secret.
Server & mode
version: 1 # config schema version; required
mode: ledger # ledger | shadow | route — the trust ladder
server:
listen: "127.0.0.1:8787" # data plane
admin: "127.0.0.1:9090" # /metrics, /health, /tt/api/*
max_body_bytes: 10485760 # bounded request buffer (default 10 MiB)
Upstreams, providers & tiers
upstreams: # a map of named upstreams
anthropic:
base_url: https://api.anthropic.com # REQUIRED (even with provider:)
dialect: anthropic # openai | anthropic (omit when provider: is set)
api_key_env: ANTHROPIC_API_KEY # optional env-var NAME; absent = forward inbound auth
- Provider-breadth fields (all optional):
provider:(names a built-in/inline descriptor),params:(URL-template vars, e.g. Azuredeployment/api_version),region:(Bedrock, required for SigV4),auth:(multi-part env-NAME references),pricing: free(declared-free$0). providers:— optional operator-defined descriptors (dialect,pricing.catalog_prefix,endpoints). See Providers.
tiers: # ordered cheapest -> most capable; names are free-form
- name: cheap
upstream: anthropic
model: claude-haiku-4-5 # model-rewrite target for this tier
# reserved per-tier generation controls: use_reasoning, reasoning_effort
Routing & classifier
routing:
default_tier: frontier # unmatched requests; also the fail-safe on classifier error
on_internal_error: passthrough # passthrough (default) | default_tier
rules: # ordered, first-match-wins (mutually exclusive with policy:)
- id: simple-to-cheap
match: { signal: complexity, below: 0.35 } # AND-ed match keys
route: cheap
mode: route # optional per-rule mode override
overrides: { tier_header: x-tt-tier } # client may force a tier; honored in route mode
cache_affinity: { enabled: true, ttl_seconds: 300 }
cooldown: { enabled: true, seconds: 30, failure_threshold: 0.5, min_requests: 5 }
allow_cross_dialect: false # opt-in openai <-> anthropic translation
# policy: <the v2 routing policy language> — see /docs/build/policy-language
Match keys (all optional, AND-ed): step_class (user_turn|tool_loop|provider_state),
signal: <name> + below/at_least, agent, model (string or glob), has_tools,
header: {name, equals}.
classifier:
ladder:
- type: lexical # stage 1, pure Go, sub-ms; the only v1 stage
calibration_file: "" # artifact from `tokentriage calibrate`; overrides thresholds
oracle: # OFF-PATH labeler; never in the hot path
enabled: false
provider: anthropic
model: claude-haiku-4-5
api_key_env: ANTHROPIC_API_KEY
sample_rate: 0.0
shadow:
duplicate: { enabled: false, sample_rate: 0.01, max_usd_per_day: 5.00, judge: oracle }
slo: { honor_headers: true } # forward-compat only in v1: parsed & logged, never acted on
Ledger
ledger:
decision_log:
path: ~/.tokentriage/decisions.jsonl
rotate_mb: 256
queue: 8192 # async writer queue; overflow drops + counts
usage_injection: true # OpenAI dialect: inject stream_options.include_usage
redact: { prompt: hash } # hash (default) | none — none stores prompt text
dimensions: # business dims from x-tt-user/tenant/feature/tag-<k>
enabled: true
max_tags: 8 # 1..64
promote_to_metrics: [] # bounded-cardinality allowlist, e.g. [tenant]
Dashboard
dashboard:
enabled: false # opt-in; serves /ui/ + /tt/api/v1 on server.admin
auth: { mode: off, session_ttl: 12h } # off | token | users ("off" requires a loopback admin bind)
state_dir: ~/.tokentriage/dash
index: { enabled: true, path: ~/.tokentriage/index/decisions.sqlite } # DERIVED, rebuildable
stream: { client_queue: 1024, max_clients: 32 }
evidence: { max_age: 168h } # promote-gate freshness window
query: { deadline: 10s, row_cap: 1000, max_concurrent: 2 }
capture: { request: false, response: false, ttl: 720h, path: ~/.tokentriage/content } # default hash-only
demo: false
Operational blocks
budgets: [] # id, filter, period (day|week|month), limit_usd, thresholds, enforce
alerts:
channels: [] # {id, type: slack|webhook|email, url_env|smtp_env, ...} — references only
rules: [] # types: spend_threshold | budget_threshold | error_rate | dropped_rows |
# shadow_cap | anomaly | verdict_transition | provider_cooldown | unpriced_rows
retention: { max_age: 0h, max_total_mb: 0 } # 0 = keep forever; rotated files only
export:
otlp: { enabled: false, endpoint: http://localhost:4318/v1/traces, headers_env: TT_OTLP_HEADERS }
instances: [] # federated peers (PREVIEW); {name, url, token_env}
pricing: { overrides_file: "" } # path to a pricing-overlay artifact
Response cache (opt-in)
cache:
enabled: false # master gate; absent => off, zero behavior change
store:
l1: { max_entries: 4096 }
l2: { path: "", max_total_mb: 512, max_entry_kb: 1024 } # max_entry_kb MUST be < server.max_body_bytes
scope: [tenant] # tenant|user|feature|trace|upstream-key (needs ledger.dimensions.enabled)
budgets: { lookup_exact_us: 500, lookup_semantic_ms: 5, judge_daily_usd: 1.0 }
embedders:
potion: { kind: static, table_file: potion-base-8M.tt } # kind http REQUIRES external: allow
rules:
- id: chat-cache
match: { endpoint: chat } # chat|responses only in v1
mode: shadow # off | shadow | on
strategy: [exact, normalized] # or add semantic
ttl: 1h
See Response caching.
Governance & shared state (opt-in)
governance:
enabled: false
require_key: "off" # off | optional | required (restart-required)
store: { path: "" } # "" => <state>/governance.db (restart-required)
keys: { prefix: "ttv_", default_expiry: "", disclose_limits: true, tls_terminated_upstream: false }
tenants: [ { org: acme, teams: [ml], users: { ml: [dev1] } } ]
limits:
- id: org_monthly
attach: { level: org } # level: global|org|team|user|key, or node: <path>
type: budget_usd # budget_usd|quota_tokens|quota_requests|rpm|tpm|concurrency
amount_usd: 1000 # (or amount for rate/quota types)
period: month # day|week|month (ledger) or 1s..1h (rate window)
posture: { mode: shadow } # off | shadow | on (+ enforce: soft|hard, fail: open|closed, thresholds)
override: { allow: true, max_amount_usd: 5000 }
shared_state: # opt-in coordination; each tier REQUIRES external: "allow"
redis: { external: "", addr: "", password_env: "", tls: false }
postgres: { external: "", dsn_env: "" }
See Governance and Scale-out.
Fleet (opt-in, cross-pod read plane)
Heartbeat membership and the coverage-typed read plane that lights the cross-pod
/query fan-out described in Scale-out: peers publish
liveness beats to the redis coordination tier and, with discovery: from_membership,
each replica discovers live siblings (by their advertised dash URL) to fan a /query
across. The block requires shared_state.redis (external: allow) — without a
coordination tier there is nothing to heartbeat to. Every leaf is restart-required;
absent ⇒ zero fleet machinery.
fleet:
heartbeat_ms: 2000 # publish cadence; 0 => default 2000
liveness_window_ms: 15000 # beat-age liveness horizon; 0 => default 15000
discovery: from_membership # "" (off) | from_membership — source peers from live beats
advertise_url: "" # this replica's dash base URL to advertise; "" => none
# must be https or bind loopback (unless peer_transport opens it)
peer_transport: "" # "" (https-or-loopback rule) | plaintext-cluster:allow
| Key | Type | Default | Meaning |
|---|---|---|---|
heartbeat_ms |
int | 2000 |
Liveness-beat publish cadence (ms). 0 ⇒ the default 2000; must be ≥ 0. |
liveness_window_ms |
int | 15000 |
Beat-age horizon: a peer older than this is not live (ms). 0 ⇒ the default 15000; must be ≥ 0. |
discovery |
string | "" |
"" disables peer discovery; from_membership sources peers from live heartbeat cells that advertise a dash URL (beside the static instances: list). |
advertise_url |
string | "" |
This replica’s dash base URL to advertise to peers. "" ⇒ advertise none. Must be https or bind loopback, else refused (unless peer_transport opens it). |
peer_transport |
string | "" |
"" applies the default https-or-loopback rule; the exact string plaintext-cluster:allow (DS11 consent grammar) permits plaintext http peer URLs for membership-discovered same-fleet peers — bearer tokens then transit unencrypted inside the cluster network (validate-time warning + /fleet disclosure). Any other value is a validation error. |
Cost-analytics (opt-in)
analytics:
enabled: true # restart-required
rollups: { enabled: true } # requires dashboard.index.enabled
recommendations:
enabled: true # detection only — applying is always an explicit operator action
interval: 1h
types: [] # empty => all; else: routing-downgrade, cache-opportunity, prompt-caching,
# provider-arbitrage, batch-eligibility, output-cap, reasoning-effort, dedup
forecast: { enabled: true } # banded P10/P50/P90
chargeback:
reports:
- { id: monthly-by-team, dimension: dim.team, period: month, tz: UTC, mode: showback }
export: { focus: { enabled: false } }
See Cost optimization.
Evals & calibration (opt-in)
The evals: block wires the measured-quality substrate: evaluators
that score outputs, the calibration that turns those scores into probabilities, and the drift
detectors that keep them fresh. There is no master enabled — each sub-block self-gates, and an
absent block is zero behavior change.
evals:
outcomes: # ingestion doors (require dashboard.enabled)
api_enabled: true # POST /tt/api/v1/outcomes
otel_ingest: false # POST /tt/api/v1/outcomes/otel (basis: reported)
evaluators:
- id: code-output-nonempty # CODE-kind: local, free — only id + sample_rate
sample_rate: 1.0
- id: judge-groundedness # JUDGE (external LLM): metered => budget mandatory
judge: { external: allow, model: gemini-2.5-flash, api_key_env: JUDGE_API_KEY }
sample_rate: 0.05
max_usd_per_day: 5.0
validation:
require_iaa: false # raise-only judge-validation gate
calibration:
quality:
enabled: true
confidence: 0.90 # nominal 1-α; REQUIRED when enabled — it IS the claim
min_n: 50 # evidence floor; may only be RAISED (default 50)
refit_interval: 24h # scheduled refit cadence; omit/0s => no periodic refit
drift:
detectors: [robust-seasonal-v1, psi-window-v1] # empty => drift UNMEASURED
- Air-gap gate. The literal
external: allowon ajudge:/sidecar:evaluator is the egress opt-in; without it the entry is refused at validation. Credentials are env-var names (api_key_env,endpoint_env), never values. evals runneeds captured bodies.judged_dataset_runre-scores stored outputs, so it requiresdashboard.capture.{request,response}: true(the default is hash-only) — otherwise the run is an honest counted refusal, not an empty success.
The full field-by-field schema — every evaluator kind, the raise-only flags, and the journey examples — is the dedicated Evals & calibration configuration page.
Keys defined only in code
Present in the code/docs but not in the example file (add from their sources as needed):
webhooks:— Standard Webhooks subscriptions ({id, url_env, secret_env, events}); restart-required (a change returns422 restart_required).analytics.reconcile.model_aliases— invoice→ledger model aliases.replica: { name, name_env }— replica identity for the fleet (namewins;name_envnames an env var, e.g.POD_NAME). Not a secret, no external gate.
(Federation PREVIEW is not a tokentriage.yaml key — it is a Helm values key,
experimental.federation, that the chart renders into the config’s instances[].
See Deployment.)