-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix(voice): honor client config for streamed STT #4532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2a89812
fff641c
0d40f22
9a18176
8b6b1a3
a0b7ac0
86d59ef
941ebc9
d7c5eae
bcc8d57
f175739
832db7e
101108b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| from __future__ import annotations | ||
|
|
||
| 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 | ||
|
|
||
|
|
||
| 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 _set_header(headers: dict[str, str], key: object, value: object) -> None: | ||
| 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) | ||
|
|
||
|
|
||
| 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 _is_openai_omitted_value(value): | ||
| continue | ||
|
Comment on lines
+43
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an Useful? React with 👍 / 👎. |
||
| _set_header(headers, key, value) | ||
|
|
||
| for key, value in (extra_headers or {}).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 | ||
|
|
||
|
|
||
| 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"): | ||
| client_base_url = str(client_base_url) | ||
| 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new module duplicates the omission detection, credential refresh, header merge, and base/query normalization already implemented in
openai_responses.py, while the Responses transport continues using its private copies. Consequently, fixes to these client semantics can land in only one transport—the preceding revisions already needed several corrections in this exact area. Route both WebSocket transports through this helper, or adapt STT to the existing normalization, so there is one source of truth.AGENTS.md reference: AGENTS.md:L93-L93
Useful? React with 👍 / 👎.