From 0fb63aa1522b650f6ddf53b74baff1a57d093463 Mon Sep 17 00:00:00 2001 From: Claudio Gallardo Date: Wed, 5 Nov 2025 11:55:28 -0300 Subject: [PATCH 1/2] feat: Emit ResponseFunctionCallArgumentsDoneEvent with name from snapshot Fixes #2723 Problem: When streaming Responses API with multiple function tools, the response.function_call_arguments.done event returns name=None, making it impossible to determine which function to call. Root Cause: ResponseStreamState.handle_event() only handles delta events for function calls, but not done events. The done event falls through to 'else: events.append(event)', returning the raw event without processing. The raw event from the API doesn't include the name field - it must be taken from the accumulated snapshot. Solution: Added elif block to handle response.function_call_arguments.done events, following the proven pattern from Chat Completions API's _add_tool_done_event() method. Changes: 1. Added ResponseFunctionCallArgumentsDoneEvent import 2. Added parse_function_tool_arguments import 3. Added elif block to emit done event with: - name from accumulated snapshot (not raw event) - parsed_arguments using input_tools 4. Exported ResponseFunctionCallArgumentsDoneEvent in __init__.py Pattern: This follows the same approach as Chat Completions streaming, which correctly emits done events with name from the snapshot. Impact: Non-breaking change - only populates a field that was previously None. Existing code continues to work, new code can now access the function name to determine which function was called. --- .../lib/streaming/responses/__init__.py | 1 + .../lib/streaming/responses/_responses.py | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/openai/lib/streaming/responses/__init__.py b/src/openai/lib/streaming/responses/__init__.py index ff073633bf..019dbaa7fd 100644 --- a/src/openai/lib/streaming/responses/__init__.py +++ b/src/openai/lib/streaming/responses/__init__.py @@ -2,6 +2,7 @@ ResponseTextDoneEvent as ResponseTextDoneEvent, ResponseTextDeltaEvent as ResponseTextDeltaEvent, ResponseFunctionCallArgumentsDeltaEvent as ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent as ResponseFunctionCallArgumentsDoneEvent, ) from ._responses import ( ResponseStream as ResponseStream, diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py index 6975a9260d..204b04b86b 100644 --- a/src/openai/lib/streaming/responses/_responses.py +++ b/src/openai/lib/streaming/responses/_responses.py @@ -12,13 +12,14 @@ ResponseCompletedEvent, ResponseTextDeltaEvent, ResponseFunctionCallArgumentsDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, ) from ...._types import Omit, omit from ...._utils import is_given, consume_sync_iterator, consume_async_iterator from ...._models import build, construct_type_unchecked from ...._streaming import Stream, AsyncStream from ....types.responses import ParsedResponse, ResponseStreamEvent as RawResponseStreamEvent -from ..._parsing._responses import TextFormatT, parse_text, parse_response +from ..._parsing._responses import TextFormatT, parse_text, parse_response, parse_function_tool_arguments from ....types.responses.tool_param import ToolParam from ....types.responses.parsed_response import ( ParsedContent, @@ -305,6 +306,30 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven ) ) + elif event.type == "response.function_call_arguments.done": + output = snapshot.output[event.output_index] + assert output.type == "function_call" + + # Parse arguments using input_tools + parsed_arguments = parse_function_tool_arguments( + input_tools=self._input_tools, + function_call=output + ) + output.parsed_arguments = parsed_arguments + + # Emit event with name from accumulated snapshot + events.append( + build( + ResponseFunctionCallArgumentsDoneEvent, + arguments=output.arguments, + item_id=event.item_id, + name=output.name, # FROM SNAPSHOT, not raw event + output_index=event.output_index, + sequence_number=event.sequence_number, + type="response.function_call_arguments.done", + ) + ) + elif event.type == "response.completed": response = self._completed_response assert response is not None From 847a96ad78f6a3801e592ffe8f7e8c3e5d062793 Mon Sep 17 00:00:00 2001 From: Claudio Gallardo Date: Wed, 5 Nov 2025 12:22:39 -0300 Subject: [PATCH 2/2] fix: Use event.arguments as source of truth for done events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address bot feedback on PR #2731. The bot correctly identified an edge case where the server might emit a response.function_call_arguments.done event WITHOUT prior delta events. In this scenario: - output.arguments would be empty (only populated by deltas) - event.arguments contains the complete arguments payload Changed to use event.arguments as the source of truth, which ensures arguments are preserved in all scenarios: ✅ With deltas: event.arguments contains accumulated result ✅ Without deltas: event.arguments contains full payload This maintains backward compatibility while fixing the edge case. Co-authored-by: chatgpt-codex-connector[bot] --- src/openai/lib/streaming/responses/_responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py index 204b04b86b..d1596f4b71 100644 --- a/src/openai/lib/streaming/responses/_responses.py +++ b/src/openai/lib/streaming/responses/_responses.py @@ -321,7 +321,7 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven events.append( build( ResponseFunctionCallArgumentsDoneEvent, - arguments=output.arguments, + arguments=event.arguments, # Use event as source of truth item_id=event.item_id, name=output.name, # FROM SNAPSHOT, not raw event output_index=event.output_index,