From 1660d306b579580a4e374cee57834e2b7feecab8 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Fri, 8 May 2026 15:24:41 +0900 Subject: [PATCH] feat: default realtime sessions to gpt-realtime-2 (#3190) --- examples/customer_service/main.py | 16 +++++++--- examples/realtime/app/agent.py | 15 ++++++--- examples/realtime/app/server.py | 2 +- examples/realtime/cli/demo.py | 2 +- examples/realtime/twilio/twilio_handler.py | 2 +- examples/realtime/twilio_sip/agents.py | 13 ++++++-- examples/realtime/twilio_sip/server.py | 2 +- src/agents/realtime/__init__.py | 4 +++ src/agents/realtime/config.py | 18 +++++++++++ src/agents/realtime/openai_realtime.py | 31 ++++++++++--------- tests/realtime/test_conversion_helpers.py | 2 +- tests/realtime/test_openai_realtime.py | 22 ++++++++++--- .../realtime/test_realtime_model_settings.py | 6 ++-- .../test_session_payload_and_formats.py | 6 ++-- tests/realtime/test_tracing.py | 8 ++--- 15 files changed, 103 insertions(+), 46 deletions(-) diff --git a/examples/customer_service/main.py b/examples/customer_service/main.py index 65191559..13055a15 100644 --- a/examples/customer_service/main.py +++ b/examples/customer_service/main.py @@ -128,13 +128,21 @@ triage_agent = Agent[AirlineAgentContext]( "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents." ), handoffs=[ - faq_agent, - handoff(agent=seat_booking_agent, on_handoff=on_seat_booking_handoff), + handoff(agent=faq_agent, tool_name_override="transfer_to_faq_agent"), + handoff( + agent=seat_booking_agent, + on_handoff=on_seat_booking_handoff, + tool_name_override="transfer_to_seat_booking_agent", + ), ], ) -faq_agent.handoffs.append(triage_agent) -seat_booking_agent.handoffs.append(triage_agent) +faq_agent.handoffs.append( + handoff(agent=triage_agent, tool_name_override="transfer_to_triage_agent") +) +seat_booking_agent.handoffs.append( + handoff(agent=triage_agent, tool_name_override="transfer_to_triage_agent") +) ### RUN diff --git a/examples/realtime/app/agent.py b/examples/realtime/app/agent.py index 61a06201..e83564e2 100644 --- a/examples/realtime/app/agent.py +++ b/examples/realtime/app/agent.py @@ -15,8 +15,6 @@ will use the agent returned from get_starting_agent() as the starting agent.""" name_override="faq_lookup_tool", description_override="Lookup frequently asked questions." ) async def faq_lookup_tool(question: str) -> str: - print("faq_lookup_tool called with question:", question) - # Simulate a slow API call await asyncio.sleep(3) @@ -91,11 +89,18 @@ triage_agent = RealtimeAgent( "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents." ), tools=[get_weather], - handoffs=[faq_agent, realtime_handoff(seat_booking_agent)], + handoffs=[ + realtime_handoff(faq_agent, tool_name_override="transfer_to_faq_agent"), + realtime_handoff(seat_booking_agent, tool_name_override="transfer_to_seat_booking_agent"), + ], ) -faq_agent.handoffs.append(triage_agent) -seat_booking_agent.handoffs.append(triage_agent) +faq_agent.handoffs.append( + realtime_handoff(triage_agent, tool_name_override="transfer_to_triage_agent") +) +seat_booking_agent.handoffs.append( + realtime_handoff(triage_agent, tool_name_override="transfer_to_triage_agent") +) def get_starting_agent() -> RealtimeAgent: diff --git a/examples/realtime/app/server.py b/examples/realtime/app/server.py index 09eb09fc..dc280bc6 100644 --- a/examples/realtime/app/server.py +++ b/examples/realtime/app/server.py @@ -52,7 +52,7 @@ class RealtimeWebSocketManager: # runner = RealtimeRunner(agent, config=runner_config) model_config: RealtimeModelConfig = { "initial_model_settings": { - "model_name": "gpt-realtime-1.5", + "model_name": "gpt-realtime-2", "turn_detection": { "type": "server_vad", "prefix_padding_ms": 300, diff --git a/examples/realtime/cli/demo.py b/examples/realtime/cli/demo.py index 068be622..6f9d9371 100644 --- a/examples/realtime/cli/demo.py +++ b/examples/realtime/cli/demo.py @@ -225,7 +225,7 @@ class NoUIDemo: model_config: RealtimeModelConfig = { "playback_tracker": self.playback_tracker, "initial_model_settings": { - "model_name": "gpt-realtime-1.5", + "model_name": "gpt-realtime-2", "turn_detection": { "type": "semantic_vad", "interrupt_response": True, diff --git a/examples/realtime/twilio/twilio_handler.py b/examples/realtime/twilio/twilio_handler.py index a0da25cb..21ad37e9 100644 --- a/examples/realtime/twilio/twilio_handler.py +++ b/examples/realtime/twilio/twilio_handler.py @@ -93,7 +93,7 @@ class TwilioHandler: model_config={ "api_key": api_key, "initial_model_settings": { - "model_name": "gpt-realtime-1.5", + "model_name": "gpt-realtime-2", "input_audio_format": "g711_ulaw", "output_audio_format": "g711_ulaw", "turn_detection": { diff --git a/examples/realtime/twilio_sip/agents.py b/examples/realtime/twilio_sip/agents.py index 2a8da238..1afb3eb4 100644 --- a/examples/realtime/twilio_sip/agents.py +++ b/examples/realtime/twilio_sip/agents.py @@ -74,11 +74,18 @@ triage_agent = RealtimeAgent( "before collecting details. Once the greeting is complete, gather context and hand off to " "the FAQ or Records agents when appropriate." ), - handoffs=[faq_agent, realtime_handoff(records_agent)], + handoffs=[ + realtime_handoff(faq_agent, tool_name_override="transfer_to_faq_agent"), + realtime_handoff(records_agent, tool_name_override="transfer_to_records_agent"), + ], ) -faq_agent.handoffs.append(triage_agent) -records_agent.handoffs.append(triage_agent) +faq_agent.handoffs.append( + realtime_handoff(triage_agent, tool_name_override="transfer_to_triage_agent") +) +records_agent.handoffs.append( + realtime_handoff(triage_agent, tool_name_override="transfer_to_triage_agent") +) def get_starting_agent() -> RealtimeAgent: diff --git a/examples/realtime/twilio_sip/server.py b/examples/realtime/twilio_sip/server.py index 9692dd89..576238f6 100644 --- a/examples/realtime/twilio_sip/server.py +++ b/examples/realtime/twilio_sip/server.py @@ -69,7 +69,7 @@ async def accept_call(call_id: str) -> None: f"/realtime/calls/{call_id}/accept", body={ "type": "realtime", - "model": "gpt-realtime-1.5", + "model": "gpt-realtime-2", "instructions": instructions_payload, }, cast_to=dict, diff --git a/src/agents/realtime/__init__.py b/src/agents/realtime/__init__.py index a7ba6160..cd170226 100644 --- a/src/agents/realtime/__init__.py +++ b/src/agents/realtime/__init__.py @@ -7,6 +7,8 @@ from .config import ( RealtimeInputAudioTranscriptionConfig, RealtimeModelName, RealtimeModelTracingConfig, + RealtimeReasoningConfig, + RealtimeReasoningEffort, RealtimeRunConfig, RealtimeSessionModelSettings, RealtimeTurnDetectionConfig, @@ -108,6 +110,8 @@ __all__ = [ "RealtimeInputAudioTranscriptionConfig", "RealtimeModelName", "RealtimeModelTracingConfig", + "RealtimeReasoningConfig", + "RealtimeReasoningEffort", "RealtimeRunConfig", "RealtimeSessionModelSettings", "RealtimeTurnDetectionConfig", diff --git a/src/agents/realtime/config.py b/src/agents/realtime/config.py index 4cc2ca55..e79c5f48 100644 --- a/src/agents/realtime/config.py +++ b/src/agents/realtime/config.py @@ -20,6 +20,7 @@ RealtimeModelName: TypeAlias = ( Literal[ "gpt-realtime", "gpt-realtime-1.5", + "gpt-realtime-2", "gpt-realtime-2025-08-28", "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01", @@ -45,6 +46,10 @@ RealtimeAudioFormat: TypeAlias = ( """The audio format for realtime audio streams.""" +RealtimeReasoningEffort: TypeAlias = Literal["minimal", "low", "medium", "high", "xhigh"] | str +"""The reasoning effort for realtime model responses.""" + + class RealtimeClientMessage(TypedDict): """A raw message to be sent to the model.""" @@ -130,6 +135,13 @@ class RealtimeAudioConfig(TypedDict, total=False): output: RealtimeAudioOutputConfig +class RealtimeReasoningConfig(TypedDict, total=False): + """Reasoning configuration for realtime sessions.""" + + effort: RealtimeReasoningEffort + """The reasoning effort to use for realtime model responses.""" + + class RealtimeSessionModelSettings(TypedDict): """Model settings for a realtime model session.""" @@ -175,6 +187,12 @@ class RealtimeSessionModelSettings(TypedDict): tool_choice: NotRequired[ToolChoice] """How the model should choose which tools to call.""" + parallel_tool_calls: NotRequired[bool] + """Whether the model may make parallel tool calls.""" + + reasoning: NotRequired[RealtimeReasoningConfig] + """Reasoning configuration for realtime model responses.""" + tools: NotRequired[list[Tool]] """List of tools available to the model.""" diff --git a/src/agents/realtime/openai_realtime.py b/src/agents/realtime/openai_realtime.py index fdad4cd8..ed1509e5 100644 --- a/src/agents/realtime/openai_realtime.py +++ b/src/agents/realtime/openai_realtime.py @@ -152,7 +152,7 @@ OpenAIRealtimeAudioOutput = _rt_audio_config.RealtimeAudioConfigOutput # type: _USER_AGENT = f"Agents/Python {__version__}" -DEFAULT_REALTIME_MODEL = "gpt-realtime-1.5" +DEFAULT_REALTIME_MODEL = "gpt-realtime-2" DEFAULT_MODEL_SETTINGS: RealtimeSessionModelSettings = { "voice": "ash", @@ -1438,23 +1438,26 @@ class OpenAIRealtimeWebSocketModel(RealtimeModel): or DEFAULT_MODEL_SETTINGS.get("modalities") ) - # Construct full session object. `type` will be excluded at serialization time for updates. - session_create_request = OpenAISessionCreateRequest( - type="realtime", - model=(model_settings.get("model_name") or self.model) or DEFAULT_REALTIME_MODEL, - output_modalities=output_modalities, - audio=OpenAIRealtimeAudioConfig( + session_create_args: dict[str, Any] = { + "type": "realtime", + "model": (model_settings.get("model_name") or self.model) or DEFAULT_REALTIME_MODEL, + "output_modalities": output_modalities, + "audio": OpenAIRealtimeAudioConfig( input=OpenAIRealtimeAudioInput(**audio_input_args), output=OpenAIRealtimeAudioOutput(**audio_output_args), ), - tools=cast( - Any, - self._tools_to_session_tools( - tools=model_settings.get("tools", []), - handoffs=model_settings.get("handoffs", []), - ), + "tools": self._tools_to_session_tools( + tools=model_settings.get("tools", []), + handoffs=model_settings.get("handoffs", []), ), - ) + } + if model_settings.get("parallel_tool_calls") is not None: + session_create_args["parallel_tool_calls"] = model_settings["parallel_tool_calls"] + if model_settings.get("reasoning") is not None: + session_create_args["reasoning"] = model_settings["reasoning"] + + # Construct full session object. `type` will be excluded at serialization time for updates. + session_create_request = OpenAISessionCreateRequest(**session_create_args) if "instructions" in model_settings: session_create_request.instructions = model_settings.get("instructions") diff --git a/tests/realtime/test_conversion_helpers.py b/tests/realtime/test_conversion_helpers.py index 9696b11e..f169171a 100644 --- a/tests/realtime/test_conversion_helpers.py +++ b/tests/realtime/test_conversion_helpers.py @@ -33,7 +33,7 @@ class TestConversionHelperTryConvertRawMessage: "type": "session.update", "other_data": { "session": { - "model": "gpt-realtime-1.5", + "model": "gpt-realtime-2", "type": "realtime", "modalities": ["text", "audio"], "voice": "ash", diff --git a/tests/realtime/test_openai_realtime.py b/tests/realtime/test_openai_realtime.py index dc485c76..b89a3c74 100644 --- a/tests/realtime/test_openai_realtime.py +++ b/tests/realtime/test_openai_realtime.py @@ -115,8 +115,8 @@ class TestConnectionLifecycle(TestOpenAIRealtimeWebSocketModel): assert model.model == "gpt-4o-realtime-preview" @pytest.mark.asyncio - async def test_connect_defaults_to_gpt_realtime_1_5(self, model, mock_websocket): - """Test that connect() uses gpt-realtime-1.5 when no model is provided.""" + async def test_connect_defaults_to_gpt_realtime_2(self, model, mock_websocket): + """Test that connect() uses gpt-realtime-2 when no model is provided.""" config = { "api_key": "test-api-key-123", "initial_model_settings": {}, @@ -139,8 +139,8 @@ class TestConnectionLifecycle(TestOpenAIRealtimeWebSocketModel): mock_connect.assert_called_once() call_args = mock_connect.call_args - assert call_args[0][0] == "wss://api.openai.com/v1/realtime?model=gpt-realtime-1.5" - assert model.model == "gpt-realtime-1.5" + assert call_args[0][0] == "wss://api.openai.com/v1/realtime?model=gpt-realtime-2" + assert model.model == "gpt-realtime-2" assert model._websocket_task is not None @@ -1488,7 +1488,7 @@ class TestSendEventAndConfig(TestOpenAIRealtimeWebSocketModel): def test_session_config_defaults_audio_formats_when_not_call(self, model): settings: dict[str, Any] = {} cfg = model._get_session_config(settings) - assert cfg.model == "gpt-realtime-1.5" + assert cfg.model == "gpt-realtime-2" assert cfg.audio is not None assert cfg.audio.input is not None assert cfg.audio.input.format is not None @@ -1497,6 +1497,18 @@ class TestSendEventAndConfig(TestOpenAIRealtimeWebSocketModel): assert cfg.audio.output.format is not None assert cfg.audio.output.format.type == "audio/pcm" + def test_session_config_includes_reasoning_capable_settings(self, model): + settings = { + "parallel_tool_calls": False, + "reasoning": {"effort": "low"}, + } + cfg = model._get_session_config(settings) + payload = cfg.model_dump(exclude_unset=True) + + assert payload["model"] == "gpt-realtime-2" + assert payload["parallel_tool_calls"] is False + assert payload["reasoning"] == {"effort": "low"} + def test_session_config_allows_tool_search_as_named_function_tool_choice(self, model): cfg = model._get_session_config( { diff --git a/tests/realtime/test_realtime_model_settings.py b/tests/realtime/test_realtime_model_settings.py index 6db201fb..bab419bd 100644 --- a/tests/realtime/test_realtime_model_settings.py +++ b/tests/realtime/test_realtime_model_settings.py @@ -51,7 +51,7 @@ async def test_build_model_settings_from_agent_merges_agent_fields(monkeypatch: monkeypatch.setattr(agent, "get_all_tools", AsyncMock(return_value=[helper])) agent.handoffs = [RealtimeAgent(name="handoff-child")] - base_settings: RealtimeSessionModelSettings = {"model_name": "gpt-realtime-1.5"} + base_settings: RealtimeSessionModelSettings = {"model_name": "gpt-realtime-2"} starting_settings: RealtimeSessionModelSettings = {"voice": "verse"} run_config: RealtimeRunConfig = {"tracing_disabled": True} @@ -68,9 +68,9 @@ async def test_build_model_settings_from_agent_merges_agent_fields(monkeypatch: assert merged["tools"][0].name == helper.name assert merged["handoffs"][0].agent_name == "handoff-child" assert merged["voice"] == "verse" - assert merged["model_name"] == "gpt-realtime-1.5" + assert merged["model_name"] == "gpt-realtime-2" assert merged["tracing"] is None - assert base_settings == {"model_name": "gpt-realtime-1.5"} + assert base_settings == {"model_name": "gpt-realtime-2"} @pytest.mark.asyncio diff --git a/tests/realtime/test_session_payload_and_formats.py b/tests/realtime/test_session_payload_and_formats.py index b60d8df8..f167a296 100644 --- a/tests/realtime/test_session_payload_and_formats.py +++ b/tests/realtime/test_session_payload_and_formats.py @@ -26,10 +26,10 @@ class _DummyModel(pydantic.BaseModel): def _session_with_output(fmt: Any | None) -> RealtimeSessionCreateRequest: if fmt is None: - return RealtimeSessionCreateRequest(type="realtime", model="gpt-realtime-1.5") + return RealtimeSessionCreateRequest(type="realtime", model="gpt-realtime-2") return RealtimeSessionCreateRequest( type="realtime", - model="gpt-realtime-1.5", + model="gpt-realtime-2", # Use dict for output to avoid importing non-exported symbols in tests audio=RealtimeAudioConfig(output=cast(Any, {"format": fmt})), ) @@ -49,7 +49,7 @@ def test_normalize_session_payload_variants() -> None: assert Model._normalize_session_payload(transcription_mapping) is None # Valid realtime mapping should be converted to model - realtime_mapping: Mapping[str, object] = {"type": "realtime", "model": "gpt-realtime-1.5"} + realtime_mapping: Mapping[str, object] = {"type": "realtime", "model": "gpt-realtime-2"} as_model = Model._normalize_session_payload(realtime_mapping) assert isinstance(as_model, RealtimeSessionCreateRequest) assert as_model.type == "realtime" diff --git a/tests/realtime/test_tracing.py b/tests/realtime/test_tracing.py index f01448e7..bacde670 100644 --- a/tests/realtime/test_tracing.py +++ b/tests/realtime/test_tracing.py @@ -103,7 +103,7 @@ class TestRealtimeTracingIntegration: "session": { "id": "session_456", "type": "realtime", - "model": "gpt-realtime-1.5", + "model": "gpt-realtime-2", }, } @@ -148,7 +148,7 @@ class TestRealtimeTracingIntegration: "session": { "id": "session_456", "type": "realtime", - "model": "gpt-realtime-1.5", + "model": "gpt-realtime-2", }, } @@ -174,7 +174,7 @@ class TestRealtimeTracingIntegration: session_created_event = { "type": "session.created", "event_id": "event_123", - "session": {"id": "session_456", "type": "realtime", "model": "gpt-realtime-1.5"}, + "session": {"id": "session_456", "type": "realtime", "model": "gpt-realtime-2"}, } with patch.object(model, "send_event") as mock_send_event: @@ -216,7 +216,7 @@ class TestRealtimeTracingIntegration: "session": { "id": "session_456", "type": "realtime", - "model": "gpt-realtime-1.5", + "model": "gpt-realtime-2", }, }