Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions lib/crewai/src/crewai/llms/providers/anthropic/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -1971,10 +1971,19 @@ def _extract_anthropic_token_usage(
cache_creation_tokens = (
getattr(usage, "cache_creation_input_tokens", 0) or 0
)
# Anthropic reports cache reads/writes as counters *separate* from
# ``input_tokens``, and bills both (writes at 1.25x, reads at 0.1x).
# Other providers (OpenAI, Gemini) already fold cached tokens into
# their prompt count and expose the cached figure as a breakdown, so
# report the billed input here to match. Leaving them out made
# ``total_tokens`` undercount every prompt-cached request.
billed_input_tokens = (
input_tokens + cache_read_tokens + cache_creation_tokens
)
result: dict[str, Any] = {
"input_tokens": input_tokens,
"input_tokens": billed_input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"total_tokens": billed_input_tokens + output_tokens,
"cached_prompt_tokens": cache_read_tokens,
"cache_creation_tokens": cache_creation_tokens,
}
Expand Down
61 changes: 59 additions & 2 deletions lib/crewai/tests/llms/anthropic/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1653,13 +1653,70 @@ def test_anthropic_cache_creation_tokens_extraction():
mock_response.model = None

usage = llm._extract_anthropic_token_usage(mock_response)
assert usage["input_tokens"] == 100
# Anthropic keeps cache reads/writes out of ``input_tokens``; both are
# billed, so the reported prompt count is the billed input (100 + 30 + 20)
# and the total covers every billed token.
assert usage["input_tokens"] == 150
assert usage["output_tokens"] == 50
assert usage["total_tokens"] == 150
assert usage["total_tokens"] == 200
assert usage["cached_prompt_tokens"] == 30
assert usage["cache_creation_tokens"] == 20


def test_anthropic_total_tokens_includes_cache_tokens():
"""Cache reads/writes are billed, so they must reach total_tokens.

Regression test for the undercount reported in #6768: ``total_tokens`` was
``input_tokens + output_tokens``, which omitted both Anthropic cache
counters and made the field unusable for cost estimation on any cached
workload.
"""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")

mock_response = MagicMock()
mock_response.content = [MagicMock(text="test response")]
mock_response.usage = MagicMock(
input_tokens=100,
output_tokens=50,
cache_read_input_tokens=30,
cache_creation_input_tokens=20,
)
mock_response.stop_reason = None
mock_response.model = None

usage = llm._extract_anthropic_token_usage(mock_response)
llm._track_token_usage_internal(usage)
summary = llm.get_token_usage_summary()

# The shared normalizer recomputes the total as prompt + completion, so the
# billed input has to be in ``input_tokens`` for the total to survive it.
assert summary.total_tokens == 200
assert summary.prompt_tokens == 150
assert summary.completion_tokens == 50
assert summary.cached_prompt_tokens == 30
assert summary.cache_creation_tokens == 20


def test_anthropic_total_tokens_unchanged_without_cache():
"""A request that touches no cache keeps input + output as the total."""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")

mock_response = MagicMock()
mock_response.content = [MagicMock(text="test response")]
mock_response.usage = MagicMock(
input_tokens=40,
output_tokens=20,
cache_read_input_tokens=0,
cache_creation_input_tokens=0,
)
mock_response.stop_reason = None
mock_response.model = None

usage = llm._extract_anthropic_token_usage(mock_response)
assert usage["input_tokens"] == 40
assert usage["total_tokens"] == 60


def test_anthropic_missing_cache_fields_default_to_zero():
"""Test that missing cache fields default to zero."""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
Expand Down