Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/agents/realtime/openai_realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,14 @@ async def connect(self, options: RealtimeModelConfig) -> None:
transport_config=self._transport_config,
)
self._websocket_task = asyncio.create_task(self._listen_for_messages())
await self._update_session_config(model_settings)
try:
await self._update_session_config(model_settings)
except BaseException:
# The websocket and its listener task are owned by this method, so release them
# when the initial session config fails. Otherwise the connection stays open and
# the model can never be reconnected because of the asserts above.
await self.close()
raise

async def _create_websocket_connection(
self,
Expand Down
26 changes: 25 additions & 1 deletion tests/realtime/test_openai_realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import websockets
from pydantic import TypeAdapter

from agents import Agent, function_tool
from agents import Agent, WebSearchTool, function_tool
from agents.exceptions import UserError
from agents.handoffs import handoff
from agents.realtime.model import RealtimeModelConfig
Expand Down Expand Up @@ -338,6 +338,30 @@ async def test_connect_websocket_failure_propagates(self, model):
assert model._websocket is None
assert model._websocket_task is None

@pytest.mark.asyncio
async def test_connect_session_config_failure_releases_websocket(self, model, mock_websocket):
"""A failure while sending the initial session config must not leak the connection."""
config: RealtimeModelConfig = {
"api_key": "test-key",
# A hosted tool is rejected by `_tools_to_session_tools`, which runs only after the
# websocket is open and the listener task has started.
"initial_model_settings": {"tools": [WebSearchTool()]},
}

async def async_websocket(*args, **kwargs):
return mock_websocket

with patch("websockets.connect", side_effect=async_websocket):
with pytest.raises(UserError, match="Must be a function tool"):
await model.connect(config)

# The websocket and its listener task are owned by connect(), so a failure after they
# were acquired must release them. Otherwise the socket stays open and the model is
# permanently unusable because connect() asserts `_websocket is None`.
assert model._websocket is None
assert model._websocket_task is None
mock_websocket.close.assert_awaited()

@pytest.mark.asyncio
async def test_connect_with_empty_transport_config(self, mock_websocket):
"""Test that empty transport configuration works without error."""
Expand Down