Skip to content

fix(claude): prefer reported cost-state over price-table estimates - #318

Open
DenisSeller wants to merge 1 commit into
chattymin:mainfrom
DenisSeller:feat/claude-reported-cost-state
Open

DenisSeller wants to merge 1 commit into
chattymin:mainfrom
DenisSeller:feat/claude-reported-cost-state

Conversation

@DenisSeller

Copy link
Copy Markdown

Summary

Claude Code writes its own cost ledger into each session file — cumulative
type:"cost-state" records carrying per-model costUSD inside modelUsage.
LocalUsageReader never read it, so Claude usage was always priced from
ModelPricing, and any model without a table row aggregated as unavailable
even 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.explicitCost that Bucket.add already prefers over an
estimate. Where no ledger exists, behaviour is unchanged: table estimate, else
unavailable.

Relationship to #304 and #313

Those PRs add claude-opus-5 / claude-sonnet-5 rows to the table and should
land 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:

model vs. reported ledger
claude-sonnet-5 +0.3%
claude-opus-5[1m] −6.4%

The gap is the context-window variant. modelUsage keys 1M-context traffic as
claude-opus-5[1m], which prices differently from claude-opus-5, and a
single 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 *.jsonl records carry no
explicit cost field: they do — cost-state has been there, with
hasUnknownModelCost false across all 128 sessions sampled.

Attribution rules

  • The ledger is cumulative, so only the final record describes the session.
  • modelUsage keys carry a context suffix that message.model never has, so
    matching is on the base id and both variants pool onto it.
  • The session sum stays exact — only the split within a session is inferred,
    which is why the amount keeps .source coverage rather than .estimate.
  • Cost for a model that produced no parsed entry is dropped, not reassigned,
    so an unattributable amount can never inflate another model's day. On the
    sample above that is a deliberate 0.7% undercount.
  • claudeParserVersion is added because Claude blobs previously invalidated on
    mtime/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

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

Checklist

  • swift build and swift test pass locally
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed
  • Tests were added or updated for this change

swift test: 1086 executed, 0 failures. New ClaudeReportedCostTests covers
context-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

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant