fix(anthropic): tolerate message_delta chunks without usage in streams

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
kerry 2026-09-16 00:27:09 +00:00
parent e4a7d2aa0b
commit 4657d43fb9
3 changed files with 19 additions and 2 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

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

View file

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