Summary
All cache-creation tokens are priced at the 5-minute cache-write rate (1.25x input), but Claude Code writes most of its cache with a 1-hour TTL, which Anthropic bills at 2x input. On a real corpus this under-reports cache-write cost by roughly 49%.
Why it happens
scanner.py reads only the aggregate field:
# scanner.py:408
cache_creation = usage.get("cache_creation_input_tokens", 0) or 0
The API also returns a per-TTL breakdown alongside it:
"usage": {
"cache_creation_input_tokens": 3904,
"cache_creation": {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 3904
}
}
cli.py then applies a single rate to the whole bucket:
# cli.py:66
cache_creation * p["cache_write"] / 1_000_000
cache_write is the 5-minute rate throughout the table (Opus: input 5.00, cache_write 6.25 = 1.25x). There is no 1-hour rate, so the 2x tokens are billed at 1.25x.
Measured impact
Scanning a 425-file ~/.claude/projects corpus (81k records, ~30 days):
|
tokens |
cache_creation_input_tokens (aggregate) |
216,447,883 |
ephemeral_5m_input_tokens |
47,745,500 |
ephemeral_1h_input_tokens |
168,726,465 |
78.0% of cache-creation tokens are 1-hour TTL.
Applying each model's own rate to its own tokens:
|
cache-write cost |
| current behaviour (all at 1.25x) |
$1,978.99 |
| correct (5m at 1.25x, 1h at 2x) |
$2,955.82 |
| under-reported |
$976.84 (49.4% low) |
Suggested fix
-
scanner.py — read the breakdown, falling back to the aggregate when it is absent (older logs have no cache_creation object):
cc = usage.get("cache_creation") or {}
cache_creation_1h = cc.get("ephemeral_1h_input_tokens", 0) or 0
cache_creation_5m = cc.get("ephemeral_5m_input_tokens", 0) or 0
if not cc:
cache_creation_5m = usage.get("cache_creation_input_tokens", 0) or 0
-
Add a cache_creation_1h_tokens column via the existing ensure_column helper, so existing databases migrate in place.
-
Add a cache_write_1h rate (2x input) to the pricing tables and split the term in calc_cost. Note the table is duplicated in dashboard.py as JS, so both need it.
Notes
- A small number of records (22 of ~41k here) have a
cache_creation object whose 5m + 1h does not equal the aggregate. Preferring the aggregate when the breakdown is absent, and treating the breakdown as authoritative when present, handles this without double-counting.
- Reference for the multipliers: cache writes are billed at 1.25x base input for the 5-minute TTL and 2x for the 1-hour TTL; cache reads at 0.1x.
Happy to open a PR implementing the above if that would be useful.
Summary
All cache-creation tokens are priced at the 5-minute cache-write rate (1.25x input), but Claude Code writes most of its cache with a 1-hour TTL, which Anthropic bills at 2x input. On a real corpus this under-reports cache-write cost by roughly 49%.
Why it happens
scanner.pyreads only the aggregate field:The API also returns a per-TTL breakdown alongside it:
cli.pythen applies a single rate to the whole bucket:cache_writeis the 5-minute rate throughout the table (Opus:input5.00,cache_write6.25 = 1.25x). There is no 1-hour rate, so the 2x tokens are billed at 1.25x.Measured impact
Scanning a 425-file
~/.claude/projectscorpus (81k records, ~30 days):cache_creation_input_tokens(aggregate)ephemeral_5m_input_tokensephemeral_1h_input_tokens78.0% of cache-creation tokens are 1-hour TTL.
Applying each model's own rate to its own tokens:
Suggested fix
scanner.py— read the breakdown, falling back to the aggregate when it is absent (older logs have nocache_creationobject):Add a
cache_creation_1h_tokenscolumn via the existingensure_columnhelper, so existing databases migrate in place.Add a
cache_write_1hrate (2x input) to the pricing tables and split the term incalc_cost. Note the table is duplicated indashboard.pyas JS, so both need it.Notes
cache_creationobject whose5m + 1hdoes not equal the aggregate. Preferring the aggregate when the breakdown is absent, and treating the breakdown as authoritative when present, handles this without double-counting.Happy to open a PR implementing the above if that would be useful.