GitHubBook a demoStart routing
Browse docs

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. Azure deployment/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: allow on a judge:/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 run needs captured bodies. judged_dataset_run re-scores stored outputs, so it requires dashboard.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 returns 422 restart_required).
  • analytics.reconcile.model_aliases — invoice→ledger model aliases.
  • replica: { name, name_env } — replica identity for the fleet (name wins; name_env names 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.)