From e14f485827ab78808f3a0e6bafda3ddfd1da2474 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:11:42 +0000 Subject: [PATCH 1/5] fix(anthropic): raise missing-credential error on /v1/messages passthrough Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../messages/transformation.py | 14 +++++- .../anthropic/test_anthropic_common_utils.py | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 032bf0130ce..75146922a39 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -8,6 +8,7 @@ from litellm.constants import ( DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, ) +from litellm.exceptions import AuthenticationError from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import verbose_logger from litellm.llms.base_llm.anthropic_messages.transformation import ( @@ -309,8 +310,17 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if "x-api-key" not in headers and "authorization" not in headers: auth_header: Final = AnthropicModelInfo.get_auth_header(api_key) - if auth_header is not None: - headers.update(auth_header) + if auth_header is None: + raise AuthenticationError( + message=( + "Missing Anthropic API Key - A call is being made to anthropic but no key is set " + "either in the environment variables or via params. Please set `ANTHROPIC_API_KEY` " + "or `ANTHROPIC_AUTH_TOKEN` in your environment vars" + ), + llm_provider=self._resolved_provider, + model=model, + ) + headers.update(auth_header) if "anthropic-version" not in headers: headers["anthropic-version"] = DEFAULT_ANTHROPIC_API_VERSION if "content-type" not in headers: diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index c27362bf49f..a96c44ac5c0 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1227,6 +1227,50 @@ class TestPassthroughAuthToken: assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY assert "authorization" not in updated_headers + def test_passthrough_missing_credentials_raises_authentication_error(self): + """Passthrough endpoint should raise locally instead of forwarding an unauthenticated request.""" + from unittest.mock import patch as mock_patch + + import litellm + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + with mock_patch.dict("os.environ", {}, clear=True): + with pytest.raises(litellm.AuthenticationError, match="Missing Anthropic API Key"): + config.validate_anthropic_messages_environment( + headers={}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + def test_passthrough_client_x_api_key_header_is_kept(self): + """A client-forwarded x-api-key header should satisfy validation without env credentials.""" + from unittest.mock import patch as mock_patch + + from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + with mock_patch.dict("os.environ", {}, clear=True): + updated_headers, _ = config.validate_anthropic_messages_environment( + headers={"x-api-key": FAKE_REGULAR_KEY}, + model="claude-sonnet-4-5-20250929", + messages=[{"role": "user", "content": "Hello"}], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY + def test_passthrough_get_complete_url_honours_base_url_env(self): """get_complete_url should use ANTHROPIC_BASE_URL when api_base is None.""" from unittest.mock import patch as mock_patch From 0fc042e37613b8e9b72c354c18d99e4b5fc2dddb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:28:52 +0000 Subject: [PATCH 2/5] test(anthropic): pass explicit api_key where passthrough env validation now raises Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../llms/anthropic/chat/test_anthropic_chat_transformation.py | 1 + .../messages/test_anthropic_messages_speed.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 4f340ee0f3f..0933638635b 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -1050,6 +1050,7 @@ def test_anthropic_messages_validate_adds_beta_header(): messages=[{"role": "user", "content": [{"type": "text", "text": "Hi"}]}], optional_params={"context_management": _sample_context_management_payload()}, litellm_params={}, + api_key="fake-anthropic-key", ) assert headers["anthropic-beta"] == "context-management-2025-06-27" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py index 6900f1062bf..efd49962ac8 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py @@ -28,6 +28,7 @@ def test_messages_drop_params_strips_speed_for_unsupported_models(): messages=[{"role": "user", "content": "Hello"}], optional_params=dict(optional_params), litellm_params={}, + api_key="fake-anthropic-key", ) result = config.transform_anthropic_messages_request( model="claude-sonnet-4-6", @@ -60,6 +61,7 @@ def test_messages_drop_params_keeps_speed_for_supporting_models(): messages=[{"role": "user", "content": "Hello"}], optional_params=dict(optional_params), litellm_params={}, + api_key="fake-anthropic-key", ) result = config.transform_anthropic_messages_request( model="claude-opus-4-6", From c449f114513a284c71b3716077afe03ac2fd16c3 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:35:39 -0700 Subject: [PATCH 3/5] fix(anthropic): detect client credential headers case-insensitively on /v1/messages --- .../messages/transformation.py | 3 ++- .../llms/anthropic/test_anthropic_common_utils.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 75146922a39..ebd514c2605 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -308,7 +308,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): # Check for Anthropic OAuth token in Authorization header headers, api_key = optionally_handle_anthropic_oauth(headers=headers, api_key=api_key) - if "x-api-key" not in headers and "authorization" not in headers: + header_names: Final = frozenset(name.lower() for name in headers) + if "x-api-key" not in header_names and "authorization" not in header_names: auth_header: Final = AnthropicModelInfo.get_auth_header(api_key) if auth_header is None: raise AuthenticationError( diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index a96c44ac5c0..0a86bcfe85a 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1249,8 +1249,9 @@ class TestPassthroughAuthToken: api_base=None, ) - def test_passthrough_client_x_api_key_header_is_kept(self): - """A client-forwarded x-api-key header should satisfy validation without env credentials.""" + @pytest.mark.parametrize("header_name", ["x-api-key", "X-Api-Key", "X-API-KEY"]) + def test_passthrough_client_x_api_key_header_is_kept(self, header_name): + """A client-forwarded x-api-key header, whatever its casing, should satisfy validation without env credentials.""" from unittest.mock import patch as mock_patch from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( @@ -1260,7 +1261,7 @@ class TestPassthroughAuthToken: config = AnthropicMessagesConfig() with mock_patch.dict("os.environ", {}, clear=True): updated_headers, _ = config.validate_anthropic_messages_environment( - headers={"x-api-key": FAKE_REGULAR_KEY}, + headers={header_name: FAKE_REGULAR_KEY}, model="claude-sonnet-4-5-20250929", messages=[{"role": "user", "content": "Hello"}], optional_params={}, @@ -1269,7 +1270,8 @@ class TestPassthroughAuthToken: api_base=None, ) - assert updated_headers["x-api-key"] == FAKE_REGULAR_KEY + assert [name for name in updated_headers if name.lower() == "x-api-key"] == [header_name] + assert updated_headers[header_name] == FAKE_REGULAR_KEY def test_passthrough_get_complete_url_honours_base_url_env(self): """get_complete_url should use ANTHROPIC_BASE_URL when api_base is None.""" From 2068066d69f58b2ead790a08b0f46ab74722bee1 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:16:17 -0700 Subject: [PATCH 4/5] fix(anthropic): detect oauth Authorization header case-insensitively --- litellm/llms/anthropic/common_utils.py | 16 +++++++------- .../anthropic/test_anthropic_common_utils.py | 21 +++++++++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 9461e40cf2e..1ed38946c59 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -78,8 +78,8 @@ def optionally_handle_anthropic_oauth(headers: dict, api_key: str | None) -> tup """ Handle Anthropic OAuth token detection and header setup. - If an OAuth token is detected in the Authorization header, extracts it - and sets the required OAuth headers. + If an OAuth token is detected in the Authorization header (any casing), + extracts it and sets the required OAuth headers. Args: headers: Request headers dict @@ -89,16 +89,18 @@ def optionally_handle_anthropic_oauth(headers: dict, api_key: str | None) -> tup Tuple of (updated headers, api_key) """ # Check Authorization header (passthrough / forwarded requests) - auth_header: Final = headers.get("authorization", "") - if auth_header and auth_header.startswith(f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"): - api_key = auth_header.replace("Bearer ", "") - headers.pop("x-api-key", None) + auth_header: Final = next((value for name, value in headers.items() if name.lower() == "authorization"), "") + if auth_header.startswith(f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"): + api_key = auth_header.removeprefix("Bearer ") + for name in tuple(header_name for header_name in headers if header_name.lower() == "x-api-key"): + headers.pop(name) headers["anthropic-beta"] = _merge_beta_headers(headers.get("anthropic-beta"), ANTHROPIC_OAUTH_BETA_HEADER) headers["anthropic-dangerous-direct-browser-access"] = "true" return headers, api_key # Check api_key directly (standard chat/completion flow) if api_key and api_key.startswith(ANTHROPIC_OAUTH_TOKEN_PREFIX): - headers.pop("x-api-key", None) + for name in tuple(header_name for header_name in headers if header_name.lower() == "x-api-key"): + headers.pop(name) headers["authorization"] = f"Bearer {api_key}" headers["anthropic-beta"] = _merge_beta_headers(headers.get("anthropic-beta"), ANTHROPIC_OAUTH_BETA_HEADER) headers["anthropic-dangerous-direct-browser-access"] = "true" diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index 0a86bcfe85a..15780c9a160 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -31,13 +31,14 @@ FAKE_AUTH_TOKEN = "sk-ant-aut01-fake-auth-token-for-testing-123456789" class TestOptionallyHandleAnthropicOAuth: """Tests for optionally_handle_anthropic_oauth function.""" - def test_oauth_token_in_authorization_header(self): + @pytest.mark.parametrize("header_name", ["authorization", "Authorization", "AUTHORIZATION"]) + def test_oauth_token_in_authorization_header(self, header_name): """OAuth token in Authorization header should be detected and headers set correctly.""" from litellm.llms.anthropic.common_utils import ( optionally_handle_anthropic_oauth, ) - headers = {"authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + headers = {header_name: f"Bearer {FAKE_OAUTH_TOKEN}"} updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( headers, None ) @@ -47,6 +48,22 @@ class TestOptionallyHandleAnthropicOAuth: assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" assert "x-api-key" not in updated_headers + @pytest.mark.parametrize("api_key_header_name", ["x-api-key", "X-Api-Key"]) + def test_oauth_removes_x_api_key_any_casing(self, api_key_header_name): + """When OAuth wins, a client x-api-key header is removed whatever its casing.""" + from litellm.llms.anthropic.common_utils import ( + optionally_handle_anthropic_oauth, + ) + + headers = {api_key_header_name: FAKE_REGULAR_KEY, "Authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} + updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( + headers, None + ) + + assert extracted_api_key == FAKE_OAUTH_TOKEN + assert [name for name in updated_headers if name.lower() == "x-api-key"] == [] + assert updated_headers["Authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + def test_oauth_token_in_api_key_directly(self): """OAuth token passed as api_key should set Authorization: Bearer header.""" from litellm.llms.anthropic.common_utils import ( From c330d466f37a2e6db778e46c79b21fad78f60685 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:45:51 -0700 Subject: [PATCH 5/5] fix(anthropic): normalize oauth authorization header to one canonical casing --- litellm/llms/anthropic/common_utils.py | 5 +- .../anthropic/test_anthropic_common_utils.py | 160 +++++------------- 2 files changed, 49 insertions(+), 116 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 1ed38946c59..d37340387ab 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -92,8 +92,11 @@ def optionally_handle_anthropic_oauth(headers: dict, api_key: str | None) -> tup auth_header: Final = next((value for name, value in headers.items() if name.lower() == "authorization"), "") if auth_header.startswith(f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}"): api_key = auth_header.removeprefix("Bearer ") - for name in tuple(header_name for header_name in headers if header_name.lower() == "x-api-key"): + for name in tuple( + header_name for header_name in headers if header_name.lower() in ("x-api-key", "authorization") + ): headers.pop(name) + headers["authorization"] = auth_header headers["anthropic-beta"] = _merge_beta_headers(headers.get("anthropic-beta"), ANTHROPIC_OAUTH_BETA_HEADER) headers["anthropic-dangerous-direct-browser-access"] = "true" return headers, api_key diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index 15780c9a160..b2984795c1c 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -18,9 +18,7 @@ from unittest.mock import patch import pytest -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) -) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))) # Fake tokens for testing (not real secrets) FAKE_OAUTH_TOKEN = "sk-ant-oat01-fake-token-for-testing-123456789abcdef" @@ -39,14 +37,14 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {header_name: f"Bearer {FAKE_OAUTH_TOKEN}"} - updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( - headers, None - ) + updated_headers, extracted_api_key = optionally_handle_anthropic_oauth(headers, None) assert extracted_api_key == FAKE_OAUTH_TOKEN assert updated_headers["anthropic-beta"] == "oauth-2025-04-20" assert updated_headers["anthropic-dangerous-direct-browser-access"] == "true" assert "x-api-key" not in updated_headers + assert [name for name in updated_headers if name.lower() == "authorization"] == ["authorization"] + assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" @pytest.mark.parametrize("api_key_header_name", ["x-api-key", "X-Api-Key"]) def test_oauth_removes_x_api_key_any_casing(self, api_key_header_name): @@ -56,13 +54,12 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {api_key_header_name: FAKE_REGULAR_KEY, "Authorization": f"Bearer {FAKE_OAUTH_TOKEN}"} - updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( - headers, None - ) + updated_headers, extracted_api_key = optionally_handle_anthropic_oauth(headers, None) assert extracted_api_key == FAKE_OAUTH_TOKEN assert [name for name in updated_headers if name.lower() == "x-api-key"] == [] - assert updated_headers["Authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" + assert [name for name in updated_headers if name.lower() == "authorization"] == ["authorization"] + assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" def test_oauth_token_in_api_key_directly(self): """OAuth token passed as api_key should set Authorization: Bearer header.""" @@ -71,9 +68,7 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {} - updated_headers, returned_api_key = optionally_handle_anthropic_oauth( - headers, FAKE_OAUTH_TOKEN - ) + updated_headers, returned_api_key = optionally_handle_anthropic_oauth(headers, FAKE_OAUTH_TOKEN) assert returned_api_key == FAKE_OAUTH_TOKEN assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" @@ -88,9 +83,7 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {"x-api-key": FAKE_OAUTH_TOKEN} - updated_headers, _ = optionally_handle_anthropic_oauth( - headers, FAKE_OAUTH_TOKEN - ) + updated_headers, _ = optionally_handle_anthropic_oauth(headers, FAKE_OAUTH_TOKEN) assert "x-api-key" not in updated_headers assert updated_headers["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" @@ -102,9 +95,7 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {} - updated_headers, returned_api_key = optionally_handle_anthropic_oauth( - headers, FAKE_REGULAR_KEY - ) + updated_headers, returned_api_key = optionally_handle_anthropic_oauth(headers, FAKE_REGULAR_KEY) assert returned_api_key == FAKE_REGULAR_KEY assert "authorization" not in updated_headers @@ -118,9 +109,7 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {"authorization": f"Bearer {FAKE_REGULAR_KEY}"} - updated_headers, returned_api_key = optionally_handle_anthropic_oauth( - headers, FAKE_REGULAR_KEY - ) + updated_headers, returned_api_key = optionally_handle_anthropic_oauth(headers, FAKE_REGULAR_KEY) assert returned_api_key == FAKE_REGULAR_KEY assert "anthropic-dangerous-direct-browser-access" not in updated_headers @@ -132,9 +121,7 @@ class TestOptionallyHandleAnthropicOAuth: ) headers = {} - updated_headers, returned_api_key = optionally_handle_anthropic_oauth( - headers, None - ) + updated_headers, returned_api_key = optionally_handle_anthropic_oauth(headers, None) assert returned_api_key is None assert "authorization" not in updated_headers @@ -556,16 +543,12 @@ class TestProxyOAuthHeaderForwarding: ) # Should preserve OAuth even with flag=False - cleaned_without_flag = clean_headers( - raw_headers, forward_llm_provider_auth_headers=False - ) + cleaned_without_flag = clean_headers(raw_headers, forward_llm_provider_auth_headers=False) assert "authorization" in cleaned_without_flag assert cleaned_without_flag["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" # Should also preserve OAuth with flag=True - cleaned_with_flag = clean_headers( - raw_headers, forward_llm_provider_auth_headers=True - ) + cleaned_with_flag = clean_headers(raw_headers, forward_llm_provider_auth_headers=True) assert "authorization" in cleaned_with_flag assert cleaned_with_flag["authorization"] == f"Bearer {FAKE_OAUTH_TOKEN}" @@ -949,9 +932,7 @@ class TestValidateEnvironmentAuthToken: config = AnthropicModelInfo() with mock_patch.dict("os.environ", {}, clear=True): - with pytest.raises( - Exception, match=r"ANTHROPIC_API_KEY.*ANTHROPIC_AUTH_TOKEN" - ): + with pytest.raises(Exception, match=r"ANTHROPIC_API_KEY.*ANTHROPIC_AUTH_TOKEN"): config.validate_environment( headers={}, model="claude-sonnet-4-5-20250929", @@ -997,9 +978,7 @@ class TestGetAuthToken: from litellm.llms.anthropic.common_utils import AnthropicModelInfo - with mock_patch.dict( - "os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True - ): + with mock_patch.dict("os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True): assert AnthropicModelInfo.get_auth_token() == FAKE_AUTH_TOKEN def test_returns_none_when_not_set(self): @@ -1123,7 +1102,9 @@ class TestGetAuthHeader: """Non-standard API key and custom api_base returns Bearer when use_bearer_for_custom_base=True.""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo - result = AnthropicModelInfo.get_auth_header(api_key="my-custom-key", api_base="https://custom-gateway.com", use_bearer_for_custom_base=True) + result = AnthropicModelInfo.get_auth_header( + api_key="my-custom-key", api_base="https://custom-gateway.com", use_bearer_for_custom_base=True + ) assert result == {"authorization": "Bearer my-custom-key"} def test_custom_api_base_get_auth_header_uses_x_api_key_when_standard(self): @@ -1141,10 +1122,7 @@ class TestGetApiBaseFallbackChain: """Explicit api_base param takes precedence over all env vars.""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert ( - AnthropicModelInfo.get_api_base("https://explicit.example.com") - == "https://explicit.example.com" - ) + assert AnthropicModelInfo.get_api_base("https://explicit.example.com") == "https://explicit.example.com" def test_defaults_to_anthropic_api(self): """get_api_base returns the default Anthropic API base when no env vars are set.""" @@ -1197,9 +1175,7 @@ class TestPassthroughAuthToken: ) config = AnthropicMessagesConfig() - with mock_patch.dict( - "os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True - ): + with mock_patch.dict("os.environ", {"ANTHROPIC_AUTH_TOKEN": FAKE_AUTH_TOKEN}, clear=True): updated_headers, _ = config.validate_anthropic_messages_environment( headers={}, model="claude-sonnet-4-5-20250929", @@ -1353,14 +1329,8 @@ class TestAnthropicThinkingSignatureSelfHeal: ) assert is_anthropic_invalid_thinking_signature_error("") is False - assert ( - is_anthropic_invalid_thinking_signature_error("rate limit exceeded") - is False - ) - assert ( - is_anthropic_invalid_thinking_signature_error("invalid_request_error: model not found") - is False - ) + assert is_anthropic_invalid_thinking_signature_error("rate limit exceeded") is False + assert is_anthropic_invalid_thinking_signature_error("invalid_request_error: model not found") is False assert is_anthropic_invalid_thinking_signature_error("thinking signature is malformed") is False def test_strip_thinking_blocks_from_anthropic_messages(self): @@ -1751,10 +1721,7 @@ class TestAnthropicThinkingSignatureSelfHeal: base = "call_abc123" sig = "CiIBDDnWx+/a==" - assert ( - normalize_anthropic_tool_use_id(f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}") - == base - ) + assert normalize_anthropic_tool_use_id(f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}") == base def test_anthropic_messages_config_http_retry_helpers(self): import httpx @@ -1778,15 +1745,11 @@ class TestAnthropicThinkingSignatureSelfHeal: resp_bad = httpx.Response(400, request=req, text="rate limit exceeded") err_bad = httpx.HTTPStatusError("bad", request=req, response=resp_bad) - assert ( - config.should_retry_anthropic_messages_on_http_error(err_bad, {}) is False - ) + assert config.should_retry_anthropic_messages_on_http_error(err_bad, {}) is False resp_500 = httpx.Response(500, request=req, text=err_text) err_500 = httpx.HTTPStatusError("bad", request=req, response=resp_500) - assert ( - config.should_retry_anthropic_messages_on_http_error(err_500, {}) is False - ) + assert config.should_retry_anthropic_messages_on_http_error(err_500, {}) is False data = { "model": "claude-sonnet-4-20250514", @@ -1809,7 +1772,6 @@ class TestAnthropicThinkingSignatureSelfHeal: assert data["messages"] == [] - class TestClaudeOpus48AdaptiveThinking: """Opus 4.8 requires adaptive thinking (``thinking.type='adaptive'`` + ``output_config.effort``). Detection is driven by the @@ -1839,9 +1801,7 @@ class TestClaudeOpus48AdaptiveThinking: assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True - def test_resolver_reads_flag_through_bedrock_invoke_prefix( - self, local_model_cost_map - ): + def test_resolver_reads_flag_through_bedrock_invoke_prefix(self, local_model_cost_map): """The resolver fix: ``bedrock/invoke/...`` resolves to the flagged Bedrock entry. Pure ``_supports_factory`` without prefix-stripping returns False here, which is why the data-only fix alone was not enough.""" @@ -1891,9 +1851,7 @@ class TestClaudeOpus48AdaptiveThinking: "claude-sonnet-4.6", ], ) - def test_adaptive_thinking_detected_for_opus_4_6_4_7_and_sonnet_4_6( - self, local_model_cost_map, model - ): + def test_adaptive_thinking_detected_for_opus_4_6_4_7_and_sonnet_4_6(self, local_model_cost_map, model): """Opus 4.6/4.7 and Sonnet 4.6 carry the ``supports_adaptive_thinking`` flag, so detection holds purely from the cost map with no version-rule fallback. Each alias form the Bedrock/anthropic paths see resolves to a flagged @@ -1913,9 +1871,7 @@ class TestClaudeOpus48AdaptiveThinking: "claude-fable-preview", ], ) - def test_unmapped_aliases_without_parseable_version_stay_non_adaptive( - self, local_model_cost_map, model - ): + def test_unmapped_aliases_without_parseable_version_stay_non_adaptive(self, local_model_cost_map, model): """An alias absent from the map, not matched by any ``fallback_generalizations`` rule, and without any parseable family version stays non-adaptive. ``fable`` without a major version matches neither the core-family 4.6+ gate nor the @@ -1941,9 +1897,7 @@ class TestClaudeOpus48AdaptiveThinking: "us.anthropic.claude-fable-5-preview", ], ) - def test_adaptive_thinking_version_fallback_for_unmapped_high_versions( - self, local_model_cost_map, model - ): + def test_adaptive_thinking_version_fallback_for_unmapped_high_versions(self, local_model_cost_map, model): """Provider-prefixed or suffixed Claude names that resolve to no mapped entry still resolve to adaptive when the id carries claude-- at version 4.6 or higher, bare 5+ majors included. The version gate is the declarative @@ -1964,9 +1918,7 @@ class TestClaudeOpus48AdaptiveThinking: "us.anthropic.claude-opus-4-20250514", ], ) - def test_adaptive_thinking_not_detected_for_unmapped_low_versions( - self, local_model_cost_map, model - ): + def test_adaptive_thinking_not_detected_for_unmapped_low_versions(self, local_model_cost_map, model): """Unmapped Claude names below 4.6 stay non-adaptive through the declarative path. The eight-digit dated Opus 4.0 id (``...-4-20250514``) is the date-safety case: the version rule caps the minor at two digits, so the date is not misread as a >= 4.6 @@ -2005,14 +1957,11 @@ class TestDefaultSuffixAdaptiveThinking: "vertex_ai/claude-fable-5@default", ], ) - def test_default_suffix_models_are_adaptive_thinking( - self, local_model_cost_map, model: str - ) -> None: + def test_default_suffix_models_are_adaptive_thinking(self, local_model_cost_map, model: str) -> None: from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True, ( - f"{model} not classified as adaptive thinking. " - "Check _model_map_lookup_candidates strips @default suffix." + f"{model} not classified as adaptive thinking. Check _model_map_lookup_candidates strips @default suffix." ) @pytest.mark.parametrize( @@ -2022,15 +1971,11 @@ class TestDefaultSuffixAdaptiveThinking: ("vertex_ai/claude-sonnet-4-6@default", "claude-sonnet-4-6"), ], ) - def test_lookup_candidates_include_bare_name( - self, model: str, expected_bare: str - ) -> None: + def test_lookup_candidates_include_bare_name(self, model: str, expected_bare: str) -> None: from litellm.llms.anthropic.common_utils import AnthropicModelInfo candidates = AnthropicModelInfo._model_map_lookup_candidates(model) - assert expected_bare in candidates, ( - f"Expected '{expected_bare}' in candidates for '{model}', got: {candidates}" - ) + assert expected_bare in candidates, f"Expected '{expected_bare}' in candidates for '{model}', got: {candidates}" class TestCapabilityProbeUsesCallerProvider: @@ -2043,42 +1988,27 @@ class TestCapabilityProbeUsesCallerProvider: BEDROCK_MODEL = "global.anthropic.claude-opus-4-8" - def test_exact_bedrock_entry_flag_is_authoritative_for_bedrock_caller( - self, local_model_cost_map, monkeypatch - ): + def test_exact_bedrock_entry_flag_is_authoritative_for_bedrock_caller(self, local_model_cost_map, monkeypatch): import litellm from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert ( - AnthropicModelInfo._is_adaptive_thinking_model(self.BEDROCK_MODEL, "bedrock") - is True - ) + assert AnthropicModelInfo._is_adaptive_thinking_model(self.BEDROCK_MODEL, "bedrock") is True - monkeypatch.setitem( - litellm.model_cost[self.BEDROCK_MODEL], "supports_adaptive_thinking", False - ) + monkeypatch.setitem(litellm.model_cost[self.BEDROCK_MODEL], "supports_adaptive_thinking", False) litellm.get_model_info.cache_clear() - assert ( - AnthropicModelInfo._is_adaptive_thinking_model(self.BEDROCK_MODEL, "bedrock") - is False - ) + assert AnthropicModelInfo._is_adaptive_thinking_model(self.BEDROCK_MODEL, "bedrock") is False - def test_native_anthropic_probe_still_reads_anthropic_entry( - self, local_model_cost_map, monkeypatch - ): + def test_native_anthropic_probe_still_reads_anthropic_entry(self, local_model_cost_map, monkeypatch): import litellm from litellm.llms.anthropic.common_utils import AnthropicModelInfo - monkeypatch.setitem( - litellm.model_cost[self.BEDROCK_MODEL], "supports_adaptive_thinking", False - ) + monkeypatch.setitem(litellm.model_cost[self.BEDROCK_MODEL], "supports_adaptive_thinking", False) litellm.get_model_info.cache_clear() - assert ( - AnthropicModelInfo._is_adaptive_thinking_model("claude-opus-4-8", "anthropic") - is True - ) + assert AnthropicModelInfo._is_adaptive_thinking_model("claude-opus-4-8", "anthropic") is True + + def test_create_anthropic_model_list_response_shape(): from litellm.llms.anthropic.common_utils import ( create_anthropic_model_list_response, @@ -2163,4 +2093,4 @@ def test_create_anthropic_model_list_response_empty(): assert response["data"] == [] assert response["has_more"] is False assert response["first_id"] is None - assert response["last_id"] is None \ No newline at end of file + assert response["last_id"] is None