[BUG] Usage "Detailed Breakdown" over-counts cache read/write/input ~2.5× — sums usage per content-block line, not per API request
Environment
- Claude Code desktop app (Code tab), v2.1.270
- Surface: the Usage pill under the composer → Detailed Breakdown
Summary
The per-session Detailed Breakdown (Input / Output / Cache read / Cache write) counts token usage once per transcript content-block entry instead of once per API request.
A normal assistant turn is written to the session .jsonl as several lines — one per content block (thinking, text, tool_use) — and every one of those lines carries the identical usage object (same requestId, byte-for-byte the same token counts). The Breakdown sums per line, so each request's usage is multiplied by its number of content blocks (~2.5 on average, 3 for a typical tool-calling turn).
Net effect: Cache read — the dominant figure — is inflated ~2.5×, which makes it misleading.
Impact
Cache read dominates token volume, so a 2.5× overstatement makes the headline number actively confusing (a session that truly read ~168M cache tokens is shown as ~418M). If the session Cost estimate is derived from the same per-line sum, it's overstated too. The 5-hour / weekly limit bars appear unaffected (presumably computed server-side from real usage) — so the Breakdown disagrees with the bars, compounding the confusion.
Evidence (one real session)
The Breakdown as displayed, vs. the two ways of summing the transcript's usage metadata:
| Field | Breakdown UI | Summed per API request (requestId) | Summed per transcript line |
|-------------|-------------:|-----------------------------------------:|-------------------------------:|
| Input | 1.6k | 629 | 1,571 |
| Cache read | 418M | 168,204,894 | 418,001,293 |
| Cache write | 5.5M | 2,004,667 | 5,539,964 |
The UI matches the per-line sum on all three fields to displayed precision.
Session shape: 315 unique API requests, 787 assistant lines carrying usage. 184 requests appear 3× in the transcript (thinking+text+tool_use), 53 appear 2×, 61 once → weighted average ≈ 2.5, matching the 2.49× inflation (168M → 418M). For every repeated request (254/254), the usage fields are byte-identical across its lines — the same request's usage is being re-counted, not distinct reads.
Root cause hypothesis
The Breakdown aggregates message.usage per transcript entry. It should dedupe by requestId (or message.id) and count each request's usage once. cache_read_input_tokens, cache_creation_input_tokens, and input_tokens describe a single API request; duplicating them per content block multi-counts them.
Minimal repro
Run against any session transcript (~/.claude/projects/<project>/<session>.jsonl):
import json, sys
from collections import defaultdict
by = defaultdict(list)
for line in open(sys.argv[1]):
try: o = json.loads(line)
except: continue
if o.get('type') != 'assistant': continue
u = (o.get('message') or {}).get('usage')
if not u: continue
rid = o.get('requestId') or o['message'].get('id')
by[rid].append(u.get('cache_read_input_tokens', 0))
naive = sum(v for xs in by.values() for v in xs)
actual = sum(xs[0] for xs in by.values()) # identical across a request's lines
print(f"per-line (what the UI shows): {naive:,}")
print(f"per-request (correct): {actual:,}")
print(f"inflation: {naive/actual:.2f}x")
Expected: the Breakdown shows the per-request total (~168M here). Actual: it shows the per-line total (~418M).
Notes
- Output does not follow the per-line rule — the UI showed 4.3k, well below even the true per-request output (~475K) — so it has separate handling and is likely a distinct bug.
- Subagents are tracked separately in the UI ("What's using your limits"), and run their own transcripts, so the figures above are main-thread only.