From cf39cab8d99657275ffdf605e94927eb3fdaedac Mon Sep 17 00:00:00 2001 From: Maxwell Calkin <101308415+MaxwellCalkin@users.noreply.github.com> Date: Sun, 8 Mar 2026 08:29:38 -0400 Subject: [PATCH] fix(streaming): add bounds check for output_index in Responses API stream accumulator Fixes IndexError (list index out of range) that occurs intermittently in ResponseStreamState.accumulate_event() and handle_event() when output_index references an item not yet added to the snapshot. This happens when using the Responses API with streaming and tools (e.g., web_search, code_interpreter) or when resuming streams with starting_after. The API may deliver events referencing output indices that have not yet been populated via response.output_item.added. The fix adds bounds checks before accessing snapshot.output[output_index] in both accumulate_event() and handle_event(). When the index is out of bounds, accumulate_event() silently skips the snapshot update (the final response.completed event will have the full data), and handle_event() falls back to emitting the raw event without snapshot enrichment. Fixes #2852 --- .../lib/streaming/responses/_responses.py | 128 ++++++++++-------- 1 file changed, 70 insertions(+), 58 deletions(-) diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py index 6975a9260d..3efc1ecbd1 100644 --- a/src/openai/lib/streaming/responses/_responses.py +++ b/src/openai/lib/streaming/responses/_responses.py @@ -250,60 +250,69 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven events: List[ResponseStreamEvent[TextFormatT]] = [] if event.type == "response.output_text.delta": - output = snapshot.output[event.output_index] - assert output.type == "message" + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + assert output.type == "message" - content = output.content[event.content_index] - assert content.type == "output_text" + content = output.content[event.content_index] + assert content.type == "output_text" - events.append( - build( - ResponseTextDeltaEvent, - content_index=event.content_index, - delta=event.delta, - item_id=event.item_id, - output_index=event.output_index, - sequence_number=event.sequence_number, - logprobs=event.logprobs, - type="response.output_text.delta", - snapshot=content.text, + events.append( + build( + ResponseTextDeltaEvent, + content_index=event.content_index, + delta=event.delta, + item_id=event.item_id, + output_index=event.output_index, + sequence_number=event.sequence_number, + logprobs=event.logprobs, + type="response.output_text.delta", + snapshot=content.text, + ) ) - ) + else: + events.append(event) elif event.type == "response.output_text.done": - output = snapshot.output[event.output_index] - assert output.type == "message" + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + assert output.type == "message" - content = output.content[event.content_index] - assert content.type == "output_text" + content = output.content[event.content_index] + assert content.type == "output_text" - events.append( - build( - ResponseTextDoneEvent[TextFormatT], - content_index=event.content_index, - item_id=event.item_id, - output_index=event.output_index, - sequence_number=event.sequence_number, - logprobs=event.logprobs, - type="response.output_text.done", - text=event.text, - parsed=parse_text(event.text, text_format=self._text_format), + events.append( + build( + ResponseTextDoneEvent[TextFormatT], + content_index=event.content_index, + item_id=event.item_id, + output_index=event.output_index, + sequence_number=event.sequence_number, + logprobs=event.logprobs, + type="response.output_text.done", + text=event.text, + parsed=parse_text(event.text, text_format=self._text_format), + ) ) - ) + else: + events.append(event) elif event.type == "response.function_call_arguments.delta": - output = snapshot.output[event.output_index] - assert output.type == "function_call" - - events.append( - build( - ResponseFunctionCallArgumentsDeltaEvent, - delta=event.delta, - item_id=event.item_id, - output_index=event.output_index, - sequence_number=event.sequence_number, - type="response.function_call_arguments.delta", - snapshot=output.arguments, + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + assert output.type == "function_call" + + events.append( + build( + ResponseFunctionCallArgumentsDeltaEvent, + delta=event.delta, + item_id=event.item_id, + output_index=event.output_index, + sequence_number=event.sequence_number, + type="response.function_call_arguments.delta", + snapshot=output.arguments, + ) ) - ) + else: + events.append(event) elif event.type == "response.completed": response = self._completed_response @@ -341,21 +350,24 @@ def accumulate_event(self, event: RawResponseStreamEvent) -> ParsedResponseSnaps else: snapshot.output.append(event.item) elif event.type == "response.content_part.added": - output = snapshot.output[event.output_index] - if output.type == "message": - output.content.append( - construct_type_unchecked(type_=cast(Any, ParsedContent), value=event.part.to_dict()) - ) + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + if output.type == "message": + output.content.append( + construct_type_unchecked(type_=cast(Any, ParsedContent), value=event.part.to_dict()) + ) elif event.type == "response.output_text.delta": - output = snapshot.output[event.output_index] - if output.type == "message": - content = output.content[event.content_index] - assert content.type == "output_text" - content.text += event.delta + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + if output.type == "message": + content = output.content[event.content_index] + assert content.type == "output_text" + content.text += event.delta elif event.type == "response.function_call_arguments.delta": - output = snapshot.output[event.output_index] - if output.type == "function_call": - output.arguments += event.delta + if event.output_index < len(snapshot.output): + output = snapshot.output[event.output_index] + if output.type == "function_call": + output.arguments += event.delta elif event.type == "response.completed": self._completed_response = parse_response( text_format=self._text_format,