From 461d73a206caf31b96bb48efb8c2682955794269 Mon Sep 17 00:00:00 2001 From: Felmon Fekadu Date: Mon, 13 Jul 2026 00:51:28 -0600 Subject: [PATCH] fix(models): preserve streamed content part order --- src/agents/models/chatcmpl_stream_handler.py | 9 +++++++-- .../test_openai_chatcompletions_stream.py | 17 +++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/agents/models/chatcmpl_stream_handler.py b/src/agents/models/chatcmpl_stream_handler.py index f365461a06..2e0acea70c 100644 --- a/src/agents/models/chatcmpl_stream_handler.py +++ b/src/agents/models/chatcmpl_stream_handler.py @@ -1119,10 +1119,15 @@ async def handle_stream( ) if state.provider_data: assistant_msg.provider_data = state.provider_data.copy() # type: ignore[attr-defined] + + content_parts: list[tuple[int, ResponseOutputText | ResponseOutputRefusal]] = [] if state.text_content_index_and_output: - assistant_msg.content.append(state.text_content_index_and_output[1]) + content_parts.append(state.text_content_index_and_output) if state.refusal_content_index_and_output: - assistant_msg.content.append(state.refusal_content_index_and_output[1]) + content_parts.append(state.refusal_content_index_and_output) + assistant_msg.content.extend( + part for _, part in sorted(content_parts, key=lambda item: item[0]) + ) outputs.append(assistant_msg) # send a ResponseOutputItemDone for the assistant message diff --git a/tests/models/test_openai_chatcompletions_stream.py b/tests/models/test_openai_chatcompletions_stream.py index 75919a6a11..5484440cf7 100644 --- a/tests/models/test_openai_chatcompletions_stream.py +++ b/tests/models/test_openai_chatcompletions_stream.py @@ -790,21 +790,30 @@ async def test_stream_handler_places_text_after_existing_refusal_part() -> None: events = await _collect_handler_events(*chunks) + refusal_part_added = next( + event + for event in events + if event.type == "response.content_part.added" + and isinstance(event.part, ResponseOutputRefusal) + ) text_part_added = next( event for event in events if event.type == "response.content_part.added" and isinstance(event.part, ResponseOutputText) ) + assert refusal_part_added.content_index == 0 assert text_part_added.content_index == 1 completed_event = next(event for event in events if event.type == "response.completed") assistant_item = completed_event.response.output[0] assert isinstance(assistant_item, ResponseOutputMessage) - assert isinstance(assistant_item.content[0], ResponseOutputText) - assert isinstance(assistant_item.content[1], ResponseOutputRefusal) - assert assistant_item.content[0].text == "partial" - assert assistant_item.content[1].refusal == "blocked" + assert isinstance( + assistant_item.content[refusal_part_added.content_index], ResponseOutputRefusal + ) + assert isinstance(assistant_item.content[text_part_added.content_index], ResponseOutputText) + assert assistant_item.content[refusal_part_added.content_index].refusal == "blocked" + assert assistant_item.content[text_part_added.content_index].text == "partial" @pytest.mark.allow_call_model_methods