Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/openai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
54 changes: 54 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}))
Expand Down Expand Up @@ -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}))
Expand Down