Skip to content

fix(parser): count Claude usage once per API response; price 1h cache writes - #43

Merged
lroolle merged 1 commit into
mainfrom
fix/usage-once-per-response
Oct 17, 2026
Merged

lroolle merged 1 commit into
mainfrom
fix/usage-once-per-response

Conversation

@lroolle

@lroolle lroolle commented Oct 17, 2026

Copy link
Copy Markdown
Member

Summary

v0.17.0 overstates Claude Code cost. Claude Code writes one API response as several transcript records, one per content block. They share message.id, and each repeats the response's usage; ccx summed every record. This backports the fix from the agent-registry branch (60278b9), where it was checked against a cctrace wire capture of 2,607 requests:

  • Usage is kept on the last record of each response (Message.ResponseID) in the full parse, the quick parse, and sidechain files. Session totals are the sum after the collapse.
  • 1-hour cache writes (usage.cache_creation.ephemeral_1h_input_tokens) are priced at 2x input instead of the 5-minute rate, on every Claude tier.
  • CacheFormatVersion goes 6 -> 10. Unreleased development builds stamped 7-9 with other shapes, so those caches must never match.

Not included: workflow-agent discovery and all-in totals. They stay on the registry branch.

Evidence

17 real sessions (Opus 5, Opus 5.5, Fable 5, Fable 5.1, Opus 4.6, Haiku 4.5), each binary parsing with its own cache dir:

  • Session-list cost matches the registry branch's main-loop figure exactly on all 17.
  • 0.17.0 was 2.2x high at the median (range 1.3x to 3.1x): $1,721 vs $758 summed.

Not verified on real data: the Sonnet 5 1h rate (no Sonnet 5 sessions in the sample). It follows the same 2x-input rule as the other tiers.

Test plan

  • gofmt -l . empty, go vet ./..., go test -race ./... green locally
  • Synthetic-fixture tests for the collapse, the quick-parse accumulator, and the TTL cost split
  • CI: test (race) and golangci-lint

🤖 Generated with Claude Code

… writes

Claude Code writes one API response as several transcript records (one
per content block) that share message.id and each repeat the
response's usage. ccx summed every record. Fable records repeat the
final figure; Opus 5 records carry the figure as it stood when the
block was cut, so the last record is the only honest one. Usage now
lives on the last record of each response (Message.ResponseID) in the
full parse, the quick parse and every sidechain file, and session
totals are the sum of what the messages carry.

Anthropic bills the 1h cache write at 2x input; ccx priced every write
at the 5m rate. MessageUsage carries the 1h subset
(usage.cache_creation.ephemeral_1h_input_tokens) and ModelPricing the
1h rate on every Claude tier, Sonnet 5 included; records without the
split keep the cheaper reading.

Backported from the agent-registry branch, where the rule was checked
against a wire capture. On 17 real sessions (Opus 5, Opus 5.5, Fable 5,
Fable 5.1, Opus 4.6, Haiku 4.5) the session-list cost now matches that
branch's main-loop figure exactly; 0.17.0 was 2.2x high at the median
(1.3x to 3.1x). Workflow-agent discovery and all-in totals stay on
that branch.

CacheFormatVersion 6 -> 10: 7-9 were stamped by unreleased development
builds with other shapes, so their caches must never match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lroolle
lroolle force-pushed the fix/usage-once-per-response branch from 415e72e to d303710 Compare October 17, 2026 09:51
@lroolle
lroolle merged commit 5aede81 into main Oct 17, 2026
2 checks passed
@lroolle
lroolle deleted the fix/usage-once-per-response branch October 17, 2026 09:53
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