fix(claude): prefer reported cost-state over price-table estimates - #318
Open
DenisSeller wants to merge 1 commit into
Open
DenisSeller wants to merge 1 commit into
DenisSeller wants to merge 1 commit into
Conversation
Claude Code writes its own cost ledger to each session file as cumulative `type:"cost-state"` records, carrying per-model `costUSD` in `modelUsage`. The reader ignored it and always priced Claude usage from `ModelPricing`, so any model the table has no rate for aggregated as unavailable — even though an exact amount was already sitting in the same file. This is visible today: the table has no `claude-opus-5` or `claude-sonnet-5` rate, and `modelUsage` keys carry a context-window suffix (`claude-opus-5[1m]`) that `message.model` never has. On a heavy Opus 5 profile that left the cost readout at "$—" while the source had priced every request, `hasUnknownModelCost` false throughout. Parse the last cost-state record per file (the ledger is cumulative) and spread each model's session total across that model's entries in proportion to tokens, feeding the existing `Entry.explicitCost` that `Bucket.add` already prefers over an estimate. The session sum stays exact, so only the split within one session is inferred and the amount keeps `.source` coverage. Cost for a model that produced no parsed entry is dropped rather than reassigned, so an unattributable amount can never inflate another model's day. Claude blobs previously invalidated on mtime/size alone, so add `claudeParserVersion` — without it, entries cached by the old rule would keep their cost-free values until each file happened to change. Behaviour is unchanged where no ledger exists: table estimate, else unavailable. Claude-Session: https://claude.ai/code/session_01KwKz8zsXemuCwftTX2gN3k
DenisSeller
marked this pull request as ready for review
September 16, 2026 08:51
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Claude Code writes its own cost ledger into each session file — cumulative
type:"cost-state"records carrying per-modelcostUSDinsidemodelUsage.LocalUsageReadernever read it, so Claude usage was always priced fromModelPricing, and any model without a table row aggregated as unavailableeven though an exact amount was already present in the same file.
This parses the last cost-state record per file and spreads each model's
session total across that model's entries in proportion to tokens, feeding the
existing
Entry.explicitCostthatBucket.addalready prefers over anestimate. Where no ledger exists, behaviour is unchanged: table estimate, else
unavailable.
Relationship to #304 and #313
Those PRs add
claude-opus-5/claude-sonnet-5rows to the table and shouldland regardless — this change does not replace them. It sits ahead of the
table, so the two compose: a reported amount wins, and the table stays the
fallback for sessions with no ledger.
They are also measurably correct for standard context. Checking both PRs'
proposed rates against the reported ledger on a local 128-session sample:
claude-sonnet-5claude-opus-5[1m]The gap is the context-window variant.
modelUsagekeys 1M-context traffic asclaude-opus-5[1m], which prices differently fromclaude-opus-5, and asingle table key cannot express both. Reading the reported amount closes that
without a new row per model or per context tier.
Worth flagging for #303, which states that Claude
*.jsonlrecords carry noexplicit cost field: they do —
cost-statehas been there, withhasUnknownModelCostfalse across all 128 sessions sampled.Attribution rules
modelUsagekeys carry a context suffix thatmessage.modelnever has, somatching is on the base id and both variants pool onto it.
which is why the amount keeps
.sourcecoverage rather than.estimate.so an unattributable amount can never inflate another model's day. On the
sample above that is a deliberate 0.7% undercount.
claudeParserVersionis added because Claude blobs previously invalidated onmtime/size alone; without it, entries cached under the old rule would keep
their cost-free values until each file happened to change.
Type of change
Checklist
swift buildandswift testpass locallyswift test: 1086 executed, 0 failures. NewClaudeReportedCostTestscoverscontext-suffix matching, cumulative last-record-wins, exact day-split across a
session boundary, unattributable-model cost, zero-token records, reported-over-
estimate precedence, and the unchanged no-ledger fallback. No files under
Sources/PokeTokenBar/UI/are touched.🤖 Generated with Claude Code
https://claude.ai/code/session_01KwKz8zsXemuCwftTX2gN3k