# Example config

One complete, production-shaped tokentriage.yaml you can copy, trim, and validate — every block active and every line commented, from upstreams and tiers through routing, cache, and governance.

Source: https://tokentriage.com/docs/reference/example-config

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';

The [Configuration reference](/docs/reference/configuration) documents every field one at a
time. This page is the other half: a single, coherent config with **every block active and
every line commented**, shaped the way a real deployment looks. Copy it, delete the blocks you
don't need, and run `tokentriage validate --config tokentriage.yaml` — it fails loudly on an
unknown key or a bad combination, so a trimmed copy stays honest.

<Callout type="key">
This exact file passes `tokentriage validate` with no errors and no warnings. It boots in
**`mode: shadow`** — TokenTriage computes and logs every routing, cache, and governance
decision but changes nothing on the wire. That is the trust ladder's middle rung: you see what
*would* happen before anything enforces. Promote to `route` only once [`eval`](/docs/build/routing#the-honesty-verdict)
is green.
</Callout>

## The complete config

```yaml
# tokentriage.yaml — a production-shaped starting point.
# Boots in shadow mode: every decision is computed and logged, nothing is enforced.

version: 1                      # config schema version — required, currently 1

mode: shadow                    # ledger | shadow | route — the trust ladder.
                                #   ledger: observe only, zero request mutation
                                #   shadow: + compute & log would-route/would-cache/would-refuse
                                #   route:  + enforce (promote here once `eval` proves out)

server:
  listen: "0.0.0.0:8787"        # data plane — proxies your LLM traffic. Serves plaintext
                                # HTTP; terminate TLS in front of it in production.
  admin:  "127.0.0.1:9090"      # admin plane — dashboard, /metrics, /health, /tt/api/*.
                                # Loopback-bound so dashboard auth may be "off" (below).
  max_body_bytes: 10485760      # bounded request buffer; default 10 MiB

upstreams:                      # named upstreams — a map, not a fixed pair.
  anthropic:
    base_url: https://api.anthropic.com
    dialect: anthropic          # openai | anthropic — the wire dialect to speak
    api_key_env: ANTHROPIC_API_KEY   # env-var NAME (never the secret); when set it
                                     # replaces inbound credentials on the way upstream
  openai:
    base_url: https://api.openai.com   # the host and any vendor prefix, and nothing
    dialect: openai                    # else — the dialect's own /v1 arrives on the
    api_key_env: OPENAI_API_KEY        # inbound request and is appended for you

tiers:                          # ordered cheapest -> most capable; names are free-form.
  - name: cheap                 # the tier `routing` downgrades simple traffic to
    upstream: anthropic         # which upstream serves this tier
    model: claude-haiku-4-5     # the model this tier rewrites requests to
  - name: frontier              # the capable default
    upstream: anthropic
    model: claude-opus-4-8

routing:
  default_tier: frontier        # matches-nothing target; also the fail-safe on classifier
                                # error (routing never downgrades on error)
  on_internal_error: passthrough  # passthrough (default) | default_tier
  rules:                        # ordered, first-match-wins, evaluated per request.
                                # (`rules:` and `policy:` are mutually exclusive — pick one.)
    - id: pin-tool-loops        # stable id; appears verbatim in the decision ledger
      match:
        step_class: tool_loop   # user_turn | tool_loop | provider_state
      route: frontier
    - id: simple-to-cheap
      match:
        signal: complexity      # a named classifier signal
        below: 0.35             # threshold; `tokentriage calibrate` can rewrite this
      route: cheap
  cooldown:                     # temporarily divert traffic off a failing upstream
    enabled: true
    seconds: 30                 # cool-off duration
    failure_threshold: 0.5      # trip when >50% of requests fail ...
    min_requests: 5             # ... over at least 5 requests in the current minute

ledger:
  decision_log:
    path: ~/.tokentriage/decisions.jsonl   # JSONL is the system of record; everything
                                           # else (index, dashboard) rebuilds from it
  redact:
    prompt: hash                # hash (default) | none — "none" stores raw prompt text
  dimensions:
    enabled: true               # capture x-tt-user / x-tt-tenant / x-tt-feature / x-tt-tag-*
                                # attribution. Required for real cache/governance isolation.

dashboard:
  enabled: true                 # mount the dashboard + /tt/api/v1 on the admin plane
  auth:
    mode: off                   # off | token | users. "off" is allowed ONLY on a loopback
                                # admin bind (as above); a non-loopback bind rejects it.
  index:
    enabled: true               # the SQLite read index; DERIVED, delete + rebuild any time

cache:                          # TokenTriage's OWN response cache. Absent ⇒ off.
  enabled: true                 # master gate
  scope: [tenant]               # closed vocab: tenant|user|feature|trace|upstream-key.
                                # Needs ledger.dimensions.enabled (set above) to isolate.
  rules:                        # ordered, first-match — the routing-rules idiom
    - id: chat-exact
      match:
        endpoint: chat          # chat | responses (the cacheable endpoints in v1)
      mode: shadow              # off | shadow | on. shadow records would-hits and forwards
                                # anyway — flip to "on" once the hit-rate proves out.
      strategy: exact           # exact | normalized | semantic (or a chain)
      ttl: 1h

governance:                     # virtual keys & per-tenant limits. Absent ⇒ off.
  enabled: true                 # master gate
  require_key: optional         # off | optional | required. RESTART-required to change.
                                # required = default-deny (anonymous traffic gets an honest 401).
  keys:
    prefix: "ttv_"              # display/namespace prefix only; length & charset are fixed
    tls_terminated_upstream: true  # ATTESTATION (not a probe): you terminate TLS in front of
                                   # the data plane, so bearer keys don't cross cleartext HTTP
  limits:                       # HOT-reloadable
    - id: org_monthly           # a TEMPLATE limit — one counter per org
      attach:
        level: org              # global | org | team | user | key (global is the root scope)
      type: budget_usd          # budget_usd | rpm | tpm | quota_tokens | quota_requests | concurrency
      amount_usd: 1000          # decoded to nanodollars, echoed back canonically
      period: month             # day | week | month (UTC) for a ledger-period limit
      posture:
        mode: shadow            # the trust ladder again — watch would-refusals before enforcing
```

<Callout type="tip">
Validate before you ship: `tokentriage validate --config tokentriage.yaml` prints a summary
and exits non-zero on any error. Strict decoding means a **typo'd or unknown key is a hard
failure**, not a silent no-op — so a config that validates is a config that means what it says.
</Callout>

## What each block does

- **`mode`** — the [trust ladder](/docs/concepts/how-it-works) master switch. Start at
  `ledger` or `shadow`; a per-rule `mode:` can enforce one rule while the rest observes.
- **`server`** — two planes on two ports. The data plane (`listen`) proxies traffic; the
  admin plane (`admin`) serves the dashboard and API. Keeping `admin` on loopback is what
  lets `dashboard.auth.mode: off` be safe.
- **`upstreams`** — every provider is a named entry with a `dialect` and an **env-var name**
  for its key (never the key itself). See [Providers](/docs/build/providers) for the built-in
  descriptors (Azure, Bedrock, Gemini, Vertex, …) and [bring-your-own-key](/docs/recipes/add-a-provider-with-your-own-key).
- **`tiers`** — cheapest first. Routing rules point at tier *names*, so you can re-map a tier
  to a different model without touching a rule.
- **`routing`** — ordered rules, a `default_tier` fail-safe, and a `cooldown` that diverts
  traffic off a failing upstream. Prefer the richer `policy:` form for anything branchier;
  see [Policy language](/docs/build/policy-language).
- **`cache` / `governance`** — both are **opt-in and default-off**, and both start in
  `mode: shadow` here so you can watch would-hits and would-refusals before they bite. See
  [Caching](/docs/build/caching) and [Governance](/docs/build/governance).

<Callout type="warning">
`require_key` is **restart-required**, and flipping it to `required` makes anonymous traffic
401 immediately. Stage that cutover — mint `ttv_` keys first, run `optional` in shadow, then
promote. The [troubleshooting page](/docs/reference/troubleshooting) walks the exact sequence.
</Callout>

## Grow from here

This config leaves several honest-by-default surfaces off. Add them when you need them, each
opt-in and each documented in the [configuration reference](/docs/reference/configuration):

- `shared_state` (Redis/Postgres) for [multi-replica coordination](/docs/operate/scale-out) —
  gated behind `external: allow`.
- `analytics` for [cost-optimization recommendations](/docs/build/cost-optimization),
  forecasts, and chargeback.
- `shadow.duplicate` to run a [counterfactual routing `eval`](/docs/build/routing#the-honesty-verdict).
- `export.otlp` to emit [one OTel span per ledger row](/docs/build/observability).

<NextSteps items={[
  {title: "Configuration reference", href: "/docs/reference/configuration", description: "Every field, its type, default, and validator rule — the field-by-field companion to this page."},
  {title: "First routing rollout", href: "/docs/get-started/first-routing-rollout", description: "Take a config like this up the trust ladder ledger → shadow → route with real commands."},
  {title: "Governance", href: "/docs/build/governance", description: "Virtual keys, budgets, and the limit semantics behind the governance block above."},
  {title: "Caching", href: "/docs/build/caching", description: "Scopes, strategies, and the shadow-first on-ramp for the cache block above."}
]} />
