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

Configuration in production

Check a config before it reaches a cluster, know which checks only run at boot, and read the banner that tells you what actually started.

How-to guideLast updated

On this page

A configuration that is wrong in production fails in one of two places, and they are not equally pleasant. It either fails on your terminal, where you fix it, or it fails at boot, where the pod crash-loops and your traffic goes nowhere.

This page is about pushing every failure into the first category: what to run before you deploy, which checks only the running binary can make, and how to confirm from the outside that the thing which started is the thing you meant to start.

Prerequisites: the tokentriage binary somewhere you can run it: your laptop, a CI job, or the pod itself. Nothing here needs a cluster.

Check it before it ships

tokentriage validate loads a config exactly as boot does and prints what it resolved:

$ tokentriage validate -c tokentriage.yaml
mode:      ledger
listen:    127.0.0.1:8787   admin: 127.0.0.1:9090
upstreams: 1
  - anthropic    anthropic http://127.0.0.1:47811
tiers:     0 (cheapest -> most capable)
rules:     0
OK: configuration is valid

Read the summary as well as the last line. It is the config as the binary understood it, so a typo that lands in the wrong block shows up as a missing upstream or a zero tier count rather than as an error.

Decoding is strict: an unknown key is a failure, never a silent ignore.

$ tokentriage validate -c tokentriage.yaml
Error: configuration is invalid: config: yaml: unmarshal errors:
  line 32: field rotate_mb not found in type config.Config

That was rotate_mb written at the top level instead of under ledger.decision_log. A looser loader would have accepted the file and rotated at the default forever.

Mode-dependent requirements are checked too, which is what catches a half-finished promotion up the trust ladder:

$ tokentriage validate -c tokentriage.yaml
Error: configuration is invalid: config tokentriage.yaml: tiers: at least one tier is required in shadow mode
routing.default_tier: required in shadow mode

Both errors are printed, not only the first. The exit code is 1, so validate && deploy works in a pipeline.

Check the config a chart will actually mount

On Kubernetes the file you wrote is not the file that runs. The chart renders a ConfigMap from your values, injecting the keys it owns. Validate that instead:

$ helm template tt deploy/helm/tokentriage -f my-values.yaml \
  | python -c "import sys,yaml;[print(d['data']['tokentriage.yaml']) for d in yaml.safe_load_all(sys.stdin) if d and d.get('kind')=='ConfigMap']" \
  | tokentriage validate --config -
mode:      ledger
listen:    0.0.0.0:8787   admin: 0.0.0.0:9090
upstreams: 1
  - anthropic    anthropic http://mock-upstream:8080
tiers:     0 (cheapest -> most capable)
rules:     0
OK: configuration is valid

Note what the chart injected: server.listen and server.admin bind 0.0.0.0, because loopback is unreachable from outside a container. That upstream is a local mock, which is why it is a plain service name.

The chart owns a small set of keys: server, shared_state, instances, dashboard.auth and cache.store.l3. It refuses them inside config: with a message naming the typed value to use instead. Everything else in config: passes through verbatim.

What validate does not catch

Validate is necessary and not sufficient, and the gap is worth naming rather than discovering.

Checked by validate Checked only when the engine starts
strict decoding, unknown keys descriptor-aware provider checks
required fields per mode the routing-policy compile matrix
bounds and enum vocabularies governance and limit registry names
credential fields are env references detector and evaluator registry names

A green validate means “this will not fail for a structural reason”. tokentriage doctor closes most of the remainder. It runs the same registry compile the boot path runs, so it catches a routing policy that decodes but cannot compile:

$ tokentriage doctor -c tokentriage.yaml
✓ binary — tokentriage dev (none), embedded pricing snapshot litellm@2026-07-14#179affb — clean
✓ config — tokentriage.yaml — strict decode clean: mode ledger, 1 upstream(s), 0 tier(s), 0 rule(s)
✓ compile — provider registry + routing policy compiled — 1 upstream(s) wired — clean
✓ ports — listen 127.0.0.1:8787 bindable, admin 127.0.0.1:9090 bindable — clean
✓ ledger — ./data/decisions.jsonl — not yet created; parent dir exists, created on first request — clean
✓ pricing — embedded table litellm@2026-07-14#179affb loaded, no overrides configured — clean
✓ keys — 1 upstream(s), no *_env credential references (inbound forwarded) — clean

ok — all checks passed (exit 0)

That capture is trimmed and comes from a source build, so a release prints its version in place of dev. Diagnose a running instance covers the full report and the checks that only make sense against something already running.

Read the boot banner

The banner is the cheapest verification surface you have: it is what the process decided, printed once, in the log you already collect.

tokentriage dev · mode: ledger
proxy: http://127.0.0.1:8787   (point your app's base_url here)
dashboard: http://127.0.0.1:9090/ui/
ledger: ./data/decisions.jsonl

Four facts, each of which you can be wrong about: the mode you are in, where traffic should point, whether the dashboard came up, and which file the ledger is being written to. That last line is the one to check after any change to ledger.decision_log.path.

The banner also reports partial failure. Start an instance whose admin port is already taken and it says so, without failing:

dashboard unavailable: listen tcp 127.0.0.1:9090: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted. — proxy traffic unaffected (run: tokentriage doctor)
tokentriage dev · mode: ledger
proxy: http://127.0.0.1:8788   (point your app's base_url here)
ledger: ./data/decisions.jsonl

The dashboard: line is gone and the data plane still serves. A request to :8788 returned 200 in that state. That long bind message is Windows’s own wording, quoted as captured; Linux says address already in use in the same position.

Zero-config quickstarts turn things on that a file does not

The quickstart flags exist so a first run needs no file, and they are not equivalent to an empty config:

$ tokentriage run --anthropic
$ tokentriage run --simple ollama/llama3.1:8b --complex anthropic/claude-sonnet-4-6

The difference that matters in production is the dashboard. The binary’s own default is dashboard off; a quickstart turns it on (add --no-dashboard to opt out), and the Helm chart also ships it on and secured. A config file you wrote yourself has it off unless you wrote dashboard.enabled: true.

The two are not the same thing, though. A quickstart’s dashboard binds loopback with auth off, which is right for a laptop and wrong for anything else; the chart flips it to authMode: token and refuses to express an open one. Do not carry a quickstart into production and assume it arrived secured.

That is not only a UI question. The retention sweeper is started as part of the dashboard composition, so a headless deployment rotates the ledger but never prunes it. The decision ledger on disk covers the consequence.

Changing config on a running deployment

Where the config file is writable, on a host install or a Compose bind-mount, the admin plane exposes the same config as a contract: POST /config/validate on bytes you send, POST /config/diff against what is running, GET /config/running for the file verbatim with its generation, and POST /config/apply behind a compare-and-swap on that generation.

A change set containing anything restart-required is refused with 422 restart_required before any write, naming the exact paths. Nothing is written and traffic is untouched, so the decision to restart stays yours.

On Kubernetes the mount is read-only and every deployment-critical key is restart-required, so the answer is always the same: change the value and helm upgrade. Upgrade and roll back is that procedure.

Where the config lives on each deployment shape
Shape Path Writable How you change it
host binary wherever --config points yes edit the file, or the admin config API
Compose /etc/tokentriage/tokentriage.yaml, a bind-mount as mounted edit the mounted file and restart
Kubernetes /etc/tokentriage/tokentriage.yaml, a ConfigMap projection no edit values, helm upgrade

The container images bake a config at that path so docker run works with no mount; a real deployment mounts over it. The full key reference is the configuration reference.