Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
schema_version: 1
generation_id: 7b7feee1-6807-4c20-98e0-610aab05b2da
generation_id: 65a0f13e-756e-4f10-b075-12ae5d3d27a9
openapi_spec_hash: 51b199ef7fd5542bc4910765f9becd60
openapi_transformed_spec_hash: 73822d924c15a6ed58dcfe8f616bd4be
config_hash: b707b209c892562880c415693d111ad6
codegen_sha: e1054c07fb03a8025411f0ff6ee0a5ff17e09109
codegen_hash: 73c66bd2bff47ee2bbd2d04a174184dec767ebe300a3bd41443938d914013ef0
codegen_sha: 52c5bafacb27ede81a3d66b62b29d946df4ed2cd
codegen_hash: 536c408e94523f4af5a3ddf0c2f8f0764014bad6b9287cacdb9b8e3eeb60ba7d
2 changes: 1 addition & 1 deletion scripts/castiron/generated-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@
},
"src/openai/resources/realtime/realtime.py": {
"mode": "100644",
"sha256": "d45ef5f2d1e25e6f8761592a9fb5739d7a344c6e64a4e7a4006418c16f05a540"
"sha256": "9cb69e1f54c6e21d9111ddfb59a2a4de18466a1a86487fc65a0e17224b72475e"
},
"src/openai/resources/realtime/translations/__init__.py": {
"mode": "100644",
Expand Down
45 changes: 39 additions & 6 deletions src/openai/lib/_client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..auth import WorkloadIdentity, X509WorkloadIdentity
from .._types import (
Omit,
Headers,
Timeout,
NotGiven,
Expand All @@ -18,7 +19,7 @@
)
from .._models import SecurityOptions, FinalRequestOptions
from .._provider import _Provider, _ProviderRuntime
from .._base_client import DEFAULT_MAX_RETRIES
from .._base_client import DEFAULT_MAX_RETRIES, _merge_headers
from .._data_residency import DataResidency

if TYPE_CHECKING:
Expand All @@ -33,14 +34,42 @@
__all__ = ["_has_header", "_has_omitted_header"]


class _MissingAPIKey(Omit):
"""Suppress shared credentials without opting out of missing-auth validation."""


def _has_header(headers: Headers, header: str) -> bool:
header = header.lower()
return any((key.lower() == header for key in headers))


def _has_omitted_header(headers: Headers, header: str) -> bool:
header = header.lower()
return any((key.lower() == header and isinstance(value, _sdk_client.Omit) for key, value in headers.items()))
return any(
key.lower() == header and isinstance(value, _sdk_client.Omit) and not isinstance(value, _MissingAPIKey)
for key, value in headers.items()
)


def _set_request_api_key(
self: _sdk_client.OpenAI | _sdk_client.AsyncOpenAI, options: FinalRequestOptions, api_key: str
) -> None:
custom_headers = options.headers if not isinstance(options.headers, NotGiven) else {}
# Keep explicit overrides (including Omit and differently cased duplicates) intact.
if _has_header(custom_headers, "Authorization"):
return

auth_headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
if not api_key and options.security.get("admin_api_key_auth", False):
auth_headers = self._admin_api_key_auth
headers = _merge_headers(auth_headers, self.default_headers, custom_headers)
authorization = next((value for name, value in headers.items() if name.lower() == "authorization"), None)
# A later refresh may change self.api_key; this request must keep its own result.
# Replace the mapping so retry inputs and caller-owned headers remain untouched.
options.headers = {
"Authorization": authorization if authorization is not None else _MissingAPIKey(),
**custom_headers,
}


def syncbase_url(self: _sdk_client.OpenAI) -> httpx2.URL:
Expand Down Expand Up @@ -343,7 +372,7 @@ def sync_prepare_options(
if self._provider_runtime.transform_request is not None:
options = self._provider_runtime.transform_request(options)
elif self._api_key_provider is not None and options.security.get("bearer_auth", False):
self._refresh_api_key()
_set_request_api_key(self, options, self._refresh_api_key())
return super(_owner, self)._prepare_options(options)


Expand All @@ -364,7 +393,9 @@ def sync_custom_auth(

def sync_refresh_api_key(self: _sdk_client.OpenAI) -> str:
if self._api_key_provider is not None:
self.api_key = self._api_key_provider()
api_key = self._api_key_provider()
self.api_key = api_key
return api_key
return self.api_key


Expand Down Expand Up @@ -812,7 +843,7 @@ async def async_prepare_options(
elif self._provider_runtime.transform_request is not None:
options = self._provider_runtime.transform_request(options)
elif self._api_key_provider is not None and options.security.get("bearer_auth", False):
await self._refresh_api_key()
_set_request_api_key(self, options, await self._refresh_api_key())
return await super(_owner, self)._prepare_options(options)


Expand All @@ -835,7 +866,9 @@ def asynccustom_auth(_owner: type[_sdk_client.AsyncOpenAI], /, self: _sdk_client

async def async_refresh_api_key(self: _sdk_client.AsyncOpenAI) -> str:
if self._api_key_provider is not None:
self.api_key = await self._api_key_provider()
api_key = await self._api_key_provider()
self.api_key = api_key
return api_key
return self.api_key


Expand Down
68 changes: 52 additions & 16 deletions src/openai/lib/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from .._streaming import Stream, AsyncStream
from ..auth._x509 import is_x509_workload_identity
from .._exceptions import OpenAIError
from .._base_client import DEFAULT_MAX_RETRIES, BaseClient
from ._client_auth import _MissingAPIKey
from .._base_client import DEFAULT_MAX_RETRIES, BaseClient, _merge_headers
from .._data_residency import DataResidency

_deployments_endpoints = set(
Expand Down Expand Up @@ -53,7 +54,10 @@ def _has_header(headers: Headers, header: str) -> bool:


def _has_auth_header(headers: Headers) -> bool:
return _has_header(headers, "Authorization") or _has_header(headers, "api-key")
return any(
name.lower() in ("authorization", "api-key") and not isinstance(value, _MissingAPIKey)
for name, value in headers.items()
)


_AZURE_AUTH_ORIGIN = "openai.azure_auth_origin"
Expand Down Expand Up @@ -489,8 +493,7 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:

@override
def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
if self._api_key_provider is not None:
self._refresh_api_key()
api_key = self._refresh_api_key() if self._api_key_provider is not None else self.api_key

headers: dict[str, str | Omit] = {**options.headers} if is_given(options.headers) else {}

Expand All @@ -501,26 +504,43 @@ def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
if azure_ad_token is not None:
if not _has_header(headers, "Authorization"):
headers["Authorization"] = f"Bearer {azure_ad_token}"
elif self.api_key and self.api_key != API_KEY_SENTINEL:
elif api_key and api_key != API_KEY_SENTINEL:
if not _has_header(headers, "api-key"):
headers["api-key"] = self.api_key
headers["api-key"] = api_key
elif _has_auth_header(headers) or _has_auth_header(self.default_headers):
pass
else:
# should never be hit
raise ValueError("Unable to handle auth")

if self._api_key_provider is not None and not _has_header(headers, "api-key"):
# An empty callable result must not inherit another request's key.
default_api_key = next(
(value for name, value in _merge_headers(self.default_headers).items() if name.lower() == "api-key"),
None,
)
headers["api-key"] = default_api_key if default_api_key is not None else _MissingAPIKey()

return options

def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx2.URL, dict[str, str]]:
def _configure_realtime_with_api_key(
self, model: str, extra_query: Query, *, api_key: str
) -> tuple[httpx2.URL, dict[str, str]]:
return self._configure_realtime(model, extra_query, api_key=api_key)

def _configure_realtime(
self, model: str, extra_query: Query, *, api_key: str | NotGiven = NOT_GIVEN
) -> tuple[httpx2.URL, dict[str, str]]:
if not is_given(api_key):
api_key = self.api_key
auth_headers = {}
query = {
**extra_query,
"api-version": self._api_version,
"deployment": self._azure_deployment or model,
}
if self.api_key and self.api_key != "<missing API key>":
auth_headers = {"api-key": self.api_key}
if api_key and api_key != "<missing API key>":
auth_headers = {"api-key": api_key}
else:
token = self._get_azure_ad_token()
if token:
Expand Down Expand Up @@ -852,8 +872,7 @@ def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None:

@override
async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions:
if self._api_key_provider is not None:
await self._refresh_api_key()
api_key = await self._refresh_api_key() if self._api_key_provider is not None else self.api_key

headers: dict[str, str | Omit] = {**options.headers} if is_given(options.headers) else {}

Expand All @@ -864,26 +883,43 @@ async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOp
if azure_ad_token is not None:
if not _has_header(headers, "Authorization"):
headers["Authorization"] = f"Bearer {azure_ad_token}"
elif self.api_key and self.api_key != API_KEY_SENTINEL:
elif api_key and api_key != API_KEY_SENTINEL:
if not _has_header(headers, "api-key"):
headers["api-key"] = self.api_key
headers["api-key"] = api_key
elif _has_auth_header(headers) or _has_auth_header(self.default_headers):
pass
else:
# should never be hit
raise ValueError("Unable to handle auth")

if self._api_key_provider is not None and not _has_header(headers, "api-key"):
# An empty callable result must not inherit another request's key.
default_api_key = next(
(value for name, value in _merge_headers(self.default_headers).items() if name.lower() == "api-key"),
None,
)
headers["api-key"] = default_api_key if default_api_key is not None else _MissingAPIKey()

return options

async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[httpx2.URL, dict[str, str]]:
async def _configure_realtime_with_api_key(
self, model: str, extra_query: Query, *, api_key: str
) -> tuple[httpx2.URL, dict[str, str]]:
return await self._configure_realtime(model, extra_query, api_key=api_key)

async def _configure_realtime(
self, model: str, extra_query: Query, *, api_key: str | NotGiven = NOT_GIVEN
) -> tuple[httpx2.URL, dict[str, str]]:
if not is_given(api_key):
api_key = self.api_key
auth_headers = {}
query = {
**extra_query,
"api-version": self._api_version,
"deployment": self._azure_deployment or model,
}
if self.api_key and self.api_key != "<missing API key>":
auth_headers = {"api-key": self.api_key}
if api_key and api_key != "<missing API key>":
auth_headers = {"api-key": api_key}
else:
token = await self._get_azure_ad_token()
if token:
Expand Down
16 changes: 12 additions & 4 deletions src/openai/resources/realtime/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,14 +668,18 @@ async def _connect_ws(self, extra_query: Query, extra_headers: Headers) -> Async
if self.__call_id is not omit:
extra_query = {**extra_query, "call_id": self.__call_id}
if is_async_azure_client(self.__client):
await self.__client._refresh_api_key()
api_key = await self.__client._refresh_api_key()
from ...lib._azure_websocket import _AzureWebSocketConnect as connect

model = self.__model
if not model:
raise OpenAIError("`model` is required for Azure Realtime API")
else:
url, auth_headers = await self.__client._configure_realtime(model, extra_query)
configure = getattr(self.__client, "_configure_realtime_with_api_key", None)
if configure is not None:
url, auth_headers = await configure(model, extra_query, api_key=api_key)
else:
url, auth_headers = await self.__client._configure_realtime(model, extra_query)
prepared_headers: Headers = extra_headers
else:
url = self._prepare_url()
Expand Down Expand Up @@ -1123,12 +1127,16 @@ def _connect_ws(self, extra_query: Query, extra_headers: Headers) -> WebSocketCo
if self.__call_id is not omit:
extra_query = {**extra_query, "call_id": self.__call_id}
if is_azure_client(self.__client):
self.__client._refresh_api_key()
api_key = self.__client._refresh_api_key()
model = self.__model
if not model:
raise OpenAIError("`model` is required for Azure Realtime API")
else:
url, auth_headers = self.__client._configure_realtime(model, extra_query)
configure = getattr(self.__client, "_configure_realtime_with_api_key", None)
if configure is not None:
url, auth_headers = configure(model, extra_query, api_key=api_key)
else:
url, auth_headers = self.__client._configure_realtime(model, extra_query)
prepared_headers: Headers = extra_headers
else:
url = self._prepare_url()
Expand Down
Loading
Loading