From 4657d43fb999972bc9c77f65a4ee5a40bb68b44c Mon Sep 17 00:00:00 2001 From: kerry Date: Wed, 16 Sep 2026 00:27:09 +0000 Subject: [PATCH 1/4] fix(anthropic): tolerate message_delta chunks without usage in streams Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/anthropic/chat/handler.py | 4 +++- litellm/types/llms/anthropic.py | 2 +- .../anthropic/chat/test_anthropic_chat_handler.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 5d3ae444b42..4dd0deeb62b 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -1167,7 +1167,9 @@ class ModelResponseIterator: # (matches OpenAI behavior and non-streaming Anthropic implementation) if self.converted_response_format_tool: finish_reason = "stop" - usage: Final = self._handle_usage(anthropic_usage_chunk=message_delta["usage"]) + usage: Final = ( + self._handle_usage(anthropic_usage_chunk=message_delta["usage"]) if "usage" in message_delta else None + ) container: Final = message_delta["delta"].get("container") return finish_reason, usage, container diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index d56ada07ed5..ecb8dbb2502 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -586,7 +586,7 @@ class MessageBlockDelta(TypedDict): type: Literal["message_delta"] delta: MessageDelta - usage: UsageDelta + usage: NotRequired[UsageDelta] context_management: NotRequired[ContextManagementResponse] diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index 83201aef143..ac05cdcf251 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -579,6 +579,21 @@ def test_text_only_streaming_has_index_zero(): ), f"Expected index=0, got {parsed.choices[0].index}" +def test_message_delta_without_usage_returns_chunk_with_no_usage(): + """A message_delta event may carry no usage field; it must not raise.""" + iterator = ModelResponseIterator(None, sync_stream=True) + + model_response = iterator.chunk_parser( + { + "type": "message_delta", + "delta": {"stop_reason": "end_turn", "stop_sequence": None}, + } + ) + + assert model_response.choices[0].finish_reason == "stop" + assert model_response.usage is None + + def test_streaming_thinking_deltas_count_reasoning_tokens_in_usage(): """Anthropic streaming usage should account for emitted thinking deltas.""" chunks = [ From 10aef224e7acc6a5f47c77d63c7a98f81728f377 Mon Sep 17 00:00:00 2001 From: kerry Date: Wed, 16 Sep 2026 01:34:28 +0000 Subject: [PATCH 2/4] style(anthropic): apply repository conventions to the missing-usage change Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/types/llms/anthropic.py | 2 +- .../llms/anthropic/chat/test_anthropic_chat_handler.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index ecb8dbb2502..bcdee86360e 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -586,7 +586,7 @@ class MessageBlockDelta(TypedDict): type: Literal["message_delta"] delta: MessageDelta - usage: NotRequired[UsageDelta] + usage: NotRequired[ReadOnly[UsageDelta]] context_management: NotRequired[ContextManagementResponse] diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index ac05cdcf251..32c98a83948 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -580,10 +580,9 @@ def test_text_only_streaming_has_index_zero(): def test_message_delta_without_usage_returns_chunk_with_no_usage(): - """A message_delta event may carry no usage field; it must not raise.""" - iterator = ModelResponseIterator(None, sync_stream=True) + iterator: Final = ModelResponseIterator(None, sync_stream=True) - model_response = iterator.chunk_parser( + model_response: Final = iterator.chunk_parser( { "type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None}, From 091a38cce6b569439e116e8be5f493f06a0974de Mon Sep 17 00:00:00 2001 From: kerry Date: Wed, 16 Sep 2026 01:43:08 +0000 Subject: [PATCH 3/4] test(anthropic): import Final for the annotated locals Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llms/anthropic/chat/test_anthropic_chat_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index 32c98a83948..c3400dc40c3 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -1,6 +1,7 @@ import json import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import httpx From a6228fab66f289b9e8014c4fc75646eae821d1aa Mon Sep 17 00:00:00 2001 From: kerry Date: Wed, 16 Sep 2026 02:03:21 +0000 Subject: [PATCH 4/4] fix(anthropic): write usage into copies of the read-only message_delta chunk Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../adapters/streaming_iterator.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index e7179aad25b..4486eb0985a 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -376,10 +376,9 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): usage_dict: UsageDelta = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta( chunk.usage ) - merged_chunk["usage"] = usage_dict if self.applied_edits and "context_management" not in merged_chunk: merged_chunk["context_management"] = ContextManagementResponse(applied_edits=list(self.applied_edits)) - return self._augment_message_delta_usage(merged_chunk) + return self._augment_message_delta_usage({**merged_chunk, "usage": usage_dict}) def _handle_choiceless_chunk(self, chunk: "ModelResponseStream") -> bool: """Consume an OpenAI-compatible chunk that carries no ``choices``. @@ -448,8 +447,7 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): } iterations.append(message_iteration) augmented_usage["iterations"] = iterations - augmented["usage"] = augmented_usage - return augmented + return {**augmented, "usage": augmented_usage} def _next_compaction_event(self) -> dict[str, object] | None: """Return the next compaction content-block SSE event, or ``None``.