Skip to content

Commit bc495da

Browse files
fern-supportwillkendall01claudejasonozuzu-cohere
authored
Omit Authorization header when api_key is empty (#795)
* Omit Authorization header when api_key is empty * 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 <noreply@anthropic.com> * bump sdk version --------- Co-authored-by: will.kendall <will.kendall@postman.com> Co-authored-by: Will Kendall <will.kendall@buildwithfern.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jason Ozuzu <jasonozuzu@cohere.com>
1 parent d12082b commit bc495da

5 files changed

Lines changed: 92 additions & 5 deletions

File tree

.fern/metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
"originGitCommit": "3f4bc18b3d90a318965a1fc2f5980339c012a6e2",
104104
"originGitCommitIsDirty": true,
105105
"invokedBy": "ci",
106-
"requestedVersion": "7.0.8",
106+
"requestedVersion": "7.0.9",
107107
"ciProvider": "github",
108-
"sdkVersion": "7.0.8"
108+
"sdkVersion": "7.0.9"
109109
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dynamic = ["version"]
44

55
[tool.poetry]
66
name = "cohere"
7-
version = "7.0.8"
7+
version = "7.0.9"
88
description = ""
99
readme = "README.md"
1010
authors = []

src/cohere/core/client_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ def get_headers(self) -> typing.Dict[str, str]:
3535
import platform
3636

3737
headers: typing.Dict[str, str] = {
38-
"User-Agent": "cohere/7.0.8",
38+
"User-Agent": "cohere/7.0.9",
3939
"X-Fern-Language": "Python",
4040
"X-Fern-Runtime": f"python/{platform.python_version()}",
4141
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
4242
"X-Fern-SDK-Name": "cohere",
43-
"X-Fern-SDK-Version": "7.0.8",
43+
"X-Fern-SDK-Version": "7.0.9",
4444
**(self.get_custom_headers() or {}),
4545
}
4646
if self._client_name is not None:

src/cohere/overrides.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,50 @@ def patched_init(self, /, **data):
5959
return cls
6060

6161

62+
def omit_authorization_header_when_api_key_is_empty() -> None:
63+
"""
64+
Do not send an `Authorization` header when the client is created with an empty API key.
65+
66+
This allows pointing the client at a proxy or a self-hosted deployment that performs its own
67+
authentication, e.g. `cohere.Client(api_key="")`.
68+
"""
69+
from .core.client_wrapper import AsyncClientWrapper, BaseClientWrapper
70+
71+
if getattr(BaseClientWrapper, "_omits_empty_authorization", False):
72+
return
73+
74+
get_headers = BaseClientWrapper.get_headers
75+
async_get_headers = AsyncClientWrapper.async_get_headers
76+
77+
def patched_get_headers(self: BaseClientWrapper) -> typing.Dict[str, str]:
78+
headers = get_headers(self)
79+
# Inspect the header that get_headers() already built rather than calling _get_token()
80+
# again: for the callable `api_key` form that would invoke the supplier twice per request,
81+
# and a supplier whose value changes between the two calls would produce the wrong header.
82+
if headers.get("Authorization") == "Bearer ":
83+
headers.pop("Authorization", None)
84+
return headers
85+
86+
async def patched_async_get_headers(self: AsyncClientWrapper) -> typing.Dict[str, str]:
87+
headers = await async_get_headers(self)
88+
if headers.get("Authorization") == "Bearer ":
89+
headers.pop("Authorization", None)
90+
return headers
91+
92+
BaseClientWrapper.get_headers = patched_get_headers # type: ignore[method-assign]
93+
AsyncClientWrapper.async_get_headers = patched_async_get_headers # type: ignore[method-assign]
94+
BaseClientWrapper._omits_empty_authorization = True # type: ignore[attr-defined]
95+
96+
6297
def run_overrides():
6398
"""
6499
These are overrides to allow us to make changes to generated code without touching the generated files themselves.
65100
Should be used judiciously!
66101
"""
67102

103+
# Override to skip the Authorization header entirely when an empty api_key is passed
104+
omit_authorization_header_when_api_key_is_empty()
105+
68106
# Override to allow access to aliases in EmbedByTypeResponseEmbeddings eg embeddings.float rather than embeddings.float_
69107
setattr(EmbedByTypeResponseEmbeddings, "__getattr__", allow_access_to_aliases)
70108

tests/test_optional_auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import asyncio
2+
import typing
3+
import unittest
4+
5+
import cohere
6+
7+
8+
def _headers(client: typing.Any) -> typing.Dict[str, str]:
9+
return client._client_wrapper.get_headers()
10+
11+
12+
async def _async_headers(client: typing.Any) -> typing.Dict[str, str]:
13+
return await client._client_wrapper.async_get_headers()
14+
15+
16+
class TestOptionalAuth(unittest.TestCase):
17+
def test_empty_api_key_omits_authorization_header(self) -> None:
18+
self.assertNotIn("Authorization", _headers(cohere.Client(api_key="")))
19+
self.assertNotIn("Authorization", _headers(cohere.ClientV2(api_key="")))
20+
self.assertNotIn("Authorization", asyncio.run(_async_headers(cohere.AsyncClient(api_key=""))))
21+
self.assertNotIn("Authorization", asyncio.run(_async_headers(cohere.AsyncClientV2(api_key=""))))
22+
23+
def test_api_key_is_sent_when_provided(self) -> None:
24+
self.assertEqual(_headers(cohere.Client(api_key="n/a"))["Authorization"], "Bearer n/a")
25+
self.assertEqual(_headers(cohere.ClientV2(api_key="n/a"))["Authorization"], "Bearer n/a")
26+
self.assertEqual(
27+
asyncio.run(_async_headers(cohere.AsyncClient(api_key="n/a")))["Authorization"], "Bearer n/a"
28+
)
29+
30+
def test_callable_api_key_returning_empty_string_omits_authorization_header(self) -> None:
31+
self.assertNotIn("Authorization", _headers(cohere.Client(api_key=lambda: "")))
32+
33+
def test_callable_api_key_is_invoked_once_per_request(self) -> None:
34+
calls = 0
35+
36+
def api_key() -> str:
37+
nonlocal calls
38+
calls += 1
39+
return "n/a"
40+
41+
self.assertEqual(_headers(cohere.Client(api_key=api_key))["Authorization"], "Bearer n/a")
42+
self.assertEqual(calls, 1)
43+
44+
def test_callable_api_key_is_not_re_read_after_the_header_is_built(self) -> None:
45+
# A supplier whose value changes between calls must not be able to strip an Authorization
46+
# header that was built from a valid token.
47+
values = iter(["real-token", ""])
48+
client = cohere.Client(api_key=lambda: next(values))
49+
self.assertEqual(_headers(client)["Authorization"], "Bearer real-token")

0 commit comments

Comments
 (0)