From fc17da0aefa2c74b4ec8799d3167526528e3d283 Mon Sep 17 00:00:00 2001 From: Kishan Date: Fri, 27 Jun 2025 18:00:23 +0100 Subject: [PATCH] [Bug Fix] Anthropic - Token Usage Null Handling in calculate_usage (#12068) * [Bug Fix] Anthropic - Token Usage Null Handling in calculate_usage (BerriAI/litellm#11920) * [Fix] Missed a null check and used a cast instead by error --- litellm/llms/anthropic/chat/transformation.py | 25 ++++-- .../test_anthropic_chat_transformation.py | 78 +++++++++++++++++++ 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index bda19caec62..6fe346167e2 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -708,7 +708,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): _litellm_metadata and isinstance(_litellm_metadata, dict) and "user_id" in _litellm_metadata - and not _valid_user_id(_litellm_metadata.get("user_id", None)) + and _litellm_metadata["user_id"] is not None + and not _valid_user_id(_litellm_metadata["user_id"]) ): optional_params["metadata"] = {"user_id": _litellm_metadata["user_id"]} @@ -805,19 +806,29 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def calculate_usage( self, usage_object: dict, reasoning_content: Optional[str] ) -> Usage: - prompt_tokens = usage_object.get("input_tokens", 0) - completion_tokens = usage_object.get("output_tokens", 0) + # NOTE: Sometimes the usage object has None set explicitly for token counts, meaning .get() & key access returns None, and we need to account for this + prompt_tokens = usage_object.get("input_tokens", 0) or 0 + completion_tokens = usage_object.get("output_tokens", 0) or 0 _usage = usage_object cache_creation_input_tokens: int = 0 cache_read_input_tokens: int = 0 web_search_requests: Optional[int] = None - if "cache_creation_input_tokens" in _usage: + if ( + "cache_creation_input_tokens" in _usage + and _usage["cache_creation_input_tokens"] is not None + ): cache_creation_input_tokens = _usage["cache_creation_input_tokens"] - if "cache_read_input_tokens" in _usage: + if ( + "cache_read_input_tokens" in _usage + and _usage["cache_read_input_tokens"] is not None + ): cache_read_input_tokens = _usage["cache_read_input_tokens"] prompt_tokens += cache_read_input_tokens - if "server_tool_use" in _usage: - if "web_search_requests" in _usage["server_tool_use"]: + if "server_tool_use" in _usage and _usage["server_tool_use"] is not None: + if ( + "web_search_requests" in _usage["server_tool_use"] + and _usage["server_tool_use"]["web_search_requests"] is not None + ): web_search_requests = cast( int, _usage["server_tool_use"]["web_search_requests"] ) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 34097669699..ff454968d9c 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -11,6 +11,7 @@ sys.path.insert( from unittest.mock import MagicMock, patch from litellm.llms.anthropic.chat.transformation import AnthropicConfig +from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse def test_response_format_transformation_unit_test(): @@ -57,6 +58,83 @@ def test_calculate_usage(): assert usage._cache_creation_input_tokens == 12304 assert usage._cache_read_input_tokens == 0 +@pytest.mark.parametrize("usage_object,expected_usage", [ + [ + { + "cache_creation_input_tokens": None, + "cache_read_input_tokens": None, + "input_tokens": None, + "output_tokens": 43, + "server_tool_use": None + }, + { + "prompt_tokens": 0, + "completion_tokens": 43, + "total_tokens": 43, + "_cache_creation_input_tokens": 0, + "_cache_read_input_tokens": 0 + } + ], + [ + { + "cache_creation_input_tokens": 100, + "cache_read_input_tokens": 200, + "input_tokens": 1, + "output_tokens": None, + "server_tool_use": None + }, + { + "prompt_tokens": 1 + 200, + "completion_tokens": 0, + "total_tokens": 1 + 200, + "_cache_creation_input_tokens": 100, + "_cache_read_input_tokens": 200, + } + ], + [ + { + "server_tool_use": { + "web_search_requests": 10 + } + }, + { + "server_tool_use": ServerToolUse(web_search_requests=10) + } + ] +]) +def test_calculate_usage_nulls(usage_object, expected_usage): + """ + Correctly deal with null values in usage object + + Fixes https://github.com/BerriAI/litellm/issues/11920 + """ + config = AnthropicConfig() + + usage = config.calculate_usage(usage_object=usage_object, reasoning_content=None) + for k, v in expected_usage.items(): + assert hasattr(usage, k) + assert getattr(usage, k) == v + +@pytest.mark.parametrize("usage_object", [ + { + "server_tool_use": { + "web_search_requests": None + } + }, + { + "server_tool_use": None + } +]) +def test_calculate_usage_server_tool_null(usage_object): + """ + Correctly deal with null values in usage object + + Fixes https://github.com/BerriAI/litellm/issues/11920 + """ + config = AnthropicConfig() + + usage = config.calculate_usage(usage_object=usage_object, reasoning_content=None) + assert not hasattr(usage, "server_tool_use") def test_extract_response_content_with_citations(): config = AnthropicConfig()