# Configuration reference

The full configuration schema — every top-level block and key, with defaults and meaning, from the annotated example config.

Source: https://tokentriage.com/docs/reference/configuration

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

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.

<Callout type="tip">
Validate before you run: `tokentriage validate --config <file>`. The opt-in blocks (`cache`,
`governance`, `shared_state`, `analytics`) are absent by default — an absent block is zero
behavior change.
</Callout>

## Server & mode

```yaml
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

```yaml
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](/docs/build/providers).

```yaml
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

```yaml
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}`.

```yaml
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

```yaml
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

```yaml
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

```yaml
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)

```yaml
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](/docs/build/caching).

## Guardrails (opt-in)

```yaml
guardrails:
  enabled: false                # master gate; absent => off, zero behavior change
  default_profile: standard     # applied to anonymous/keyless traffic; "" => off
  local_budget_ms: 50           # cumulative request-local embedded detector work; provider wait excluded
  judge:                        # opt-in LLM tier — REFUSED at boot without external: allow
    external: allow
    api_key_env: ANTHROPIC_API_KEY
    model: claude-haiku-4-5
    timeout_ms: 2000
    max_usd_per_request: 0      # deny before send; stock positive caps lack a proven reservation bound
  sidecar:                      # configuration syntax; stock detector activation remains rejected
    external: allow
    url: http://127.0.0.1:8900/classify
    timeout_ms: 200
    model_license: Apache-2.0   # operator declaration, not model/license qualification evidence
  guards:
    - id: pii_in
      category: pii             # pii|secrets|injection|toxicity|topical|code|multimodal|format|custom
      phases: [input]           # input|output|stream — a phase the detector does not declare is
                                #   REFUSED AT BOOT, not degraded at run time
      detectors: [pii.card, pii.email]
      mode: monitor             # enforce | monitor. `simulate` is NOT a configurable mode
      # action: redact          # alternative to ladder below; do not set both
      threshold: 0.5
      severity_bands: [{ min: 0.5, severity: low }, { min: 0.9, severity: high }]
      ladder: { low: observe, high: redact }
      on_error: open            # open | closed — `closed` REQUIRES fail_closed_ack
      stream_fallback: monitor  # monitor | acknowledged closed; stock buffer activation is rejected
      rule_ref: POL-PII-1       # your auditable policy pointer; it rides every fire
  profiles:
    standard: [pii_in]
```

See [Configuring a guard](/docs/guardrails/configuring-a-guard) for the whole binding schema, and
[Guardrails](/docs/guardrails/overview) for what the layer does and does not cover.

## Governance & shared state (opt-in)

```yaml
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](/docs/build/governance) and [Scale-out](/docs/operate/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](/docs/operate/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.

```yaml
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. |

<Callout type="warning">
`fleet` without `shared_state.redis` (`external: allow`) is a validation error. Peer
bearer tokens are supplied out of band as `TT_FEDERATION_TOKEN_<NAME>` env vars.
</Callout>

## Cost-analytics (opt-in)

```yaml
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](/docs/build/cost-optimization).

## Evals & calibration (opt-in)

The `evals:` block wires up [calibration & evals](/docs/calibration/overview): 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. One leaf inside `outcomes` is not the switch it looks like,
and it is called out under the sample.

```yaml
evals:
  outcomes:                    # ingestion doors (require dashboard.enabled)
    api_enabled: true          # a DECLARATION: read at config load, not per request
    otel_ingest: false         # POST /tt/api/v1/outcomes/otel; read per request (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
```

- **`outcomes.api_enabled` is read at config load and nowhere else.** It catches one combination,
  the flag set against a disabled dashboard (`internal/config/evals.go:899`); the handler for
  `POST /tt/api/v1/outcomes` is registered unconditionally
  (`internal/api/dashsrv/outcomes.go:118-122`). Its neighbour `otel_ingest` **is** consulted on every
  request (`internal/api/dashsrv/outcomes_otel.go:441`), so two leaves in one block behave in opposite
  ways. Keep `api_enabled` set, because it is what a reviewer of your config reads, and do not
  reach for it as an off switch.
- **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 a 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](/docs/reference/evals-config) 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](/docs/operate/deployment).)

<NextSteps items={[
  { title: "Scale-out", href: "/docs/operate/scale-out", description: "How the fleet block lights cross-pod /query fan-out and fleet-wide governance." },
  { title: "Evals & calibration configuration", href: "/docs/reference/evals-config", description: "The full evals: block, field by field — evaluators, the air-gap gate, and the calibration knobs." },
  { title: "CLI reference", href: "/docs/reference/cli", description: "tokentriage validate and the rest of the command surface." }
]} />
