From cc09b52616de5fa744095cc73e01ba764c951586 Mon Sep 17 00:00:00 2001 From: tin-berri Date: Mon, 26 Oct 2026 22:42:38 -0400 Subject: [PATCH] fix(proxy): include Claude web search fees in streamed usage cost (#48528) --- litellm/proxy/common_request_processing.py | 1 + .../proxy/test_common_request_processing.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 61cf4d696a1..8e681c615ec 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -4687,6 +4687,7 @@ class ProxyBaseLLMRequestProcessing: total_tokens=total_tokens, completion_tokens_details=usage.get("completion_tokens_details"), prompt_tokens_details=usage.get("prompt_tokens_details"), + server_tool_use=usage.get("server_tool_use"), ) @staticmethod diff --git a/tests/unit/proxy/test_common_request_processing.py b/tests/unit/proxy/test_common_request_processing.py index 82dd3479321..561577094df 100644 --- a/tests/unit/proxy/test_common_request_processing.py +++ b/tests/unit/proxy/test_common_request_processing.py @@ -10175,6 +10175,32 @@ class TestInjectCostIntoUsageDict: class TestProcessChunkWithCostInjection: + @pytest.mark.parametrize("model", ["claude-opus-5-5", "vertex_ai/claude-opus-5-5"]) + @pytest.mark.parametrize("search_requests", [None, "null", 0, 1, 2]) + def test_claude_search_stream_cost_matches_complete_usage( + self, monkeypatch: pytest.MonkeyPatch, model: str, search_requests: int | Literal["null"] | None + ) -> None: + monkeypatch.setattr(litellm, "include_cost_in_streaming_usage", True) + usage: Final = litellm.Usage( + prompt_tokens=10760, + completion_tokens=399, + total_tokens=11159, + server_tool_use={"web_search_requests": search_requests} if isinstance(search_requests, int) else None, + ) + wire_usage: Final = usage.model_dump() | ({"server_tool_use": None} if search_requests == "null" else {}) + event: Final = {"object": "chat.completion.chunk", "choices": [], "usage": wire_usage} + chunk: Final = f"data: {json.dumps(event)}\n\n".encode() + + result: Final = ProxyBaseLLMRequestProcessing.process_chunk_with_cost_injection(chunk, model) + + assert isinstance(result, bytes) + payload: Final = json.loads(result.decode().removeprefix("data: ")) + expected_cost: Final = litellm.completion_cost( + model=model, completion_response=litellm.ModelResponse(usage=usage) + ) + assert payload["usage"]["cost"] == pytest.approx(expected_cost) + assert "cost" not in event["usage"] + def test_complete_usage_frame_chunk_is_injected(self, monkeypatch): monkeypatch.setattr(litellm, "include_cost_in_streaming_usage", True) chunk = (