diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 08fa9a5b9e9..c73376ba498 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -93,8 +93,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 @@ -104,16 +104,21 @@ 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() 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 # 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/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 032bf0130ce..ebd514c2605 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 ( @@ -307,10 +308,20 @@ 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 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/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", 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..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" @@ -31,21 +29,37 @@ 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}"} - updated_headers, extracted_api_key = optionally_handle_anthropic_oauth( - headers, None - ) + headers = {header_name: f"Bearer {FAKE_OAUTH_TOKEN}"} + 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): + """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 [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.""" @@ -54,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}" @@ -71,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}" @@ -85,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 @@ -101,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 @@ -115,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 @@ -539,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}" @@ -932,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", @@ -980,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): @@ -1106,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): @@ -1124,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.""" @@ -1180,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", @@ -1227,6 +1220,52 @@ 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, + ) + + @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 ( + AnthropicMessagesConfig, + ) + + config = AnthropicMessagesConfig() + with mock_patch.dict("os.environ", {}, clear=True): + updated_headers, _ = config.validate_anthropic_messages_environment( + headers={header_name: 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 [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 unittest.mock import patch as mock_patch @@ -1290,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): @@ -1688,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 @@ -1715,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", @@ -1746,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 @@ -1776,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.""" @@ -1828,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 @@ -1850,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 @@ -1878,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 @@ -1901,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 @@ -1942,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( @@ -1959,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: @@ -1980,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, @@ -2100,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