From 1edcfa1c8b4b393996e28b01701b03a4681def6a Mon Sep 17 00:00:00 2001 From: "shuwen.wu" Date: Wed, 2 Sep 2026 15:38:00 +0800 Subject: [PATCH 1/3] fix(exceptions): coerce APIError.code to str to match Optional[str] annotation Replace construct_type call with explicit str coercion to properly handle numeric code values returned by the API. Fixes #3781 --- src/openai/_exceptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openai/_exceptions.py b/src/openai/_exceptions.py index 7a30e4a336..5f5f911faf 100644 --- a/src/openai/_exceptions.py +++ b/src/openai/_exceptions.py @@ -69,7 +69,8 @@ def __init__(self, message: str, request: httpx2.Request, *, body: object | None self.body = body if is_dict(body): - self.code = cast(Any, construct_type(type_=Optional[str], value=body.get("code"))) + raw_code = body.get("code") + self.code = str(raw_code) if raw_code is not None else None self.param = cast(Any, construct_type(type_=Optional[str], value=body.get("param"))) self.type = cast(Any, construct_type(type_=str, value=body.get("type"))) else: From 4eee6b354edb7f99e0d6108f76d7c1cf1c0453b2 Mon Sep 17 00:00:00 2001 From: "shuwen.wu" Date: Wed, 2 Sep 2026 15:47:36 +0800 Subject: [PATCH 2/3] fix(auth): reject non-positive and non-finite token exchange expires_in Validate expires_in to reject negative values, zero, NaN, infinity, and overflow values (e.g. 10**400). Convert bool to int first since bool is a subclass of int. Fixes #3735 --- src/openai/auth/_workload.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/openai/auth/_workload.py b/src/openai/auth/_workload.py index 4f9797f092..f8d047ac1e 100644 --- a/src/openai/auth/_workload.py +++ b/src/openai/auth/_workload.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math import time import threading from typing import Any, Generic, TypeVar, Callable, TypedDict, cast @@ -305,8 +306,10 @@ def _handle_token_response(self, response: httpx2.Response) -> dict[str, Any]: ) def _validate_expires_in(self, expires_in: object) -> float: - if not isinstance(expires_in, (int, float)): - raise OpenAIError("Token exchange response did not include a valid expires_in") + if isinstance(expires_in, bool): + expires_in = int(expires_in) + if not isinstance(expires_in, (int, float)) or math.isnan(expires_in) or math.isinf(expires_in) or expires_in <= 0: + raise ValueError("Token exchange response did not include a valid expires_in") return float(expires_in) def _token_unusable(self) -> bool: From a4bc984c938caa799b3bafc38c0c809f5d808792 Mon Sep 17 00:00:00 2001 From: "shuwen.wu" Date: Wed, 2 Sep 2026 19:23:33 +0800 Subject: [PATCH 3/3] fix(azure): treat JWT-like api_key as Bearer token for AAD authentication When users pass an Azure AD access token (which is a JWT) as the api_key parameter, the SDK now detects this and sends it as an Authorization: Bearer header instead of api-key header. This fixes issue #3282 where AAD tokens passed via api_key started returning 401 in v2.34.0+ because they were being sent as api-key instead of Bearer. The detection is based on the JWT format: tokens starting with "eyJ" followed by two dots are treated as JWT/AAD tokens. Fixes: openai/openai-python#3282 --- src/openai/lib/azure.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index c7e61767a2..45fc9cf512 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -56,6 +56,13 @@ def _has_auth_header(headers: Headers) -> bool: return _has_header(headers, "Authorization") or _has_header(headers, "api-key") +def _is_jwt(token: str) -> bool: + """Check if a token looks like a JWT (used for Azure AD tokens).""" + # JWT tokens have three parts separated by dots: header.payload.signature + # The header is base64-encoded and typically starts with "eyJ" for RS256/HS256 tokens + return token.startswith("eyJ") and token.count(".") == 2 + + _AZURE_AUTH_ORIGIN = "openai.azure_auth_origin" @@ -459,6 +466,9 @@ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: # noqa: A return {"Authorization": f"Bearer {self._azure_ad_token}"} if self.api_key and self.api_key != API_KEY_SENTINEL: + # If api_key looks like a JWT (Azure AD token), send as Bearer + if _is_jwt(self.api_key): + return {"Authorization": f"Bearer {self.api_key}"} return {"api-key": self.api_key} return {} @@ -813,6 +823,9 @@ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: # noqa: A return {"Authorization": f"Bearer {self._azure_ad_token}"} if self.api_key and self.api_key != API_KEY_SENTINEL: + # If api_key looks like a JWT (Azure AD token), send as Bearer + if _is_jwt(self.api_key): + return {"Authorization": f"Bearer {self.api_key}"} return {"api-key": self.api_key} return {}