fix(responses): keep sync text deltas and give the message item its own output index

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-09-17 05:05:54 +00:00
parent 441021fc96
commit d4e54a0f34
2 changed files with 23 additions and 14 deletions

View file

@ -112,6 +112,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
self.completed_response = None
self.final_text: str = ""
self._cached_item_id: str | None = None
self._message_output_index: int = 0
self._cached_response_id: str | None = None
self._buffered_chunk: ModelResponseStream | None = None
self._upstream_exhausted: bool = False
@ -564,7 +565,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
self._sequence_number += 1
event: Final = OutputItemAddedEvent(
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
output_index=0,
output_index=self._message_output_index,
item=BaseLiteLLMOpenAIResponseObject(
**{
"id": self._cached_item_id,
@ -586,7 +587,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
event: Final = ContentPartAddedEvent(
type=ResponsesAPIStreamEvents.CONTENT_PART_ADDED,
item_id=self._cached_item_id,
output_index=0,
output_index=self._message_output_index,
content_index=0,
part=BaseLiteLLMOpenAIResponseObject(**{"type": "output_text", "text": "", "annotations": []}),
)
@ -598,10 +599,11 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
self._cached_item_id = f"msg_{uuid.uuid4()}"
self.sent_message_item_added_event = True
self.sent_content_part_added_event = True
self._message_output_index = 1 if self._cached_reasoning_item_id is not None else 0
self._sequence_number += 1
event: Final = OutputItemAddedEvent(
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
output_index=0,
output_index=self._message_output_index,
item=BaseLiteLLMOpenAIResponseObject(
**{
"id": self._cached_item_id,
@ -735,7 +737,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
return OutputTextDoneEvent(
type=ResponsesAPIStreamEvents.OUTPUT_TEXT_DONE,
item_id=self._cached_item_id,
output_index=0,
output_index=self._message_output_index,
content_index=0,
text=getattr(litellm_complete_object.choices[0].message, "content", "") or "",
)
@ -771,7 +773,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
return ContentPartDoneEvent(
type=ResponsesAPIStreamEvents.CONTENT_PART_DONE,
item_id=self._cached_item_id,
output_index=0,
output_index=self._message_output_index,
content_index=0,
part=part,
)
@ -790,7 +792,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
)
return OutputItemDoneEvent(
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE,
output_index=0,
output_index=self._message_output_index,
sequence_number=1,
item=BaseLiteLLMOpenAIResponseObject(
**{
@ -951,6 +953,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
if self._cached_reasoning_item_id is None:
self._cached_reasoning_item_id = f"rs_{uuid.uuid4()}"
self._reasoning_item_id = self._cached_reasoning_item_id
self._next_tool_output_index = max(self._next_tool_output_index, 2)
event = OutputItemAddedEvent(
type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
@ -1130,12 +1133,11 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
self.collected_chat_completion_chunks.append(
self._snapshot_chunk_for_stream_chunk_builder(cast(ModelResponseStream, chunk))
)
# Emit any just-queued output_item event
if self._pending_response_events:
return self._pending_response_events.pop(0)
response_api_chunk = self._transform_chat_completion_chunk_to_response_api_chunk(chunk)
if response_api_chunk:
return response_api_chunk
self._pending_response_events.append(response_api_chunk)
if self._pending_response_events:
return self._pending_response_events.pop(0)
# Otherwise, loop to next chunk
except StopIteration:
return self.common_done_event_logic(sync_mode=True)
@ -1177,7 +1179,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
event = OutputTextAnnotationAddedEvent(
type=ResponsesAPIStreamEvents.OUTPUT_TEXT_ANNOTATION_ADDED,
item_id=item_id,
output_index=0,
output_index=self._message_output_index,
content_index=0,
annotation_index=idx,
annotation=annotation_dict,
@ -1210,7 +1212,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
text_delta_event: Final = OutputTextDeltaEvent(
type=ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA,
item_id=item_id,
output_index=0,
output_index=self._message_output_index,
content_index=0,
delta=delta_content,
)

View file

@ -1034,12 +1034,15 @@ async def test_reasoning_then_text_announces_message_item_before_text_events(syn
events: Final = await _collect_events(iterator, sync_mode)
announced_message_ids: set[str] = set()
announced_indexes_by_item_type: dict[str, int] = {}
content_part_added_seen = False
saw_text_delta = False
for event in events:
event_type = getattr(event, "type", None)
if event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED and _is_message_item(event):
announced_message_ids.add(event.item.id)
if event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED:
announced_indexes_by_item_type[event.item.type] = event.output_index
if _is_message_item(event):
announced_message_ids.add(event.item.id)
elif event_type == ResponsesAPIStreamEvents.CONTENT_PART_ADDED:
content_part_added_seen = True
elif event_type in (
@ -1054,6 +1057,10 @@ async def test_reasoning_then_text_announces_message_item_before_text_events(syn
elif event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE and _is_message_item(event):
assert event.item.id in announced_message_ids
assert saw_text_delta
assert "".join(
event.delta for event in events if getattr(event, "type", None) == ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA
) == "Hello!"
assert announced_indexes_by_item_type["message"] != announced_indexes_by_item_type["reasoning"]
@pytest.mark.parametrize("sync_mode", [True, False])