Skip to content

Cache-creation cost under-reported ~49%: 1-hour TTL cache writes billed at the 5-minute rate #162

Description

@MildlyMeticulous

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

  1. 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
  2. Add a cache_creation_1h_tokens column via the existing ensure_column helper, so existing databases migrate in place.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions