Repository navigation
Description
Symptom
Claude usage shows "Unavailable" / $— instead of a cost, for every current Claude Code user. The token counts are correct; only the cost is missing.
Root cause
claude-opus-5 and claude-sonnet-5 are absent from ModelPricing.table (Sources/PokeTokenBar/Core/ModelPricing.swift).
Chain:
- Claude Code
*.jsonlrecords carry no explicit cost field, soEntry.explicitCostisnil. ModelPricing.estimatedCost(model: "claude-opus-5", …)returnsnil— no table row, and the family fallback no longer exists.LocalUsageReader.Bucket.addtakes the last branch and merges.unavailableintocostCoverage.UsageCost.texthitscoverage.unknown && !coverage.hasKnownand rendersl.costUnavailable.
Because the Claude 5 family is the current generation, effectively all Claude usage lands on that branch — the coverage is unknown-only, not partial, so no amount is shown at all.
Measured on a real profile: 12,492 of 13,318 "model" occurrences over the last 7 days were claude-opus-5; for the current day it was 417 of 417.
Regression origin
Before #289 (fix: distinguish estimated and unavailable usage costs, shipped in v2.5.4), ModelPricing.rate(for:) had a model-family fallback:
if m.contains("opus") { return .perMillion(5, 25, 6.25, 0.5) }claude-opus-5 was priced solely through that fallback. #289 removed the fallback — correct in itself, since #277 showed the fallback mispriced Claude Fable 5.1 by 4× — but no exact rows were added for the Claude 5 family to replace it.
This was foreseen and written down. docs/reference/defect-log.md:217:
폴백이 우연히 맞는 모델(예:
claude-opus-5→ opus 폴백)은 건드리지 않는다
The guidance was to add exact rows whenever the fallback stops covering a model. The fallback was removed without that step.
Why no test caught it
ModelPricingTests.testCurrentOpenAIStandardRatesasserts GPT rows only. Nothing asserts that the Claude models actually read from logs are priced.ModelPricingTests.testUnknownNamesNeverBorrowFamilyPricesencodes "unknown model →nil" as correct behaviour.claude-opus-5silently joined that class, so the suite confirms the regression instead of reporting it.- Every other test uses models that are in the table (
claude-fable-5-1,claude-opus-4-8,claude-sonnet-4-6,gemini-2.5-pro), so all of them pass. - Line coverage does not help: the
.unavailablebranch is executed by existing tests. This is the "coverage number is not evidence" case fromCLAUDE.md.
Class sweep
Models observed across local provider logs that currently resolve to nil:
| Model | Priced? | Note |
|---|---|---|
claude-opus-5 |
no | current default model |
claude-sonnet-5 |
no | present in logs |
gemini-3-pro |
no | separate question, not covered here |
codex-auto-review |
no | not a real model id, correct as-is |
The whole Claude 5 family is missing; the remaining Claude rows (claude-opus-4-8, claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5-20251001, claude-fable-5, claude-fable-5-1) were verified against the official pricing page and are correct.
Fix
Add exact rows, taken from https://platform.claude.com/docs/en/about-claude/pricing (base input / 5m cache write / cache read / output):
| Model | Input | Output | 5m cache write | Cache read |
|---|---|---|---|---|
| Claude Opus 5 | $5 / MTok | $25 / MTok | $6.25 / MTok | $0.50 / MTok |
| Claude Sonnet 5 | $2 / MTok | $10 / MTok | $2.50 / MTok | $0.20 / MTok |
"claude-opus-5": .perMillion(5, 25, 6.25, 0.5),
"claude-sonnet-5": .perMillion(2, 10, 2.5, 0.2),Permanent capture
A row-by-row table test is not enough — it would have been written against the same stale list. Add a regression test that asserts the currently shipping model identities resolve to a non-nil estimate, i.e. the inverse of testUnknownNamesNeverBorrowFamilyPrices. That test fails the moment a new generation ships unpriced, which is exactly the trigger condition here, and it is the guard the removal in #289 lacked.
Also record the class in docs/reference/defect-log.md: removing a fallback is a behaviour change for every input the fallback used to serve, and those inputs must be enumerated before the fallback goes.
Activity
florianbehrend commentedon Sep 17, 2026
Correction to this issue. @DenisSeller points out in #318 that the claim above — that Claude Code
*.jsonlrecords carry no explicit cost field — is wrong, and reports cumulativetype:"cost-state"records with per-modelcostUSDundermodelUsage, present across all 128 sessions sampled.I went and checked rather than just taking the correction, and the picture is genuinely split. On my machine, across all 93
.jsonlfiles under~/.claude/projects(not just recent ones), there are zero occurrences ofcost-state,modelUsage,costUSD, orhasUnknownModelCost. So the observation behind this issue was accurate for the data in front of me — but I stated it as a property of the format, and it clearly isn't one. Something (client version, entrypoint, or a setting) decides whether the ledger is written, and I don't know which.The accurate version: where no cost ledger is present, Claude usage can only be priced from the table, and a missing row renders as unavailable. That's the condition this issue actually describes, and it's the condition on my install.
Two consequences worth recording:
modelUsagekeys 1M-context traffic asclaude-opus-5[1m], which prices differently fromclaude-opus-5, and one key can't express both. Measured at −6.4% against the reported ledger. That's a real gap in the table-only approach and worth its own issue if fix(claude): prefer reported cost-state over price-table estimates #318 doesn't land.If anyone can pin down what makes the ledger appear or not, that's probably the most useful next data point — it decides whether the table is a fallback for a rare case or the primary path for most users.
fix: price the Claude 5 family so Claude usage costs are no longer "U…