From 81951fc08592fe415f3a30c27d29e434c45bc034 Mon Sep 17 00:00:00 2001 From: Johan Stenberg Date: Fri, 11 Jul 2025 16:06:21 -0700 Subject: [PATCH 1/3] Add refresh auth headers (sync and async) as alternate approach to allow bearer tokens to be updated --- src/openai/_client.py | 50 ++++++++++++++----- src/openai/lib/azure.py | 2 +- .../resources/beta/realtime/realtime.py | 2 + 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/openai/_client.py b/src/openai/_client.py index 4ed9a2f52e..f5f1781653 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -3,11 +3,13 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Any, Union, Mapping +from typing import TYPE_CHECKING, Any, Union, Mapping, Callable, Awaitable from typing_extensions import Self, override import httpx +from openai._models import FinalRequestOptions + from . import _exceptions from ._qs import Querystring from ._types import ( @@ -91,6 +93,7 @@ def __init__( self, *, api_key: str | None = None, + bearer_token_provider: Callable[[], str] | None = None, organization: str | None = None, project: str | None = None, base_url: str | httpx.URL | None = None, @@ -122,11 +125,12 @@ def __init__( """ if api_key is None: api_key = os.environ.get("OPENAI_API_KEY") - if api_key is None: + if api_key is None and bearer_token_provider is None: raise OpenAIError( "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" ) - self.api_key = api_key + self.bearer_token_provider = bearer_token_provider + self.api_key = api_key or '' if organization is None: organization = os.environ.get("OPENAI_ORG_ID") @@ -155,6 +159,7 @@ def __init__( ) self._default_stream_cls = Stream + self._auth_headers: dict[str, str] = {} @cached_property def completions(self) -> Completions: @@ -259,18 +264,26 @@ def with_raw_response(self) -> OpenAIWithRawResponse: @cached_property def with_streaming_response(self) -> OpenAIWithStreamedResponse: return OpenAIWithStreamedResponse(self) - @property @override def qs(self) -> Querystring: return Querystring(array_format="brackets") + def refresh_auth_headers(self): + bearer_token = self.bearer_token_provider() if self.bearer_token_provider else self.api_key + self._auth_headers = {"Authorization": f"Bearer {bearer_token}"} + + + @override + def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: + self.refresh_auth_headers() + return super()._prepare_options(options) + @property @override def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} - + return self._auth_headers + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -392,6 +405,7 @@ def __init__( self, *, api_key: str | None = None, + bearer_token_provider: Callable[[], Awaitable[str]] | None = None, organization: str | None = None, project: str | None = None, base_url: str | httpx.URL | None = None, @@ -423,11 +437,12 @@ def __init__( """ if api_key is None: api_key = os.environ.get("OPENAI_API_KEY") - if api_key is None: + if api_key is None and bearer_token_provider is None: raise OpenAIError( "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" ) - self.api_key = api_key + self.bearer_token_provider = bearer_token_provider + self.api_key = api_key or '' if organization is None: organization = os.environ.get("OPENAI_ORG_ID") @@ -456,6 +471,7 @@ def __init__( ) self._default_stream_cls = AsyncStream + self._auth_headers: dict[str, str] = {} @cached_property def completions(self) -> AsyncCompletions: @@ -566,12 +582,22 @@ def with_streaming_response(self) -> AsyncOpenAIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="brackets") + async def refresh_auth_headers(self): + if self.bearer_token_provider: + bearer_token = await self.bearer_token_provider() + else: + bearer_token = self.api_key + self._auth_headers = {"Authorization": f"Bearer {bearer_token}"} + + @override + async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: + await self.refresh_auth_headers() + return await super()._prepare_options(options) + @property @override def auth_headers(self) -> dict[str, str]: - api_key = self.api_key - return {"Authorization": f"Bearer {api_key}"} - + return self._auth_headers @property @override def default_headers(self) -> dict[str, str | Omit]: diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 655dd71d4c..6a64df1517 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -614,7 +614,7 @@ async def _configure_realtime(self, model: str, extra_query: Query) -> tuple[htt "api-version": self._api_version, "deployment": self._azure_deployment or model, } - if self.api_key != "": + if self.api_key and self.api_key != "": auth_headers = {"api-key": self.api_key} else: token = await self._get_azure_ad_token() diff --git a/src/openai/resources/beta/realtime/realtime.py b/src/openai/resources/beta/realtime/realtime.py index 8e1b558cf3..beff8eb582 100644 --- a/src/openai/resources/beta/realtime/realtime.py +++ b/src/openai/resources/beta/realtime/realtime.py @@ -358,6 +358,7 @@ async def __aenter__(self) -> AsyncRealtimeConnection: raise OpenAIError("You need to install `openai[realtime]` to use this method") from exc extra_query = self.__extra_query + await self.__client.refresh_auth_headers() auth_headers = self.__client.auth_headers if is_async_azure_client(self.__client): url, auth_headers = await self.__client._configure_realtime(self.__model, extra_query) @@ -540,6 +541,7 @@ def __enter__(self) -> RealtimeConnection: raise OpenAIError("You need to install `openai[realtime]` to use this method") from exc extra_query = self.__extra_query + self.__client.refresh_auth_headers() auth_headers = self.__client.auth_headers if is_azure_client(self.__client): url, auth_headers = self.__client._configure_realtime(self.__model, extra_query) From 64403243edbd53e7d3261cd2e4cbd5088843f760 Mon Sep 17 00:00:00 2001 From: Johan Stenberg Date: Mon, 14 Jul 2025 15:54:11 -0700 Subject: [PATCH 2/3] Validate only one of api_key and bearer_token_provider are passed in. Propagate bearer_token_provider in the `copy` method. --- src/openai/_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/openai/_client.py b/src/openai/_client.py index f5f1781653..9e92bfd707 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -123,6 +123,8 @@ def __init__( - `organization` from `OPENAI_ORG_ID` - `project` from `OPENAI_PROJECT_ID` """ + if api_key and bearer_token_provider: + raise ValueError("The `api_key` and `bearer_token_provider` arguments are mutually exclusive") if api_key is None: api_key = os.environ.get("OPENAI_API_KEY") if api_key is None and bearer_token_provider is None: @@ -299,6 +301,7 @@ def copy( self, *, api_key: str | None = None, + bearer_token_provider: Callable[[], str] | None = None, organization: str | None = None, project: str | None = None, websocket_base_url: str | httpx.URL | None = None, @@ -336,6 +339,7 @@ def copy( http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, + bearer_token_provider = bearer_token_provider or self.bearer_token_provider, organization=organization or self.organization, project=project or self.project, websocket_base_url=websocket_base_url or self.websocket_base_url, @@ -435,6 +439,8 @@ def __init__( - `organization` from `OPENAI_ORG_ID` - `project` from `OPENAI_PROJECT_ID` """ + if api_key and bearer_token_provider: + raise ValueError("The `api_key` and `bearer_token_provider` arguments are mutually exclusive") if api_key is None: api_key = os.environ.get("OPENAI_API_KEY") if api_key is None and bearer_token_provider is None: @@ -613,6 +619,7 @@ def copy( self, *, api_key: str | None = None, + bearer_token_provider: Callable[[], Awaitable[str]] | None = None, organization: str | None = None, project: str | None = None, websocket_base_url: str | httpx.URL | None = None, @@ -650,6 +657,7 @@ def copy( http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, + bearer_token_provider = bearer_token_provider or self.bearer_token_provider, organization=organization or self.organization, project=project or self.project, websocket_base_url=websocket_base_url or self.websocket_base_url, From beffcc2d36c09daed43b86b9bc30154db25dfa2e Mon Sep 17 00:00:00 2001 From: Krista Pratico Date: Wed, 23 Jul 2025 13:27:19 -0700 Subject: [PATCH 3/3] add tests, fix copy, add token provider to module client (#18) * add tests, fix copy, add token provider to module client * fix lint * ignore for azure copy * revert change --- src/openai/__init__.py | 14 ++++ src/openai/_client.py | 55 +++++++++----- src/openai/lib/azure.py | 8 +- tests/test_client.py | 148 +++++++++++++++++++++++++++++++++++- tests/test_module_client.py | 33 ++++++++ 5 files changed, 232 insertions(+), 26 deletions(-) diff --git a/src/openai/__init__.py b/src/openai/__init__.py index 92beeb5da1..0cb892aaae 100644 --- a/src/openai/__init__.py +++ b/src/openai/__init__.py @@ -116,6 +116,8 @@ api_key: str | None = None +bearer_token_provider: _t.Callable[[], str] | None = None + organization: str | None = None project: str | None = None @@ -160,6 +162,17 @@ def api_key(self, value: str | None) -> None: # type: ignore api_key = value + @property # type: ignore + @override + def bearer_token_provider(self) -> _t.Callable[[], str] | None: + return bearer_token_provider + + @bearer_token_provider.setter # type: ignore + def bearer_token_provider(self, value: _t.Callable[[], str] | None) -> None: # type: ignore + global bearer_token_provider + + bearer_token_provider = value + @property # type: ignore @override def organization(self) -> str | None: @@ -332,6 +345,7 @@ def _load_client() -> OpenAI: # type: ignore[reportUnusedFunction] _client = _ModuleClient( api_key=api_key, + bearer_token_provider=bearer_token_provider, organization=organization, project=project, base_url=base_url, diff --git a/src/openai/_client.py b/src/openai/_client.py index 9e92bfd707..ca04861297 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -129,10 +129,10 @@ def __init__( api_key = os.environ.get("OPENAI_API_KEY") if api_key is None and bearer_token_provider is None: raise OpenAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" + "The api_key or bearer_token_provider client option must be set either by passing api_key or bearer_token_provider to the client or by setting the OPENAI_API_KEY environment variable" ) self.bearer_token_provider = bearer_token_provider - self.api_key = api_key or '' + self.api_key = api_key or "" if organization is None: organization = os.environ.get("OPENAI_ORG_ID") @@ -266,26 +266,32 @@ def with_raw_response(self) -> OpenAIWithRawResponse: @cached_property def with_streaming_response(self) -> OpenAIWithStreamedResponse: return OpenAIWithStreamedResponse(self) + @property @override def qs(self) -> Querystring: return Querystring(array_format="brackets") - def refresh_auth_headers(self): - bearer_token = self.bearer_token_provider() if self.bearer_token_provider else self.api_key - self._auth_headers = {"Authorization": f"Bearer {bearer_token}"} - + def refresh_auth_headers(self) -> None: + secret = self.bearer_token_provider() if self.bearer_token_provider else self.api_key + if not secret: + # if the api key is an empty string, encoding the header will fail + # so we set it to an empty dict + # this is to avoid sending an invalid Authorization header + self._auth_headers = {} + else: + self._auth_headers = {"Authorization": f"Bearer {secret}"} @override def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: self.refresh_auth_headers() return super()._prepare_options(options) - + @property @override def auth_headers(self) -> dict[str, str]: return self._auth_headers - + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -336,10 +342,13 @@ def copy( elif set_default_query is not None: params = set_default_query + bearer_token_provider = bearer_token_provider or self.bearer_token_provider + if bearer_token_provider is not None: + _extra_kwargs = {**_extra_kwargs, "bearer_token_provider": bearer_token_provider} + http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, - bearer_token_provider = bearer_token_provider or self.bearer_token_provider, organization=organization or self.organization, project=project or self.project, websocket_base_url=websocket_base_url or self.websocket_base_url, @@ -445,10 +454,10 @@ def __init__( api_key = os.environ.get("OPENAI_API_KEY") if api_key is None and bearer_token_provider is None: raise OpenAIError( - "The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable" + "The api_key or bearer_token_provider client option must be set either by passing api_key or bearer_token_provider to the client or by setting the OPENAI_API_KEY environment variable" ) self.bearer_token_provider = bearer_token_provider - self.api_key = api_key or '' + self.api_key = api_key or "" if organization is None: organization = os.environ.get("OPENAI_ORG_ID") @@ -588,22 +597,29 @@ def with_streaming_response(self) -> AsyncOpenAIWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="brackets") - async def refresh_auth_headers(self): + async def refresh_auth_headers(self) -> None: if self.bearer_token_provider: - bearer_token = await self.bearer_token_provider() + secret = await self.bearer_token_provider() else: - bearer_token = self.api_key - self._auth_headers = {"Authorization": f"Bearer {bearer_token}"} - + secret = self.api_key + if not secret: + # if the api key is an empty string, encoding the header will fail + # so we set it to an empty dict + # this is to avoid sending an invalid Authorization header + self._auth_headers = {} + else: + self._auth_headers = {"Authorization": f"Bearer {secret}"} + @override async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: await self.refresh_auth_headers() return await super()._prepare_options(options) - + @property @override def auth_headers(self) -> dict[str, str]: return self._auth_headers + @property @override def default_headers(self) -> dict[str, str | Omit]: @@ -654,10 +670,13 @@ def copy( elif set_default_query is not None: params = set_default_query + bearer_token_provider = bearer_token_provider or self.bearer_token_provider + if bearer_token_provider is not None: + _extra_kwargs = {**_extra_kwargs, "bearer_token_provider": bearer_token_provider} + http_client = http_client or self._client return self.__class__( api_key=api_key or self.api_key, - bearer_token_provider = bearer_token_provider or self.bearer_token_provider, organization=organization or self.organization, project=project or self.project, websocket_base_url=websocket_base_url or self.websocket_base_url, diff --git a/src/openai/lib/azure.py b/src/openai/lib/azure.py index 6a64df1517..7a04935647 100644 --- a/src/openai/lib/azure.py +++ b/src/openai/lib/azure.py @@ -250,7 +250,7 @@ def __init__( self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None @override - def copy( + def copy( # type: ignore self, *, api_key: str | None = None, @@ -294,7 +294,7 @@ def copy( }, ) - with_options = copy + with_options = copy # type: ignore def _get_azure_ad_token(self) -> str | None: if self._azure_ad_token is not None: @@ -524,7 +524,7 @@ def __init__( self._azure_endpoint = httpx.URL(azure_endpoint) if azure_endpoint else None @override - def copy( + def copy( # type: ignore self, *, api_key: str | None = None, @@ -568,7 +568,7 @@ def copy( }, ) - with_options = copy + with_options = copy # type: ignore async def _get_azure_ad_token(self) -> str | None: if self._azure_ad_token is not None: diff --git a/tests/test_client.py b/tests/test_client.py index 1026b78921..cfe9e562ab 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,7 +11,7 @@ import inspect import subprocess import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, Protocol, cast from textwrap import dedent from unittest import mock from typing_extensions import Literal @@ -44,6 +44,10 @@ api_key = "My API Key" +class MockRequestCall(Protocol): + request: httpx.Request + + def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -339,7 +343,9 @@ def test_default_headers_option(self) -> None: def test_validate_headers(self) -> None: client = OpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + options = client._prepare_options(FinalRequestOptions(method="get", url="/foo")) + request = client._build_request(options) + assert request.headers.get("Authorization") == f"Bearer {api_key}" with pytest.raises(OpenAIError): @@ -964,6 +970,63 @@ def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + def test_refresh_auth_headers_token(self) -> None: + client = OpenAI(base_url=base_url, bearer_token_provider=lambda: "test_bearer_token") + client.refresh_auth_headers() + assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token" + + def test_refresh_auth_headers_key(self) -> None: + client = OpenAI(base_url=base_url, api_key="test_api_key") + client.refresh_auth_headers() + assert client.auth_headers.get("Authorization") == "Bearer test_api_key" + + @pytest.mark.respx() + def test_bearer_token_refresh(self, respx_mock: MockRouter) -> None: + respx_mock.post(base_url + "/chat/completions").mock( + side_effect=[ + httpx.Response(500, json={"error": "server error"}), + httpx.Response(200, json={"foo": "bar"}), + ] + ) + + counter = 0 + + def token_provider() -> str: + nonlocal counter + + counter += 1 + + if counter == 1: + return "first" + + return "second" + + client = OpenAI(base_url=base_url, bearer_token_provider=token_provider) + client.chat.completions.create(messages=[], model="gpt-4") + + calls = cast("list[MockRequestCall]", respx_mock.calls) + assert len(calls) == 2 + + assert calls[0].request.headers.get("Authorization") == "Bearer first" + assert calls[1].request.headers.get("Authorization") == "Bearer second" + + def test_auth_mutually_exclusive(self) -> None: + with pytest.raises(ValueError) as exc_info: + OpenAI(base_url=base_url, api_key=api_key, bearer_token_provider=lambda: "test_bearer_token") + assert str(exc_info.value) == "The `api_key` and `bearer_token_provider` arguments are mutually exclusive" + + def test_copy_auth(self) -> None: + client = OpenAI(base_url=base_url, bearer_token_provider=lambda: "test_bearer_token_1").copy( + bearer_token_provider=lambda: "test_bearer_token_2" + ) + client.refresh_auth_headers() + assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"} + + def test_copy_auth_mutually_exclusive(self) -> None: + with pytest.raises(ValueError) as exc_info: + OpenAI(base_url=base_url, api_key=api_key).copy(bearer_token_provider=lambda: "test_bearer_token") + assert str(exc_info.value) == "The `api_key` and `bearer_token_provider` arguments are mutually exclusive" + class TestAsyncOpenAI: client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) @@ -1244,9 +1307,10 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" - def test_validate_headers(self) -> None: + async def test_validate_headers(self) -> None: client = AsyncOpenAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) - request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + options = await client._prepare_options(FinalRequestOptions(method="get", url="/foo")) + request = client._build_request(options) assert request.headers.get("Authorization") == f"Bearer {api_key}" with pytest.raises(OpenAIError): @@ -1934,3 +1998,79 @@ async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None: assert exc_info.value.response.status_code == 302 assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + + @pytest.mark.asyncio + async def test_refresh_auth_headers_token_async(self) -> None: + async def token_provider() -> str: + return "test_bearer_token" + + client = AsyncOpenAI(base_url=base_url, bearer_token_provider=token_provider) + await client.refresh_auth_headers() + assert client.auth_headers.get("Authorization") == "Bearer test_bearer_token" + + @pytest.mark.asyncio + async def test_refresh_auth_headers_key_async(self) -> None: + client = AsyncOpenAI(base_url=base_url, api_key="test_api_key") + await client.refresh_auth_headers() + assert client.auth_headers.get("Authorization") == "Bearer test_api_key" + + @pytest.mark.asyncio + @pytest.mark.respx() + async def test_bearer_token_refresh_async(self, respx_mock: MockRouter) -> None: + respx_mock.post(base_url + "/chat/completions").mock( + side_effect=[ + httpx.Response(500, json={"error": "server error"}), + httpx.Response(200, json={"foo": "bar"}), + ] + ) + + counter = 0 + + async def token_provider() -> str: + nonlocal counter + + counter += 1 + + if counter == 1: + return "first" + + return "second" + + client = AsyncOpenAI(base_url=base_url, bearer_token_provider=token_provider) + await client.chat.completions.create(messages=[], model="gpt-4") + + calls = cast("list[MockRequestCall]", respx_mock.calls) + assert len(calls) == 2 + + assert calls[0].request.headers.get("Authorization") == "Bearer first" + assert calls[1].request.headers.get("Authorization") == "Bearer second" + + def test_auth_mutually_exclusive_async(self) -> None: + async def token_provider() -> str: + return "test_bearer_token" + + with pytest.raises(ValueError) as exc_info: + AsyncOpenAI(base_url=base_url, api_key=api_key, bearer_token_provider=token_provider) + assert str(exc_info.value) == "The `api_key` and `bearer_token_provider` arguments are mutually exclusive" + + @pytest.mark.asyncio + async def test_copy_auth(self) -> None: + async def token_provider_1() -> str: + return "test_bearer_token_1" + + async def token_provider_2() -> str: + return "test_bearer_token_2" + + client = AsyncOpenAI(base_url=base_url, bearer_token_provider=token_provider_1).copy( + bearer_token_provider=token_provider_2 + ) + await client.refresh_auth_headers() + assert client.auth_headers == {"Authorization": "Bearer test_bearer_token_2"} + + def test_copy_auth_mutually_exclusive_async(self) -> None: + async def token_provider() -> str: + return "test_bearer_token" + + with pytest.raises(ValueError) as exc_info: + AsyncOpenAI(base_url=base_url, api_key=api_key).copy(bearer_token_provider=token_provider) + assert str(exc_info.value) == "The `api_key` and `bearer_token_provider` arguments are mutually exclusive" diff --git a/tests/test_module_client.py b/tests/test_module_client.py index 6bab33a1d7..daa008f881 100644 --- a/tests/test_module_client.py +++ b/tests/test_module_client.py @@ -15,6 +15,7 @@ def reset_state() -> None: openai._reset_client() openai.api_key = None or "My API Key" + openai.bearer_token_provider = None openai.organization = None openai.project = None openai.base_url = None @@ -96,6 +97,17 @@ def test_http_client_option() -> None: assert openai.completions._client._client is new_client +def test_bearer_token_provider_option() -> None: + assert openai.bearer_token_provider is None + assert openai.completions._client.bearer_token_provider is None + + openai.bearer_token_provider = lambda: "foo" + + assert openai.bearer_token_provider() == "foo" + assert openai.completions._client.bearer_token_provider + assert openai.completions._client.bearer_token_provider() == "foo" + + import contextlib from typing import Iterator @@ -122,6 +134,27 @@ def test_only_api_key_results_in_openai_api() -> None: assert type(openai.completions._client).__name__ == "_ModuleClient" +def test_only_bearer_token_provider_in_openai_api() -> None: + with fresh_env(): + openai.api_type = None + openai.api_key = None + openai.bearer_token_provider = lambda: "example bearer token" + + assert type(openai.completions._client).__name__ == "_ModuleClient" + + +def test_both_api_key_and_bearer_token_provider_in_openai_api() -> None: + with fresh_env(): + openai.api_key = "example API key" + openai.bearer_token_provider = lambda: "example bearer token" + + with pytest.raises( + ValueError, + match=r"The `api_key` and `bearer_token_provider` arguments are mutually exclusive", + ): + openai.completions._client # noqa: B018 + + def test_azure_api_key_env_without_api_version() -> None: with fresh_env(): openai.api_type = None