fix(voice): honor client config for streamed STT (#4575)

Co-authored-by: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com>
This commit is contained in:
Kazuhiro Sera
2026-08-22 08:32:57 +09:00
committed by GitHub
parent 119ad2a492
commit d22234480f
7 changed files with 430 additions and 92 deletions
+137
View File
@@ -0,0 +1,137 @@
from __future__ import annotations
import logging
from collections.abc import Mapping
from typing import Any
import httpx2
from openai import AsyncOpenAI, NotGiven, Omit
from .._httpx_compat import is_legacy_httpx_instance
from ..exceptions import UserError
class _OpenAIWebSocketLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
"""Prevent the WebSocket dependency from logging sensitive connection data."""
def isEnabledFor(self, level: int) -> bool:
if level <= logging.DEBUG:
return False
return super().isEnabledFor(level)
_OPENAI_WEBSOCKET_LOGGER = _OpenAIWebSocketLoggerAdapter(
logging.getLogger("websockets.client"),
{},
)
def get_openai_websocket_logger() -> logging.LoggerAdapter[logging.Logger]:
"""Return the logger used for OpenAI WebSocket connections."""
return _OPENAI_WEBSOCKET_LOGGER
def _is_openai_omitted_value(value: Any) -> bool:
return isinstance(value, Omit | NotGiven)
async def refresh_openai_client_api_key_if_supported(client: Any) -> None:
"""Refresh dynamic OpenAI client credentials before materializing handshake headers."""
refresh_api_key = getattr(client, "_refresh_api_key", None)
if callable(refresh_api_key):
await refresh_api_key()
def _remove_header(headers: dict[str, str], key: object) -> None:
header_key = str(key)
for existing_key in list(headers):
if existing_key.lower() == header_key.lower():
del headers[existing_key]
def _set_header(headers: dict[str, str], key: object, value: object) -> None:
header_key = str(key)
_remove_header(headers, header_key)
headers[header_key] = str(value)
def merge_openai_client_websocket_headers(
client: AsyncOpenAI,
*,
extra_headers: Mapping[str, Any] | None = None,
) -> dict[str, str]:
"""Materialize OpenAI client auth/default headers for a WebSocket handshake."""
headers: dict[str, str] = {}
for source in (
getattr(client, "auth_headers", {}),
getattr(client, "default_headers", {}),
):
for key, value in source.items():
if isinstance(value, NotGiven):
continue
if isinstance(value, Omit):
_remove_header(headers, key)
continue
_set_header(headers, key, value)
for key, value in (extra_headers or {}).items():
if isinstance(value, NotGiven):
continue
_remove_header(headers, key)
if isinstance(value, Omit):
continue
headers[str(key)] = str(value)
return headers
def _merge_query_values(params: dict[str, Any], values: Mapping[str, Any]) -> None:
for key, value in values.items():
query_key = str(key)
if isinstance(value, Omit):
params.pop(query_key, None)
continue
if isinstance(value, NotGiven):
continue
params[query_key] = value
def prepare_openai_client_websocket_base_url(
client: AsyncOpenAI,
*,
extra_query: Any = None,
context: str,
) -> httpx2.URL:
"""Build the client-derived WebSocket base URL and normalized query parameters.
Endpoint suffixes and transport-specific fixed query parameters are intentionally left to
each caller.
"""
websocket_base_url = getattr(client, "websocket_base_url", None)
if websocket_base_url is not None:
if is_legacy_httpx_instance(websocket_base_url, "URL"):
websocket_base_url = str(websocket_base_url)
base_url = httpx2.URL(websocket_base_url)
else:
client_base_url = client.base_url
if is_legacy_httpx_instance(client_base_url, "URL"):
base_url = httpx2.URL(str(client_base_url))
else:
base_url = httpx2.URL(client_base_url)
ws_scheme = {"http": "ws", "https": "wss"}.get(base_url.scheme, base_url.scheme)
base_url = base_url.copy_with(scheme=ws_scheme)
params: dict[str, Any] = dict(base_url.params)
default_query = getattr(client, "default_query", None)
if default_query is not None and not _is_openai_omitted_value(default_query):
if not isinstance(default_query, Mapping):
raise UserError(f"{context} client default_query must be a mapping.")
_merge_query_values(params, default_query)
if extra_query is not None and not _is_openai_omitted_value(extra_query):
if not isinstance(extra_query, Mapping):
raise UserError(f"{context} extra_query must be a mapping.")
_merge_query_values(params, extra_query)
return base_url.copy_with(params=params)
+19 -71
View File
@@ -91,6 +91,12 @@ from ..util._error_tracing import (
from ..util._json import _to_dump_compatible
from ..version import __version__
from ._openai_retry import get_openai_retry_advice
from ._openai_websocket import (
get_openai_websocket_logger,
merge_openai_client_websocket_headers,
prepare_openai_client_websocket_base_url,
refresh_openai_client_api_key_if_supported,
)
from ._response_terminal import response_error_event_failure_error, response_terminal_failure_error
from ._retry_runtime import (
should_disable_provider_managed_retries,
@@ -177,10 +183,8 @@ def _materialize_responses_tool_params(
async def _refresh_openai_client_api_key_if_supported(client: Any) -> None:
"""Refresh client auth if the current OpenAI SDK exposes a refresh hook."""
refresh_api_key = getattr(client, "_refresh_api_key", None)
if callable(refresh_api_key):
await refresh_api_key()
"""Backward-compatible wrapper around shared WebSocket client credential refresh."""
await refresh_openai_client_api_key_if_supported(client)
def _construct_response_stream_event_from_payload(
@@ -1533,76 +1537,19 @@ class OpenAIResponsesWSModel(OpenAIResponsesModel):
return frame, ws_url, handshake_headers
def _merge_websocket_headers(self, extra_headers: Mapping[str, Any]) -> dict[str, str]:
headers: dict[str, str] = {}
for source in (
getattr(self._client, "auth_headers", {}),
self._client.default_headers,
):
for key, value in source.items():
if _is_openai_omitted_value(value):
continue
header_key = str(key)
for existing_key in list(headers):
if existing_key.lower() == header_key.lower():
del headers[existing_key]
headers[header_key] = str(value)
for key, value in extra_headers.items():
if isinstance(value, NotGiven):
continue
header_key = str(key)
for existing_key in list(headers):
if existing_key.lower() == header_key.lower():
del headers[existing_key]
if isinstance(value, Omit):
continue
headers[header_key] = str(value)
return headers
return merge_openai_client_websocket_headers(
self._client,
extra_headers=extra_headers,
)
def _prepare_websocket_url(self, extra_query: Any) -> str:
if self._client.websocket_base_url is not None:
websocket_base_url = self._client.websocket_base_url
if is_legacy_httpx_instance(websocket_base_url, "URL"):
websocket_base_url = str(websocket_base_url)
base_url = httpx2.URL(websocket_base_url)
ws_scheme = {"http": "ws", "https": "wss"}.get(base_url.scheme, base_url.scheme)
base_url = base_url.copy_with(scheme=ws_scheme)
else:
client_base_url = self._client.base_url
ws_scheme = {"http": "ws", "https": "wss"}.get(
client_base_url.scheme, client_base_url.scheme
)
base_url = client_base_url.copy_with(scheme=ws_scheme)
params: dict[str, Any] = dict(base_url.params)
default_query = getattr(self._client, "default_query", None)
if default_query is not None and not _is_openai_omitted_value(default_query):
if not isinstance(default_query, Mapping):
raise UserError("Responses websocket client default_query must be a mapping.")
for key, value in default_query.items():
query_key = str(key)
if isinstance(value, Omit):
params.pop(query_key, None)
continue
if isinstance(value, NotGiven):
continue
params[query_key] = value
if extra_query is not None and not _is_openai_omitted_value(extra_query):
if not isinstance(extra_query, Mapping):
raise UserError("Responses websocket extra_query must be a mapping.")
for key, value in extra_query.items():
query_key = str(key)
if isinstance(value, Omit):
params.pop(query_key, None)
continue
if isinstance(value, NotGiven):
continue
params[query_key] = value
base_url = prepare_openai_client_websocket_base_url(
self._client,
extra_query=extra_query,
context="Responses websocket",
)
path = base_url.path.rstrip("/") + "/responses"
return str(base_url.copy_with(path=path, params=params))
return str(base_url.copy_with(path=path))
async def _ensure_websocket_connection(
self,
@@ -1746,6 +1693,7 @@ class OpenAIResponsesWSModel(OpenAIResponsesModel):
connect_kwargs: dict[str, Any] = {
"user_agent_header": None,
"additional_headers": dict(headers),
"logger": get_openai_websocket_logger(),
"max_size": None,
"open_timeout": connect_timeout,
}
+28 -5
View File
@@ -13,6 +13,12 @@ from openai import AsyncOpenAI
from ... import _debug
from ...exceptions import AgentsException, UserError
from ...logger import logger
from ...models._openai_websocket import (
get_openai_websocket_logger,
merge_openai_client_websocket_headers,
prepare_openai_client_websocket_base_url,
refresh_openai_client_api_key_if_supported,
)
from ...tracing import Span, SpanError, TranscriptionSpanData, transcription_span
from ...util._error_tracing import get_trace_error
from ..exceptions import STTWebsocketConnectionError
@@ -58,6 +64,24 @@ def _audio_buffer_to_base64(buffer: npt.NDArray[np.int16 | np.float32]) -> str:
return base64.b64encode(buffer.tobytes()).decode("utf-8")
def _prepare_websocket_url(client: AsyncOpenAI) -> str:
base_url = prepare_openai_client_websocket_base_url(
client,
context="Streamed STT websocket",
)
params: dict[str, Any] = dict(base_url.params)
params["intent"] = "transcription"
path = base_url.path.rstrip("/") + "/realtime"
return str(base_url.copy_with(path=path, params=params))
def _prepare_websocket_headers(client: AsyncOpenAI) -> dict[str, str]:
return merge_openai_client_websocket_headers(
client,
extra_headers={"OpenAI-Log-Session": "1"},
)
async def _wait_for_event(
event_queue: asyncio.Queue[dict[str, Any] | ErrorSentinel],
expected_types: list[str],
@@ -312,12 +336,11 @@ class OpenAISTTTranscriptionSession(StreamedTranscriptionSession):
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={
"Authorization": f"Bearer {self._client.api_key}",
"OpenAI-Log-Session": "1",
},
_prepare_websocket_url(self._client),
additional_headers=_prepare_websocket_headers(self._client),
logger=get_openai_websocket_logger(),
) as ws:
await self._setup_connection(ws)
self._process_events_task = asyncio.create_task(self._handle_events())
+2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
import json
import logging
from types import SimpleNamespace
from typing import Any, cast
@@ -1986,6 +1987,7 @@ async def test_websocket_model_passes_keepalive_options_to_connect(monkeypatch):
assert opened is ws
assert captured_kwargs["ws_url"] == "wss://example.test/v1/responses"
assert captured_kwargs["additional_headers"] == {"Authorization": "Bearer test-key"}
assert captured_kwargs["logger"].isEnabledFor(logging.DEBUG) is False
assert captured_kwargs["open_timeout"] == 10.0
assert captured_kwargs["ping_interval"] == 45.0
assert captured_kwargs["ping_timeout"] is None
+30 -16
View File
@@ -9,9 +9,11 @@ from collections.abc import AsyncGenerator
from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import httpx2
import numpy as np
import numpy.typing as npt
import pytest
from openai import AsyncOpenAI
import agents._debug as _debug
from agents import trace
@@ -55,6 +57,17 @@ def create_mock_websocket(messages: list[str]) -> AsyncMock:
return mock_ws
def create_mock_openai_client(api_key: str = "FAKE_KEY") -> AsyncOpenAI:
client = AsyncMock(api_key=api_key)
client.websocket_base_url = None
client.base_url = httpx2.URL("https://api.openai.com/v1/")
client.default_query = {}
client.auth_headers = {"Authorization": f"Bearer {api_key}"}
client.default_headers = {}
client._refresh_api_key = AsyncMock()
return cast(AsyncOpenAI, client)
def fake_time(increment: int):
current = 1000
while True:
@@ -67,7 +80,7 @@ def fake_time(increment: int):
async def test_transcribe_turns_propagates_consumer_cancellation(monkeypatch) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -105,7 +118,7 @@ async def test_transcribe_turns_propagates_consumer_cancellation(monkeypatch) ->
async def test_transcribe_turns_closes_owned_tasks_after_yield(monkeypatch) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -165,7 +178,7 @@ async def test_transcribe_turns_closes_owned_tasks_after_yield(monkeypatch) -> N
async def test_close_finishes_span_started_while_websocket_close_is_pending() -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -223,7 +236,7 @@ async def test_transcribe_turns_preserves_consumer_exception_when_cleanup_fails(
) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -270,7 +283,7 @@ async def test_transcribe_turns_preserves_consumer_exception_when_cleanup_fails(
async def test_transcribe_turns_propagates_cancellation_during_cleanup(monkeypatch) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -307,7 +320,7 @@ async def test_transcribe_turns_preserves_terminal_error_when_close_fails(
) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -372,7 +385,7 @@ async def test_non_json_messages_should_crash():
session = OpenAISTTTranscriptionSession(
input=input_audio,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -412,7 +425,7 @@ async def test_session_connects_and_configures_successfully():
session = OpenAISTTTranscriptionSession(
input=input_audio,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -430,6 +443,7 @@ async def test_session_connects_and_configures_successfully():
assert "wss://api.openai.com/v1/realtime?intent=transcription" in args[0]
headers = kwargs.get("additional_headers", {})
assert headers.get("Authorization") == "Bearer FAKE_KEY"
assert kwargs["logger"].isEnabledFor(logging.DEBUG) is False
assert headers.get("OpenAI-Beta") is None
assert headers.get("OpenAI-Log-Session") == "1"
@@ -472,7 +486,7 @@ async def test_stream_audio_sends_pcm16(
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -548,7 +562,7 @@ async def test_transcription_event_puts_output_in_queue(created, updated, comple
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -594,7 +608,7 @@ async def test_timeout_waiting_for_created_event(monkeypatch):
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -643,7 +657,7 @@ async def test_session_error_event(monkeypatch: pytest.MonkeyPatch):
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -679,7 +693,7 @@ async def test_session_error_event_before_session_created():
audio_input = await StreamedAudioInputFactory.get(count=2)
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -722,7 +736,7 @@ async def test_listener_timeout_drains_buffered_transcript_before_setup():
audio_input = await StreamedAudioInputFactory.get(count=2)
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -778,7 +792,7 @@ async def test_inactivity_timeout():
session = OpenAISTTTranscriptionSession(
input=audio_input,
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=stt_settings,
trace_include_sensitive_data=False,
@@ -804,7 +818,7 @@ async def test_stream_audio_buffers_turn_audio_only_for_audio_tracing(
) -> None:
session = OpenAISTTTranscriptionSession(
input=StreamedAudioInput(),
client=AsyncMock(api_key="FAKE_KEY"),
client=create_mock_openai_client(),
model="whisper-1",
settings=STTModelSettings(),
trace_include_sensitive_data=False,
@@ -0,0 +1,71 @@
from typing import Any, cast
from unittest.mock import AsyncMock
import httpx2
import pytest
from openai import AsyncOpenAI
from agents.voice import StreamedAudioInput, STTModelSettings
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
self.websocket_base_url = None
self.base_url = httpx2.URL("https://api.openai.com/v1/")
self.default_query: dict[str, str] = {}
self.auth_headers = {"Authorization": "Bearer stale"}
self.default_headers: dict[str, str] = {}
async def _refresh_api_key(self) -> None:
self.refresh_calls += 1
self.api_key = "sk-refreshed"
self.auth_headers = {"Authorization": f"Bearer {self.api_key}"}
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],
logger: object,
) -> _WebSocketContext:
captured_headers.update(additional_headers)
return _WebSocketContext()
monkeypatch.setattr(openai_stt.websockets, "connect", connect)
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()
assert client.refresh_calls == 1
assert captured_headers["Authorization"] == "Bearer sk-refreshed"
@@ -0,0 +1,143 @@
import logging
from typing import cast
from unittest.mock import MagicMock
import httpx2
from openai import NOT_GIVEN, AsyncOpenAI, omit
from agents.models._openai_websocket import get_openai_websocket_logger
from agents.voice.models.openai_stt import (
_prepare_websocket_headers,
_prepare_websocket_url,
)
def _mock_client(**attributes: object) -> AsyncOpenAI:
attributes.setdefault("default_query", {})
return cast(AsyncOpenAI, MagicMock(**attributes))
def test_openai_websocket_logger_does_not_emit_debug_connection_data(caplog) -> None:
logger = get_openai_websocket_logger()
with caplog.at_level(logging.DEBUG, logger="websockets.client"):
logger.debug("> GET %s HTTP/1.1", "/v1/realtime?proxy_token=query-secret")
logger.debug("> %s: %s", "X-Proxy-Token", "header-secret")
logger.debug("> TEXT %r", "audio-or-model-data")
assert "query-secret" not in caplog.text
assert "header-secret" not in caplog.text
assert "audio-or-model-data" not in caplog.text
def test_streaming_stt_websocket_url_uses_client_base_url() -> None:
client = _mock_client(
websocket_base_url=None,
base_url=httpx2.URL("https://voice-proxy.example.test/v1/"),
)
url = httpx2.URL(_prepare_websocket_url(client))
assert url.scheme == "wss"
assert url.host == "voice-proxy.example.test"
assert url.path == "/v1/realtime"
assert url.params["intent"] == "transcription"
def test_streaming_stt_websocket_url_prefers_websocket_base_url() -> None:
client = _mock_client(
websocket_base_url="https://voice-ws.example.test/custom/?tenant=one",
base_url=httpx2.URL("https://ignored.example.test/v1/"),
)
url = httpx2.URL(_prepare_websocket_url(client))
assert url.scheme == "wss"
assert url.host == "voice-ws.example.test"
assert url.path == "/custom/realtime"
assert url.params["tenant"] == "one"
assert url.params["intent"] == "transcription"
def test_streaming_stt_websocket_url_merges_client_default_query() -> None:
client = _mock_client(
websocket_base_url="wss://voice-ws.example.test/custom/?tenant=one&remove=base",
base_url=httpx2.URL("https://ignored.example.test/v1/"),
default_query={
"api-version": "2026-08-01-preview",
"remove": omit,
"skip": NOT_GIVEN,
},
)
url = httpx2.URL(_prepare_websocket_url(client))
assert url.params["tenant"] == "one"
assert url.params["api-version"] == "2026-08-01-preview"
assert url.params["intent"] == "transcription"
assert "remove" not in url.params
assert "skip" not in url.params
def test_streaming_stt_websocket_headers_use_client_configuration() -> None:
client = _mock_client(
auth_headers={"Authorization": "Bearer sk-client"},
default_headers={
"OpenAI-Organization": "org-client",
"OpenAI-Project": "proj-client",
"X-Proxy-Token": "proxy-token",
},
)
headers = _prepare_websocket_headers(client)
assert headers["Authorization"] == "Bearer sk-client"
assert headers["OpenAI-Organization"] == "org-client"
assert headers["OpenAI-Project"] == "proj-client"
assert headers["X-Proxy-Token"] == "proxy-token"
assert headers["OpenAI-Log-Session"] == "1"
def test_streaming_stt_websocket_headers_skip_openai_omission_sentinels() -> None:
client = _mock_client(
auth_headers={"Authorization": "Bearer sk-client"},
default_headers={
"OpenAI-Organization": omit,
"OpenAI-Project": NOT_GIVEN,
"X-Proxy-Token": "proxy-token",
},
)
headers = _prepare_websocket_headers(client)
assert headers["Authorization"] == "Bearer sk-client"
assert headers["X-Proxy-Token"] == "proxy-token"
assert "OpenAI-Organization" not in headers
assert "OpenAI-Project" not in headers
assert headers["OpenAI-Log-Session"] == "1"
def test_streaming_stt_websocket_headers_omit_removes_inherited_header() -> None:
client = _mock_client(
auth_headers={"Authorization": "Bearer sk-client"},
default_headers={"authorization": omit},
)
headers = _prepare_websocket_headers(client)
assert all(key.lower() != "authorization" for key in headers)
assert headers["OpenAI-Log-Session"] == "1"
def test_streaming_stt_websocket_fixed_session_header_replaces_client_casing() -> None:
client = _mock_client(
auth_headers={},
default_headers={"openai-log-session": "0"},
)
headers = _prepare_websocket_headers(client)
session_headers = {
key: value for key, value in headers.items() if key.lower() == "openai-log-session"
}
assert session_headers == {"OpenAI-Log-Session": "1"}