Skip to content

core: price an input count that already includes cache - #50

Open
sam-bretz wants to merge 2 commits into
mainfrom
fix/input-includes-cache-and-google-cache-rule
Open

sam-bretz wants to merge 2 commits into
mainfrom
fix/input-includes-cache-and-google-cache-rule

Conversation

@sam-bretz

Copy link
Copy Markdown
Collaborator

Unblocks a fix in metergraph-internal for MET-61.

Problem

The OTel GenAI semantic conventions define gen_ai.usage.input_tokens as the whole input, cached tokens included: "This value SHOULD include all types of input tokens, including cached tokens." Every OTLP source reports that one shape, whatever the provider.

The catalog records the opposite thing: each entry's own convention, because Anthropic's usage reports the uncached remainder and OpenAI's reports the total. Pricing an inclusive count against an Anthropic entry bills every cached token twice, at the input rate and again at the cache rate. A customer's LiteLLM span (148,661 input including 148,290 cache reads and 369 cache writes, claude-opus-4-8) cost $0.835 instead of $0.092.

metergraph-internal #454 works around this by re-basing the count before calling cost(), but it can only pass one number, which _price_tokens uses both for billing and for the long-context size tier. So a prompt that is over the threshold only because of cached tokens now misses the premium. On gemini-2.5-pro with 200,001 input including 50,000 cache reads and 1,000 output, it bills $0.213 where $0.421 is correct. That workaround is live in production today.

Change

cost() and price_deployment() take input_includes_cache (default False, so every existing caller prices exactly as before). When set:

  • the uncached remainder is billed at the input rate, and the entry's input_includes_cache_read / input_includes_cache_write rules are not applied a second time;
  • the long-context tier is judged on the whole prompt the provider processed, not the remainder;
  • a total smaller than its own cache counts cannot include them, so it is billed as uncached input, sized on the whole of it, and reported with cache_exceeds_input_total.

The catalog data is unchanged. Ships as 0.2.24.

Verification

  • pytest core/tests -q: 235 passed (230 before, 5 new).
  • The 5 new tests fail with the catalog.py change reverted, so they detect the feature being absent:
    • inclusive Anthropic usage matches native Anthropic usage exactly, at $0.09178625, using MET-61's numbers;
    • the long-context tier applies at 200,001 total with 50,000 cached and not at 199,999;
    • a total below its cache counts bills as uncached and reports cache_exceeds_input_total;
    • an entry that already deducts cache reads (gpt-5.6-luna) prices the same either way, so the cache is never deducted twice;
    • price_deployment is unchanged unless the option is set.
  • python core/tests/package/verify_artifacts.py: OK for 0.2.24.
  • Server tests were not run locally (they need Postgres); CI covers them. No server or catalog data change.

After this merges

metergraph-internal drops its probe in worker/main.py _catalog_input_tokens, pins metergraph-core>=0.2.24, and passes the option straight through, which also fixes the long-context underbilling above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DkYVigksoSN9N7coWLgmvS

An OpenTelemetry GenAI source reports one input number for every provider:
the semantic conventions define `gen_ai.usage.input_tokens` as the whole
input, cached tokens included. The catalog instead records each entry's own
convention, because Anthropic reports the uncached remainder while OpenAI
reports the total. Pricing an inclusive count against an entry that expects
the remainder bills every cached token twice, once at the input rate and
again at the cache rate.

`cost()` and `price_deployment()` take `input_includes_cache`. When set, the
uncached remainder is billed at the input rate and the entry's own rules are
not applied a second time, while the long-context size tier is still judged
on the whole prompt the provider processed. A total smaller than its own
cache counts cannot include them, so it is billed as uncached input and says
so with `cache_exceeds_input_total`.

The option is opt-in and off by default, so every existing caller prices
exactly as before.

Ships as metergraph-core 0.2.24. The catalog data is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkYVigksoSN9N7coWLgmvS
`load_catalog().price` is the public entry point, and it rejected the new
keyword rather than passing it to `price_deployment`, so a caller that went
through the wrapper got a TypeError and the mode was unreachable there.

Found in Codex review of this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkYVigksoSN9N7coWLgmvS
@sam-bretz

Copy link
Copy Markdown
Collaborator Author

Codex reviewed this branch and found one real gap, now fixed in 2817422.

[P1] LoadedCatalog.price did not forward the new keyword. load_catalog().price is the public entry point, and it rejected input_includes_cache rather than passing it to price_deployment, so the mode raised TypeError for anyone using the wrapper. The snapshot methods worked, which is why the first round of tests missed it.

Fixed, with a test that uses the wrapper and asserts an inclusive count prices the same as the equivalent native usage ($0.09178625) and strictly less than pricing the same total as uncached input. The test fails with the one-line forward reverted (TypeError). Core suite: 236 passed.

No other findings from the review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant