feat: default realtime sessions to gpt-realtime-2 (#3190)

This commit is contained in:
Kazuhiro Sera
2026-05-08 15:24:41 +09:00
committed by GitHub
parent ee36d43584
commit 1660d306b5
15 changed files with 103 additions and 46 deletions
+12 -4
View File
@@ -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
+10 -5
View File
@@ -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:
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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": {
+10 -3
View File
@@ -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:
+1 -1
View File
@@ -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,
+4
View File
@@ -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",
+18
View File
@@ -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."""
+17 -14
View File
@@ -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")
+1 -1
View File
@@ -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",
+17 -5
View File
@@ -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(
{
@@ -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
@@ -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"
+4 -4
View File
@@ -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",
},
}