docs(anthropic): state why usage-shape detection requires a cache key, pin Responses-shape rejection

This commit is contained in:
mateo-berri 2026-08-05 02:01:53 -07:00
parent bd04520d98
commit e8a80b9883
3 changed files with 28 additions and 0 deletions

View file

@ -2109,6 +2109,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"""Anthropic reports prompt cache tokens as top-level ``cache_read_input_tokens`` /
``cache_creation_input_tokens``; no other API surface uses those keys, and the
Responses API mapping would silently drop them.
Requiring a cache key is deliberate: Responses API usage also carries top-level
``input_tokens``, so the cache keys are the only shape discriminator between the
two. A cache-free Anthropic payload falls through to the Responses API mapping,
which is safe because both mappings agree whenever no cache tokens are present.
"""
if "prompt_tokens" in usage_object or "input_tokens" not in usage_object:
return False

View file

@ -5867,3 +5867,19 @@ def test_is_anthropic_usage_object_distinguishes_chat_usage():
).model_dump()
)
assert not AnthropicConfig.is_anthropic_usage_object({"input_tokens": 3, "output_tokens": 5})
def test_is_anthropic_usage_object_rejects_responses_api_usage():
"""completion_cost checks the Anthropic shape before the Responses API shape, so a
Responses API usage payload, whose cache reads live in nested input_tokens_details,
must never match; matching would route it past the converter that reads the nested
field and its cache reads would be billed at the full input rate."""
assert not AnthropicConfig.is_anthropic_usage_object(
{
"input_tokens": 4017,
"output_tokens": 5,
"total_tokens": 4022,
"input_tokens_details": {"cached_tokens": 4014},
"output_tokens_details": {"reasoning_tokens": 0},
}
)

View file

@ -989,6 +989,13 @@ class TestTranslateResponse:
"cache_read_input_tokens": 4004,
}
def test_missing_usage_maps_to_zero_tokens(self):
"""A response without a usage object must map to zeroed Anthropic usage."""
assert LiteLLMAnthropicToResponsesAPIAdapter.translate_responses_api_usage_to_anthropic_usage(None) == {
"input_tokens": 0,
"output_tokens": 0,
}
def test_model_and_id_preserved(self):
"""Model and response ID from the Responses API are forwarded."""
response = _make_mock_response(