diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 5741513903c..46b36c3b846 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -3,6 +3,7 @@ This file contains common utils for anthropic calls. """ import copy +import re from typing import Any, Dict, List, Optional, Union import httpx @@ -271,6 +272,20 @@ class AnthropicModelInfo(BaseLLMModelInfo): ) ) + @staticmethod + def _is_claude_4_8_model(model: str) -> bool: + """Check if the model is a Claude 4.8 model (Opus 4.8).""" + model_lower = model.lower() + return any( + v in model_lower + for v in ( + "opus-4-8", + "opus_4_8", + "opus-4.8", + "opus_4.8", + ) + ) + @staticmethod def _supports_sampling_params(model: str) -> bool: """Claude 4.7+ (Opus 4.7/4.8, Fable 5) removed sampling params: the API @@ -335,6 +350,15 @@ class AnthropicModelInfo(BaseLLMModelInfo): """Model-map keys to try for ``model``, stripping bedrock/vertex prefixes so a provider-routed Claude still resolves to its entry.""" candidates = [model] + # Vertex appends ``@`` (e.g. ``@default``, ``@20251101``) to + # the model id; the bare name is what carries the capability flags in + # the model map. Without this strip, ``claude-opus-4-8@default`` + # misses ``claude-opus-4-8`` and downstream + # ``supports_adaptive_thinking`` / ``supports_output_config`` checks + # silently fall back to the partial name-substring path (#30101). + suffix_stripped = re.sub(r"@[^@/]+$", "", model) + if suffix_stripped != model: + candidates.append(suffix_stripped) for prefix in ( "bedrock/converse/", "bedrock/invoke/", @@ -343,6 +367,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): ): if model.startswith(prefix): candidates.append(model[len(prefix) :]) + if suffix_stripped != model and suffix_stripped.startswith(prefix): + candidates.append(suffix_stripped[len(prefix) :]) try: from litellm.llms.bedrock.common_utils import BedrockModelInfo @@ -399,9 +425,11 @@ class AnthropicModelInfo(BaseLLMModelInfo): model, "supports_adaptive_thinking" ): return True - return AnthropicModelInfo._is_claude_4_6_model( - model - ) or AnthropicModelInfo._is_claude_4_7_model(model) + return ( + AnthropicModelInfo._is_claude_4_6_model(model) + or AnthropicModelInfo._is_claude_4_7_model(model) + or AnthropicModelInfo._is_claude_4_8_model(model) + ) def is_effort_used( self, optional_params: Optional[dict], model: Optional[str] = None 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 a09e55d4ed7..32b4791e062 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1450,3 +1450,73 @@ class TestClaudeOpus48AdaptiveThinking: from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert AnthropicModelInfo._is_adaptive_thinking_model(model) is False + + +class TestVertexVersionSuffixModelResolution: + """Regression for #30101. + + Vertex appends ``@`` (``@default``, ``@20251101``, ...) to the + routed model id. ``_model_map_lookup_candidates`` stripped bedrock/vertex + prefixes but not the trailing suffix, so a request id like + ``vertex_ai/claude-opus-4-8@default`` never resolved to the bare + ``claude-opus-4-8`` map entry that carries the ``supports_adaptive_thinking`` + and ``supports_output_config`` flags. Downstream adaptive-thinking + + effort detection fell through to the partial name-substring path. + """ + + @pytest.mark.parametrize( + "model", + [ + "claude-opus-4-7@default", + "claude-opus-4-8@default", + "claude-opus-4-8@20251101", + "vertex_ai/claude-opus-4-7@default", + "vertex_ai/claude-opus-4-8@default", + ], + ) + def test_adaptive_thinking_detected_with_vertex_version_suffix( + self, local_model_cost_map, model + ): + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + + @pytest.mark.parametrize( + "model", + [ + "claude-opus-4-7@default", + "claude-opus-4-8@default", + "vertex_ai/claude-opus-4-8@default", + ], + ) + def test_capability_resolves_through_version_suffix( + self, local_model_cost_map, model + ): + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert ( + AnthropicModelInfo._supports_model_capability( + model, "supports_adaptive_thinking" + ) + is True + ) + assert ( + AnthropicModelInfo._supports_model_capability( + model, "supports_output_config" + ) + is True + ) + + def test_is_claude_4_8_model_recognizes_suffix_and_provider_prefix(self): + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert AnthropicModelInfo._is_claude_4_8_model("claude-opus-4-8") is True + assert ( + AnthropicModelInfo._is_claude_4_8_model("claude-opus-4-8@default") is True + ) + assert ( + AnthropicModelInfo._is_claude_4_8_model("vertex_ai/claude-opus-4-8@default") + is True + ) + assert AnthropicModelInfo._is_claude_4_8_model("claude-opus-4-7") is False + assert AnthropicModelInfo._is_claude_4_8_model("claude-opus-4-5") is False