Merge pull request #38240 from BerriAI/devin_ai_anthropic_messages_missing_key

fix(anthropic): raise missing-credential error on /v1/messages passthrough
This commit is contained in:
Mateo Wang 2026-08-26 12:06:37 -07:00 committed by GitHub
commit 8dc17e808a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 123 deletions

View file

@ -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"

View file

@ -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:

View file

@ -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"

View file

@ -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",

View file

@ -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-<family>- 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
assert response["last_id"] is None