Merge pull request #41336 from BerriAI/litellm_fix_anthropic_stream_absent_usage

fix(anthropic): tolerate message_delta events without usage when streaming
This commit is contained in:
kerry-berri 2026-09-15 20:23:01 -07:00 committed by GitHub
commit 4b84fa9230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 21 additions and 6 deletions

View file

@ -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

View file

@ -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``.

View file

@ -586,7 +586,7 @@ class MessageBlockDelta(TypedDict):
type: Literal["message_delta"]
delta: MessageDelta
usage: UsageDelta
usage: NotRequired[ReadOnly[UsageDelta]]
context_management: NotRequired[ContextManagementResponse]

View file

@ -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
@ -579,6 +580,20 @@ 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():
iterator: Final = ModelResponseIterator(None, sync_stream=True)
model_response: Final = 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 = [