From 3c23038df5a9a497f3e1049cbd50e6570c47d865 Mon Sep 17 00:00:00 2001 From: Will Koh Date: Thu, 30 Apr 2026 19:29:17 -0700 Subject: [PATCH] Fix usage returned for cache_creation_tokens --- .../transformation.py | 12 ++- .../test_litellm_completion_responses.py | 80 +++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 32aef002a74..58278a32c05 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -2101,7 +2101,7 @@ class LiteLLMCompletionResponsesConfig: and usage.prompt_tokens_details is not None ): prompt_details = usage.prompt_tokens_details - input_details_dict: Dict[str, int] = {} + input_details_dict: Dict[str, Any] = {} if ( hasattr(prompt_details, "cached_tokens") @@ -2123,6 +2123,16 @@ class LiteLLMCompletionResponsesConfig: ): input_details_dict["audio_tokens"] = prompt_details.audio_tokens + # Forward Anthropic prompt-caching fields so callers see cache + # creation token counts on the Responses API usage payload. + for cache_field in ( + "cache_creation_tokens", + "cache_creation_token_details", + ): + value = getattr(prompt_details, cache_field, None) + if value is not None: + input_details_dict[cache_field] = value + if input_details_dict: response_usage.input_tokens_details = InputTokensDetails( **input_details_dict diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 93e45c7dd11..0d4d780fec2 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -1565,6 +1565,86 @@ class TestUsageTransformation: assert response_usage.input_tokens_details.cached_tokens == 3 assert response_usage.input_tokens_details.text_tokens == 6 + def test_transform_usage_with_cache_creation_tokens_anthropic(self): + """Test that Anthropic cache creation token fields are propagated onto input_tokens_details""" + usage = Usage( + prompt_tokens=20, + completion_tokens=10, + total_tokens=30, + prompt_tokens_details=PromptTokensDetailsWrapper( + cached_tokens=4, # cache_read_input_tokens + cache_creation_tokens=12, # cache_creation_input_tokens + text_tokens=4, + ), + ) + + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="claude-sonnet-4", + object="chat.completion", + usage=usage, + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message(content="Hi!", role="assistant"), + ) + ], + ) + + response_usage = LiteLLMCompletionResponsesConfig._transform_chat_completion_usage_to_responses_usage( + chat_completion_response=chat_completion_response + ) + + assert response_usage.input_tokens_details is not None + assert response_usage.input_tokens_details.cached_tokens == 4 + assert ( + getattr( + response_usage.input_tokens_details, "cache_creation_tokens", None + ) + == 12 + ) + + def test_transform_usage_without_cache_creation_tokens(self): + """Test that cache_creation_tokens is absent when not present on prompt_tokens_details""" + usage = Usage( + prompt_tokens=10, + completion_tokens=5, + total_tokens=15, + prompt_tokens_details=PromptTokensDetailsWrapper( + cached_tokens=0, + text_tokens=10, + ), + ) + + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="gpt-4", + object="chat.completion", + usage=usage, + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message(content="Hi!", role="assistant"), + ) + ], + ) + + response_usage = LiteLLMCompletionResponsesConfig._transform_chat_completion_usage_to_responses_usage( + chat_completion_response=chat_completion_response + ) + + assert response_usage.input_tokens_details is not None + assert ( + getattr( + response_usage.input_tokens_details, "cache_creation_tokens", None + ) + is None + ) + def test_transform_usage_with_reasoning_tokens_gemini(self): """Test that reasoning_tokens from Gemini are properly transformed to output_tokens_details""" # Setup: Simulate Gemini usage with thoughtsTokenCount