Skip to content

Claude usage counted 2-3x when requestId is absent: content-block lines of one response have distinct timestamps #1837

Closed

Description

@RakshithBhat03

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:

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

  1. pullfrog commented on Oct 8, 2026

    @pullfrog
    Contributor
  2. added
    bugSomething isn't working
    priority:highHigh user impact or an important regression.
    triage:maintainableFits the repository's maintainable scope and needs review.
    on Oct 8, 2026
  3. github-actions commented on Oct 8, 2026

    @github-actions
    Contributor

    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.

  4. Srivatsa03 commented on Oct 8, 2026

    @Srivatsa03

    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, because keeps_requestless_usage_from_same_session_at_distinct_timestamps and keeps_timestamp_different_requestless_daily_entries_without_sidechain both 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_id at rust/crates/ccusage/src/main.rs:311 uses msg_123 at two timestamps and asserts both records are kept. So the prefix cannot be the discriminator; this repo already treats msg_-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:

    fixture usage outcome
    keeps_reused_message_id_from_same_session_without_request_id 25 vs 250 differs, kept
    keeps_requestless_usage_from_same_session_at_distinct_timestamps 25 vs 250 differs, kept
    keeps_timestamp_different_requestless_daily_entries_without_sidechain 10 vs 20 differs, kept
    dedupes_repeated_requestless_writes_at_the_same_timestamp 25 vs 250 same timestamp, collapses

    On 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 to sidechain_replay_dedupe_hash plus its index registration and survivor-replacement bookkeeping. Three sites share the current behavior: usage_dedupe_hash and loaded_entry_matches_dedupe_key in adapters/claude/src/lib.rs, and the inline comparison in push_deduped_daily_entry in daily.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.

  5. added a commit that references this issue on Oct 10, 2026
    05311f3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:highHigh user impact or an important regression.triage:maintainableFits the repository's maintainable scope and needs review.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions