Where Claude Code keeps its prices, and how the vocab is pinned to them
Issue #3878. The divergence review called models.vocab.json a second source of truth beside CC's own pricing and suggested reading CC's numbers instead. Measured on CC 2.1.258: there is nothing to read, so the fix is a pin, not a redirect.
Three surfaces, measured on the 2.1.258 binary
| surface | what it is | readable from outside? |
|---|---|---|
| baked catalog | a pricing_tiers table (8 tiers with input, output, 5m and 1h cache write, cache read) plus rows like {id:"claude-fable-5-1",…,pricing:"tier_10_50_cache_read_0_25",…}, 19 rows | only by scanning the binary |
modelPricing managed setting | multiplier in (0, 1] plus per-model overrides; log lines say "priced at the managed modelPricing setting; catalog: list price" | an enterprise override, not a source of list price |
claude -p --output-format json | modelUsage[model].costUSD with costBasis: "list"; one haiku call reproduced list arithmetic to the cent | yes, but it costs a real API call, so never in CI |
The tier table, as read from the 2.1.258 binary
Tier names are opaque labels: haiku_45 sits beside tier_10_50_cache_read_0_25, and the first draft of this canary, which decoded names as a grammar, failed on haiku 4.5 within one run. The binary carries the numbers themselves in a pricing_tiers table, so the canary reads that and never guesses. Pick a tier to see what the vocab must say.
Dollars per million tokens. The vocab stores input, output, cache read and the 5-minute cache write; the 1-hour write (2x input) is in the binary but not in the vocab.
The canary
scripts/check-model-pricing-tiers.mjs --check runs in the nightly CC Contract Probe, the one workflow that installs the binary. It reads the pricing_tiers table and every {id:"claude-…" row with a tier name, matches vocab models by row id or by first_party id, and compares four numbers.
| exit | verdict | meaning |
|---|---|---|
| 0 | OK | every priced vocab model with a catalog row matches |
| 1 | DRIFT | a vocab row disagrees with the binary; fix the vocab |
| 2 | CANNOT-OBSERVE | no binary, or no rows parsed; nothing was verified and the gate says so |
| 3 | UNKNOWN-TIER | a catalog row names a tier the table does not define; read the binary before changing anything |
Not in this change: honouring the managed modelPricing override inside the cost estimator, so an org-contract user sees the same number in the plugin as in /cost. That stays open on #3878.