Skip to content

fix(anthropic): count billed cache tokens in token usage (#6768) - #6770

Open
Anai-Guo wants to merge 1 commit into
crewAIInc:mainfrom
Anai-Guo:fix/anthropic-total-tokens-cache
Open

fix(anthropic): count billed cache tokens in token usage (#6768)#6770
Anai-Guo wants to merge 1 commit into
crewAIInc:mainfrom
Anai-Guo:fix/anthropic-total-tokens-cache

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Aug 2, 2026

Copy link
Copy Markdown

Fixes #6768.

Problem

Anthropic reports cache reads (cache_read_input_tokens) and cache writes (cache_creation_input_tokens) as counters that are separate from input_tokens, and bills both (writes at 1.25×, reads at 0.1×). _extract_anthropic_token_usage captured both values and surfaced them individually, but left them out of the totals — so every prompt-cached request reported a total_tokens well below what was billed, and the undercount grew with cache hit rate.

Why the one-line fix in the issue isn't enough

The issue points at total_tokens: input_tokens + output_tokens. Fixing only that line does not change what users actually read (llm._token_usage / get_token_usage_summary()), because UsageMetrics.from_provider_dict discards the provider's total and recomputes it:

return cls(
    total_tokens=prompt_tokens + completion_tokens,   # provider total_tokens ignored
    ...
)

Verified against main:

provider dict total_tokens : 200      # after the issue's suggested one-line fix
UsageMetrics.total_tokens  : 150      # unchanged

So the cache tokens have to be part of the prompt count to survive normalization.

Fix

Report the billed input from the Anthropic extractor:

billed_input_tokens = input_tokens + cache_read_tokens + cache_creation_tokens

cached_prompt_tokens and cache_creation_tokens stay as they are, now as a breakdown of that prompt count. This is the convention the other providers already follow — OpenAI's prompt_tokens and Gemini's prompt_token_count both include cached tokens and expose the cached figure separately — and it is exactly what from_provider_dict assumes. It also keeps the change inside the Anthropic provider: no shared normalizer change, so OpenAI/Gemini/Azure numbers are untouched.

Verification

Ran an old-vs-new harness against the installed crewai==1.15.9, whose completion.py, usage_metrics.py and base_llm.py are byte-identical to main:

case (input / output / cache read / cache write) total_tokens before after
100 / 50 / 30 / 20 150 200
40 / 20 / 0 / 0 60 60
missing cache fields (None) 60 60
10 / 5 / 0 / 200 (cache write only) 15 215

Non-cached requests are unaffected; the two cached rows were undercounting 50 and 200 billed tokens respectively.

Tests (run against the patched module, then re-run against unpatched main to confirm they actually guard the regression):

  • test_anthropic_total_tokens_includes_cache_tokens — new; asserts end-to-end through _track_token_usage_internalget_token_usage_summary(), which is the path the issue's repro reads.
  • test_anthropic_total_tokens_unchanged_without_cache — new; pins the no-cache path.
  • test_anthropic_cache_creation_tokens_extraction — existing; its input_tokens/total_tokens numbers moved to 150/200. That test's subject is cache-field extraction (cached_prompt_tokens == 30, cache_creation_tokens == 20, both unchanged); the totals were incidental and encoded the undercount.
  • test_anthropic_missing_cache_fields_default_to_zero — existing, unchanged, still passes.
4 passed          # patched
2 failed, 2 passed  # unpatched main — the two cache tests fail

ruff==0.15.1 format --check clean; ruff check output on the touched file is identical to main.

Note

If you'd rather input_tokens keep mirroring the raw Anthropic field, the alternative is to have from_provider_dict honor an explicit provider total_tokens. I did not take that route: it is shared code, and Gemini's total_token_count includes thinking tokens, so honoring it there would move Gemini's numbers too — a bigger change than this issue calls for. Happy to switch if you prefer that shape.

🤖 Generated with Claude Code

)

Anthropic reports cache reads (cache_read_input_tokens) and cache writes
(cache_creation_input_tokens) as counters separate from input_tokens, and
bills both. _extract_anthropic_token_usage captured them but left them out
of the prompt and total counts, so every prompt-cached request reported a
total_tokens well below what was billed, and the undercount grew with cache
hit rate.

Report the billed input instead. This matches the convention the other
providers already use — OpenAI and Gemini fold cached tokens into their
prompt count and expose the cached figure as a breakdown — and it is what
UsageMetrics.from_provider_dict assumes: it recomputes the total as
prompt + completion and discards the provider total, so the cache tokens
have to be in the prompt count to survive normalization.

Requests that touch no cache are unaffected.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f00bdb2-01fc-4f48-8ba8-b0b8a1541b04

📥 Commits

Reviewing files that changed from the base of the PR and between c8f441c and 2ec3490.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/llms/providers/anthropic/completion.py
  • lib/crewai/tests/llms/anthropic/test_anthropic.py

📝 Walkthrough

Walkthrough

Changes

Anthropic token accounting

Layer / File(s) Summary
Update usage extraction and regression coverage
lib/crewai/src/crewai/llms/providers/anthropic/completion.py, lib/crewai/tests/llms/anthropic/test_anthropic.py
Anthropic billed input tokens now include regular, cache read, and cache creation tokens. Total tokens derive from the billed input count. Tests cover cached and uncached usage while preserving separate cache counters.

Suggested reviewers: lorenzejay

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the Anthropic token usage fix for billed cache tokens.
Description check ✅ Passed The description directly explains the cache-token undercount, implementation, scope, and regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direction is right and, unusually for this kind of fix, it is complete in one edit. _extract_anthropic_token_usage is the only place cache_read_input_tokens and cache_creation_input_tokens are read in completion.py, and it is called from all six paths:

1006  sync completion
1240  streaming final_message
1445  tool-call follow-up
1554  async completion
1764  async streaming final_message
1870  async tool-call follow-up

so streaming and the tool follow-ups are covered without separate edits. That is worth stating in the PR body, since the usual failure mode for a usage fix is landing it on the non-streaming path only.

The detail that makes the fix necessary rather than cosmetic is the one your second test pins: the shared normalizer recomputes the total as prompt plus completion, so putting the cache counts anywhere other than input_tokens would have them silently dropped again one layer up. Asserting through get_token_usage_summary() rather than only on the dict is what actually proves that, and it is the assertion I would protect if this ever gets refactored.

One correction to the stated motivation. The comment cites the billing multipliers:

writes at 1.25x, reads at 0.1x

and the test docstring says the field was "unusable for cost estimation". Both are true, but folding at face value does not make total_tokens usable for cost estimation either. 100 cache-read tokens and 100 fresh input tokens now contribute equally to total_tokens while differing tenfold in price. What the fix actually delivers is a correct token count, which is the right thing for a field named total_tokens to be, and it makes cached_prompt_tokens and cache_creation_tokens load-bearing rather than informational, since a cost estimate has to reconstruct the split from them. I would reword the docstring to claim the token count rather than the cost, otherwise the next person reading it will assume total_tokens * price is meaningful.

The double-count trap, and why it deserves a changelog line. input_tokens now includes both cache counters while cached_prompt_tokens and cache_creation_tokens still sit beside it. Anyone who wrote

usage["input_tokens"] + usage["cached_prompt_tokens"]

against the current shape, which was the correct way to get billed input before this PR, now double counts. The changed assertion in test_anthropic_cache_creation_tokens_extraction from 100 to 150 is the honest signal that this is a public-number change rather than a bug fix that nobody can observe. Existing dashboards will show a step change on cached workloads. That is the right outcome, but it should be announced rather than discovered.

test_anthropic_total_tokens_unchanged_without_cache is a good inclusion for exactly that reason: it draws the line showing uncached workloads see no movement at all, which is what most users will want to confirm first.

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.

Conversational Flow golden use case improvements - Anthropic total_tokens omits cache read/write tokens, undercounting cost

2 participants