diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c16f8..c717cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ the GitHub Release body, so a release with no entry here fails. Versioning follows [docs/versioning.md](docs/versioning.md). +## [0.11.2] - 2026-09-25 + +### Fixed + +- `OpenAIClient` and `OpenAIResponsesClient` now treat an empty `api_key` as unset and fall back to `OPENAI_API_KEY`, matching the behavior of `None`. + ## [0.11.1] - 2026-09-24 ### Added diff --git a/agent_core/providers/openai_chat.py b/agent_core/providers/openai_chat.py index 2c9af0a..62e6c3e 100644 --- a/agent_core/providers/openai_chat.py +++ b/agent_core/providers/openai_chat.py @@ -180,7 +180,10 @@ def __init__( # (see ``mirror_session_query``) — mirror a construction-time session # header into ``default_query`` so it rides every request's URL. self._client = AsyncOpenAI( - api_key=api_key, + # The SDK consults OPENAI_API_KEY only for ``None``; an empty + # string (a config read before the environment was populated) + # raises "Missing credentials" even with the variable set. + api_key=api_key or None, base_url=base_url or None, timeout=timeout, default_headers=default_headers, diff --git a/agent_core/providers/openai_responses.py b/agent_core/providers/openai_responses.py index 4f26a37..3e3d029 100644 --- a/agent_core/providers/openai_responses.py +++ b/agent_core/providers/openai_responses.py @@ -71,7 +71,10 @@ def __init__( self._reasoning = reasoning or None self._store = store self._client = AsyncOpenAI( - api_key=api_key, + # The SDK consults OPENAI_API_KEY only for ``None``; an empty + # string (a config read before the environment was populated) + # raises "Missing credentials" even with the variable set. + api_key=api_key or None, base_url=base_url or None, timeout=timeout, default_headers=default_headers, diff --git a/pyproject.toml b/pyproject.toml index 5522835..65ce5ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apodex-agent-core" -version = "0.11.1" +version = "0.11.2" description = "Shared, product-neutral runtime primitives for Apodex agents" readme = "README.md" license = "Apache-2.0" diff --git a/tests/test_openai_empty_api_key.py b/tests/test_openai_empty_api_key.py new file mode 100644 index 0000000..198ea6a --- /dev/null +++ b/tests/test_openai_empty_api_key.py @@ -0,0 +1,28 @@ +"""An empty ``api_key`` must fall back to ``OPENAI_API_KEY`` like ``None``.""" + +from __future__ import annotations + +import pytest + +from agent_core.providers.openai_chat import OpenAIClient +from agent_core.providers.openai_responses import OpenAIResponsesClient + + +@pytest.mark.parametrize("client_cls", [OpenAIClient, OpenAIResponsesClient]) +def test_empty_api_key_uses_the_environment( + client_cls: type, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("OPENAI_API_KEY", "from-env") + + client = client_cls("gpt-test", api_key="") + + assert client._client.api_key == "from-env" + + +@pytest.mark.parametrize("client_cls", [OpenAIClient, OpenAIResponsesClient]) +def test_explicit_api_key_wins(client_cls: type, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENAI_API_KEY", "from-env") + + client = client_cls("gpt-test", api_key="explicit") + + assert client._client.api_key == "explicit" diff --git a/uv.lock b/uv.lock index cf914dd..459ea30 100644 --- a/uv.lock +++ b/uv.lock @@ -50,7 +50,7 @@ wheels = [ [[package]] name = "apodex-agent-core" -version = "0.11.1" +version = "0.11.2" source = { editable = "." } dependencies = [ { name = "anthropic", extra = ["bedrock"] },