diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 9721b797584..6033e54fb77 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -266,6 +266,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def custom_llm_provider(self) -> Optional[str]: return "anthropic" + @property + def _resolved_provider(self) -> str: + return self.custom_llm_provider or "anthropic" + @classmethod def get_config(cls, *, model: Optional[str] = None): config = super().get_config() @@ -335,23 +339,26 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): return any(v in model_lower for v in ("opus-4-7", "opus_4_7", "opus-4.7", "opus_4.7")) @staticmethod - def _supports_effort_level(model: str, level: str) -> bool: + def _supports_effort_level(model: str, level: str, custom_llm_provider: str) -> bool: """Check ``supports_{level}_reasoning_effort`` in the model map.""" - return AnthropicConfig._supports_model_capability(model, f"supports_{level}_reasoning_effort") + return AnthropicConfig._supports_model_capability( + model, f"supports_{level}_reasoning_effort", custom_llm_provider + ) @staticmethod - def _validate_effort_for_model(model: str, effort: Optional[str]) -> Optional[str]: + def _validate_effort_for_model(model: str, effort: Optional[str], custom_llm_provider: str) -> Optional[str]: """Return ``None`` if ``effort`` is allowed on ``model``, else an error message.""" if effort == "max" and not ( - AnthropicConfig._is_adaptive_thinking_model(model) or AnthropicConfig._supports_effort_level(model, "max") + AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider) + or AnthropicConfig._supports_effort_level(model, "max", custom_llm_provider) ): return f"effort='max' is not supported by this model. Got model: {model}" - if effort == "xhigh" and not AnthropicConfig._supports_effort_level(model, "xhigh"): + if effort == "xhigh" and not AnthropicConfig._supports_effort_level(model, "xhigh", custom_llm_provider): return f"effort='xhigh' is not supported by this model. Got model: {model}" return None @staticmethod - def _model_supports_effort_param(model: str) -> bool: + def _model_supports_effort_param(model: str, custom_llm_provider: str) -> bool: """Whether the model accepts ``output_config.effort`` at all. A model qualifies if its map entry advertises ``supports_output_config`` @@ -359,10 +366,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): signals: e.g. Claude Opus 4.5 supports ``output_config`` without advertising a non-default (max/xhigh) effort level. """ - if AnthropicConfig._supports_model_capability(model, "supports_output_config"): + if AnthropicConfig._supports_model_capability(model, "supports_output_config", custom_llm_provider): return True return any( - AnthropicConfig._supports_effort_level(model, level) + AnthropicConfig._supports_effort_level(model, level, custom_llm_provider) for level in ("low", "minimal", "medium", "high", "xhigh", "max") ) @@ -451,7 +458,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if ( "claude-3-7-sonnet" in model - or AnthropicConfig._is_adaptive_thinking_model(model) + or AnthropicConfig._is_adaptive_thinking_model(model, self._resolved_provider) or supports_reasoning( model=model, custom_llm_provider=self.custom_llm_provider, @@ -1159,11 +1166,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _map_reasoning_effort( reasoning_effort: Optional[Union[REASONING_EFFORT, str]], model: str, + custom_llm_provider: str, llm_provider: str = "anthropic", ) -> Optional[AnthropicThinkingParam]: + """Capability probes read the cost map under ``custom_llm_provider``; ``llm_provider`` only tags raised exceptions.""" if reasoning_effort is None or reasoning_effort == "none": return None - if AnthropicConfig._is_adaptive_thinking_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider): return AnthropicThinkingParam( type="adaptive", ) @@ -1471,20 +1480,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=effort_value, model=model, - llm_provider=self.custom_llm_provider or "anthropic", + custom_llm_provider=self._resolved_provider, + llm_provider=self._resolved_provider, ) if mapped_thinking is None: optional_params.pop("thinking", None) optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - if AnthropicConfig._is_adaptive_thinking_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model, self._resolved_provider): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(effort_value) if mapped_effort is None: AnthropicConfig._raise_invalid_reasoning_effort( model=model, value=effort_value, - llm_provider=self.custom_llm_provider or "anthropic", + llm_provider=self._resolved_provider, ) optional_params["output_config"] = {"effort": mapped_effort} elif param == "web_search_options" and isinstance(value, dict): @@ -1813,7 +1823,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): anthropic_messages = anthropic_messages_pt( model=model, messages=messages, - llm_provider=self.custom_llm_provider or "anthropic", + llm_provider=self._resolved_provider, ) except Exception as e: raise AnthropicError( @@ -1902,7 +1912,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): output_config = optional_params.get("output_config") if not output_config or not isinstance(output_config, dict): return - if litellm.drop_params is True and not self._model_supports_effort_param(model): + if litellm.drop_params is True and not self._model_supports_effort_param(model, self._resolved_provider): litellm.verbose_logger.warning( DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model, @@ -1916,14 +1926,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): raise litellm.exceptions.BadRequestError( message=(f"Invalid effort value: {effort!r}. Must be one of: 'high', 'medium', 'low', 'xhigh', 'max'"), model=model, - llm_provider=self.custom_llm_provider or "anthropic", + llm_provider=self._resolved_provider, ) - gate_error = self._validate_effort_for_model(model, effort) + gate_error = self._validate_effort_for_model(model, effort, self._resolved_provider) if gate_error is not None: raise litellm.exceptions.BadRequestError( message=gate_error, model=model, - llm_provider=self.custom_llm_provider or "anthropic", + llm_provider=self._resolved_provider, ) data["output_config"] = output_config diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 964186fd76a..0bcf34a45d6 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -360,18 +360,43 @@ class AnthropicModelInfo(BaseLLMModelInfo): return value if isinstance(value, bool) else None @staticmethod - def _supports_model_capability(model: str, key: str) -> bool: - """Check a boolean capability ``key`` in the model map. + def _get_provider_resolved_capability(model: str, key: str, custom_llm_provider: str) -> Optional[bool]: + """Resolve boolean capability ``key`` for ``model`` under the caller's provider. - Strips bedrock/vertex prefixes so a provider-routed Claude still - resolves to the Anthropic model-map entry. + Returns the flag when the provider-aware lookup resolves ``model`` to an + entry (or fallback rule) that sets it explicitly, and ``None`` when the + model does not resolve under that provider or the resolved entry has no + opinion on ``key``. + """ + from litellm.utils import _get_model_info_helper + + try: + resolved_model, resolved_provider, _, _ = litellm.get_llm_provider( + model=model, custom_llm_provider=custom_llm_provider + ) + value = _get_model_info_helper(model=resolved_model, custom_llm_provider=resolved_provider).get(key) + except Exception: # noqa: BLE001 # _get_model_info_helper raises bare Exception for unmapped models + return None + return value if isinstance(value, bool) else None + + @staticmethod + def _supports_model_capability(model: str, key: str, custom_llm_provider: str) -> bool: + """Check a boolean capability ``key`` in the model map under the caller's provider. + + The provider-aware lookup is authoritative when it resolves an explicit flag, + so ``key: false`` on the provider-namespaced entry wins over every fallback. + Otherwise ``_supports_factory``'s provider-level fallbacks and the raw + model-map walk remain as backstops for alias forms the lookup misses. """ from litellm.utils import _supports_factory + resolved = AnthropicModelInfo._get_provider_resolved_capability(model, key, custom_llm_provider) + if resolved is not None: + return resolved try: if _supports_factory( model=model, - custom_llm_provider="anthropic", + custom_llm_provider=custom_llm_provider, key=key, ): return True @@ -380,17 +405,24 @@ class AnthropicModelInfo(BaseLLMModelInfo): return AnthropicModelInfo._get_model_capability(model, key) is True @staticmethod - def _is_adaptive_thinking_model(model: str) -> bool: + def _is_adaptive_thinking_model(model: str, custom_llm_provider: str) -> bool: """Whether ``model`` uses adaptive thinking (``output_config.effort``). The model cost map is authoritative: an explicit ``supports_adaptive_thinking`` - entry, or a ``fallback_generalizations`` rule for unknown Claude models. The - version gate (>= 4.6, including provider-prefixed Bedrock/Vertex ids that map to - no exact entry) lives entirely in that declarative rule, not here. + entry resolved under ``custom_llm_provider``, or a ``fallback_generalizations`` + rule for unknown Claude models. The version gate (>= 4.6, including + provider-prefixed Bedrock/Vertex ids that map to no exact entry) lives entirely + in that declarative rule, not here. """ - return AnthropicModelInfo._supports_model_capability(model, "supports_adaptive_thinking") + return AnthropicModelInfo._supports_model_capability(model, "supports_adaptive_thinking", custom_llm_provider) - def is_effort_used(self, optional_params: Optional[dict], model: Optional[str] = None) -> bool: + def is_effort_used( + self, + optional_params: Optional[dict], + model: Optional[str] = None, + *, + custom_llm_provider: str, + ) -> bool: """ Check if effort parameter is being used and requires a beta header. @@ -402,7 +434,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): return False # Claude 4.6+ models use output_config as a stable API feature — no beta header needed - if model and self._is_adaptive_thinking_model(model): + if model and self._is_adaptive_thinking_model(model, custom_llm_provider): return False # Check if reasoning_effort is provided for Claude Opus 4.5 @@ -483,6 +515,8 @@ class AnthropicModelInfo(BaseLLMModelInfo): prompt_caching_set: bool = False, file_id_used: bool = False, mcp_server_used: bool = False, + *, + custom_llm_provider: str, ) -> List[str]: """ Get list of common beta headers based on the features that are active. @@ -495,7 +529,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): betas = [] # Detect features - effort_used = self.is_effort_used(optional_params, model) + effort_used = self.is_effort_used(optional_params, model, custom_llm_provider=custom_llm_provider) if effort_used: betas.append(ANTHROPIC_EFFORT_BETA_HEADER) # effort-2025-11-24 @@ -651,7 +685,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): tool_search_used = self.is_tool_search_used(tools=tools) programmatic_tool_calling_used = self.is_programmatic_tool_calling_used(tools=tools) input_examples_used = self.is_input_examples_used(tools=tools) - effort_used = self.is_effort_used(optional_params=optional_params, model=model) + effort_used = self.is_effort_used(optional_params=optional_params, model=model, custom_llm_provider="anthropic") code_execution_tool_used = self.is_code_execution_tool_used(tools=tools) container_with_skills_used = self.is_container_with_skills_used(optional_params=optional_params) user_anthropic_beta_headers = self._get_user_anthropic_beta_headers( diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index b74bbda0b66..39713e0f003 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -41,6 +41,14 @@ DROP_UNSUPPORTED_ADAPTIVE_EFFORT_WARNING = ( class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): + @property + def custom_llm_provider(self) -> Optional[str]: + return "anthropic" + + @property + def _resolved_provider(self) -> str: + return self.custom_llm_provider or "anthropic" + def get_supported_anthropic_messages_params(self, model: str) -> list: return [ "messages", @@ -181,7 +189,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return headers, api_base @staticmethod - def _translate_reasoning_effort_to_anthropic(model: str, optional_params: Dict) -> None: + def _translate_reasoning_effort_to_anthropic(model: str, optional_params: Dict, custom_llm_provider: str) -> None: """Map OpenAI-style ``reasoning_effort`` to native Anthropic params. Caller-supplied ``thinking`` / ``output_config`` win over the alias. @@ -198,7 +206,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return try: - mapped_thinking = AnthropicConfig._map_reasoning_effort(reasoning_effort=reasoning_effort, model=model) + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=reasoning_effort, + model=model, + custom_llm_provider=custom_llm_provider, + ) except _BadRequestError as e: raise AnthropicError(message=str(e.message), status_code=400) @@ -208,7 +220,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return optional_params.setdefault("thinking", mapped_thinking) - if AnthropicModelInfo._is_adaptive_thinking_model(model): + if AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort) if mapped_effort is None: raise AnthropicError( @@ -219,7 +231,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ), status_code=400, ) - gate_error = AnthropicConfig._validate_effort_for_model(model, mapped_effort) + gate_error = AnthropicConfig._validate_effort_for_model(model, mapped_effort, custom_llm_provider) if gate_error is not None: raise AnthropicError(message=gate_error, status_code=400) existing_output_config = optional_params.get("output_config") @@ -229,13 +241,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params["output_config"] = existing_output_config @staticmethod - def _translate_legacy_thinking_for_adaptive_model(model: str, optional_params: Dict) -> None: + def _translate_legacy_thinking_for_adaptive_model( + model: str, optional_params: Dict, custom_llm_provider: str + ) -> None: """Translate legacy ``thinking.type=enabled`` to adaptive for 4.6/4.7. Caller-provided ``output_config.effort`` is never overridden. """ from litellm.llms.anthropic.chat.transformation import AnthropicConfig - if not AnthropicModelInfo._is_adaptive_thinking_model(model): + if not AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider): return thinking = optional_params.get("thinking") if not isinstance(thinking, dict) or thinking.get("type") != "enabled": @@ -243,7 +257,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): budget = int(thinking.get("budget_tokens") or 0) if budget >= DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET and ( - AnthropicConfig._supports_effort_level(model, "xhigh") + AnthropicConfig._supports_effort_level(model, "xhigh", custom_llm_provider) ): effort = "xhigh" elif budget >= DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET: @@ -262,7 +276,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): @staticmethod def _translate_adaptive_effort_for_non_adaptive_model( - model: str, optional_params: Dict, max_tokens: Optional[int] + model: str, optional_params: Dict, max_tokens: Optional[int], custom_llm_provider: str ) -> None: """Translate the 4.6+ adaptive-thinking interface (``thinking.type=adaptive`` and/or ``output_config.effort``) down to what an older Anthropic model @@ -305,7 +319,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): from litellm.exceptions import BadRequestError as _BadRequestError from litellm.llms.anthropic.chat.transformation import AnthropicConfig - if AnthropicConfig._is_adaptive_thinking_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider): return output_config = optional_params.get("output_config") @@ -315,17 +329,24 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if effort is None and not adaptive_thinking: return - if AnthropicConfig._model_supports_effort_param(model) and ( - not adaptive_thinking or AnthropicConfig._validate_effort_for_model(model, effort) is None + if AnthropicConfig._model_supports_effort_param(model, custom_llm_provider) and ( + not adaptive_thinking + or AnthropicConfig._validate_effort_for_model(model, effort, custom_llm_provider) is None ): if adaptive_thinking: optional_params.pop("thinking", None) return - supports_thinking = AnthropicModelInfo._supports_model_capability(model, "supports_reasoning") + supports_thinking = AnthropicModelInfo._supports_model_capability( + model, "supports_reasoning", custom_llm_provider + ) try: legacy_thinking = ( - AnthropicConfig._map_reasoning_effort(reasoning_effort=effort or "medium", model=model) + AnthropicConfig._map_reasoning_effort( + reasoning_effort=effort or "medium", + model=model, + custom_llm_provider=custom_llm_provider, + ) if supports_thinking else None ) @@ -389,17 +410,20 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): self._translate_reasoning_effort_to_anthropic( model=model, optional_params=anthropic_messages_optional_request_params, + custom_llm_provider=self._resolved_provider, ) self._translate_legacy_thinking_for_adaptive_model( model=model, optional_params=anthropic_messages_optional_request_params, + custom_llm_provider=self._resolved_provider, ) self._translate_adaptive_effort_for_non_adaptive_model( model=model, optional_params=anthropic_messages_optional_request_params, max_tokens=max_tokens, + custom_llm_provider=self._resolved_provider, ) system_param = anthropic_messages_optional_request_params.get("system") diff --git a/litellm/llms/azure_ai/anthropic/messages_transformation.py b/litellm/llms/azure_ai/anthropic/messages_transformation.py index 1de18701a2f..8cee35989af 100644 --- a/litellm/llms/azure_ai/anthropic/messages_transformation.py +++ b/litellm/llms/azure_ai/anthropic/messages_transformation.py @@ -21,6 +21,10 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig): and Azure endpoint format. """ + @property + def custom_llm_provider(self) -> Optional[str]: + return "azure_ai" + def should_strip_billing_metadata(self) -> bool: return True diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 5a8ada45651..be904fb27be 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -423,6 +423,7 @@ class AmazonConverseConfig(BaseConfig): mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort, model=model, + custom_llm_provider="bedrock", llm_provider="bedrock_converse", ) if mapped_thinking is None: @@ -430,7 +431,7 @@ class AmazonConverseConfig(BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - if AnthropicConfig._is_adaptive_thinking_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model, "bedrock"): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort) if mapped_effort is None: AnthropicConfig._raise_invalid_reasoning_effort( @@ -465,7 +466,7 @@ class AmazonConverseConfig(BaseConfig): model=model, llm_provider="bedrock_converse", ) - error = AnthropicConfig._validate_effort_for_model(model=model, effort=effort) + error = AnthropicConfig._validate_effort_for_model(model=model, effort=effort, custom_llm_provider="bedrock") if error is not None: raise litellm.exceptions.BadRequestError( message=error, @@ -1279,7 +1280,7 @@ class AmazonConverseConfig(BaseConfig): if anthropic_output_config is not None and isinstance(anthropic_output_config, dict): if base_model.startswith("anthropic"): - if litellm.drop_params is True and not AnthropicConfig._model_supports_effort_param(model): + if litellm.drop_params is True and not AnthropicConfig._model_supports_effort_param(model, "bedrock"): litellm.verbose_logger.warning( DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model, @@ -1422,7 +1423,7 @@ class AmazonConverseConfig(BaseConfig): if ( isinstance(output_config, dict) and output_config.get("effort") is not None - and not AnthropicConfig._is_adaptive_thinking_model(model) + and not AnthropicConfig._is_adaptive_thinking_model(model, "bedrock") ): from litellm.types.llms.anthropic import ( ANTHROPIC_EFFORT_BETA_HEADER, diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 60d532eb8c5..6b5cb304bec 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -115,7 +115,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): keeps working. Non-adaptive models and models without a ceiling are left untouched. """ - if not AnthropicConfig._is_adaptive_thinking_model(model): + if not AnthropicConfig._is_adaptive_thinking_model(model, "bedrock"): return effort = params.get("reasoning_effort") if not isinstance(effort, str): @@ -228,7 +228,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): custom_llm_provider="bedrock", key="supports_output_config", ) - or AnthropicConfig._model_supports_effort_param(model) + or AnthropicConfig._model_supports_effort_param(model, "bedrock") ): if anthropic_request.pop("output_config", None) is not None: verbose_logger.warning( @@ -269,6 +269,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): prompt_caching_set=False, file_id_used=self.is_file_id_used(messages), mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), + custom_llm_provider="bedrock", ) beta_set.update(auto_betas) diff --git a/litellm/llms/bedrock/claude_platform/transformation.py b/litellm/llms/bedrock/claude_platform/transformation.py index 0868d9bddfe..6f5ccececc7 100644 --- a/litellm/llms/bedrock/claude_platform/transformation.py +++ b/litellm/llms/bedrock/claude_platform/transformation.py @@ -54,7 +54,9 @@ class BedrockClaudePlatformConfig(BedrockClaudePlatformMixin, AnthropicConfig): tool_search_used=self.is_tool_search_used(tools=optional_params.get("tools")), programmatic_tool_calling_used=self.is_programmatic_tool_calling_used(tools=optional_params.get("tools")), input_examples_used=self.is_input_examples_used(tools=optional_params.get("tools")), - effort_used=self.is_effort_used(optional_params=optional_params, model=model), + effort_used=self.is_effort_used( + optional_params=optional_params, model=model, custom_llm_provider="anthropic" + ), user_anthropic_beta_headers=self._get_user_anthropic_beta_headers( anthropic_beta_header=headers.get("anthropic-beta") ), diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index 56a47432997..a00d3ba1363 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -77,6 +77,10 @@ class AmazonAnthropicClaudeMessagesConfig( DEFAULT_BEDROCK_ANTHROPIC_API_VERSION = "bedrock-2023-05-31" + @property + def custom_llm_provider(self) -> Optional[str]: + return "bedrock" + BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS = frozenset(BedrockInvokeAnthropicMessagesRequest.__annotations__.keys()) def __init__(self, **kwargs): @@ -269,7 +273,7 @@ class AmazonAnthropicClaudeMessagesConfig( Returns: True if the model supports extended thinking on Bedrock """ - if AnthropicModelInfo._is_adaptive_thinking_model(model): + if AnthropicModelInfo._is_adaptive_thinking_model(model, "bedrock"): return True model_lower = model.lower() @@ -319,7 +323,7 @@ class AmazonAnthropicClaudeMessagesConfig( if not self._supports_extended_thinking_on_bedrock(model): return False - is_adaptive_thinking_model = AnthropicModelInfo._is_adaptive_thinking_model(model) + is_adaptive_thinking_model = AnthropicModelInfo._is_adaptive_thinking_model(model, "bedrock") thinking = anthropic_messages_request.get("thinking") if isinstance(thinking, dict): @@ -596,6 +600,7 @@ class AmazonAnthropicClaudeMessagesConfig( mcp_server_used=anthropic_model_info.is_mcp_server_used( anthropic_messages_optional_request_params.get("mcp_servers") ), + custom_llm_provider="bedrock", ) beta_set.update(auto_betas) @@ -662,7 +667,7 @@ class AmazonAnthropicClaudeMessagesConfig( path degrades ``xhigh`` -> ``max`` rather than 400-ing. Non-adaptive models and models without a ceiling are left untouched. """ - if not AnthropicModelInfo._is_adaptive_thinking_model(model): + if not AnthropicModelInfo._is_adaptive_thinking_model(model, "bedrock"): return effort = optional_params.get("reasoning_effort") if not isinstance(effort, str): @@ -750,7 +755,7 @@ class AmazonAnthropicClaudeMessagesConfig( custom_llm_provider="bedrock", key="supports_output_config", ) - or AnthropicConfig._model_supports_effort_param(model) + or AnthropicConfig._model_supports_effort_param(model, "bedrock") ): if anthropic_messages_request.pop("output_config", None) is not None: verbose_logger.warning( @@ -787,7 +792,7 @@ class AmazonAnthropicClaudeMessagesConfig( if ( litellm.drop_params is True and "output_config" in anthropic_messages_request - and not AnthropicConfig._model_supports_effort_param(model) + and not AnthropicConfig._model_supports_effort_param(model, "bedrock") ): verbose_logger.warning( DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index ba8c312ea51..9c05899c719 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -181,6 +181,10 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): if key != "self" and value is not None: setattr(self.__class__, key, value) + @property + def custom_llm_provider(self) -> Optional[str]: + return "databricks" + @classmethod def get_config(cls): return super().get_config() @@ -372,6 +376,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort_value, model=model, + custom_llm_provider="databricks", llm_provider="databricks", ) if mapped_thinking is None: @@ -379,7 +384,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - if AnthropicConfig._is_adaptive_thinking_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model, "databricks"): mapped_effort: Optional[str] = None if isinstance(reasoning_effort_value, str): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort_value) diff --git a/litellm/llms/github_copilot/messages/transformation.py b/litellm/llms/github_copilot/messages/transformation.py index fb3f0a4e159..4d7b003c48f 100644 --- a/litellm/llms/github_copilot/messages/transformation.py +++ b/litellm/llms/github_copilot/messages/transformation.py @@ -25,6 +25,10 @@ class GithubCopilotAnthropicMessagesConfig(AnthropicMessagesConfig): super().__init__() self.authenticator = Authenticator() + @property + def custom_llm_provider(self) -> Optional[str]: + return "github_copilot" + def handles_web_search_natively(self) -> bool: """ Copilot's /v1/messages endpoint does not execute ``web_search`` tools, so diff --git a/litellm/llms/openai_like/messages/transformation.py b/litellm/llms/openai_like/messages/transformation.py index 4963bcca9ac..0d593d8d0f4 100644 --- a/litellm/llms/openai_like/messages/transformation.py +++ b/litellm/llms/openai_like/messages/transformation.py @@ -85,6 +85,10 @@ class JSONProviderAnthropicMessagesConfig(OpenAILikeAnthropicMessagesConfig): super().__init__() self._provider = provider + @property + def custom_llm_provider(self) -> Optional[str]: + return self._provider.slug + def should_strip_billing_metadata(self) -> bool: return True diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py index 8566496bf9c..de72795cabc 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py @@ -17,6 +17,10 @@ from ..output_params_utils import sanitize_vertex_anthropic_output_params class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, VertexBase): + @property + def custom_llm_provider(self) -> Optional[str]: + return "vertex_ai" + def should_strip_billing_metadata(self) -> bool: return True diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py index 280cc1c888a..b87d05ab1fd 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py @@ -26,7 +26,7 @@ def _model_accepts_output_config_effort(model: str) -> bool: """ from litellm.llms.anthropic.chat.transformation import AnthropicConfig - return AnthropicConfig._model_supports_effort_param(model) + return AnthropicConfig._model_supports_effort_param(model, "vertex_ai") def sanitize_vertex_anthropic_output_params(data: dict, model: str) -> None: diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py index c8d91be359b..8fcefb04b34 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py @@ -112,6 +112,7 @@ class VertexAIAnthropicConfig(AnthropicConfig): prompt_caching_set=self.is_cache_control_set(messages), file_id_used=self.is_file_id_used(messages), mcp_server_used=self.is_mcp_server_used(optional_params.get("mcp_servers")), + custom_llm_provider="vertex_ai", ) beta_set = set(auto_betas) diff --git a/tests/litellm/llms/anthropic/test_anthropic_reasoning_effort.py b/tests/litellm/llms/anthropic/test_anthropic_reasoning_effort.py index 98ae7148c77..ef74249ca8e 100644 --- a/tests/litellm/llms/anthropic/test_anthropic_reasoning_effort.py +++ b/tests/litellm/llms/anthropic/test_anthropic_reasoning_effort.py @@ -12,39 +12,39 @@ class TestMapReasoningEffort: def test_none_returns_none_for_opus_4_6(self): """reasoning_effort=None should return None for Opus 4.6, not adaptive.""" result = AnthropicConfig._map_reasoning_effort( - reasoning_effort=None, model="claude-opus-4-6" + reasoning_effort=None, model="claude-opus-4-6", custom_llm_provider="anthropic" ) assert result is None def test_none_returns_none_for_other_models(self): """reasoning_effort=None should return None for non-Opus models.""" result = AnthropicConfig._map_reasoning_effort( - reasoning_effort=None, model="claude-4-sonnet-20250514" + reasoning_effort=None, model="claude-4-sonnet-20250514", custom_llm_provider="anthropic" ) assert result is None def test_opus_4_6_returns_adaptive_for_low(self): result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="low", model="claude-opus-4-6" + reasoning_effort="low", model="claude-opus-4-6", custom_llm_provider="anthropic" ) assert result["type"] == "adaptive" def test_opus_4_6_returns_adaptive_for_high(self): result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="high", model="claude-opus-4-6" + reasoning_effort="high", model="claude-opus-4-6", custom_llm_provider="anthropic" ) assert result["type"] == "adaptive" def test_other_model_low_returns_enabled_with_budget(self): result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="low", model="claude-4-sonnet-20250514" + reasoning_effort="low", model="claude-4-sonnet-20250514", custom_llm_provider="anthropic" ) assert result["type"] == "enabled" assert "budget_tokens" in result def test_other_model_high_returns_enabled_with_budget(self): result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="high", model="claude-4-sonnet-20250514" + reasoning_effort="high", model="claude-4-sonnet-20250514", custom_llm_provider="anthropic" ) assert result["type"] == "enabled" assert "budget_tokens" in result @@ -52,13 +52,13 @@ class TestMapReasoningEffort: def test_none_string_returns_none_for_opus_4_6(self): """reasoning_effort='none' should return None for Opus 4.6.""" result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="none", model="claude-opus-4-6" + reasoning_effort="none", model="claude-opus-4-6", custom_llm_provider="anthropic" ) assert result is None def test_none_string_returns_none_for_other_models(self): """reasoning_effort='none' should return None for non-Opus models.""" result = AnthropicConfig._map_reasoning_effort( - reasoning_effort="none", model="claude-4-sonnet-20250514" + reasoning_effort="none", model="claude-4-sonnet-20250514", custom_llm_provider="anthropic" ) assert result is None diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py index 0ea09463c16..0414836fa79 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py @@ -520,8 +520,8 @@ def test_shipped_adaptive_rule_gates_on_version_not_pricing(shipped_cost_map): non_adaptive = "us.anthropic.claude-opus-4-20250514" assert adaptive not in litellm.model_cost assert non_adaptive not in litellm.model_cost - assert AnthropicModelInfo._is_adaptive_thinking_model(adaptive) is True - assert AnthropicModelInfo._is_adaptive_thinking_model(non_adaptive) is False + assert AnthropicModelInfo._is_adaptive_thinking_model(adaptive, "anthropic") is True + assert AnthropicModelInfo._is_adaptive_thinking_model(non_adaptive, "anthropic") is False def test_shipped_rules_resolve_unmapped_future_bedrock_claude_with_both_flags(shipped_cost_map): 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 852479e81e4..7fb38544c52 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 @@ -1661,7 +1661,7 @@ def test_effort_beta_header_injection(): # Test with effort parameter optional_params = {"output_config": {"effort": "low"}} - effort_used = model_info.is_effort_used(optional_params=optional_params) + effort_used = model_info.is_effort_used(optional_params=optional_params, custom_llm_provider="anthropic") assert effort_used is True headers = model_info.get_anthropic_headers( @@ -1877,7 +1877,7 @@ def test_anthropic_drop_params_false_forwards_to_unsupported_model(): ], ) def test_anthropic_model_supports_effort_param_recognizes_supporting_models(model): - assert AnthropicConfig._model_supports_effort_param(model) is True + assert AnthropicConfig._model_supports_effort_param(model, "anthropic") is True @pytest.mark.parametrize( @@ -1890,7 +1890,7 @@ def test_anthropic_model_supports_effort_param_recognizes_supporting_models(mode ], ) def test_anthropic_model_supports_effort_param_rejects_non_supporting_models(model): - assert AnthropicConfig._model_supports_effort_param(model) is False + assert AnthropicConfig._model_supports_effort_param(model, "anthropic") is False @pytest.mark.parametrize( @@ -2217,7 +2217,7 @@ def test_get_config_does_not_leak_module_constants(): ) def test_supports_effort_level_handles_provider_prefixes(model, level, expected): """``_supports_effort_level`` resolves bedrock/vertex/azure-prefixed model ids.""" - assert AnthropicConfig._supports_effort_level(model, level) is expected + assert AnthropicConfig._supports_effort_level(model, level, "anthropic") is expected @pytest.mark.parametrize( @@ -2239,7 +2239,7 @@ def test_supports_effort_level_handles_provider_prefixes(model, level, expected) def test_validate_effort_for_model_centralises_per_model_gating( model, effort, expect_error ): - err = AnthropicConfig._validate_effort_for_model(model, effort) + err = AnthropicConfig._validate_effort_for_model(model, effort, "anthropic") if expect_error: assert err is not None assert effort in err @@ -2490,7 +2490,7 @@ def test_is_adaptive_thinking_model_is_sourced_from_cost_map( fallback for ids the cost map cannot resolve. The dated Claude 4.0 names stay non-adaptive because the date suffix is not read as a minor version, while 4.8/4.9/5.x are covered without a code change.""" - assert AnthropicConfig._is_adaptive_thinking_model(model) is expected + assert AnthropicConfig._is_adaptive_thinking_model(model, "anthropic") is expected def test_get_supported_params_includes_reasoning_for_sonnet_4_6_alias( @@ -2836,6 +2836,7 @@ def test_effort_beta_header_not_injected_for_46_models(): result = model_info.is_effort_used( optional_params={"output_config": {"effort": "high"}}, model=model, + custom_llm_provider="anthropic", ) assert result is False, f"is_effort_used should return False for {model}" @@ -2947,6 +2948,7 @@ def test_effort_beta_header_still_injected_for_older_models(): result = model_info.is_effort_used( optional_params={"output_config": {"effort": "low"}}, model="claude-opus-4-5-20251101", + custom_llm_provider="anthropic", ) assert result is 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 4bd270bb7fb..3c410cf84df 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -1578,7 +1578,7 @@ class TestClaudeOpus48AdaptiveThinking: def test_adaptive_thinking_detected_for_opus_4_8(self, local_model_cost_map, model): from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True def test_resolver_reads_flag_through_bedrock_invoke_prefix( self, local_model_cost_map @@ -1592,6 +1592,7 @@ class TestClaudeOpus48AdaptiveThinking: AnthropicModelInfo._supports_model_capability( "bedrock/invoke/us.anthropic.claude-opus-4-8", "supports_adaptive_thinking", + "anthropic", ) is True ) @@ -1609,7 +1610,7 @@ class TestClaudeOpus48AdaptiveThinking: def test_adaptive_thinking_detected_for_fable_5(self, local_model_cost_map, model): from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True @pytest.mark.parametrize( "model", @@ -1644,7 +1645,7 @@ class TestClaudeOpus48AdaptiveThinking: version (``4.6`` -> ``4-6``).""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True @pytest.mark.parametrize( "model", @@ -1665,7 +1666,7 @@ class TestClaudeOpus48AdaptiveThinking: from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert model not in litellm.model_cost - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is False + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is False @pytest.mark.parametrize( "model", @@ -1693,7 +1694,7 @@ class TestClaudeOpus48AdaptiveThinking: from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert model not in litellm.model_cost - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True @pytest.mark.parametrize( "model", @@ -1716,7 +1717,7 @@ class TestClaudeOpus48AdaptiveThinking: from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert model not in litellm.model_cost - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is False + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is False @pytest.mark.parametrize( "model", @@ -1725,7 +1726,7 @@ class TestClaudeOpus48AdaptiveThinking: def test_non_adaptive_models_not_detected(self, local_model_cost_map, model): from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is False + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is False class TestDefaultSuffixAdaptiveThinking: @@ -1750,7 +1751,7 @@ class TestDefaultSuffixAdaptiveThinking: ) -> None: from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True, ( + 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." ) @@ -1771,3 +1772,51 @@ class TestDefaultSuffixAdaptiveThinking: assert expected_bare in candidates, ( f"Expected '{expected_bare}' in candidates for '{model}', got: {candidates}" ) + + +class TestCapabilityProbeUsesCallerProvider: + """``_supports_model_capability`` must probe under the caller's real provider + namespace instead of a pinned ``"anthropic"``. With the pin, the exact Bedrock + cost-map entry for ``global.anthropic.claude-opus-4-8`` was rejected by the + provider match and the anthropic-scoped fallback rule answered instead, so + flipping ``supports_adaptive_thinking`` on the exact entry changed nothing and + the documented "exact entry beats rule" precedence was silently violated.""" + + 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 + ): + import litellm + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert ( + AnthropicModelInfo._is_adaptive_thinking_model(self.BEDROCK_MODEL, "bedrock") + is True + ) + + 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 + ) + + 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 + ) + litellm.get_model_info.cache_clear() + + assert ( + AnthropicModelInfo._is_adaptive_thinking_model("claude-opus-4-8", "anthropic") + is True + ) diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py index 5983597196a..5e9af6bd34d 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py @@ -331,3 +331,59 @@ class TestProviderConfigManagerAzureAnthropicMessages: ) assert config is None + + +@pytest.fixture +def local_model_cost_map(monkeypatch): + """Force the bundled backup cost map so capability flags match this branch.""" + import litellm + + original = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original + litellm.get_model_info.cache_clear() + + +def test_messages_thinking_shape_follows_exact_azure_entry_flag(local_model_cost_map, monkeypatch): + """The Azure messages config must probe capabilities under ``azure_ai`` so an + operator setting ``supports_adaptive_thinking: false`` on the exact + ``azure_ai/claude-opus-4-8`` entry beats the unmodified ``anthropic`` entry. + With the inherited ``"anthropic"`` provider default the flip was ignored and + the transform kept emitting ``thinking.type='adaptive'``.""" + import litellm + + config = AzureAnthropicMessagesConfig() + + def transform(): + return config.transform_anthropic_messages_request( + model="claude-opus-4-8", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params={ + "max_tokens": 4096, + "reasoning_effort": "medium", + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + result = transform() + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": "medium"} + + monkeypatch.setitem( + litellm.model_cost["azure_ai/claude-opus-4-8"], "supports_adaptive_thinking", False + ) + litellm.get_model_info.cache_clear() + assert litellm.model_cost["claude-opus-4-8"]["supports_adaptive_thinking"] is True + + flipped = transform() + thinking = flipped.get("thinking") + assert isinstance(thinking, dict) + assert thinking.get("type") == "enabled" + assert isinstance(thinking.get("budget_tokens"), int) + assert "output_config" not in flipped diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 52f136a1fd9..3b8b4af78d9 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -2472,3 +2472,46 @@ def test_filter_and_transform_beta_headers_passes_context_management_for_bedrock ) assert out_converse == [] + + +def test_bedrock_messages_thinking_shape_follows_exact_bedrock_entry_flag( + local_model_cost_map, monkeypatch +): + """The outbound thinking payload must follow the exact Bedrock cost-map entry. + Before threading the caller's provider through the capability probes, the probe + was pinned to ``"anthropic"``: the exact ``global.anthropic.claude-opus-4-8`` + entry was rejected by the provider match and the anthropic-scoped fallback rule + forced ``thinking.type='adaptive'`` even with ``supports_adaptive_thinking`` + explicitly set to ``false`` on the entry.""" + import litellm + + from litellm.types.router import GenericLiteLLMParams + + model = "global.anthropic.claude-opus-4-8" + cfg = AmazonAnthropicClaudeMessagesConfig() + + def transform(): + return cfg.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": [{"type": "text", "text": "Hello"}]}], + anthropic_messages_optional_request_params={ + "max_tokens": 4096, + "reasoning_effort": "medium", + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + result = transform() + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": "medium"} + + monkeypatch.setitem(litellm.model_cost[model], "supports_adaptive_thinking", False) + litellm.get_model_info.cache_clear() + + flipped = transform() + thinking = flipped.get("thinking") + assert isinstance(thinking, dict) + assert thinking.get("type") == "enabled" + assert isinstance(thinking.get("budget_tokens"), int) + assert "output_config" not in flipped diff --git a/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py b/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py index cfdb76a97f4..00f3e7a6faf 100644 --- a/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py +++ b/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py @@ -416,3 +416,10 @@ def test_transform_request_keeps_parallel_tool_calls_for_claude(): )["messages"] assert len([m for m in result if m.get("role") == "assistant"]) == 1 + + +def test_databricks_config_probes_capabilities_under_databricks_namespace(): + """Inherited AnthropicConfig capability probes read ``self.custom_llm_provider``; + without this override they probed the ``anthropic`` cost-map namespace and + ignored the exact ``databricks/databricks-claude-*`` entries.""" + assert DatabricksConfig().custom_llm_provider == "databricks" diff --git a/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py b/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py index 01787c07d27..8ed84b3ed8d 100644 --- a/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py +++ b/tests/test_litellm/llms/github_copilot/messages/test_github_copilot_messages_transformation.py @@ -326,3 +326,11 @@ def test_github_copilot_config_does_not_handle_web_search_natively(): assert GithubCopilotAnthropicMessagesConfig().handles_web_search_natively() is False assert AnthropicMessagesConfig().handles_web_search_natively() is True + + +def test_github_copilot_messages_config_probes_capabilities_under_copilot_namespace(): + """Capability probes in the shared pass-through helpers read + ``self.custom_llm_provider``; without this override they probed the + ``anthropic`` namespace and ignored the exact ``github_copilot/claude-*`` + cost-map entries.""" + assert GithubCopilotAnthropicMessagesConfig().custom_llm_provider == "github_copilot" diff --git a/tests/test_litellm/llms/openai_like/messages/test_openai_like_anthropic_messages_transformation.py b/tests/test_litellm/llms/openai_like/messages/test_openai_like_anthropic_messages_transformation.py index 534d7aefda4..33e677b000e 100644 --- a/tests/test_litellm/llms/openai_like/messages/test_openai_like_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/openai_like/messages/test_openai_like_anthropic_messages_transformation.py @@ -299,3 +299,21 @@ def test_anthropic_beta_survives_provider_filter_on_passthrough_path(config): stripped = update_headers_with_filtered_beta(headers=dict(headers), provider="openai") assert "anthropic-beta" not in stripped + + +def test_json_provider_messages_config_probes_capabilities_under_provider_slug(): + """Capability probes in the shared pass-through helpers read + ``self.custom_llm_provider``. The JSON-provider config knows its slug, so it + must expose it; the generic OpenAI-like config has no class-level namespace + and keeps the inherited ``anthropic`` default.""" + from litellm.llms.openai_like.json_loader import SimpleProviderConfig + from litellm.llms.openai_like.messages.transformation import ( + JSONProviderAnthropicMessagesConfig, + ) + + provider = SimpleProviderConfig( + slug="exampleprovider", + data={"base_url": "https://api.example.com/v1", "api_key_env": "EXAMPLE_API_KEY"}, + ) + assert JSONProviderAnthropicMessagesConfig(provider).custom_llm_provider == "exampleprovider" + assert OpenAILikeAnthropicMessagesConfig().custom_llm_provider == "anthropic" diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py index bfd37f73b2d..ce770221ceb 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py @@ -509,3 +509,59 @@ def test_vertex_claude_completion_does_not_mutate_shared_extra_headers(): assert ( shared_extra_headers == {} ), "extra_headers must not be mutated by completion()" + + +@pytest.fixture +def local_model_cost_map(monkeypatch): + """Force the bundled backup cost map so capability flags match this branch.""" + import litellm + + original = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original + litellm.get_model_info.cache_clear() + + +def test_messages_thinking_shape_follows_exact_vertex_entry_flag(local_model_cost_map, monkeypatch): + """The Vertex messages config must probe capabilities under ``vertex_ai`` so an + operator setting ``supports_adaptive_thinking: false`` on the exact + ``vertex_ai/claude-opus-4-8`` entry beats the unmodified ``anthropic`` entry. + With the inherited ``"anthropic"`` provider default the flip was ignored and + the transform kept emitting ``thinking.type='adaptive'``.""" + import litellm + + config = VertexAIPartnerModelsAnthropicMessagesConfig() + + def transform(): + return config.transform_anthropic_messages_request( + model="claude-opus-4-8", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params={ + "max_tokens": 4096, + "reasoning_effort": "medium", + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + result = transform() + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": "medium"} + + monkeypatch.setitem( + litellm.model_cost["vertex_ai/claude-opus-4-8"], "supports_adaptive_thinking", False + ) + litellm.get_model_info.cache_clear() + assert litellm.model_cost["claude-opus-4-8"]["supports_adaptive_thinking"] is True + + flipped = transform() + thinking = flipped.get("thinking") + assert isinstance(thinking, dict) + assert thinking.get("type") == "enabled" + assert isinstance(thinking.get("budget_tokens"), int) + assert "output_config" not in flipped diff --git a/tests/test_litellm/test_claude_fable_5_config.py b/tests/test_litellm/test_claude_fable_5_config.py index d8d95fba0da..3a9ebf65bbb 100644 --- a/tests/test_litellm/test_claude_fable_5_config.py +++ b/tests/test_litellm/test_claude_fable_5_config.py @@ -204,7 +204,7 @@ def test_adaptive_thinking_detected_for_fable_5(local_model_cost_map, model): maps to ``thinking.type='adaptive'`` + ``output_config.effort``.""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo - assert AnthropicModelInfo._is_adaptive_thinking_model(model) is True + assert AnthropicModelInfo._is_adaptive_thinking_model(model, "anthropic") is True @pytest.mark.parametrize(