Skip to content

Cost double-counts cache_creation tokens (billed at both input and cache_write rates) for Anthropic models #6

Description

@gebinic

Cost double-counts cache_creation tokens (billed at both input and cache_write rates) for Anthropic models

Version / commit: main @ 4f7ccfd (pricing.snapshot.yaml fetched_at: 2026-05-21)

Summary

For Anthropic models, computeCost bills cache_creation (cache-write) tokens
twice: once at the full input rate and once at the cache_write rate. This
over-estimates USD cost for any call that creates cache (i.e. most first-turn or
large-context calls). The behavior also contradicts the token mapping documented in
pricing.snapshot.yaml itself.

Root cause

gen_ai.usage.input_tokens for Anthropic already includes both cache-read and
cache-creation tokens — as your own snapshot header states:

# gen_ai.usage.input_tokens                -> input + cache_read + cache_creation (Anthropic)
# gen_ai.usage.cache_creation.input_tokens -> cache_creation (Anthropic only; subtract too)

But normalizeSpan subtracts only cache_read, never cache_creation:

src/otel/parser.ts:123

const freshInput = Math.max(rawInput - cacheRead, 0); // cache_creation NOT subtracted

It then calls computeCost with:

src/otel/parser.ts:138

computeCost({ input: freshInput + cacheRead + cacheCreation, cache_read: cacheRead, cache_write: cacheCreation, output }, price)

Inside computeCost (src/pricing/loader.ts):

const fresh = Math.max(totalInput - cacheRead - cacheWrite, 0);
// totalInput = freshInput + cacheRead + cacheCreation
// => fresh   = freshInput = rawInput - cacheRead   <-- still contains cache_creation
return (fresh/1e6)*input
     + (cacheRead/1e6)*cached_input
     + (cacheWrite/1e6)*cache_write   // cacheWrite = cacheCreation
     + (output/1e6)*output;

So cache_creation sits inside fresh (billed at the input rate) and is billed
again as cacheWrite (at the cache_write rate). Net overcharge per call =
cache_creation × input_rate.

Reproduction (self-contained, using your own snapshot prices)

One claude-opus-4.5 call (input 5, cached_input 0.5, cache_write 6.25,
output 25 per 1M tokens):

  • input_tokens = 20000, cache_read = 0, cache_creation = 19000, output = 200
  • True fresh input = 20000 − 0 − 19000 = 1000
fresh billed @ input rate cost
copilot-cost (current) 20000 $0.22375
correct 1000 $0.12875

Overcharge = $0.095 = 19000 × $5 / 1M = cache_creation × input_rate.

I also validated this against real Copilot CLI sessions, where GitHub's own billed
amount is emitted as github.copilot.nano_aiu (USD = nano_aiu / 1e11). On a full
multi-model run, the corrected formula matches GitHub's billed total to the cent,
while the current formula over-reports the cost by ~35%.

Suggested fix (one line)

Subtract cache_creation in freshInput, matching the documented mapping:

src/otel/parser.ts:123

const freshInput = Math.max(rawInput - cacheRead - cacheCreation, 0);

This corrects the cost (the input argument to computeCost becomes rawInput, so
fresh resolves to rawInput − cacheRead − cacheCreation) and also makes the
reported input_tokens the true fresh input. It is a no-op for OpenAI/Google models,
where cache_creation is 0.

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