From a7a625a8e07ad1b5670cc5ef92fd024958e2de58 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Tue, 23 Jun 2026 21:11:51 -0700 Subject: [PATCH] chore(types): coerce dict server_tool_use to ServerToolUse in Usage.__init__ The usage-only recovery path added in #31035 builds a Usage whose server_tool_use is a plain dict; on stable/1.86.x Usage.__init__ assigned it verbatim, so attribute access on the recovered usage failed. Replicate the staging Usage.__init__ coercion so server tool-use requests are typed and priced. This dependency has no discrete commit to cherry-pick; it predates internal staging's squashed-root history, so it is carried as a small verbatim copy of current staging behavior. The same coercion makes server_tool_use a ServerToolUse on the pre-existing calculate_usage round-trip (Usage(**model_dump())), so the matching staging test assertion update is carried too: test_stream_chunk_builder_anthropic_web_search now asserts attribute access (and isinstance ServerToolUse) instead of dict subscript, matching staging and the typed representation. --- litellm/types/utils.py | 5 ++++- .../litellm_core_utils/test_streaming_chunk_builder_utils.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index db598d85e55..4ecac4e82b1 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1554,7 +1554,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]] = None, cost: Optional[float] = None, **params, ): @@ -1655,6 +1655,9 @@ class Usage(SafeAttributeModel, CompletionUsage): prompt_tokens_details=_prompt_tokens_details or None, ) + if isinstance(server_tool_use, dict): + server_tool_use = ServerToolUse(**server_tool_use) + if server_tool_use is not None: self.server_tool_use = server_tool_use else: # maintain openai compatibility in usage object if possible 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..35aca525f6c 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 @@ -520,7 +520,10 @@ 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 + # server_tool_use must be a ServerToolUse pydantic so downstream cost-calc + # (which uses attribute access) works. See issue #26153. + assert isinstance(usage.server_tool_use, ServerToolUse) + assert usage.server_tool_use.web_search_requests == 2 def test_sort_chunks_handles_dict_hidden_params_created_at():