From 818b2a9c143173a96772ba89d63fd8c131e94bcc Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:46:15 +0100 Subject: [PATCH 1/3] fix(voice): refresh callable API keys before streamed STT --- src/agents/voice/models/openai_stt.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/agents/voice/models/openai_stt.py b/src/agents/voice/models/openai_stt.py index cf504d8892..ac328418cb 100644 --- a/src/agents/voice/models/openai_stt.py +++ b/src/agents/voice/models/openai_stt.py @@ -58,6 +58,13 @@ def _audio_buffer_to_base64(buffer: npt.NDArray[np.int16 | np.float32]) -> str: return base64.b64encode(buffer.tobytes()).decode("utf-8") +async def _refresh_openai_client_api_key_if_supported(client: Any) -> None: + """Refresh callable/rotating OpenAI credentials before a manual websocket handshake.""" + refresh_api_key = getattr(client, "_refresh_api_key", None) + if callable(refresh_api_key): + await refresh_api_key() + + async def _wait_for_event( event_queue: asyncio.Queue[dict[str, Any] | ErrorSentinel], expected_types: list[str], @@ -303,6 +310,7 @@ async def _stream_audio( async def _process_websocket_connection(self) -> None: try: + await _refresh_openai_client_api_key_if_supported(self._client) async with websockets.connect( "wss://api.openai.com/v1/realtime?intent=transcription", additional_headers={ @@ -553,4 +561,4 @@ async def create_session( settings, trace_include_sensitive_data, trace_include_sensitive_audio_data, - ) + ) \ No newline at end of file From 016383190d43ddd97f362775dd2160b99c1b7f2f Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:46:33 +0100 Subject: [PATCH 2/3] test(voice): cover streamed STT API key refresh --- .../voice/test_openai_stt_api_key_refresh.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/voice/test_openai_stt_api_key_refresh.py diff --git a/tests/voice/test_openai_stt_api_key_refresh.py b/tests/voice/test_openai_stt_api_key_refresh.py new file mode 100644 index 0000000000..e1a4ddc2f6 --- /dev/null +++ b/tests/voice/test_openai_stt_api_key_refresh.py @@ -0,0 +1,55 @@ +from typing import Any, cast +from unittest.mock import AsyncMock + +import pytest +from openai import AsyncOpenAI + +from agents.voice import STTModelSettings, StreamedAudioInput +from agents.voice.models import openai_stt +from agents.voice.models.openai_stt import OpenAISTTTranscriptionSession + + +class _RotatingClient: + def __init__(self) -> None: + self.api_key = "" + self.refresh_calls = 0 + + async def _refresh_api_key(self) -> None: + self.refresh_calls += 1 + self.api_key = "sk-refreshed" + + +class _WebSocketContext: + async def __aenter__(self) -> Any: + return object() + + async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: + return False + + +@pytest.mark.asyncio +async def test_streamed_stt_refreshes_callable_api_key_before_handshake(monkeypatch) -> None: + client = _RotatingClient() + session = OpenAISTTTranscriptionSession( + input=StreamedAudioInput(), + client=cast(AsyncOpenAI, client), + model="gpt-4o-mini-transcribe", + settings=STTModelSettings(), + trace_include_sensitive_data=False, + trace_include_sensitive_audio_data=False, + ) + + captured_headers: dict[str, str] = {} + + def connect(_url: str, *, additional_headers: dict[str, str]) -> _WebSocketContext: + captured_headers.update(additional_headers) + return _WebSocketContext() + + monkeypatch.setattr(openai_stt.websockets, "connect", connect) + session._setup_connection = AsyncMock(side_effect=RuntimeError("stop after handshake")) + + with pytest.raises(RuntimeError, match="stop after handshake"): + await session._process_websocket_connection() + + assert client.refresh_calls == 1 + assert captured_headers["Authorization"] == "Bearer sk-refreshed" From d6e48117cea2453803e0351fe9f585576a0c26b1 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:46:54 +0100 Subject: [PATCH 3/3] test(voice): avoid assigning patched method directly --- tests/voice/test_openai_stt_api_key_refresh.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/voice/test_openai_stt_api_key_refresh.py b/tests/voice/test_openai_stt_api_key_refresh.py index e1a4ddc2f6..bbe0a44e24 100644 --- a/tests/voice/test_openai_stt_api_key_refresh.py +++ b/tests/voice/test_openai_stt_api_key_refresh.py @@ -46,7 +46,11 @@ def connect(_url: str, *, additional_headers: dict[str, str]) -> _WebSocketConte return _WebSocketContext() monkeypatch.setattr(openai_stt.websockets, "connect", connect) - session._setup_connection = AsyncMock(side_effect=RuntimeError("stop after handshake")) + monkeypatch.setattr( + session, + "_setup_connection", + AsyncMock(side_effect=RuntimeError("stop after handshake")), + ) with pytest.raises(RuntimeError, match="stop after handshake"): await session._process_websocket_connection()