From b2d47e68a128be241e119a690d05857dcda31dd9 Mon Sep 17 00:00:00 2001 From: "will.kendall" Date: Thu, 13 Aug 2026 17:21:38 +0000 Subject: [PATCH 1/3] Omit Authorization header when api_key is empty --- src/cohere/overrides.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_optional_auth.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/test_optional_auth.py diff --git a/src/cohere/overrides.py b/src/cohere/overrides.py index 8827df6fa..6f438d7a9 100644 --- a/src/cohere/overrides.py +++ b/src/cohere/overrides.py @@ -59,12 +59,47 @@ def patched_init(self, /, **data): return cls +def omit_authorization_header_when_api_key_is_empty() -> None: + """ + Do not send an `Authorization` header when the client is created with an empty API key. + + This allows pointing the client at a proxy or a self-hosted deployment that performs its own + authentication, e.g. `cohere.Client(api_key="")`. + """ + from .core.client_wrapper import AsyncClientWrapper, BaseClientWrapper + + if getattr(BaseClientWrapper, "_omits_empty_authorization", False): + return + + get_headers = BaseClientWrapper.get_headers + async_get_headers = AsyncClientWrapper.async_get_headers + + def patched_get_headers(self: BaseClientWrapper) -> typing.Dict[str, str]: + headers = get_headers(self) + if not self._get_token(): + headers.pop("Authorization", None) + return headers + + async def patched_async_get_headers(self: AsyncClientWrapper) -> typing.Dict[str, str]: + headers = await async_get_headers(self) + if headers.get("Authorization") == "Bearer ": + headers.pop("Authorization", None) + return headers + + BaseClientWrapper.get_headers = patched_get_headers # type: ignore[method-assign] + AsyncClientWrapper.async_get_headers = patched_async_get_headers # type: ignore[method-assign] + BaseClientWrapper._omits_empty_authorization = True # type: ignore[attr-defined] + + def run_overrides(): """ These are overrides to allow us to make changes to generated code without touching the generated files themselves. Should be used judiciously! """ + # Override to skip the Authorization header entirely when an empty api_key is passed + omit_authorization_header_when_api_key_is_empty() + # Override to allow access to aliases in EmbedByTypeResponseEmbeddings eg embeddings.float rather than embeddings.float_ setattr(EmbedByTypeResponseEmbeddings, "__getattr__", allow_access_to_aliases) diff --git a/tests/test_optional_auth.py b/tests/test_optional_auth.py new file mode 100644 index 000000000..cd5801a20 --- /dev/null +++ b/tests/test_optional_auth.py @@ -0,0 +1,31 @@ +import asyncio +import typing +import unittest + +import cohere + + +def _headers(client: typing.Any) -> typing.Dict[str, str]: + return client._client_wrapper.get_headers() + + +async def _async_headers(client: typing.Any) -> typing.Dict[str, str]: + return await client._client_wrapper.async_get_headers() + + +class TestOptionalAuth(unittest.TestCase): + def test_empty_api_key_omits_authorization_header(self) -> None: + self.assertNotIn("Authorization", _headers(cohere.Client(api_key=""))) + self.assertNotIn("Authorization", _headers(cohere.ClientV2(api_key=""))) + self.assertNotIn("Authorization", asyncio.run(_async_headers(cohere.AsyncClient(api_key="")))) + self.assertNotIn("Authorization", asyncio.run(_async_headers(cohere.AsyncClientV2(api_key="")))) + + def test_api_key_is_sent_when_provided(self) -> None: + self.assertEqual(_headers(cohere.Client(api_key="n/a"))["Authorization"], "Bearer n/a") + self.assertEqual(_headers(cohere.ClientV2(api_key="n/a"))["Authorization"], "Bearer n/a") + self.assertEqual( + asyncio.run(_async_headers(cohere.AsyncClient(api_key="n/a")))["Authorization"], "Bearer n/a" + ) + + def test_callable_api_key_returning_empty_string_omits_authorization_header(self) -> None: + self.assertNotIn("Authorization", _headers(cohere.Client(api_key=lambda: ""))) From 741510f5d47d2b9edbdb374e7a8556cedb72c94e Mon Sep 17 00:00:00 2001 From: Will Kendall Date: Fri, 14 Aug 2026 14:52:12 -0400 Subject: [PATCH 2/3] Resolve the api_key supplier once per request patched_get_headers called _get_token() a second time, after get_headers() had already called it. For the documented callable api_key form this invoked the supplier twice per request, and a supplier whose value changed between the two calls produced the wrong header: returning "real-token" then "" stripped the Authorization header despite a valid token, and the reverse sent "Bearer " while a valid token was available. Both yield a 401. Inspect the header get_headers() already built instead, matching what patched_async_get_headers has been doing. Co-Authored-By: Claude --- src/cohere/overrides.py | 5 ++++- tests/test_optional_auth.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/cohere/overrides.py b/src/cohere/overrides.py index 6f438d7a9..026ef2a7f 100644 --- a/src/cohere/overrides.py +++ b/src/cohere/overrides.py @@ -76,7 +76,10 @@ def omit_authorization_header_when_api_key_is_empty() -> None: def patched_get_headers(self: BaseClientWrapper) -> typing.Dict[str, str]: headers = get_headers(self) - if not self._get_token(): + # Inspect the header that get_headers() already built rather than calling _get_token() + # again: for the callable `api_key` form that would invoke the supplier twice per request, + # and a supplier whose value changes between the two calls would produce the wrong header. + if headers.get("Authorization") == "Bearer ": headers.pop("Authorization", None) return headers diff --git a/tests/test_optional_auth.py b/tests/test_optional_auth.py index cd5801a20..652f233db 100644 --- a/tests/test_optional_auth.py +++ b/tests/test_optional_auth.py @@ -29,3 +29,21 @@ def test_api_key_is_sent_when_provided(self) -> None: def test_callable_api_key_returning_empty_string_omits_authorization_header(self) -> None: self.assertNotIn("Authorization", _headers(cohere.Client(api_key=lambda: ""))) + + def test_callable_api_key_is_invoked_once_per_request(self) -> None: + calls = 0 + + def api_key() -> str: + nonlocal calls + calls += 1 + return "n/a" + + self.assertEqual(_headers(cohere.Client(api_key=api_key))["Authorization"], "Bearer n/a") + self.assertEqual(calls, 1) + + def test_callable_api_key_is_not_re_read_after_the_header_is_built(self) -> None: + # A supplier whose value changes between calls must not be able to strip an Authorization + # header that was built from a valid token. + values = iter(["real-token", ""]) + client = cohere.Client(api_key=lambda: next(values)) + self.assertEqual(_headers(client)["Authorization"], "Bearer real-token") From 31871087a13655f3a082089491ccc3c014152c64 Mon Sep 17 00:00:00 2001 From: Jason Ozuzu Date: Mon, 17 Aug 2026 14:07:34 +0100 Subject: [PATCH 3/3] bump sdk version --- .fern/metadata.json | 4 ++-- pyproject.toml | 2 +- src/cohere/core/client_wrapper.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.fern/metadata.json b/.fern/metadata.json index f0eb33565..6335779a5 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -103,7 +103,7 @@ "originGitCommit": "3f4bc18b3d90a318965a1fc2f5980339c012a6e2", "originGitCommitIsDirty": true, "invokedBy": "ci", - "requestedVersion": "7.0.8", + "requestedVersion": "7.0.9", "ciProvider": "github", - "sdkVersion": "7.0.8" + "sdkVersion": "7.0.9" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f6b908415..596c3df95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "cohere" -version = "7.0.8" +version = "7.0.9" description = "" readme = "README.md" authors = [] diff --git a/src/cohere/core/client_wrapper.py b/src/cohere/core/client_wrapper.py index 10de6f4cc..ac495af13 100644 --- a/src/cohere/core/client_wrapper.py +++ b/src/cohere/core/client_wrapper.py @@ -35,12 +35,12 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "cohere/7.0.8", + "User-Agent": "cohere/7.0.9", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "cohere", - "X-Fern-SDK-Version": "7.0.8", + "X-Fern-SDK-Version": "7.0.9", **(self.get_custom_headers() or {}), } if self._client_name is not None: