From 60cf6f902378784c51e14efba7318b96b475269b Mon Sep 17 00:00:00 2001 From: oss-agent-shin <279349115+oss-agent-shin@users.noreply.github.com> Date: Thu, 7 May 2026 00:02:32 +0000 Subject: [PATCH] Fix Anthropic streaming web search usage Co-authored-by: ishaan-berri --- litellm/types/utils.py | 4 +- .../test_streaming_chunk_builder_utils.py | 63 ++++++++++++++++++- tests/test_litellm/types/test_types_utils.py | 11 ++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 00a7748309b..60b20f8a129 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1553,7 +1553,7 @@ class Usage(SafeAttributeModel, CompletionUsage): completion_tokens_details: Optional[ Union[CompletionTokensDetailsWrapper, dict] ] = None, - server_tool_use: Optional[ServerToolUse] = None, + server_tool_use: Optional[Union[ServerToolUse, Dict[str, Any]]] = None, cost: Optional[float] = None, **params, ): @@ -1655,6 +1655,8 @@ class Usage(SafeAttributeModel, CompletionUsage): ) if server_tool_use is not None: + if isinstance(server_tool_use, dict): + server_tool_use = ServerToolUse(**server_tool_use) self.server_tool_use = server_tool_use else: # maintain openai compatibility in usage object if possible del self.server_tool_use diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py b/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py index e40a0817fd9..521e2841dac 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_chunk_builder_utils.py @@ -8,7 +8,7 @@ sys.path.insert( 0, os.path.abspath("../../..") ) # Adds the parent directory to the system path -from litellm import stream_chunk_builder +from litellm import completion_cost, stream_chunk_builder from litellm.litellm_core_utils.streaming_chunk_builder_utils import ChunkProcessor from litellm.types.utils import ( ChatCompletionDeltaToolCall, @@ -520,7 +520,66 @@ def test_stream_chunk_builder_anthropic_web_search(): assert usage.prompt_tokens == 50 assert usage.completion_tokens == 27 assert usage.total_tokens == 77 - assert usage.server_tool_use["web_search_requests"] == 2 + assert usage.server_tool_use.web_search_requests == 2 + + +def test_stream_chunk_builder_anthropic_web_search_cost_calculation(): + chunk1 = ModelResponseStream( + id="chatcmpl-mocked-usage-1", + created=1745513206, + model="claude-sonnet-4-5-20250929", + object="chat.completion.chunk", + system_fingerprint=None, + choices=[ + StreamingChoices( + finish_reason=None, + index=0, + delta=Delta(content="", role="assistant"), + logprobs=None, + ) + ], + provider_specific_fields=None, + stream_options={"include_usage": True}, + usage=Usage( + completion_tokens=0, + prompt_tokens=50, + total_tokens=50, + completion_tokens_details=None, + server_tool_use=ServerToolUse(web_search_requests=2), + prompt_tokens_details=None, + ), + ) + + chunk2 = ModelResponseStream( + id="chatcmpl-mocked-usage-1", + created=1745513207, + model="claude-sonnet-4-5-20250929", + object="chat.completion.chunk", + system_fingerprint=None, + choices=[ + StreamingChoices( + finish_reason="stop", + index=0, + delta=Delta(content="done", role=None), + logprobs=None, + ) + ], + provider_specific_fields=None, + stream_options={"include_usage": True}, + usage=Usage( + completion_tokens=27, + prompt_tokens=0, + total_tokens=27, + completion_tokens_details=None, + prompt_tokens_details=None, + ), + ) + + response = stream_chunk_builder(chunks=[chunk1, chunk2]) + + assert response is not None + assert isinstance(response.usage.server_tool_use, ServerToolUse) + assert completion_cost(completion_response=response) > 0 def test_sort_chunks_handles_dict_hidden_params_created_at(): diff --git a/tests/test_litellm/types/test_types_utils.py b/tests/test_litellm/types/test_types_utils.py index c146847f391..b190500dc5d 100644 --- a/tests/test_litellm/types/test_types_utils.py +++ b/tests/test_litellm/types/test_types_utils.py @@ -45,6 +45,7 @@ def test_usage_dump(): from litellm.types.utils import ( CompletionTokensDetailsWrapper, PromptTokensDetailsWrapper, + ServerToolUse, Usage, ) @@ -74,6 +75,16 @@ def test_usage_dump(): new_usage = Usage(**current_usage.model_dump()) assert new_usage.prompt_tokens_details.web_search_requests == 1 + server_tool_usage = Usage( + completion_tokens=10, + prompt_tokens=5, + total_tokens=15, + server_tool_use=ServerToolUse(web_search_requests=2), + ) + new_server_tool_usage = Usage(**server_tool_usage.model_dump()) + assert isinstance(new_server_tool_usage.server_tool_use, ServerToolUse) + assert new_server_tool_usage.server_tool_use.web_search_requests == 2 + def test_usage_completion_tokens_details_text_tokens(): from litellm.types.utils import Usage