mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
commit
8dc17e808a
5 changed files with 135 additions and 123 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue