This commit is contained in:
JinquanZhang 2026-10-28 02:05:57 +00:00 • committed by GitHub
commit 0b2d4bebb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 97 additions and 1 deletions

View file

@ -1,5 +1,5 @@
from collections.abc import AsyncIterator, Mapping, Sequence
from typing import ClassVar, Final
from typing import ClassVar, Final, cast # noqa: TID251 # narrows the shared untyped litellm_params dict
import httpx
@ -310,12 +310,16 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
headers, api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key)
if not _carries_caller_credential(headers):
bearer_params: Final = cast( # cast-ok: litellm_params is the untyped dict of the shared signature
dict[str, object], litellm_params
)
self._apply_env_auth_header(
headers,
self._require_auth_header(
AnthropicModelInfo.get_auth_header(
api_key,
api_base=api_base,
use_bearer_for_custom_base=_bearer_for_custom_base(bearer_params),
litellm_params=litellm_params,
allow_workload_identity=self._allows_workload_identity,
),
@ -350,12 +354,16 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
oauth_headers, oauth_api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key)
if not _carries_caller_credential(oauth_headers):
bearer_params: Final = cast( # cast-ok: litellm_params is the untyped dict of the shared signature
dict[str, object], litellm_params
)
self._apply_env_auth_header(
oauth_headers,
self._require_auth_header(
await AnthropicModelInfo.aget_auth_header(
oauth_api_key,
api_base=api_base,
use_bearer_for_custom_base=_bearer_for_custom_base(bearer_params),
litellm_params=litellm_params,
allow_workload_identity=self._allows_workload_identity,
),
@ -872,3 +880,12 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
if forwarded_beta_values:
merged["anthropic-beta"] = ",".join(sorted(forwarded_beta_values))
return merged
def _bearer_for_custom_base(litellm_params: object) -> bool:
if isinstance(litellm_params, dict):
params: Final = cast( # cast-ok: callers pass the untyped dict of the shared signature
dict[str, object], litellm_params
)
return bool(params.get("use_bearer_for_custom_base", False))
return bool(getattr(litellm_params, "use_bearer_for_custom_base", False))

View file

@ -416,3 +416,82 @@ def test_drop_params_forwards_a_non_object_message_item_as_sent_on_the_native_re
assert request["messages"][1] == 7
assert request["messages"][2]["content"][0]["content"] == [{"type": "text", "text": "sunny"}]
class TestPassthroughBearerForCustomBase:
"""Regression tests for #33055: the /v1/messages passthrough dropped the deployment-level
``use_bearer_for_custom_base`` flag, so gateways that accept only ``Authorization: Bearer``
answered 401 while the chat path worked. The flag now threads from ``litellm_params`` into
``AnthropicModelInfo.get_auth_header`` on both the sync and async environment validation."""
@staticmethod
def _env(
monkeypatch: pytest.MonkeyPatch,
litellm_params: Mapping[str, object] | object,
api_key: str,
api_base: str,
) -> tuple[Mapping[str, object], str | None]:
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
return AnthropicMessagesConfig().validate_anthropic_messages_environment(
headers={},
model="claude-sonnet-4-6",
messages=[],
optional_params={},
litellm_params=litellm_params,
api_key=api_key,
api_base=api_base,
)
def test_flag_from_litellm_params_uses_bearer(self, monkeypatch: pytest.MonkeyPatch) -> None:
headers, _ = self._env(
monkeypatch, {"use_bearer_for_custom_base": True}, "test-custom-key", "https://internal-gateway.example.com"
)
assert headers.get("authorization") == "Bearer test-custom-key"
assert "x-api-key" not in headers
def test_no_flag_keeps_x_api_key_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
headers, _ = self._env(monkeypatch, {}, "test-custom-key", "https://internal-gateway.example.com")
assert headers.get("x-api-key") == "test-custom-key"
assert "authorization" not in headers
def test_flag_is_inert_on_the_real_anthropic_base(self, monkeypatch: pytest.MonkeyPatch) -> None:
headers, _ = self._env(
monkeypatch, {"use_bearer_for_custom_base": True}, "test-custom-key", "https://api.anthropic.com"
)
assert headers.get("x-api-key") == "test-custom-key"
assert "authorization" not in headers
def test_flag_is_inert_for_standard_anthropic_keys(self, monkeypatch: pytest.MonkeyPatch) -> None:
headers, _ = self._env(
monkeypatch,
{"use_bearer_for_custom_base": True},
"sk-ant-api03-test",
"https://internal-gateway.example.com",
)
assert headers.get("x-api-key") == "sk-ant-api03-test"
assert "authorization" not in headers
def test_flag_from_generic_params_model_uses_bearer(self, monkeypatch: pytest.MonkeyPatch) -> None:
# The proxy passes a GenericLiteLLMParams instance, not a bare dict: the helper must
# fall through to the attribute read when ``litellm_params`` is not a Mapping.
params: Final = GenericLiteLLMParams(use_bearer_for_custom_base=True)
headers, _ = self._env(monkeypatch, params, "test-custom-key", "https://internal-gateway.example.com")
assert headers.get("authorization") == "Bearer test-custom-key"
assert "x-api-key" not in headers
@pytest.mark.asyncio
async def test_async_validation_forwards_the_flag(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
headers, _ = await AnthropicMessagesConfig().avalidate_anthropic_messages_environment(
headers={},
model="claude-sonnet-4-6",
messages=[],
optional_params={},
litellm_params={"use_bearer_for_custom_base": True},
api_key="test-custom-key",
api_base="https://internal-gateway.example.com",
)
assert headers.get("authorization") == "Bearer test-custom-key"
assert "x-api-key" not in headers