diff --git a/src/openai/lib/streaming/responses/_responses.py b/src/openai/lib/streaming/responses/_responses.py index 6975a9260d..5b4a7d2848 100644 --- a/src/openai/lib/streaming/responses/_responses.py +++ b/src/openai/lib/streaming/responses/_responses.py @@ -11,6 +11,7 @@ ResponseTextDoneEvent, ResponseCompletedEvent, ResponseTextDeltaEvent, + ResponseFunctionCallArgumentsDoneEvent, ResponseFunctionCallArgumentsDeltaEvent, ) from ...._types import Omit, omit @@ -305,6 +306,22 @@ 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" + + events.append( + build( + ResponseFunctionCallArgumentsDoneEvent, + arguments=event.arguments, + item_id=event.item_id, + name=event.name or output.name, + 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 diff --git a/tests/lib/responses/test_responses.py b/tests/lib/responses/test_responses.py index 8e5f16df95..82b31befbf 100644 --- a/tests/lib/responses/test_responses.py +++ b/tests/lib/responses/test_responses.py @@ -1,7 +1,9 @@ from __future__ import annotations +import json from typing_extensions import TypeVar +import httpx import pytest from respx import MockRouter from inline_snapshot import snapshot @@ -52,6 +54,69 @@ def test_stream_method_definition_in_sync(sync: bool, client: OpenAI, async_clie ) +@pytest.mark.respx(base_url=base_url) +@pytest.mark.parametrize("client", [False], indirect=True) +def test_stream_function_call_arguments_done_includes_name(client: OpenAI, respx_mock: MockRouter) -> None: + response = { + "id": "resp_123", + "object": "response", + "created_at": 1720000000, + "status": "in_progress", + "error": None, + "incomplete_details": None, + "instructions": None, + "model": "gpt-4o-mini-2024-07-18", + "output": [], + "parallel_tool_calls": True, + "temperature": 1.0, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "metadata": {}, + } + arguments = '{"part_number":"ABC-123"}' + events = [ + { + "type": "response.created", + "sequence_number": 0, + "response": response, + }, + { + "type": "response.output_item.added", + "sequence_number": 1, + "output_index": 0, + "item": { + "id": "fc_123", + "type": "function_call", + "status": "in_progress", + "call_id": "call_123", + "name": "search_parts", + "arguments": "", + }, + }, + { + "type": "response.function_call_arguments.done", + "sequence_number": 2, + "item_id": "fc_123", + "output_index": 0, + "name": None, + "arguments": arguments, + }, + ] + sse = "".join(f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" for event in events) + + respx_mock.post("/responses").mock( + return_value=httpx.Response(200, text=sse, headers={"Content-Type": "text/event-stream"}) + ) + + with client.responses.stream(model="gpt-4o-mini", input="search for a part") as stream: + streamed_events = list(stream) + + done_event = next(event for event in streamed_events if event.type == "response.function_call_arguments.done") + assert done_event.name == "search_parts" + assert done_event.arguments == arguments + + @pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) def test_parse_method_definition_in_sync(sync: bool, client: OpenAI, async_client: AsyncOpenAI) -> None: checking_client: OpenAI | AsyncOpenAI = client if sync else async_client