Repository navigation
Description
What happened?
When Claude Code talks to Anthropic through a local proxy that doesn't forward the request-id response header (CLIProxyAPI in my case), every transcript line has message.id but no requestId. Claude Code still writes one line per content block (thinking, text, each tool_use), all with the same message.id, the same sessionId, and usually identical usage, but each line has its own timestamp.
Since #1799, requestless entries are deduped by (message.id, session, timestamp). Because the timestamps differ, none of these lines collapse, and every Claude report counts each response about 2-3x.
From my logs for 2026-10-08 (Claude Code 2.1.292/2.1.293, entrypoint: sdk-ts):
| count | |
|---|---|
| assistant lines with usage | 4,494 |
lines with requestId |
0 |
unique message.id |
2,143 |
| ids repeated within the same session/file | 1,673 (all have distinct timestamps; 1,616 have identical usage) |
| ids repeated across sessions | 0 |
Output tokens: ccusage claude daily reports 3.93M; summing one record per message.id gives 1.46M.
On 20.0.25, weekly/monthly collapsed these by message+session and were correct; daily was already inflated. #1797 reported that mismatch, and #1799 resolved it by moving every report to the higher, inflated count.
Suggested fix
The timestamp scope exists for gateways that reuse one fake message ID for every response (#1635, e.g. "ocgo"). Real Anthropic IDs (msg_01…) are unique per response, so the two cases can be told apart:
- If
requestIdis absent andmessage.idlooks like an Anthropic message ID (msg_prefix), dedupe by(message.id, session)across timestamps and keep the most complete record, as you already do for same-key rewrites. - Otherwise keep the current
(message.id, session, timestamp)key, so Claude usage collapses to one entry when sessions share a message id (gateway setups) #1635 setups are unaffected.
A narrower alternative: for requestless entries with the same message.id in the same session, collapse only when usage is identical or one record is a growing superset (output tokens increasing). A gateway reusing one ID wouldn't normally produce that.
Steps to reproduce
Two lines in one session file, same message.id, no requestId, timestamps a few hundred ms apart, identical usage → counted twice by claude daily|weekly|monthly.
Expected behavior
Each Claude response is counted once, as when requestId is present.
Version
20.0.26
Activity
pullfrog commentedon Oct 8, 2026
github-actions commentedon Oct 8, 2026
Pullfrog triage: priority:high
Kind: bug
Maintenance fit: maintainable
Confidence: high
Requestless entries with same message.id in one session are keyed by timestamp, so one response split across content-block lines counts 2-3x. Issue gives counts showing 3.93M vs 1.46M tokens and a minimal two-line repro. This is a material usage reporting error in core Claude dedupe.
Decision: kept open; Pullfrog will attempt a focused implementation PR.
Srivatsa03 commentedon Oct 8, 2026
I tried the
msg_prefix approach and it does not hold. Posting what I found in case it saves someone the same detour.Gating the timestamp on
message_id.starts_with("msg_")passes the whole claude adapter suite, including both fixtures that pin current behavior, becausekeeps_requestless_usage_from_same_session_at_distinct_timestampsandkeeps_timestamp_different_requestless_daily_entries_without_sidechainboth use"ocgo"rather than an Anthropic ID. It then fails one test in the wider crate:keeps_reused_message_id_from_same_session_without_request_idatrust/crates/ccusage/src/main.rs:311usesmsg_123at two timestamps and asserts both records are kept. So the prefix cannot be the discriminator; this repo already treatsmsg_-shaped IDs as potentially reused.The narrower alternative in your issue does survive every fixture. Collapse requestless same-ID same-session entries across timestamps only when usage is identical, and leave same-timestamp collapse unconditional:
keeps_reused_message_id_from_same_session_without_request_idkeeps_requestless_usage_from_same_session_at_distinct_timestampskeeps_timestamp_different_requestless_daily_entries_without_sidechaindedupes_repeated_requestless_writes_at_the_same_timestampOn your numbers that covers 1,616 of the 1,673 repeated IDs, so it would fix most of the overcount and not all of it. Worth saying out loud rather than claiming it closes the gap.
The implementation is not a one-liner. A usage fingerprint cannot just replace the timestamp in
usage_dedupe_hash, because same-timestamp collapse has to stay unconditional, so it needs a second lookup route next tosidechain_replay_dedupe_hashplus its index registration and survivor-replacement bookkeeping. Three sites share the current behavior:usage_dedupe_hashandloaded_entry_matches_dedupe_keyinadapters/claude/src/lib.rs, and the inline comparison inpush_deduped_daily_entryindaily.rs.I have two red regression tests written for the content-block case, one per loader, both using identical usage so they stay valid under this rule. Happy to implement the route and send it.
I am a new contributor here and have not been approved with
lgtm, so per CONTRIBUTING I will wait rather than open a PR. Two things I would want your call on first, since they change the shape of the change: whether a second route is the structure you want, and whether "identical usage" should mean exact equality or also accept a growing superset where output tokens increase.fix(claude): dedupe requestless Anthropic message IDs across timestam…