diff --git a/src/openai/_client.py b/src/openai/_client.py index 499a62dfe5..19184f7fd6 100644 --- a/src/openai/_client.py +++ b/src/openai/_client.py @@ -163,6 +163,7 @@ def __init__( self.workload_identity = workload_identity + _api_key_explicitly_set = api_key is not None if workload_identity is not None: self.api_key = WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER self._api_key_provider = None @@ -187,6 +188,7 @@ def __init__( if ( _enforce_credentials and not self.api_key + and not _api_key_explicitly_set and self._api_key_provider is None and workload_identity is None and self.admin_api_key is None @@ -669,6 +671,7 @@ def __init__( self.workload_identity = workload_identity + _api_key_explicitly_set = api_key is not None if workload_identity is not None: self.api_key = WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER self._api_key_provider = None @@ -693,6 +696,7 @@ def __init__( if ( _enforce_credentials and not self.api_key + and not _api_key_explicitly_set and self._api_key_provider is None and workload_identity is None and self.admin_api_key is None diff --git a/tests/test_client.py b/tests/test_client.py index e2bb6ea966..b2e57437fe 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -524,6 +524,33 @@ def test_validate_headers(self) -> None: with pytest.raises(OpenAIError, match="Missing credentials"): OpenAI(base_url=base_url, api_key=None, admin_api_key=None, _strict_response_validation=True) + # Explicitly passing api_key="" should not raise, even with _enforce_credentials=True. + # This is important for OpenAI-compatible local servers that don't require authentication. + with update_env( + **{ + "OPENAI_API_KEY": Omit(), + "OPENAI_ADMIN_KEY": Omit(), + } + ): + client = OpenAI( + base_url=base_url, + api_key="", + admin_api_key=None, + _strict_response_validation=True, + ) + assert client.api_key == "" + + # OPENAI_API_KEY="" in the environment (without explicit api_key arg) should still raise, + # as an empty env var likely indicates misconfiguration rather than intentional use. + with update_env( + **{ + "OPENAI_API_KEY": "", + "OPENAI_ADMIN_KEY": Omit(), + } + ): + with pytest.raises(OpenAIError, match="Missing credentials"): + OpenAI(base_url=base_url, admin_api_key=None, _strict_response_validation=True) + @pytest.mark.respx(base_url=base_url) def test_api_key_provider_preserves_admin_auth(self, respx_mock: MockRouter) -> None: respx_mock.get("/organization/projects").mock(return_value=httpx.Response(200, json={"ok": True})) @@ -1788,6 +1815,33 @@ async def test_validate_headers(self) -> None: with pytest.raises(OpenAIError, match="Missing credentials"): AsyncOpenAI(base_url=base_url, api_key=None, admin_api_key=None, _strict_response_validation=True) + # Explicitly passing api_key="" should not raise, even with _enforce_credentials=True. + # This is important for OpenAI-compatible local servers that don't require authentication. + with update_env( + **{ + "OPENAI_API_KEY": Omit(), + "OPENAI_ADMIN_KEY": Omit(), + } + ): + client = AsyncOpenAI( + base_url=base_url, + api_key="", + admin_api_key=None, + _strict_response_validation=True, + ) + assert client.api_key == "" + + # OPENAI_API_KEY="" in the environment (without explicit api_key arg) should still raise, + # as an empty env var likely indicates misconfiguration rather than intentional use. + with update_env( + **{ + "OPENAI_API_KEY": "", + "OPENAI_ADMIN_KEY": Omit(), + } + ): + with pytest.raises(OpenAIError, match="Missing credentials"): + AsyncOpenAI(base_url=base_url, admin_api_key=None, _strict_response_validation=True) + @pytest.mark.respx(base_url=base_url) async def test_api_key_provider_preserves_admin_auth(self, respx_mock: MockRouter) -> None: respx_mock.get("/organization/projects").mock(return_value=httpx.Response(200, json={"ok": True}))