mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 04:21:34 +00:00
Merge pull request #24445 from quora/fix/missing-content-part-added
fix(responses): emit content_part.added event for non-OpenAI models
This commit is contained in:
@@ -845,6 +845,15 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||
)
|
||||
event.__dict__["sequence_number"] = self._sequence_number
|
||||
self._pending_response_events.append(event)
|
||||
|
||||
# Emit content_part.added immediately after output_item.added for message
|
||||
# items. The OpenAI Responses spec requires this event before any
|
||||
# output_text.delta events so downstream parsers can initialize the
|
||||
# text part structure.
|
||||
if not self.sent_content_part_added_event:
|
||||
self.sent_content_part_added_event = True
|
||||
content_part_event = self.create_content_part_added_event()
|
||||
self._pending_response_events.append(content_part_event)
|
||||
return
|
||||
|
||||
async def __anext__(
|
||||
|
||||
@@ -1600,6 +1600,7 @@ def test_anthropic_via_responses_api():
|
||||
ResponsesAPIStreamEvents.RESPONSE_CREATED,
|
||||
ResponsesAPIStreamEvents.RESPONSE_IN_PROGRESS,
|
||||
ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
|
||||
ResponsesAPIStreamEvents.CONTENT_PART_ADDED,
|
||||
ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA, # Can occur multiple times
|
||||
ResponsesAPIStreamEvents.OUTPUT_TEXT_DONE,
|
||||
ResponsesAPIStreamEvents.CONTENT_PART_DONE,
|
||||
|
||||
+94
@@ -1899,3 +1899,97 @@ class TestStreamingIDConsistency:
|
||||
else getattr(assistant_messages[0], "tool_calls", None)
|
||||
)
|
||||
assert tool_calls is not None and len(tool_calls) == 1
|
||||
|
||||
|
||||
class TestEnsureOutputItemContentPartAdded:
|
||||
"""Test that _ensure_output_item_for_chunk emits content_part.added after
|
||||
output_item.added for message items."""
|
||||
|
||||
def _make_iterator(self):
|
||||
"""Create a minimal LiteLLMCompletionStreamingIterator for testing."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
||||
LiteLLMCompletionStreamingIterator,
|
||||
)
|
||||
|
||||
iterator = LiteLLMCompletionStreamingIterator.__new__(
|
||||
LiteLLMCompletionStreamingIterator
|
||||
)
|
||||
iterator.sent_output_item_added_event = False
|
||||
iterator.sent_content_part_added_event = False
|
||||
iterator._sequence_number = 0
|
||||
iterator._cached_item_id = None
|
||||
iterator._cached_reasoning_item_id = None
|
||||
iterator._reasoning_active = False
|
||||
iterator._pending_response_events = []
|
||||
return iterator
|
||||
|
||||
def _make_text_chunk(self):
|
||||
"""Create a mock ModelResponseStream with a text delta."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
chunk = MagicMock()
|
||||
delta = MagicMock()
|
||||
delta.reasoning_content = None
|
||||
delta.tool_calls = None
|
||||
chunk.choices = [MagicMock(delta=delta)]
|
||||
return chunk
|
||||
|
||||
def _make_reasoning_chunk(self):
|
||||
"""Create a mock ModelResponseStream with a reasoning delta."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
chunk = MagicMock()
|
||||
delta = MagicMock()
|
||||
delta.reasoning_content = "thinking..."
|
||||
delta.tool_calls = None
|
||||
chunk.choices = [MagicMock(delta=delta)]
|
||||
return chunk
|
||||
|
||||
def test_message_item_emits_content_part_added(self):
|
||||
"""content_part.added must follow output_item.added for message items."""
|
||||
from litellm.types.llms.openai import (
|
||||
ContentPartAddedEvent,
|
||||
OutputItemAddedEvent,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
|
||||
iterator = self._make_iterator()
|
||||
chunk = self._make_text_chunk()
|
||||
|
||||
iterator._ensure_output_item_for_chunk(chunk)
|
||||
|
||||
events = iterator._pending_response_events
|
||||
assert len(events) == 2
|
||||
assert isinstance(events[0], OutputItemAddedEvent)
|
||||
assert events[0].type == ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED
|
||||
assert isinstance(events[1], ContentPartAddedEvent)
|
||||
assert events[1].type == ResponsesAPIStreamEvents.CONTENT_PART_ADDED
|
||||
assert events[1].part.type == "output_text"
|
||||
assert iterator.sent_content_part_added_event is True
|
||||
|
||||
def test_reasoning_item_does_not_emit_content_part_added(self):
|
||||
"""Reasoning items should not get a content_part.added event."""
|
||||
from litellm.types.llms.openai import OutputItemAddedEvent
|
||||
|
||||
iterator = self._make_iterator()
|
||||
chunk = self._make_reasoning_chunk()
|
||||
|
||||
iterator._ensure_output_item_for_chunk(chunk)
|
||||
|
||||
events = iterator._pending_response_events
|
||||
assert len(events) == 1
|
||||
assert isinstance(events[0], OutputItemAddedEvent)
|
||||
assert iterator.sent_content_part_added_event is False
|
||||
|
||||
def test_only_emits_once(self):
|
||||
"""Calling _ensure_output_item_for_chunk twice should not duplicate events."""
|
||||
iterator = self._make_iterator()
|
||||
chunk = self._make_text_chunk()
|
||||
|
||||
iterator._ensure_output_item_for_chunk(chunk)
|
||||
iterator._ensure_output_item_for_chunk(chunk)
|
||||
|
||||
events = iterator._pending_response_events
|
||||
assert len(events) == 2
|
||||
|
||||
Reference in New Issue
Block a user