From a6c673e7b91fbe9e176f67c41d8975689805d032 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 03:56:11 +0000 Subject: [PATCH 01/25] fix(anthropic,bedrock,vertex): forward output_config.effort + 400 on garbage reasoning_effort Follow-up bugs surfaced by the QA sweep on PR #27039 (https://github.com/BerriAI/litellm/pull/27039#issuecomment-4363363610). 1. Stop stripping output_config.effort on Bedrock + Vertex adaptive routes. - Vertex AI Claude 4.6/4.7 accepts output_config.effort on rawPredict (verified end-to-end against us-east5 / global). The strip helper now no-ops for effort. - Bedrock Converse routes output_config into additionalModelRequestFields for anthropic base models so the requested adaptive tier (low/medium/ high/xhigh/max) actually reaches the wire instead of all collapsing to identical thinking. - Bedrock Invoke chat transformation (AmazonAnthropicClaudeConfig) stops popping output_config from the post-AnthropicConfig request body. - Bedrock Invoke /v1/messages allowlist (BedrockInvokeAnthropicMessagesRequest) now lists output_config so the runtime allowlist filter forwards it. 2. Validate effort across Bedrock Converse so 'disabled' / 'invalid' / '' / unsupported tiers (xhigh/max on Sonnet 4.6 or budget-mode 4.5 models) surface as a clean 400 BadRequestError instead of 500. 3. ValueError -> BadRequestError throughout (AnthropicConfig.map_openai_params, _apply_output_config, AmazonConverseConfig._handle_reasoning_effort_parameter). Empty-string effort is now rejected (was silently passing the 'if effort and ...' short-circuit). 4. Floor reasoning_effort='minimal' at the Anthropic provider minimum (1024 budget_tokens) via new ANTHROPIC_MIN_THINKING_BUDGET_TOKENS so it's a usable tier on direct Anthropic / Azure AI Anthropic / Vertex AI Anthropic / Bedrock Invoke (all of which 400 below 1024). 5. model_prices: dedupe duplicate supports_max_reasoning_effort key on claude-opus-4-7 / claude-opus-4-7-20260416. Adds regression tests across all five affected paths; existing tests asserting the silent-strip behavior were updated to reflect the new pass-through and clean 400 surfaces. Co-authored-by: Mateo Wang --- litellm/constants.py | 10 ++ litellm/llms/anthropic/chat/transformation.py | 69 ++++++-- .../bedrock/chat/converse_transformation.py | 147 ++++++++++++++++-- .../anthropic_claude3_transformation.py | 6 +- .../transformation.py | 9 +- .../anthropic/output_params_utils.py | 11 +- .../anthropic/transformation.py | 9 +- litellm/types/llms/bedrock.py | 9 ++ model_prices_and_context_window.json | 2 - .../test_anthropic_chat_transformation.py | 108 ++++++++++++- ...ations_anthropic_claude3_transformation.py | 23 +-- .../chat/test_converse_transformation.py | 105 +++++++++++++ .../test_anthropic_claude3_transformation.py | 54 ++++--- ...partner_models_anthropic_transformation.py | 68 ++++---- 14 files changed, 519 insertions(+), 111 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 334ef8d48a..177fb024d6 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -399,6 +399,16 @@ BEDROCK_MAX_POLICY_SIZE = int(os.getenv("BEDROCK_MAX_POLICY_SIZE", 75)) BEDROCK_MIN_THINKING_BUDGET_TOKENS = int( os.getenv("BEDROCK_MIN_THINKING_BUDGET_TOKENS", 1024) ) +# Anthropic's Messages API rejects ``thinking.budget_tokens < 1024`` with a +# 400. ``reasoning_effort='minimal'`` historically mapped to 128 (the global +# default) which always 400'd against direct Anthropic, Azure AI Anthropic, +# Vertex AI Anthropic, and Bedrock Invoke. Use the provider minimum so +# ``minimal`` is a usable tier on all Anthropic-backed routes; Bedrock +# Converse already clamped to 1024 server-side, so this just unifies the +# behavior. +ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = int( + os.getenv("ANTHROPIC_MIN_THINKING_BUDGET_TOKENS", 1024) +) REPLICATE_POLLING_DELAY_SECONDS = float( os.getenv("REPLICATE_POLLING_DELAY_SECONDS", 0.5) ) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 35624c93b3..7b07a96ca5 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -7,6 +7,7 @@ import httpx import litellm from litellm.constants import ( + ANTHROPIC_MIN_THINKING_BUDGET_TOKENS, ANTHROPIC_WEB_SEARCH_TOOL_MAX_USES, DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS, DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, @@ -819,9 +820,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, ) elif reasoning_effort == "minimal": + # Anthropic Messages API rejects ``budget_tokens < 1024`` with a + # 400. Floor at the provider minimum so ``minimal`` is a usable + # tier on Anthropic / Azure AI Anthropic / Vertex AI Anthropic / + # Bedrock Invoke. Bedrock Converse already clamps server-side. return AnthropicThinkingParam( type="enabled", - budget_tokens=DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET, + budget_tokens=max( + DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET, + ANTHROPIC_MIN_THINKING_BUDGET_TOKENS, + ), ) else: raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}") @@ -1088,9 +1096,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): elif param == "thinking": optional_params["thinking"] = value elif param == "reasoning_effort" and isinstance(value, str): - mapped_thinking = AnthropicConfig._map_reasoning_effort( - reasoning_effort=value, model=model - ) + # Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on + # unmapped efforts (``disabled`` / ``invalid`` / ``""`` / + # ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean + # 400 ``BadRequestError`` instead of letting it surface as + # 500. + try: + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=value, model=model + ) + except ValueError as e: + raise litellm.exceptions.BadRequestError( + message=str(e), + model=model, + llm_provider=self.custom_llm_provider or "anthropic", + ) if mapped_thinking is None: optional_params.pop("thinking", None) optional_params.pop("output_config", None) @@ -1526,18 +1546,31 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _apply_output_config( self, data: dict, model: str, optional_params: dict ) -> None: - """Validate and apply output_config to the request data.""" + """Validate and apply output_config to the request data. + + Validation errors raise ``BadRequestError`` (clean 400) so callers + passing ``effort="disabled"`` / ``effort=""`` / unsupported tiers + for the model see a client-side error rather than a 500. + """ if "output_config" not in optional_params: return output_config = optional_params.get("output_config") if not output_config or not isinstance(output_config, dict): return effort = output_config.get("effort") + # ``effort=""`` (empty string) and unmapped strings should be treated + # as invalid, not silently passed through. We use ``effort is not None`` + # here so empty string fails the membership check below. (The legacy + # ``if effort and ...`` short-circuit silently accepted ``""``.) valid_efforts = ["high", "medium", "low", "xhigh", "max"] - if effort and effort not in valid_efforts: - raise ValueError( - f"Invalid effort value: {effort}. Must be one of: " - f"'high', 'medium', 'low', 'xhigh', 'max'" + if effort is not None and effort not in valid_efforts: + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid effort value: {effort!r}. Must be one of: " + f"'high', 'medium', 'low', 'xhigh', 'max'" + ), + model=model, + llm_provider=self.custom_llm_provider or "anthropic", ) # ``max`` is for Opus 4.6+ output effort (not Sonnet 4.6, not Opus 4.5). # Accept known Opus 4.6/4.7 id patterns and/or ``supports_max_reasoning_effort`` @@ -1547,14 +1580,24 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): or self._is_opus_4_7_model(model) or self._supports_effort_level(model, "max") ): - raise ValueError( - f"effort='max' is not supported by this model. Got model: {model}" + raise litellm.exceptions.BadRequestError( + message=( + f"effort='max' is not supported by this model. " + f"Got model: {model}" + ), + model=model, + llm_provider=self.custom_llm_provider or "anthropic", ) # ``xhigh`` is data-driven via ``supports_xhigh_reasoning_effort`` so # enabling it for a new model is a pure model-map change. if effort == "xhigh" and not self._supports_effort_level(model, "xhigh"): - raise ValueError( - f"effort='xhigh' is not supported by this model. Got model: {model}" + raise litellm.exceptions.BadRequestError( + message=( + f"effort='xhigh' is not supported by this model. " + f"Got model: {model}" + ), + model=model, + llm_provider=self.custom_llm_provider or "anthropic", ) data["output_config"] = output_config diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 8b8500ac06..9e46a5f2ae 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -415,6 +415,15 @@ class AmazonConverseConfig(BaseConfig): - Nova 2 models: Transform to reasoningConfig structure - Other models (Anthropic, etc.): Convert to thinking parameter + For Claude 4.6 / 4.7 (adaptive thinking) the tier is carried via + ``output_config.effort`` rather than ``thinking.budget_tokens``. We + validate the effort with the same rules ``AnthropicConfig._apply_output_config`` + uses (low/medium/high/xhigh/max + per-model gating) and stage the + validated dict on ``optional_params["output_config"]`` so it rides + along to ``additionalModelRequestFields`` on the Anthropic-on-Bedrock + wire path. Without this the silent strip in ``_prepare_request_params`` + collapsed every adaptive tier to identical behavior. + Args: model: The model identifier reasoning_effort: The reasoning effort value @@ -448,14 +457,115 @@ class AmazonConverseConfig(BaseConfig): ) optional_params.update(reasoning_config) else: - # Anthropic and other models: convert to thinking parameter - mapped_thinking = AnthropicConfig._map_reasoning_effort( - reasoning_effort=reasoning_effort, model=model - ) + # Anthropic and other models: convert to thinking parameter. + # Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on + # unmapped efforts (``disabled`` / ``invalid`` / ``""`` / + # ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean 400 + # ``BadRequestError`` instead of letting it surface as 500. + try: + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=reasoning_effort, model=model + ) + except ValueError as e: + raise litellm.exceptions.BadRequestError( + message=str(e), + model=model, + llm_provider="bedrock_converse", + ) if mapped_thinking is None: optional_params.pop("thinking", None) + optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking + # Adaptive-thinking models (Claude 4.6 / 4.7) take the tier + # via output_config.effort. Mirror the mapping used by + # AnthropicConfig.map_openai_params and apply the same + # validation rules so unmapped/garbage efforts surface as a + # 400 instead of being silently flattened on the wire. + if AnthropicConfig._is_claude_4_6_model( + model + ) or AnthropicConfig._is_claude_4_7_model(model): + effort_map = { + "low": "low", + "minimal": "low", + "medium": "medium", + "high": "high", + "xhigh": "xhigh", + "max": "max", + } + mapped_effort = effort_map.get(reasoning_effort, reasoning_effort) + self._validate_anthropic_adaptive_effort( + model=model, effort=mapped_effort + ) + optional_params["output_config"] = {"effort": mapped_effort} + + @staticmethod + def _supports_effort_level_on_bedrock(model: str, level: str) -> bool: + """Look up ``supports_{level}_reasoning_effort`` for a Bedrock-routed + model id directly in ``litellm.model_cost`` so the bedrock provider + prefix is irrelevant to the lookup. ``AnthropicConfig._supports_effort_level`` + hard-codes ``custom_llm_provider="anthropic"`` and returns False for + the same effort level on a Bedrock model id. + """ + try: + base_model = BedrockModelInfo.get_base_model(model) + for key in (model, base_model, f"bedrock/{base_model}"): + if key and key in litellm.model_cost: + if ( + litellm.model_cost[key].get( + f"supports_{level}_reasoning_effort" + ) + is True + ): + return True + except Exception: + pass + return False + + @staticmethod + def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None: + """Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7 + on Bedrock. Raises ``BadRequestError`` (clean 400) instead of letting + a downstream ``ValueError`` surface as 500. + """ + valid_efforts = {"high", "medium", "low", "xhigh", "max"} + if effort not in valid_efforts: + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid reasoning_effort/output_config.effort value: " + f"{effort!r}. Must be one of: 'low', 'medium', 'high', " + f"'xhigh', 'max', or 'none'." + ), + model=model, + llm_provider="bedrock_converse", + ) + if effort == "max" and not ( + AnthropicConfig._is_opus_4_6_model(model) + or AnthropicConfig._is_opus_4_7_model(model) + or AmazonConverseConfig._supports_effort_level_on_bedrock(model, "max") + ): + raise litellm.exceptions.BadRequestError( + message=( + f"effort='max' is not supported by this model. " + f"Got model: {model}" + ), + model=model, + llm_provider="bedrock_converse", + ) + if ( + effort == "xhigh" + and not AmazonConverseConfig._supports_effort_level_on_bedrock( + model, "xhigh" + ) + ): + raise litellm.exceptions.BadRequestError( + message=( + f"effort='xhigh' is not supported by this model. " + f"Got model: {model}" + ), + model=model, + llm_provider="bedrock_converse", + ) @staticmethod def _clamp_thinking_budget_tokens(optional_params: dict) -> None: @@ -1196,9 +1306,15 @@ class AmazonConverseConfig(BaseConfig): + supported_config_params ) inference_params.pop("json_mode", None) # used for handling json_schema - # Anthropic-only key. Bedrock expects `outputConfig` (camelCase) and - # will reject `output_config` if it leaks through pass-through routes. - inference_params.pop("output_config", None) + + # Anthropic-only ``output_config`` (snake_case) is the adaptive- + # thinking effort payload (e.g. ``{"effort": "max"}``) for Claude + # 4.6/4.7. On Bedrock Converse it must ride along inside + # ``additionalModelRequestFields`` so the model actually sees the + # tier; stripping it (the prior behavior) silently flattened every + # adaptive tier to identical thinking. Only the Bedrock-native + # ``outputConfig`` (camelCase) goes at the top level. + anthropic_output_config = inference_params.pop("output_config", None) # Extract requestMetadata before processing other parameters request_metadata = inference_params.pop("requestMetadata", None) @@ -1208,9 +1324,6 @@ class AmazonConverseConfig(BaseConfig): output_config: Optional[OutputConfigBlock] = inference_params.pop( "outputConfig", None ) - inference_params.pop( - "output_config", None - ) # Bedrock Converse doesn't support it # keep supported params in 'inference_params', and set all model-specific params in 'additional_request_params' additional_request_params = { @@ -1253,6 +1366,20 @@ class AmazonConverseConfig(BaseConfig): additional_request_params ) + # Re-attach the Anthropic ``output_config`` (e.g. adaptive thinking + # ``{"effort": "max"}``) onto additional_request_params for Anthropic + # Bedrock models so the wire request carries the requested tier. Other + # model families (Nova, GPT-OSS, ...) don't accept it; drop it for them. + if anthropic_output_config is not None and isinstance( + anthropic_output_config, dict + ): + base_model = BedrockModelInfo.get_base_model(model) + if base_model.startswith("anthropic"): + effort = anthropic_output_config.get("effort") + if effort is not None: + self._validate_anthropic_adaptive_effort(model=model, effort=effort) + additional_request_params["output_config"] = anthropic_output_config + return ( inference_params, additional_request_params, 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 cff415d49e..7de05c977c 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -169,7 +169,11 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): anthropic_request.pop("model", None) anthropic_request.pop("stream", None) anthropic_request.pop("output_format", None) - anthropic_request.pop("output_config", None) + # ``output_config`` (e.g. ``{"effort": "max"}``) is the adaptive-thinking + # tier payload for Claude 4.6 / 4.7. Bedrock Invoke accepts it for + # those models — stripping it (the prior behavior) silently flattened + # every adaptive tier on this route. Forward it; if the model rejects + # it the surfaced error is correct, vs. swallowing the user's knob. if "anthropic_version" not in anthropic_request: anthropic_request["anthropic_version"] = self.anthropic_version 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 d450f7a463..714877457f 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 @@ -159,10 +159,11 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert "model", None ) # do not pass model in request body to vertex ai - # Vertex AI Claude accepts ``output_config.format`` (structured outputs) - # and ``output_format``, but rejects ``output_config.effort`` with 400 - # "Extra inputs are not permitted". Sanitize in place so the supported - # bits flow through. + # Vertex AI Claude accepts ``output_config.format`` (structured outputs), + # ``output_format``, and ``output_config.effort`` (adaptive-thinking + # tier on Claude 4.6 / 4.7, verified end-to-end). The shared sanitize + # helper now no-ops for ``effort`` and remains the single hook for any + # future Vertex-only key drift. sanitize_vertex_anthropic_output_params(anthropic_messages_request) return anthropic_messages_request 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 982d8edbf2..891c8e4d05 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 @@ -11,11 +11,12 @@ keeps the parent module's import surface narrow. """ # Keys inside ``output_config`` that Vertex AI Claude does not accept. -# Today only ``effort`` triggers "Extra inputs are not permitted"; add new -# entries here as Vertex parity drifts. Keep this list narrow — anything -# Vertex DOES accept (e.g. ``format`` for structured outputs) must be -# preserved so callers can rely on Anthropic-native features. -VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS: frozenset = frozenset({"effort"}) +# Vertex now accepts ``output_config.effort`` for the adaptive-thinking +# Claude 4.6 / 4.7 models on direct ``:rawPredict`` (verified end-to-end +# against ``us-east5`` for ``opus-4-6`` and ``global`` for ``opus-4-7``). +# Keep this set narrow and only add a key here once a 400 "Extra inputs are +# not permitted" is reproducible against the live Vertex endpoint. +VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS: frozenset = frozenset() def sanitize_vertex_anthropic_output_params(data: dict) -> 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 914c7e92e5..48920402e2 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 @@ -106,11 +106,10 @@ class VertexAIAnthropicConfig(AnthropicConfig): data.pop("model", None) # vertex anthropic doesn't accept 'model' parameter - # Vertex AI Claude accepts ``output_config.format`` (structured outputs / - # JSON Schema) but NOT ``output_config.effort`` — sending ``effort`` to - # Vertex returns 400 "Extra inputs are not permitted". Sanitize in place: - # forward the structured-output bits, drop the unsupported keys. - # Same treatment for the legacy top-level ``output_format`` field. + # Sanitize ``output_config`` / ``output_format`` for Vertex parity. + # Vertex now accepts ``output_config.effort`` for adaptive-thinking Claude + # 4.6 / 4.7 models, so the helper is a no-op for ``effort``; it remains + # the single hook for future Vertex-only sanitization. sanitize_vertex_anthropic_output_params(data) tools = optional_params.get("tools") diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 9ffb52ef88..72e509d178 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -1041,3 +1041,12 @@ class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False): # `metadata` is part of the common Anthropic Messages API shape. thinking: dict metadata: dict + + # ``output_config`` is the adaptive-thinking effort payload for + # Claude 4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke + # accepts it for these models when ``thinking={"type": "adaptive"}``. + # Without this field in the allowlist, the runtime filter in + # ``AmazonAnthropicClaudeMessagesConfig.transform_anthropic_messages_request`` + # silently drops it and every adaptive tier collapses to identical + # behavior on /v1/messages. + output_config: dict diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 76dcfc55c1..cc34446be0 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -9491,7 +9491,6 @@ "us": 1.1, "fast": 6.0 }, - "supports_max_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, "claude-opus-4-7-20260416": { @@ -9526,7 +9525,6 @@ "us": 1.1, "fast": 6.0 }, - "supports_max_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, "claude-sonnet-4-20250514": { 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 cbb9b21d62..0381b850d6 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 @@ -8,6 +8,7 @@ sys.path.insert( ) # Adds the parent directory to the system path from unittest.mock import MagicMock, patch +import litellm from litellm.constants import RESPONSE_FORMAT_TOOL_NAME from litellm.llms.anthropic.chat.transformation import AnthropicConfig from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( @@ -1631,8 +1632,10 @@ def test_effort_validation(): ) assert result["output_config"]["effort"] == effort - # Invalid value should raise error - with pytest.raises(ValueError, match="Invalid effort value"): + # Invalid value should raise BadRequestError (clean 400, not a 500). + with pytest.raises( + litellm.exceptions.BadRequestError, match="Invalid effort value" + ): optional_params = {"output_config": {"effort": "invalid"}} config.transform_request( model="claude-opus-4-5-20251101", @@ -1682,12 +1685,18 @@ def test_effort_validation_with_opus_46(): def test_max_effort_rejected_for_opus_45(): - """Test that effort='max' is rejected when using Claude Opus 4.5.""" + """Test that effort='max' is rejected when using Claude Opus 4.5. + + Surfaces as a clean 400 BadRequestError, not a 500 ValueError. + """ config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] - with pytest.raises(ValueError, match="effort='max' is not supported by this model"): + with pytest.raises( + litellm.exceptions.BadRequestError, + match="effort='max' is not supported by this model", + ): optional_params = {"output_config": {"effort": "max"}} config.transform_request( model="claude-opus-4-5-20251101", @@ -2153,12 +2162,14 @@ def test_reasoning_effort_maps_to_budget_thinking_for_non_opus_4_6(): """ config = AnthropicConfig() - # Test with Claude Sonnet 4.5 (non-Opus 4.6 model) + # Test with Claude Sonnet 4.5 (non-Opus 4.6 model). + # ``minimal`` floors at the Anthropic provider minimum (1024) because + # Anthropic / Azure / Vertex / Bedrock Invoke 400 below that. test_cases = [ ("low", 1024), # DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET ("medium", 2048), # DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET ("high", 4096), # DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET - ("minimal", 128), # DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET + ("minimal", 1024), # ANTHROPIC_MIN_THINKING_BUDGET_TOKENS (provider floor) ] for effort, expected_budget in test_cases: @@ -2245,11 +2256,17 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): def test_max_effort_rejected_for_sonnet_46(): - """Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level).""" + """Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level). + + Surfaces as a clean 400 BadRequestError, not a 500 ValueError. + """ config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] - with pytest.raises(ValueError, match="effort='max' is not supported by this model"): + with pytest.raises( + litellm.exceptions.BadRequestError, + match="effort='max' is not supported by this model", + ): config.transform_request( model="claude-sonnet-4-6-20260219", messages=messages, @@ -2335,6 +2352,81 @@ def test_reasoning_effort_none_omits_thinking_and_output_config(model): assert "output_config" not in result +@pytest.mark.parametrize( + "effort", + ["disabled", "invalid", ""], +) +def test_reasoning_effort_garbage_raises_bad_request(effort): + """Unmapped / garbage / empty-string reasoning_effort surfaces as a clean + 400 ``BadRequestError`` instead of letting ``ValueError`` propagate as 500. + """ + config = AnthropicConfig() + + with pytest.raises(litellm.exceptions.BadRequestError): + config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + +@pytest.mark.parametrize( + "effort", + ["xhigh", "max"], +) +def test_reasoning_effort_unsupported_tier_on_budget_model_raises_bad_request( + effort, +): + """``xhigh`` / ``max`` aren't defined for budget-mode (4.5) Claude models; + surface as a clean 400 instead of 500. + """ + config = AnthropicConfig() + + with pytest.raises(litellm.exceptions.BadRequestError): + config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + +def test_output_config_effort_empty_string_raises_bad_request(): + """``output_config={"effort": ""}`` must be rejected with a 400 — the + legacy ``if effort and ...`` short-circuit silently let it pass + through (verified end-to-end on the QA sweep for PR #27039). + """ + config = AnthropicConfig() + + with pytest.raises(litellm.exceptions.BadRequestError, match="Invalid effort"): + config.transform_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "hi"}], + optional_params={"output_config": {"effort": ""}, "max_tokens": 32}, + litellm_params={}, + headers={}, + ) + + +def test_reasoning_effort_minimal_floors_at_anthropic_provider_minimum(): + """Anthropic Messages API rejects ``budget_tokens < 1024``. ``minimal`` + must floor at the provider minimum so it's a usable tier on direct + Anthropic / Azure AI Anthropic / Vertex AI Anthropic / Bedrock Invoke. + """ + config = AnthropicConfig() + + result = config.map_openai_params( + non_default_params={"reasoning_effort": "minimal"}, + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + assert result["thinking"]["type"] == "enabled" + assert result["thinking"]["budget_tokens"] >= 1024 + + def test_effort_beta_header_still_injected_for_older_models(): """ Test that is_effort_used still returns True for pre-4.6 models diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index 80d6d26e7e..8689a05e8e 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -406,36 +406,37 @@ def test_opus_4_5_model_detection(): # f"computer-use beta should be kept, got: {anthropic_beta}" -def test_output_config_removed_from_bedrock_chat_invoke_request(): +def test_output_config_forwarded_for_bedrock_chat_invoke_request(): """ - Test that output_config parameter is stripped from Bedrock Chat Invoke requests. + Bedrock Invoke (chat/completions route) must forward + ``output_config`` for Anthropic adaptive-thinking models. The earlier + behavior stripped it unconditionally, which silently flattened every + adaptive tier (``low``/``medium``/``high``/``xhigh``/``max``) to identical + behavior on the wire. - Bedrock Invoke API doesn't support the output_config parameter (Anthropic-only). - Ensures the chat/invoke path mirrors the messages/invoke path fix. - - Fixes: https://github.com/BerriAI/litellm/issues/22797 + The wire QA at https://github.com/BerriAI/litellm/pull/27039 showed + ``thinking.type: adaptive`` was forwarded but ``output_config.effort`` + was always missing, even though direct curls to Anthropic's Bedrock + Invoke endpoint accept it. """ config = AmazonAnthropicClaudeConfig() messages = [{"role": "user", "content": "test"}] - # Inject output_config into optional_params (simulates Anthropic SDK forwarding it) optional_params = { "max_tokens": 100, "output_config": {"effort": "high"}, } result = config.transform_request( - model="anthropic.claude-sonnet-4-20250514-v1:0", + model="anthropic.claude-opus-4-7", messages=messages, optional_params=optional_params, litellm_params={}, headers={}, ) - assert ( - "output_config" not in result - ), f"output_config should be stripped for Bedrock Chat Invoke, got keys: {list(result.keys())}" + assert result.get("output_config") == {"effort": "high"} # Verify normal params survive assert result["max_tokens"] == 100 diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 5bb51ac619..01dbc81fbb 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -310,6 +310,111 @@ def test_reasoning_effort_none_omits_thinking_for_anthropic_converse(model): assert "thinking" not in optional_params +@pytest.mark.parametrize( + "model,effort,expected_effort", + [ + ("bedrock/converse/us.anthropic.claude-opus-4-7", "low", "low"), + ("bedrock/converse/us.anthropic.claude-opus-4-7", "medium", "medium"), + ("bedrock/converse/us.anthropic.claude-opus-4-7", "high", "high"), + ("bedrock/converse/us.anthropic.claude-opus-4-7", "xhigh", "xhigh"), + ("bedrock/converse/us.anthropic.claude-opus-4-7", "max", "max"), + ("bedrock/converse/us.anthropic.claude-opus-4-6-v1", "max", "max"), + ("bedrock/converse/us.anthropic.claude-sonnet-4-6", "high", "high"), + ("bedrock/converse/us.anthropic.claude-sonnet-4-6", "minimal", "low"), + ], +) +def test_reasoning_effort_sets_output_config_for_adaptive_models_converse( + model, effort, expected_effort +): + """Adaptive-thinking Claude 4.6 / 4.7 on Bedrock Converse must carry the + requested tier via ``output_config.effort``. The prior strip silently + flattened every adaptive tier on the wire (verified in the PR #27039 + QA sweep).""" + config = AmazonConverseConfig() + + optional_params = config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model=model, + drop_params=False, + ) + + assert optional_params["thinking"]["type"] == "adaptive" + assert optional_params["output_config"] == {"effort": expected_effort} + + +@pytest.mark.parametrize( + "model", + [ + "bedrock/converse/us.anthropic.claude-opus-4-7", + "bedrock/converse/us.anthropic.claude-opus-4-6-v1", + "bedrock/converse/us.anthropic.claude-sonnet-4-6", + ], +) +def test_output_config_effort_forwarded_into_additional_request_fields(model): + """``output_config`` must ride along inside ``additionalModelRequestFields`` + so the Anthropic-on-Bedrock wire request actually carries the effort + tier. The prior ``inference_params.pop("output_config")`` dropped it + on the floor for every adaptive tier.""" + config = AmazonConverseConfig() + messages = [{"role": "user", "content": "hi"}] + + result = config._transform_request( + model=model, + messages=messages, + optional_params={ + "maxTokens": 256, + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "high"}, + }, + litellm_params={}, + headers={}, + ) + + additional = result.get("additionalModelRequestFields", {}) + assert additional.get("output_config") == {"effort": "high"} + + +@pytest.mark.parametrize( + "effort", + ["disabled", "invalid", ""], +) +def test_reasoning_effort_garbage_raises_bad_request_converse(effort): + """Garbage / empty-string reasoning_effort on Bedrock Converse Anthropic + must surface as a clean 400 ``BadRequestError`` instead of 500. The + earlier ``ValueError`` from ``_map_reasoning_effort`` propagated up as + a generic 500 in the proxy and ate the request.""" + config = AmazonConverseConfig() + + with pytest.raises(litellm.exceptions.BadRequestError): + config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="bedrock/converse/us.anthropic.claude-opus-4-7", + drop_params=False, + ) + + +def test_output_config_effort_unsupported_max_on_sonnet_46_raises_bad_request(): + """``effort='max'`` is Opus-only. On Sonnet 4.6 the explicit-output_config + path must surface a 400 (matching the chat-completion validation), not + silently forward an unsupported tier to Bedrock.""" + config = AmazonConverseConfig() + messages = [{"role": "user", "content": "hi"}] + + with pytest.raises(litellm.exceptions.BadRequestError): + config._transform_request( + model="bedrock/converse/us.anthropic.claude-sonnet-4-6", + messages=messages, + optional_params={ + "maxTokens": 256, + "output_config": {"effort": "max"}, + }, + litellm_params={}, + headers={}, + ) + + def test_get_supported_openai_params(): config = AmazonConverseConfig() supported_params = config.get_supported_openai_params( 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 93d56d4cd0..725bc9503b 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 @@ -592,12 +592,16 @@ def test_remove_scope_from_cache_control(): assert request["messages"][0]["content"][0]["cache_control"]["type"] == "ephemeral" -def test_bedrock_messages_strips_output_config(): +def test_bedrock_messages_forwards_output_config(): """ - Ensure output_config is stripped from the request before sending to - Bedrock Invoke, which doesn't support this Anthropic-specific parameter. + ``output_config`` is the adaptive-thinking effort payload for Claude + 4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke accepts it for + those models — the prior behavior of stripping it silently flattened + every adaptive tier on /v1/messages so ``low`` / ``medium`` / ``high`` / + ``xhigh`` / ``max`` all collapsed to identical thinking with no tier + differentiation. - Regression test for: https://github.com/BerriAI/litellm/issues/22797 + Regression coverage for the QA bug listed on PR #27039. """ from litellm.types.router import GenericLiteLLMParams @@ -611,25 +615,24 @@ def test_bedrock_messages_strips_output_config(): } result = cfg.transform_anthropic_messages_request( - model="anthropic.claude-3-haiku-20240307-v1:0", + model="anthropic.claude-opus-4-7", messages=messages, anthropic_messages_optional_request_params=optional_params, litellm_params=GenericLiteLLMParams(), headers={}, ) - assert ( - "output_config" not in result - ), "output_config should be stripped — Bedrock Invoke rejects it" + assert result.get("output_config") == {"effort": "high"} # Other params should be preserved assert result.get("max_tokens") == 4096 -def test_bedrock_messages_strips_output_config_with_output_format(): +def test_bedrock_messages_forwards_output_config_with_output_format(): """ - When both output_config and output_format are present, both should be - stripped (output_format is converted to inline schema, output_config - is simply dropped). + When both output_config and output_format are present, output_format is + converted to inline schema (Bedrock Invoke doesn't accept output_format + natively), and output_config is forwarded for Claude 4.6/4.7 adaptive + thinking. """ from litellm.types.router import GenericLiteLLMParams @@ -648,14 +651,14 @@ def test_bedrock_messages_strips_output_config_with_output_format(): } result = cfg.transform_anthropic_messages_request( - model="anthropic.claude-3-haiku-20240307-v1:0", + model="anthropic.claude-opus-4-7", messages=messages, anthropic_messages_optional_request_params=optional_params, litellm_params=GenericLiteLLMParams(), headers={}, ) - assert "output_config" not in result + assert result.get("output_config") == {"effort": "low"} assert "output_format" not in result @@ -728,13 +731,18 @@ def test_bedrock_messages_allowlist_filters_anthropic_only_fields(): "mcp_servers", "container", "inference_geo", - "output_config", "context_management", "model", "stream", ): assert bad not in result, f"{bad} should be stripped by the allowlist" + # ``output_config`` rides along — Bedrock Invoke accepts it for Claude + # 4.6/4.7 adaptive thinking and stripping it silently flattens every + # adaptive tier. (Bedrock will reject it for non-adaptive models, which + # is the correct behavior — surface the model error rather than swallow + # the knob.) + assert result.get("output_config") == {"effort": "low"} # Supported fields pass through. assert result["max_tokens"] == 4096 assert result["temperature"] == 0.5 @@ -882,7 +890,10 @@ async def test_promote_message_start_cache_when_message_stop_omits_cache_fields( "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"input_tokens": 10, "output_tokens": 181}, } - yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}} + yield { + "type": "message_stop", + "usage": {"input_tokens": 10, "output_tokens": 181}, + } merged: list[dict] = [] async for chunk in cfg._promote_message_stop_usage(_stream()): @@ -936,7 +947,11 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( }, }, } - yield {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} + yield { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + } yield { "type": "content_block_delta", "index": 0, @@ -948,7 +963,10 @@ async def test_unified_bedrock_messages_cache_on_start_only_never_negative_cost( "delta": {"stop_reason": "end_turn", "stop_sequence": None}, "usage": {"output_tokens": 181, "input_tokens": 10}, } - yield {"type": "message_stop", "usage": {"input_tokens": 10, "output_tokens": 181}} + yield { + "type": "message_stop", + "usage": {"input_tokens": 10, "output_tokens": 181}, + } logging_obj = LiteLLMLoggingObj( model="bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py index d0be476d72..0a5ba12f18 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py @@ -498,11 +498,14 @@ def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_hea ), "Header should be removed if no supported values remain" -def test_vertex_ai_anthropic_output_config_effort_only_dropped(): +def test_vertex_ai_anthropic_output_config_effort_only_forwarded(): """ - ``output_config`` containing only ``effort`` (an Anthropic-only key Vertex - rejects with "Extra inputs are not permitted") is dropped entirely so the - request body has no empty dict. + Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct + ``:rawPredict`` (verified end-to-end against ``us-east5`` for + ``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). The earlier + strip silently flattened every adaptive tier on Vertex, so ``low`` / + ``medium`` / ``high`` / ``xhigh`` / ``max`` all produced identical + thinking with no tier differentiation. """ config = VertexAIAnthropicConfig() @@ -515,16 +518,14 @@ def test_vertex_ai_anthropic_output_config_effort_only_dropped(): } result = config.transform_request( - model="claude-3-5-sonnet-20241022", + model="claude-opus-4-6", messages=messages, optional_params=optional_params, litellm_params={}, headers=headers, ) - assert ( - "output_config" not in result - ), "output_config containing only effort must be dropped" + assert result["output_config"] == {"effort": "high"} assert result["max_tokens"] == 1024 assert "messages" in result @@ -566,13 +567,15 @@ def test_vertex_ai_anthropic_output_config_format_passes_through(): assert result["output_config"] == output_config -def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort(): +def test_vertex_ai_anthropic_output_config_format_plus_effort_preserved(): """ - Greptile P1 on PR #23396: when ``output_config`` contains BOTH ``format`` - and ``effort``, the prior conditional-passthrough logic forwarded the - full dict including the unsupported ``effort`` key, reproducing the - 400 error the fix was meant to resolve. Only ``effort`` (and any future - Vertex-unsupported keys) should be filtered; ``format`` must survive. + Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct + ``:rawPredict`` (verified end-to-end against ``us-east5`` for + ``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). Since the + strip was unjustified, ``effort`` must now ride along with ``format``. + + We use a Claude 4.6 model id here because ``_apply_output_config`` only + accepts ``effort`` on adaptive-thinking 4.6/4.7 model ids. """ config = VertexAIAnthropicConfig() messages = [{"role": "user", "content": "Return a person object."}] @@ -591,7 +594,7 @@ def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort optional_params = {"max_tokens": 1024, "output_config": output_config} result = config.transform_request( - model="claude-3-5-sonnet-20241022", + model="claude-opus-4-6", messages=messages, optional_params=optional_params, litellm_params={}, @@ -599,9 +602,7 @@ def test_vertex_ai_anthropic_output_config_format_plus_effort_strips_only_effort ) assert "output_config" in result - assert ( - "effort" not in result["output_config"] - ), "effort must be stripped — Vertex returns 400 on unknown keys" + assert result["output_config"]["effort"] == "high" assert result["output_config"]["format"] == output_config["format"] @@ -623,14 +624,14 @@ def test_vertex_ai_anthropic_output_config_non_dict_dropped(): assert "output_config" not in result -def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_dropped(): +def test_vertex_ai_anthropic_output_format_and_output_config_effort_preserved(): """ - When the request carries both ``output_format`` (top-level structured - outputs) AND an ``output_config`` whose only useful key for Vertex is - ``effort``: ``output_format`` must be forwarded (Vertex accepts it), - while ``output_config`` is dropped because Vertex returns 400 on - ``effort``. This replaces the old "drop both" behavior, which was the - silent strip the bug report flagged. + Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct + ``:rawPredict`` (verified end-to-end). When both ``output_format`` and + ``output_config: {effort}`` are present, both must be forwarded — the + earlier ``effort`` strip caused silent loss of the requested adaptive + thinking tier on Vertex routes (``low``/``medium``/``high``/``xhigh``/``max`` + all collapsed to identical adaptive thinking with no tier differentiation). """ config = VertexAIAnthropicConfig() messages = [{"role": "user", "content": "Extract structured data"}] @@ -653,7 +654,7 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe } test_data = { - "model": "claude-3-5-sonnet-20241022", + "model": "claude-opus-4-6", "messages": messages, "max_tokens": 2048, "output_format": output_format, @@ -671,7 +672,7 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe try: result = config.transform_request( - model="claude-3-5-sonnet-20241022", + model="claude-opus-4-6", messages=messages, optional_params=optional_params, litellm_params={}, @@ -680,9 +681,8 @@ def test_vertex_ai_anthropic_output_format_preserved_output_config_effort_droppe # output_format flows through unchanged — Vertex AI Claude accepts it. assert result["output_format"] == output_format - # output_config containing only ``effort`` is dropped to avoid the - # 400 "Extra inputs are not permitted" the silent strip used to mask. - assert "output_config" not in result + # output_config.effort now flows through (Vertex accepts it on 4.6/4.7). + assert result["output_config"] == {"effort": "high"} assert result["max_tokens"] == 2048 assert "model" not in result, "model is still stripped (Vertex routes by URL)" finally: @@ -702,10 +702,10 @@ def test_sanitize_vertex_anthropic_output_params_unit(): sanitize_vertex_anthropic_output_params(data) assert data == {"max_tokens": 8} - # Effort-only → dropped entirely. + # Effort-only → preserved (Vertex 4.6/4.7 accept it on rawPredict). data = {"output_config": {"effort": "high"}} sanitize_vertex_anthropic_output_params(data) - assert "output_config" not in data + assert data["output_config"] == {"effort": "high"} # Format-only → preserved unchanged. fmt = {"format": {"type": "json_schema", "schema": {"type": "object"}}} @@ -713,10 +713,10 @@ def test_sanitize_vertex_anthropic_output_params_unit(): sanitize_vertex_anthropic_output_params(data) assert data["output_config"] == fmt - # Mixed → effort filtered, format kept. + # Mixed → both effort and format kept (no current Vertex-unsupported keys). data = {"output_config": {"format": fmt["format"], "effort": "high"}} sanitize_vertex_anthropic_output_params(data) - assert data["output_config"] == fmt + assert data["output_config"] == {"format": fmt["format"], "effort": "high"} # Non-dict → dropped defensively. data = {"output_config": "garbage"} From 13714586b63a026df9dbdac6b793fef1366b0461 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 04:02:29 +0000 Subject: [PATCH 02/25] fix(constants): make ANTHROPIC_MIN_THINKING_BUDGET_TOKENS a plain constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documentation CI test (tests/documentation_tests/test_env_keys.py) asserts every os.getenv() key in the source has a matching entry in the litellm-docs config_settings.md table. ANTHROPIC_MIN_THINKING_BUDGET_TOKENS tracks Anthropic's published wire-protocol minimum (1024) — it's not a user-tunable, so making it env-overridable was wrong anyway. Drop the os.getenv() wrapper; the value is now a plain literal. Co-authored-by: Mateo Wang --- litellm/constants.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 177fb024d6..7bf94971a3 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -402,13 +402,12 @@ BEDROCK_MIN_THINKING_BUDGET_TOKENS = int( # Anthropic's Messages API rejects ``thinking.budget_tokens < 1024`` with a # 400. ``reasoning_effort='minimal'`` historically mapped to 128 (the global # default) which always 400'd against direct Anthropic, Azure AI Anthropic, -# Vertex AI Anthropic, and Bedrock Invoke. Use the provider minimum so -# ``minimal`` is a usable tier on all Anthropic-backed routes; Bedrock -# Converse already clamped to 1024 server-side, so this just unifies the -# behavior. -ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = int( - os.getenv("ANTHROPIC_MIN_THINKING_BUDGET_TOKENS", 1024) -) +# Vertex AI Anthropic, and Bedrock Invoke. Floor at the provider minimum so +# ``minimal`` is a usable tier on every Anthropic-backed route; Bedrock +# Converse already clamps server-side, this just unifies the behavior. +# Constant — not env-overridable — because it tracks Anthropic's published +# wire-protocol minimum, not a tunable. +ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = 1024 REPLICATE_POLLING_DELAY_SECONDS = float( os.getenv("REPLICATE_POLLING_DELAY_SECONDS", 0.5) ) From ec022fd1bb0c827e47bd97e333f7dc675b53fb4d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 07:46:38 +0000 Subject: [PATCH 03/25] fix(anthropic,bedrock): correct effort error message and dedupe effort_map - Remove 'none' from the Bedrock _validate_anthropic_adaptive_effort error message; it was listed as a valid value but rejected by the membership check, leaving users in a feedback loop if they tried 'none'. - Hoist the duplicated reasoning_effort -> output_config.effort mapping out of AnthropicConfig.map_openai_params and AmazonConverseConfig._handle_reasoning_effort_parameter into a single AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT class constant so the two routes cannot drift. --- litellm/llms/anthropic/chat/transformation.py | 24 ++++++++++++------- .../bedrock/chat/converse_transformation.py | 16 +++++-------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 7b07a96ca5..a1cff6b2e2 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -108,6 +108,18 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): metadata: Optional[dict] = None system: Optional[str] = None + # Shared mapping from OpenAI ``reasoning_effort`` values to Anthropic + # ``output_config.effort`` tier values. Used by both the direct Anthropic + # path and the Bedrock Converse path so the two routes cannot drift. + REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Dict[str, str] = { + "low": "low", + "minimal": "low", + "medium": "medium", + "high": "high", + "xhigh": "xhigh", + "max": "max", + } + def __init__( self, max_tokens: Optional[int] = None, @@ -1121,15 +1133,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if AnthropicConfig._is_claude_4_6_model( model ) or AnthropicConfig._is_claude_4_7_model(model): - effort_map = { - "low": "low", - "minimal": "low", - "medium": "medium", - "high": "high", - "xhigh": "xhigh", - "max": "max", - } - mapped_effort = effort_map.get(value, value) + mapped_effort = AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + value, value + ) optional_params["output_config"] = {"effort": mapped_effort} elif param == "web_search_options" and isinstance(value, dict): hosted_web_search_tool = self.map_web_search_tool( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 9e46a5f2ae..7f42743cfc 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -485,15 +485,11 @@ class AmazonConverseConfig(BaseConfig): if AnthropicConfig._is_claude_4_6_model( model ) or AnthropicConfig._is_claude_4_7_model(model): - effort_map = { - "low": "low", - "minimal": "low", - "medium": "medium", - "high": "high", - "xhigh": "xhigh", - "max": "max", - } - mapped_effort = effort_map.get(reasoning_effort, reasoning_effort) + mapped_effort = ( + AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort, reasoning_effort + ) + ) self._validate_anthropic_adaptive_effort( model=model, effort=mapped_effort ) @@ -534,7 +530,7 @@ class AmazonConverseConfig(BaseConfig): message=( f"Invalid reasoning_effort/output_config.effort value: " f"{effort!r}. Must be one of: 'low', 'medium', 'high', " - f"'xhigh', 'max', or 'none'." + f"'xhigh', or 'max'." ), model=model, llm_provider="bedrock_converse", From e400de4654404749a64af7cbda985a47e8855fb7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 08:28:28 +0000 Subject: [PATCH 04/25] fix(anthropic): translate reasoning_effort on /v1/messages route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the remaining QA-sweep gap on PR #27074: Bedrock Invoke /v1/messages was silently ignoring ``reasoning_effort`` because the shared param filter only kept native Anthropic keys, so every effort tier collapsed to the same behavior on the wire (27/231 cells failing across opus-4-5 / opus-4-6 / sonnet-4-6). Map ``reasoning_effort`` to native Anthropic ``thinking`` / ``output_config.effort`` at the ``AnthropicMessagesConfig`` layer so all four /v1/messages routes (direct Anthropic, Azure AI, Vertex AI, Bedrock Invoke) inherit the same translation: - Add ``reasoning_effort`` to ``AnthropicMessagesRequestOptionalParams`` so the param filter in ``AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param`` no longer drops it before the transformation runs. - Add ``_translate_reasoning_effort_to_anthropic`` and call it from ``transform_anthropic_messages_request``. Mirrors ``AnthropicConfig.map_openai_params`` on the chat completion path (re-uses ``_map_reasoning_effort`` and ``REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT``) so the two routes cannot drift. Pops ``reasoning_effort`` so it never reaches the wire. - Caller-supplied native ``thinking`` / ``output_config.effort`` always win — same precedence as ``_translate_legacy_thinking_for_adaptive_model``. - Garbage values (``""``, ``"disabled"``, ``"invalid"``) raise ``AnthropicError(status_code=400)`` instead of falling through and surfacing as 500s from the provider. - ``"none"`` clears thinking + output_config so callers can opt out per request. Also restores the non-adaptive-model test coverage on Bedrock Invoke /v1/messages that the previous commit lost when ``test_bedrock_messages_strips_output_config`` was renamed to the ``forwards`` variant on Opus 4.7. Adds a new test file ``test_reasoning_effort_translation.py`` covering the translation at the shared config level (adaptive + non-adaptive models, none, garbage, caller precedence) so all four /v1/messages routes are exercised by a single suite. Adds parametrized + behavioral tests on the Bedrock Invoke /v1/messages suite covering: minimal/low/medium/high/xhigh/max mapping for adaptive models, thinking-budget mapping for non-adaptive Opus 4.5, ``none`` clears both, garbage raises 400, explicit ``output_config`` wins. Refs: https://github.com/BerriAI/litellm/pull/27074 --- litellm/llms/anthropic/chat/transformation.py | 5 +- .../messages/transformation.py | 77 +++++++ litellm/types/llms/anthropic.py | 7 + .../test_reasoning_effort_translation.py | 184 ++++++++++++++++ .../test_anthropic_claude3_transformation.py | 205 ++++++++++++++++++ 5 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index a1cff6b2e2..098a074892 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1580,7 +1580,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) # ``max`` is for Opus 4.6+ output effort (not Sonnet 4.6, not Opus 4.5). # Accept known Opus 4.6/4.7 id patterns and/or ``supports_max_reasoning_effort`` - # in the model map (same pattern as ``xhigh`` below). + # in the model map (same pattern as ``xhigh`` below). The hardcoded + # patterns cover OpenRouter/GitHub Copilot/Vercel variants that don't + # carry the model-map flag yet — keep both checks until those provider + # entries are fully populated. if effort == "max" and not ( self._is_opus_4_6_model(model) or self._is_opus_4_7_model(model) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 7617ad52ab..b737bf9aaf 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -47,6 +47,10 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): "inference_geo", "speed", "output_config", + # OpenAI-style tier knob — translated to native ``thinking`` + + # ``output_config`` in ``transform_anthropic_messages_request`` + # and popped before the request is forwarded. + "reasoning_effort", # TODO: Add Anthropic `metadata` support # "metadata", ] @@ -166,6 +170,74 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return headers, api_base + @staticmethod + def _translate_reasoning_effort_to_anthropic( + model: str, optional_params: Dict + ) -> None: + """Map OpenAI-style ``reasoning_effort`` to native Anthropic params. + + The /v1/messages spec doesn't include ``reasoning_effort`` — without + this translation it gets silently dropped, leaving every adaptive + tier collapsed to the same behavior on Bedrock Invoke /v1/messages + (and on Anthropic / Azure AI / Vertex AI when callers pass it on + the messages route). Mirrors ``AnthropicConfig.map_openai_params`` + on the chat completion path so the two routes can't drift. + + - Pops ``reasoning_effort`` from ``optional_params`` so it never + reaches the wire. + - Caller-supplied ``thinking`` / ``output_config`` always win — we + don't override an explicit native value. + - Effort=``none`` clears thinking + output_config so callers can + opt out per request. + - Invalid efforts raise ``BadRequestError`` (clean 400) instead of + surfacing as 500s downstream. + """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + reasoning_effort = optional_params.pop("reasoning_effort", None) + if not isinstance(reasoning_effort, str): + return + + try: + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=reasoning_effort, model=model + ) + except ValueError as e: + raise AnthropicError(message=str(e), status_code=400) + + if mapped_thinking is None: + optional_params.pop("thinking", None) + optional_params.pop("output_config", None) + return + + optional_params.setdefault("thinking", mapped_thinking) + if AnthropicModelInfo._is_adaptive_thinking_model(model): + mapped_effort = ( + AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort + ) + ) + # ``_map_reasoning_effort`` returns ``type=adaptive`` for any + # string on adaptive models without checking the value. The + # chat completion path validates the resolved effort downstream + # via ``_apply_output_config``; /v1/messages has no equivalent + # downstream check, so reject unmapped values here so callers + # see a clean 400 instead of a 500 from the provider. + if mapped_effort is None: + raise AnthropicError( + message=( + f"Invalid reasoning_effort: {reasoning_effort!r}. " + f"Must be one of: 'minimal', 'low', 'medium', 'high', " + f"'xhigh', 'max', 'none'" + ), + status_code=400, + ) + existing_output_config = optional_params.get("output_config") + if not isinstance(existing_output_config, dict): + existing_output_config = {} + existing_output_config.setdefault("effort", mapped_effort) + optional_params["output_config"] = existing_output_config + @staticmethod def _translate_legacy_thinking_for_adaptive_model( model: str, optional_params: Dict @@ -217,6 +289,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): status_code=400, ) + self._translate_reasoning_effort_to_anthropic( + model=model, + optional_params=anthropic_messages_optional_request_params, + ) + self._translate_legacy_thinking_for_adaptive_model( model=model, optional_params=anthropic_messages_optional_request_params, diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index c376b8694a..6c57d3dc07 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -393,6 +393,13 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False): AnthropicOutputConfig ] # Configuration for Claude's output behavior cache_control: Optional[Dict[str, Any]] # Automatic prompt caching + # OpenAI-style ``reasoning_effort`` is accepted on /v1/messages so callers + # can drive adaptive/extended thinking with a single tier-name knob (the + # same vocabulary as the chat completion path). The transformation layer + # maps it to native Anthropic ``thinking`` + ``output_config`` and pops + # this key before the request is forwarded — no provider receives + # ``reasoning_effort`` on the wire. + reasoning_effort: Optional[str] class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False): diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py new file mode 100644 index 0000000000..a2fcf1595b --- /dev/null +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -0,0 +1,184 @@ +""" +Tests for OpenAI-style ``reasoning_effort`` translation on the Anthropic +/v1/messages route. + +The /v1/messages spec doesn't include ``reasoning_effort`` — without +translation it gets silently dropped at the filter step, leaving every +adaptive tier collapsed to the same behavior on Bedrock Invoke /v1/messages +(and on Anthropic / Azure AI / Vertex AI when callers pass it on the +messages route). + +These tests pin the translation and validation behavior at the shared +``AnthropicMessagesConfig`` level so all four /v1/messages routes +(direct Anthropic, Azure AI, Vertex AI, Bedrock Invoke) inherit the +same mapping. +""" + +import pytest + +from litellm.llms.anthropic.common_utils import AnthropicError +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) + + +@pytest.mark.parametrize( + "reasoning_effort,expected_effort", + [ + ("minimal", "low"), + ("low", "low"), + ("medium", "medium"), + ("high", "high"), + ("xhigh", "xhigh"), + ("max", "max"), + ], +) +def test_reasoning_effort_maps_to_output_config_for_adaptive_model( + reasoning_effort, expected_effort +): + """ + For Claude 4.6 / 4.7, ``reasoning_effort`` is mapped to + ``thinking={"type": "adaptive"}`` plus ``output_config.effort=``, + using the same mapping table as the chat completion path so the two + routes can't drift. + """ + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "reasoning_effort": reasoning_effort} + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "reasoning_effort" not in result + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": expected_effort} + + +def test_reasoning_effort_none_clears_thinking_and_output_config(): + """``reasoning_effort='none'`` opts out of extended thinking entirely.""" + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "reasoning_effort": "none", + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "high"}, + } + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "reasoning_effort" not in result + assert "thinking" not in result + assert "output_config" not in result + + +def test_reasoning_effort_on_non_adaptive_model_uses_thinking_budget(): + """ + Non-adaptive models (Opus 4.5 / earlier) take ``thinking.budget_tokens`` + rather than ``output_config.effort``. The translation falls back to + the budget mapping in that case. + """ + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "reasoning_effort": "high"} + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-5", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "reasoning_effort" not in result + assert "output_config" not in result + thinking = result.get("thinking") + assert isinstance(thinking, dict) + assert thinking.get("type") == "enabled" + assert isinstance(thinking.get("budget_tokens"), int) + assert thinking["budget_tokens"] >= 1024 + + +@pytest.mark.parametrize("bad_effort", ["invalid", "disabled", ""]) +def test_invalid_reasoning_effort_raises_400(bad_effort): + """ + Garbage ``reasoning_effort`` values surface as a clean 400 instead of + silently passing through to the provider as an unknown + ``output_config.effort`` (which would 500). + """ + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "reasoning_effort": bad_effort} + + with pytest.raises(AnthropicError) as exc_info: + config.transform_anthropic_messages_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert exc_info.value.status_code == 400 + + +def test_explicit_output_config_wins_over_reasoning_effort(): + """ + Explicit native ``output_config.effort`` is never overridden by the + OpenAI alias. Same precedence as + ``_translate_legacy_thinking_for_adaptive_model``. + """ + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "reasoning_effort": "low", + "output_config": {"effort": "max"}, + } + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-7", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "reasoning_effort" not in result + assert result.get("output_config") == {"effort": "max"} + + +def test_explicit_thinking_wins_over_reasoning_effort(): + """Explicit native ``thinking`` is never overridden by the alias.""" + config = AnthropicMessagesConfig() + optional_params = { + "max_tokens": 1024, + "reasoning_effort": "low", + "thinking": {"type": "enabled", "budget_tokens": 8000}, + } + + result = config.transform_anthropic_messages_request( + model="claude-opus-4-5", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "reasoning_effort" not in result + assert result.get("thinking") == {"type": "enabled", "budget_tokens": 8000} + + +def test_reasoning_effort_in_supported_params(): + """``reasoning_effort`` is advertised as a supported messages param so + callers and validation paths can introspect the schema.""" + config = AnthropicMessagesConfig() + assert "reasoning_effort" in config.get_supported_anthropic_messages_params( + "claude-opus-4-7" + ) 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 725bc9503b..1bc9093b62 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 @@ -662,6 +662,211 @@ def test_bedrock_messages_forwards_output_config_with_output_format(): assert "output_format" not in result +def test_bedrock_messages_forwards_output_config_for_non_adaptive_model(): + """ + ``output_config`` is forwarded for non-adaptive models too (e.g. haiku). + Bedrock will reject the unsupported key for those models — surfacing the + provider error is the correct behavior, since silently swallowing the + knob would hide caller bugs. + + Restores coverage previously asserted by + ``test_bedrock_messages_strips_output_config`` (renamed to the + ``forwards`` variant on Opus 4.7); the strip path no longer exists, + but the non-adaptive pass-through path needs its own explicit test + so a future regression that silently re-adds the strip can't sneak + through. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "output_config": {"effort": "high"}, + } + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-3-haiku-20240307-v1:0", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert result.get("output_config") == {"effort": "high"} + assert result.get("max_tokens") == 4096 + + +@pytest.mark.parametrize( + "reasoning_effort,expected_effort", + [ + ("minimal", "low"), + ("low", "low"), + ("medium", "medium"), + ("high", "high"), + ("xhigh", "xhigh"), + ("max", "max"), + ], +) +def test_bedrock_messages_maps_reasoning_effort_for_adaptive_model( + reasoning_effort, expected_effort +): + """ + OpenAI-style ``reasoning_effort`` is mapped to native Anthropic + ``thinking`` + ``output_config.effort`` on the /v1/messages route so + callers can drive adaptive thinking with the same tier vocabulary as + the chat completion path. ``reasoning_effort`` itself is popped — the + /v1/messages spec doesn't define it and Bedrock rejects unknown + top-level fields. + + Closes the QA-sweep gap on PR #27074 where Bedrock Invoke /v1/messages + silently dropped ``reasoning_effort`` and every effort tier collapsed + to the same behavior. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "reasoning_effort": reasoning_effort, + } + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "reasoning_effort" not in result + assert result.get("thinking") == {"type": "adaptive"} + assert result.get("output_config") == {"effort": expected_effort} + + +def test_bedrock_messages_reasoning_effort_on_non_adaptive_uses_thinking_budget(): + """ + For non-adaptive thinking models (e.g. Opus 4.5), ``reasoning_effort`` + is mapped to ``thinking.type=enabled`` with a budget_tokens value + instead of ``output_config.effort``. ``output_config`` is not set on + these models because they don't accept it. + + Mirrors ``AnthropicConfig._map_reasoning_effort`` behavior for the + non-adaptive branch on Opus 4.5 / earlier Claude 4 models, applied to + the /v1/messages route. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "reasoning_effort": "medium", + } + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-5-20251101-v1:0", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "reasoning_effort" not in result + assert "output_config" not in result + thinking = result.get("thinking") + assert isinstance(thinking, dict) + assert thinking.get("type") == "enabled" + assert isinstance(thinking.get("budget_tokens"), int) + assert thinking["budget_tokens"] >= 1024 + + +def test_bedrock_messages_reasoning_effort_none_clears_thinking(): + """ + ``reasoning_effort='none'`` opts out — both ``thinking`` and + ``output_config`` are cleared so the request goes out without + extended thinking. Mirrors the chat completion path's behavior. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "reasoning_effort": "none", + "output_config": {"effort": "high"}, + "thinking": {"type": "adaptive"}, + } + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "reasoning_effort" not in result + assert "thinking" not in result + assert "output_config" not in result + + +def test_bedrock_messages_invalid_reasoning_effort_raises_400(): + """ + Garbage ``reasoning_effort`` values (``invalid`` / ``disabled`` / ``""``) + surface as a clean 400 ``AnthropicError`` instead of silently passing + an invalid string through to Bedrock as ``output_config.effort``. + """ + from litellm.llms.anthropic.common_utils import AnthropicError + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + + for bad_effort in ("invalid", "disabled", ""): + with pytest.raises(AnthropicError): + cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params={ + "max_tokens": 4096, + "reasoning_effort": bad_effort, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + +def test_bedrock_messages_explicit_output_config_wins_over_reasoning_effort(): + """ + Caller-supplied native ``output_config.effort`` wins over the OpenAI + ``reasoning_effort`` knob. Same precedence as + ``_translate_legacy_thinking_for_adaptive_model``: explicit native + Anthropic params are never overridden by the alias. + """ + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "reasoning_effort": "low", + "output_config": {"effort": "max"}, + } + + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "reasoning_effort" not in result + assert result.get("output_config") == {"effort": "max"} + + def test_bedrock_messages_strips_context_management(): """ Ensure context_management is stripped from the request before sending to From 039b26626f3dc081fbaef9bf281c1922cdf6d83a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 08:42:22 +0000 Subject: [PATCH 05/25] fix(anthropic,bedrock): reject unmapped reasoning_effort at mapping site Both the chat completion path (AnthropicConfig.map_openai_params) and the Bedrock Converse path (_handle_reasoning_effort_parameter) used REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(value, value) which falls back to the raw input on unmapped keys. Combined with _map_reasoning_effort returning type='adaptive' for any string on Claude 4.6/4.7, garbage values (e.g. 'disabled') could leak into optional_params['output_config']['effort'] unvalidated if map_openai_params ran without the downstream transform_request or _validate_anthropic_adaptive_effort check. Mirror the /v1/messages pattern: use .get(value) (no fallback) and raise BadRequestError immediately when the value is unmapped, co-locating validation with the mapping for defense in depth. --- litellm/llms/anthropic/chat/transformation.py | 21 ++++++++++++++++++- .../bedrock/chat/converse_transformation.py | 19 ++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 098a074892..d890b78624 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1133,9 +1133,28 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if AnthropicConfig._is_claude_4_6_model( model ) or AnthropicConfig._is_claude_4_7_model(model): + # ``_map_reasoning_effort`` returns ``type=adaptive`` + # for any string on adaptive models without checking + # the value, so reject unmapped efforts here (matching + # the /v1/messages path) instead of relying on the + # downstream ``_apply_output_config`` check. Co-locating + # validation with the mapping prevents garbage from + # leaking into ``optional_params`` if ``map_openai_params`` + # is ever called without a subsequent ``transform_request``. mapped_effort = AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - value, value + value ) + if mapped_effort is None: + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid reasoning_effort: {value!r}. " + f"Must be one of: 'minimal', 'low', " + f"'medium', 'high', 'xhigh', 'max', 'none'" + ), + model=model, + llm_provider=self.custom_llm_provider + or "anthropic", + ) optional_params["output_config"] = {"effort": mapped_effort} elif param == "web_search_options" and isinstance(value, dict): hosted_web_search_tool = self.map_web_search_tool( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 7f42743cfc..6f9e8de62e 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -485,11 +485,28 @@ class AmazonConverseConfig(BaseConfig): if AnthropicConfig._is_claude_4_6_model( model ) or AnthropicConfig._is_claude_4_7_model(model): + # Use ``.get()`` without a fallback so unmapped efforts + # (e.g. ``"disabled"``) surface as a clean 400 here + # rather than leaking the raw garbage string through to + # ``_validate_anthropic_adaptive_effort`` (which does + # catch it, but only because validation happens to run). + # Matches the /v1/messages pattern where validation is + # co-located with the mapping. mapped_effort = ( AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - reasoning_effort, reasoning_effort + reasoning_effort ) ) + if mapped_effort is None: + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid reasoning_effort: {reasoning_effort!r}. " + f"Must be one of: 'minimal', 'low', 'medium', " + f"'high', 'xhigh', 'max', 'none'" + ), + model=model, + llm_provider="bedrock_converse", + ) self._validate_anthropic_adaptive_effort( model=model, effort=mapped_effort ) From 47031c08d228e0ffe44f93e5a6cc5ebcbf62899c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 08:45:08 +0000 Subject: [PATCH 06/25] style: black formatting Co-authored-by: Mateo Wang --- litellm/llms/anthropic/chat/transformation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index d890b78624..49f0543b7e 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1152,8 +1152,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): f"'medium', 'high', 'xhigh', 'max', 'none'" ), model=model, - llm_provider=self.custom_llm_provider - or "anthropic", + llm_provider=self.custom_llm_provider or "anthropic", ) optional_params["output_config"] = {"effort": mapped_effort} elif param == "web_search_options" and isinstance(value, dict): From 82d7405c6fbb8acf841715b172fe774037770e93 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 3 May 2026 02:29:29 -0700 Subject: [PATCH 07/25] fix(anthropic): stop class-attr leak; gate xhigh/max on every route The reasoning-effort mapping dict was a public class attribute on AnthropicConfig, so BaseConfig.get_config returned it as a request parameter and every Anthropic-backed call (Anthropic / Azure / Vertex / Bedrock Invoke) hit a 400 'REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Extra inputs are not permitted' from the provider. Move the mapping to a module-level constant. _supports_effort_level only looked the model up under custom_llm_provider='anthropic', so bedrock-prefixed model ids (e.g. bedrock/invoke/us.anthropic.claude-opus-4-7) returned False for both 'max' and 'xhigh' even when the underlying model entry has the flag set. Strip known provider prefixes and retry the lookup against litellm.model_cost directly so per-model gating works on every route. Mirror the per-model xhigh/max gate from AnthropicConfig._apply_output_config in AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic so the /v1/messages route also raises a clean 400 instead of forwarding the unsupported tier. --- litellm/llms/anthropic/chat/transformation.py | 71 ++++++++++++++----- .../messages/transformation.py | 38 ++++++++-- .../bedrock/chat/converse_transformation.py | 11 +-- .../test_anthropic_chat_transformation.py | 39 ++++++++++ .../test_reasoning_effort_translation.py | 31 ++++++++ 5 files changed, 163 insertions(+), 27 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 49f0543b7e..d800a86583 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -93,6 +93,16 @@ else: LoggingClass = Any +REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Dict[str, str] = { + "low": "low", + "minimal": "low", + "medium": "medium", + "high": "high", + "xhigh": "xhigh", + "max": "max", +} + + class AnthropicConfig(AnthropicModelInfo, BaseConfig): """ Reference: https://docs.anthropic.com/claude/reference/messages_post @@ -108,18 +118,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): metadata: Optional[dict] = None system: Optional[str] = None - # Shared mapping from OpenAI ``reasoning_effort`` values to Anthropic - # ``output_config.effort`` tier values. Used by both the direct Anthropic - # path and the Bedrock Converse path so the two routes cannot drift. - REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Dict[str, str] = { - "low": "low", - "minimal": "low", - "medium": "medium", - "high": "high", - "xhigh": "xhigh", - "max": "max", - } - def __init__( self, max_tokens: Optional[int] = None, @@ -217,15 +215,54 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): Mirrors the pattern used in ``openai/chat/gpt_5_transformation.py`` so that adding support for a new effort level is a pure model-map change. + Handles bedrock-prefixed and vertex-prefixed model ids by stripping + the prefix and re-checking against ``litellm.model_cost`` directly, + so a Bedrock-routed Claude 4.6/4.7 keeps its model-map flag. """ + key = f"supports_{level}_reasoning_effort" try: - return _supports_factory( + if _supports_factory( model=model, custom_llm_provider="anthropic", - key=f"supports_{level}_reasoning_effort", - ) + key=key, + ): + return True except Exception: - return False + pass + # Bedrock and Vertex route the model id with a provider-prefix + # (e.g. ``bedrock/invoke/us.anthropic.claude-opus-4-7``). Strip + # known prefixes and look the resulting Anthropic-flavoured key + # up directly in ``litellm.model_cost`` so the lookup keeps + # working regardless of which route the request arrived on. + candidates = [model] + for prefix in ( + "bedrock/converse/", + "bedrock/invoke/", + "bedrock/", + "vertex_ai/", + ): + if model.startswith(prefix): + candidates.append(model[len(prefix) :]) + try: + from litellm.llms.bedrock.common_utils import BedrockModelInfo + + base = BedrockModelInfo.get_base_model(model) + if base: + candidates.append(base) + candidates.append(f"bedrock/{base}") + except Exception: + pass + try: + import litellm + + for cand in candidates: + if cand in litellm.model_cost and ( + litellm.model_cost[cand].get(key) is True + ): + return True + except Exception: + pass + return False def get_supported_openai_params(self, model: str): params = [ @@ -1141,7 +1178,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # validation with the mapping prevents garbage from # leaking into ``optional_params`` if ``map_openai_params`` # is ever called without a subsequent ``transform_request``. - mapped_effort = AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( value ) if mapped_effort is None: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index b737bf9aaf..98004f647e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -192,7 +192,10 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): - Invalid efforts raise ``BadRequestError`` (clean 400) instead of surfacing as 500s downstream. """ - from litellm.llms.anthropic.chat.transformation import AnthropicConfig + from litellm.llms.anthropic.chat.transformation import ( + REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, + AnthropicConfig, + ) reasoning_effort = optional_params.pop("reasoning_effort", None) if not isinstance(reasoning_effort, str): @@ -212,10 +215,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params.setdefault("thinking", mapped_thinking) if AnthropicModelInfo._is_adaptive_thinking_model(model): - mapped_effort = ( - AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - reasoning_effort - ) + mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort ) # ``_map_reasoning_effort`` returns ``type=adaptive`` for any # string on adaptive models without checking the value. The @@ -232,6 +233,33 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ), status_code=400, ) + # Per-model gating: ``xhigh`` and ``max`` are only valid on + # specific tiers (Opus 4.6/4.7 for max; data-driven for xhigh). + # The chat completion path enforces this via + # ``_apply_output_config``; mirror it here so /v1/messages + # callers see a clean 400 instead of a provider-side error. + if mapped_effort == "max" and not ( + AnthropicConfig._is_opus_4_6_model(model) + or AnthropicConfig._is_opus_4_7_model(model) + or AnthropicConfig._supports_effort_level(model, "max") + ): + raise AnthropicError( + message=( + f"effort='max' is not supported by this model. " + f"Got model: {model}" + ), + status_code=400, + ) + if mapped_effort == "xhigh" and not AnthropicConfig._supports_effort_level( + model, "xhigh" + ): + raise AnthropicError( + message=( + f"effort='xhigh' is not supported by this model. " + f"Got model: {model}" + ), + status_code=400, + ) existing_output_config = optional_params.get("output_config") if not isinstance(existing_output_config, dict): existing_output_config = {} diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 6f9e8de62e..92fef67a60 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -31,7 +31,10 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( _bedrock_converse_messages_pt, _bedrock_tools_pt, ) -from litellm.llms.anthropic.chat.transformation import AnthropicConfig +from litellm.llms.anthropic.chat.transformation import ( + REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, + AnthropicConfig, +) from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException from litellm.types.llms.bedrock import * from litellm.types.llms.openai import ( @@ -492,10 +495,8 @@ class AmazonConverseConfig(BaseConfig): # catch it, but only because validation happens to run). # Matches the /v1/messages pattern where validation is # co-located with the mapping. - mapped_effort = ( - AnthropicConfig.REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - reasoning_effort - ) + mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort ) if mapped_effort is None: raise litellm.exceptions.BadRequestError( 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 0381b850d6..da4684ec9c 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 @@ -1959,6 +1959,45 @@ def test_get_config_without_model_uses_fallback(): assert config["max_tokens"] == 4096 +def test_get_config_does_not_leak_module_constants(): + """``BaseConfig.get_config`` returns class attributes; the + reasoning-effort mapping must not be one of them or it ends up + serialised onto the wire as an extra request key. + """ + cfg = AnthropicConfig.get_config(model="claude-opus-4-7") + for forbidden in ( + "REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT", + "_REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT", + ): + assert forbidden not in cfg + + +@pytest.mark.parametrize( + "model,level,expected", + [ + ("claude-opus-4-7", "max", True), + ("claude-opus-4-7", "xhigh", True), + ("claude-opus-4-6", "max", True), + ("claude-opus-4-6", "xhigh", False), + ("claude-sonnet-4-6", "max", False), + ("claude-sonnet-4-6", "xhigh", False), + ("bedrock/invoke/us.anthropic.claude-opus-4-7", "max", True), + ("bedrock/invoke/us.anthropic.claude-opus-4-7", "xhigh", True), + ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "max", True), + ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh", False), + ("bedrock/invoke/us.anthropic.claude-sonnet-4-6", "max", False), + ("vertex_ai/claude-opus-4-7", "xhigh", True), + ("azure_ai/claude-opus-4-7", "xhigh", True), + ], +) +def test_supports_effort_level_handles_provider_prefixes(model, level, expected): + """``_supports_effort_level`` must handle bedrock/ vertex_ai/ azure_ai/ + prefixed model ids so per-model gating works on every route, not just + the bare-Anthropic chat completion path. + """ + assert AnthropicConfig._supports_effort_level(model, level) is expected + + def test_transform_request_uses_dynamic_max_tokens(): """ Test that transform_request uses dynamic max_tokens based on model diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index a2fcf1595b..900bb15545 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -129,6 +129,37 @@ def test_invalid_reasoning_effort_raises_400(bad_effort): assert exc_info.value.status_code == 400 +@pytest.mark.parametrize( + "model,bad_effort", + [ + ("claude-opus-4-6", "xhigh"), + ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh"), + ("claude-sonnet-4-6", "xhigh"), + ("claude-sonnet-4-6", "max"), + ("bedrock/invoke/us.anthropic.claude-sonnet-4-6", "max"), + ], +) +def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort): + """``xhigh`` and ``max`` are gated per-model. The /v1/messages route must + surface a clean 400 client-side instead of forwarding the unsupported + tier and letting the provider 500/400 it. + """ + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "reasoning_effort": bad_effort} + + with pytest.raises(AnthropicError) as exc_info: + config.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert exc_info.value.status_code == 400 + assert "not supported by this model" in str(exc_info.value) + + def test_explicit_output_config_wins_over_reasoning_effort(): """ Explicit native ``output_config.effort`` is never overridden by the From 09d37db8d90558d517d55a135c54c7314e8b1711 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 3 May 2026 03:39:02 -0700 Subject: [PATCH 08/25] feat(anthropic,bedrock): strip output_config under drop_params for non-effort models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a proxy fronts Claude Code (which always sends `output_config.effort`) at a pre-4.5 Anthropic model — haiku-3, sonnet-3.5, opus-3, sonnet-4 — the forwarded knob causes a forced 400 the client can't fix. Gating a strip behind the existing `drop_params` flag lets operators opt into silent fixup once and stop worrying about per-model param hygiene. Default (`drop_params=False`) still forwards and surfaces the provider's error, preserving the strict, debuggable contract from #27074. Per https://platform.claude.com/docs/en/build-with-claude/effort the supporting set is Opus 4.5+, Sonnet 4.6+, and Mythos Preview; everything else is dropped (with a verbose_logger warning so the strip is visible). Recognition uses model-name patterns plus a fallback to any `supports_*_reasoning_effort` flag in the model map for forward compatibility with new entries. https://claude.ai/code/session_01WjHq31rvXT6xYNdVmSJvRp (cherry picked from commit 1233943e7861ba8a9062f792310ebd401cb03db8) --- litellm/llms/anthropic/chat/transformation.py | 73 +++++++++++++- .../bedrock/chat/converse_transformation.py | 24 ++++- .../anthropic_claude3_transformation.py | 19 ++++ .../test_anthropic_chat_transformation.py | 98 +++++++++++++++++++ .../chat/test_converse_transformation.py | 53 ++++++++++ .../test_anthropic_claude3_transformation.py | 64 ++++++++++++ 6 files changed, 326 insertions(+), 5 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index d800a86583..da435f8816 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1,7 +1,17 @@ import json import re import time -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + List, + Optional, + Tuple, + Union, + cast, +) import httpx @@ -264,6 +274,52 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): pass return False + # Per https://platform.claude.com/docs/en/build-with-claude/effort the + # ``output_config.effort`` parameter is supported on Opus 4.5+, Sonnet 4.6+ + # and Mythos Preview. Older Claude models (haiku-3, sonnet-3.5, opus-3, + # sonnet-4, ...) reject it with a 400. The patterns below let us recognize + # the supporting families regardless of route prefix (``anthropic.``, + # ``us.anthropic.``, ``vertex_ai/``, ``azure_ai/``, ...). + _EFFORT_SUPPORTING_MODEL_PATTERNS: ClassVar[Tuple[str, ...]] = ( + "opus-4-5", + "opus_4_5", + "opus-4.5", + "opus_4.5", + "opus-4-6", + "opus_4_6", + "opus-4.6", + "opus_4.6", + "opus-4-7", + "opus_4_7", + "opus-4.7", + "opus_4.7", + "sonnet-4-6", + "sonnet_4_6", + "sonnet-4.6", + "sonnet_4.6", + "mythos", + ) + + @staticmethod + def _model_supports_effort_param(model: str) -> bool: + """Whether the model accepts ``output_config.effort`` at all. + + Used to decide whether to strip ``output_config`` for known-incompatible + models when ``drop_params`` is set. New models that land in + ``model_prices_and_context_window.json`` with a ``supports_*_reasoning_effort`` + flag are auto-recognized; otherwise we fall back to the documented + family patterns above. + """ + model_lower = model.lower() + if any( + p in model_lower for p in AnthropicConfig._EFFORT_SUPPORTING_MODEL_PATTERNS + ): + return True + for level in ("low", "minimal", "medium", "high", "xhigh", "max"): + if AnthropicConfig._supports_effort_level(model, level): + return True + return False + def get_supported_openai_params(self, model: str): params = [ "stream", @@ -1618,6 +1674,21 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): output_config = optional_params.get("output_config") if not output_config or not isinstance(output_config, dict): return + # When ``drop_params`` is set, strip ``output_config`` for models that + # cannot accept it (e.g. proxy fronting Claude Code at haiku-3, where + # the client always sends effort but the model rejects it). The user + # opted into silent fixup via the global flag — log a warning so the + # strip is still visible in logs. + if litellm.drop_params is True and not self._model_supports_effort_param(model): + litellm.verbose_logger.warning( + "Dropping unsupported `output_config` for model=%s " + "(drop_params=True). Effort is only supported on Opus 4.5+, " + "Sonnet 4.6+, and Mythos Preview.", + model, + ) + optional_params.pop("output_config", None) + data.pop("output_config", None) + return effort = output_config.get("effort") # ``effort=""`` (empty string) and unmapped strings should be treated # as invalid, not silently passed through. We use ``effort is not None`` diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 92fef67a60..7fd6a87d9d 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1389,10 +1389,26 @@ class AmazonConverseConfig(BaseConfig): ): base_model = BedrockModelInfo.get_base_model(model) if base_model.startswith("anthropic"): - effort = anthropic_output_config.get("effort") - if effort is not None: - self._validate_anthropic_adaptive_effort(model=model, effort=effort) - additional_request_params["output_config"] = anthropic_output_config + # When ``drop_params`` is set, strip for models that don't + # accept effort (e.g. proxy routing Claude Code at haiku-3). + # Otherwise forward and let Bedrock surface the model's error. + if ( + litellm.drop_params is True + and not AnthropicConfig._model_supports_effort_param(model) + ): + litellm.verbose_logger.warning( + "Dropping unsupported `output_config` for model=%s " + "(drop_params=True). Effort is only supported on " + "Opus 4.5+, Sonnet 4.6+, and Mythos Preview.", + model, + ) + else: + effort = anthropic_output_config.get("effort") + if effort is not None: + self._validate_anthropic_adaptive_effort( + model=model, effort=effort + ) + additional_request_params["output_config"] = anthropic_output_config return ( inference_params, 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 1b15ebaa76..a717dfb78b 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -12,9 +12,11 @@ from typing import ( import httpx +import litellm from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers from litellm.constants import BEDROCK_MIN_THINKING_BUDGET_TOKENS from litellm.litellm_core_utils.litellm_logging import verbose_logger +from litellm.llms.anthropic.chat.transformation import AnthropicConfig from litellm.llms.anthropic.common_utils import AnthropicModelInfo from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( AnthropicMessagesConfig, @@ -580,6 +582,23 @@ class AmazonAnthropicClaudeMessagesConfig( if filtered_betas: anthropic_messages_request["anthropic_beta"] = filtered_betas + # 6a. When ``drop_params`` is set, strip ``output_config`` for models + # that don't accept it (e.g. proxy fronting Claude Code at haiku-3). + # Without this, every Claude Code request to a pre-4.5 Anthropic model + # routes a forced 400 from Bedrock that the client can't fix. + if ( + litellm.drop_params is True + and "output_config" in anthropic_messages_request + and not AnthropicConfig._model_supports_effort_param(model) + ): + verbose_logger.warning( + "Dropping unsupported `output_config` for model=%s " + "(drop_params=True). Effort is only supported on Opus 4.5+, " + "Sonnet 4.6+, and Mythos Preview.", + model, + ) + anthropic_messages_request.pop("output_config", None) + # 7. Final safety net: filter top-level fields to the Bedrock Invoke allowlist. # Catches Anthropic-only extensions (context_management, output_config, speed, # mcp_servers, ...) and any future additions Claude Code may start sending. 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 da4684ec9c..eb40e76cc1 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 @@ -1748,6 +1748,104 @@ def test_effort_with_other_features(): assert "thinking" in result +def test_anthropic_drop_params_strips_output_config_for_pre_4_5_models(): + """ + Proxies fronting Claude Code at pre-4.5 Anthropic models receive + ``output_config`` injected by the client; without ``drop_params`` Bedrock / + Anthropic 400s. With ``drop_params=True`` we strip it (logged) so the + request can succeed. + """ + config = AnthropicConfig() + messages = [{"role": "user", "content": "Hello"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config.transform_request( + model="claude-3-haiku-20240307", + messages=messages, + optional_params={"output_config": {"effort": "low"}}, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + assert "output_config" not in result + + +def test_anthropic_drop_params_keeps_output_config_for_supporting_models(): + """``drop_params=True`` must not strip on models that support effort.""" + config = AnthropicConfig() + messages = [{"role": "user", "content": "Hello"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config.transform_request( + model="claude-opus-4-7", + messages=messages, + optional_params={"output_config": {"effort": "high"}}, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + assert result.get("output_config") == {"effort": "high"} + + +def test_anthropic_drop_params_false_forwards_to_unsupported_model(): + """ + Default behavior: forward ``output_config`` and let the provider 400. + This is the contract for users who want strict, debuggable failures. + """ + config = AnthropicConfig() + messages = [{"role": "user", "content": "Hello"}] + + original = litellm.drop_params + litellm.drop_params = False + try: + result = config.transform_request( + model="claude-3-haiku-20240307", + messages=messages, + optional_params={"output_config": {"effort": "low"}}, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + assert result.get("output_config") == {"effort": "low"} + + +@pytest.mark.parametrize( + "model", + [ + "claude-opus-4-5-20251101", + "claude-opus-4-6", + "claude-opus-4-7", + "claude-sonnet-4-6", + "claude-mythos-preview", + ], +) +def test_anthropic_model_supports_effort_param_recognizes_supporting_models(model): + assert AnthropicConfig._model_supports_effort_param(model) is True + + +@pytest.mark.parametrize( + "model", + [ + "claude-3-haiku-20240307", + "claude-3-5-sonnet-20241022", + "claude-3-opus-20240229", + "claude-sonnet-4-20250514", + ], +) +def test_anthropic_model_supports_effort_param_rejects_non_supporting_models(model): + assert AnthropicConfig._model_supports_effort_param(model) is False + + def test_translate_system_message_skips_empty_string_content(): """ Test that translate_system_message skips system messages with empty string content. diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 01dbc81fbb..9da7a04604 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -3434,6 +3434,59 @@ def test_transform_request_strips_anthropic_output_config(): assert "output_config" not in additional_fields +def test_converse_drop_params_strips_output_config_for_pre_4_5_anthropic(): + """``drop_params=True`` strips ``output_config`` for pre-4.5 Anthropic + models on Bedrock Converse so a proxy fronting Claude Code at haiku doesn't + force a 400 on every request.""" + config = AmazonConverseConfig() + messages = [{"role": "user", "content": "hi"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config._transform_request( + model="bedrock/converse/anthropic.claude-3-haiku-20240307-v1:0", + messages=messages, + optional_params={ + "maxTokens": 256, + "output_config": {"effort": "low"}, + }, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + additional = result.get("additionalModelRequestFields", {}) + assert "output_config" not in additional + + +def test_converse_drop_params_keeps_output_config_for_supporting_anthropic(): + """``drop_params=True`` must not strip on supporting models.""" + config = AmazonConverseConfig() + messages = [{"role": "user", "content": "hi"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config._transform_request( + model="bedrock/converse/us.anthropic.claude-opus-4-7", + messages=messages, + optional_params={ + "maxTokens": 256, + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "high"}, + }, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + additional = result.get("additionalModelRequestFields", {}) + assert additional.get("output_config") == {"effort": "high"} + + def test_transform_response_native_structured_output(): """Test response handling when model returns JSON as text content (native structured output).""" response_json = { 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 1bc9093b62..644ff81f6f 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 @@ -697,6 +697,70 @@ def test_bedrock_messages_forwards_output_config_for_non_adaptive_model(): assert result.get("max_tokens") == 4096 +def test_bedrock_messages_drop_params_strips_output_config_for_pre_4_5(): + """ + ``drop_params=True`` is the operator opt-in for "silently fix up" + behavior. When a proxy fronts Claude Code at a pre-4.5 Anthropic model + (haiku-3, sonnet-3.5, ...) on the /v1/messages route, the client always + sends ``output_config.effort`` and the model rejects it. Stripping under + ``drop_params`` lets those requests succeed; otherwise we forward and + surface the model's 400 as designed. + """ + import litellm + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "output_config": {"effort": "low"}, + } + + original = litellm.drop_params + litellm.drop_params = True + try: + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-3-haiku-20240307-v1:0", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.drop_params = original + + assert "output_config" not in result + + +def test_bedrock_messages_drop_params_keeps_output_config_for_4_7(): + """``drop_params=True`` must not strip on supporting models — opus-4-7 + accepts effort, so the client's tier knob has to land on the wire.""" + import litellm + from litellm.types.router import GenericLiteLLMParams + + cfg = AmazonAnthropicClaudeMessagesConfig() + messages = [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] + optional_params = { + "max_tokens": 4096, + "output_config": {"effort": "high"}, + } + + original = litellm.drop_params + litellm.drop_params = True + try: + result = cfg.transform_anthropic_messages_request( + model="anthropic.claude-opus-4-7", + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + finally: + litellm.drop_params = original + + assert result.get("output_config") == {"effort": "high"} + + @pytest.mark.parametrize( "reasoning_effort,expected_effort", [ From e780a7c1dc624259a55da019dfb5e6896413e4d8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 3 May 2026 03:39:36 -0700 Subject: [PATCH 09/25] fix(base_llm): filter all _-prefixed class attrs from get_config The drop_params strip work added `AnthropicConfig._EFFORT_SUPPORTING_MODEL_PATTERNS` as a private class-level lookup tuple. `BaseConfig.get_config()` only filtered the `__`-prefixed names plus `_abc` / `_is_base_class`, so `_EFFORT_SUPPORTING_MODEL_PATTERNS` would have leaked into the request body the same way `REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT` did before the previous commit. Generalize the existing `_abc` / `_is_base_class` carve-outs to skip every `_`-prefixed name. `AmazonConverseConfig.get_config()` overrides the base method, so apply the same change there. Also unblocks future internal helpers from accidentally serialising into the wire body. --- litellm/llms/base_llm/chat/transformation.py | 10 +++++++--- litellm/llms/bedrock/chat/converse_transformation.py | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index b71ae0fdde..e36ba0c14a 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -84,12 +84,16 @@ class BaseConfig(ABC): @classmethod def get_config(cls): + # Subclasses lean on this to surface their public default settings + # (e.g. ``max_tokens``) as request params. Anything ``_``-prefixed is + # treated as private (lookup tables, ABC machinery, internal flags) + # and must not leak into the wire body — a tuple/dict/frozenset class + # attribute would otherwise serialise into the request as an extra + # top-level key and the provider would 400 it. return { k: v for k, v in cls.__dict__.items() - if not k.startswith("__") - and not k.startswith("_abc") - and not k.startswith("_is_base_class") + if not k.startswith("_") and not isinstance( v, ( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 7fd6a87d9d..f711e88df6 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -189,10 +189,12 @@ class AmazonConverseConfig(BaseConfig): @classmethod def get_config(cls): + # ``_``-prefixed names are private (lookup tables, ABC machinery, + # internal flags) and must not leak into the wire body. return { k: v for k, v in cls.__dict__.items() - if not k.startswith("__") + if not k.startswith("_") and not isinstance( v, ( From 36f1f13925354d1c9b33d721b2552e404ab58311 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 3 May 2026 11:47:19 +0000 Subject: [PATCH 10/25] fix(anthropic): drive output_config.effort support from model map flags Replace hardcoded _EFFORT_SUPPORTING_MODEL_PATTERNS with a JSON-backed check that uses supports_*_reasoning_effort flags from the model map. Add supports_minimal_reasoning_effort: true to opus-4-5 and mythos-preview entries (which previously only carried supports_reasoning) so the JSON remains the single source of truth for effort capability. --- litellm/llms/anthropic/chat/transformation.py | 43 +++---------------- ...odel_prices_and_context_window_backup.json | 36 ++++++++++++++-- model_prices_and_context_window.json | 22 ++++++++-- .../test_anthropic_chat_transformation.py | 3 +- 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index da435f8816..21edf01f82 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -4,7 +4,6 @@ import time from typing import ( TYPE_CHECKING, Any, - ClassVar, Dict, List, Optional, @@ -274,47 +273,17 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): pass return False - # Per https://platform.claude.com/docs/en/build-with-claude/effort the - # ``output_config.effort`` parameter is supported on Opus 4.5+, Sonnet 4.6+ - # and Mythos Preview. Older Claude models (haiku-3, sonnet-3.5, opus-3, - # sonnet-4, ...) reject it with a 400. The patterns below let us recognize - # the supporting families regardless of route prefix (``anthropic.``, - # ``us.anthropic.``, ``vertex_ai/``, ``azure_ai/``, ...). - _EFFORT_SUPPORTING_MODEL_PATTERNS: ClassVar[Tuple[str, ...]] = ( - "opus-4-5", - "opus_4_5", - "opus-4.5", - "opus_4.5", - "opus-4-6", - "opus_4_6", - "opus-4.6", - "opus_4.6", - "opus-4-7", - "opus_4_7", - "opus-4.7", - "opus_4.7", - "sonnet-4-6", - "sonnet_4_6", - "sonnet-4.6", - "sonnet_4.6", - "mythos", - ) - @staticmethod def _model_supports_effort_param(model: str) -> bool: """Whether the model accepts ``output_config.effort`` at all. - Used to decide whether to strip ``output_config`` for known-incompatible - models when ``drop_params`` is set. New models that land in - ``model_prices_and_context_window.json`` with a ``supports_*_reasoning_effort`` - flag are auto-recognized; otherwise we fall back to the documented - family patterns above. + Per https://platform.claude.com/docs/en/build-with-claude/effort the + ``output_config.effort`` parameter is supported on Opus 4.5+, Sonnet 4.6+ + and Mythos Preview; older Claude models reject it with a 400. Support is + encoded in ``model_prices_and_context_window.json`` via the + ``supports_*_reasoning_effort`` flags, so adding a new effort-capable + model is a pure model-map change. """ - model_lower = model.lower() - if any( - p in model_lower for p in AnthropicConfig._EFFORT_SUPPORTING_MODEL_PATTERNS - ): - return True for level in ("low", "minimal", "medium", "high", "xhigh", "max"): if AnthropicConfig._supports_effort_level(model, level): return True diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b49d97dc4e..392fafcfc2 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -977,12 +977,28 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 159, "supports_native_structured_output": true }, + "anthropic.claude-mythos-preview": { + "input_cost_per_token": 0, + "output_cost_per_token": 0, + "litellm_provider": "bedrock", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, + "supports_tool_choice": true + }, "anthropic.claude-opus-4-6-v1": { "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -1915,6 +1931,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true @@ -9347,6 +9364,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -9374,6 +9392,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -10790,6 +10809,7 @@ "supports_assistant_prefill": true, "supports_function_calling": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_tool_choice": true }, "databricks/databricks-claude-sonnet-4": { @@ -17150,7 +17170,8 @@ ], "supports_function_calling": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_minimal_reasoning_effort": true }, "github_copilot/claude-opus-4.6-fast": { "litellm_provider": "github_copilot", @@ -17663,7 +17684,8 @@ "mode": "chat", "output_cost_per_token": 2.5e-05, "supports_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_minimal_reasoning_effort": true }, "gmi/anthropic/claude-sonnet-4.5": { "input_cost_per_token": 3e-06, @@ -26105,6 +26127,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true, @@ -28026,7 +28049,8 @@ "mode": "responses", "supports_web_search": true, "supports_reasoning": false, - "supports_function_calling": true + "supports_function_calling": true, + "supports_minimal_reasoning_effort": true }, "perplexity/anthropic/claude-sonnet-4-5": { "litellm_provider": "perplexity", @@ -30503,6 +30527,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -30531,6 +30556,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -30558,6 +30584,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -31138,6 +31165,7 @@ "output_cost_per_token": 2.5e-05, "supports_assistant_prefill": true, "supports_computer_use": true, + "supports_minimal_reasoning_effort": true, "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -32330,6 +32358,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -32356,6 +32385,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index cc34446be0..03b13d743f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -977,6 +977,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -1174,6 +1175,7 @@ "supports_vision": true, "supports_prompt_caching": false, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_tool_choice": true }, "global.anthropic.claude-opus-4-7": { @@ -1929,6 +1931,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true @@ -9361,6 +9364,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -9388,6 +9392,7 @@ "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -10802,6 +10807,7 @@ "supports_assistant_prefill": true, "supports_function_calling": true, "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, "supports_tool_choice": true }, "databricks/databricks-claude-sonnet-4": { @@ -17162,7 +17168,8 @@ ], "supports_function_calling": true, "supports_parallel_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_minimal_reasoning_effort": true }, "github_copilot/claude-opus-4.6-fast": { "litellm_provider": "github_copilot", @@ -17675,7 +17682,8 @@ "mode": "chat", "output_cost_per_token": 2.5e-05, "supports_function_calling": true, - "supports_vision": true + "supports_vision": true, + "supports_minimal_reasoning_effort": true }, "gmi/anthropic/claude-sonnet-4.5": { "input_cost_per_token": 3e-06, @@ -26111,6 +26119,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true, @@ -28076,7 +28085,8 @@ "mode": "responses", "supports_web_search": true, "supports_reasoning": false, - "supports_function_calling": true + "supports_function_calling": true, + "supports_minimal_reasoning_effort": true }, "perplexity/anthropic/claude-sonnet-4-5": { "litellm_provider": "perplexity", @@ -30555,6 +30565,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -30583,6 +30594,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -30610,6 +30622,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -31190,6 +31203,7 @@ "output_cost_per_token": 2.5e-05, "supports_assistant_prefill": true, "supports_computer_use": true, + "supports_minimal_reasoning_effort": true, "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -32382,6 +32396,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -32408,6 +32423,7 @@ "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, + "supports_minimal_reasoning_effort": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, 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 eb40e76cc1..c496df429e 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 @@ -1826,7 +1826,8 @@ def test_anthropic_drop_params_false_forwards_to_unsupported_model(): "claude-opus-4-6", "claude-opus-4-7", "claude-sonnet-4-6", - "claude-mythos-preview", + "anthropic.claude-mythos-preview", + "bedrock/anthropic.claude-mythos-preview", ], ) def test_anthropic_model_supports_effort_param_recognizes_supporting_models(model): From 108b87fb246ff5da4d216b9fd2b862c3378590f8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 3 May 2026 10:03:53 -0700 Subject: [PATCH 11/25] fix(anthropic,bedrock,databricks): four reasoning_effort follow-ups - claude-sonnet-4-6 + reasoning_effort=max no longer 400s. Renamed _is_opus_4_6_model to _is_claude_4_6_model at three sites and added supports_max_reasoning_effort: true to 12 model entries in the JSON cost map (10 sonnet 4.6 ids + OpenRouter opus 4.6/4.7). - _map_reasoning_effort now raises BadRequestError(400) directly with llm_provider, instead of letting Databricks (and similar callers) surface its raw ValueError as a 500. - output_config.effort on Opus 4.5 over Bedrock no longer 400s for missing effort-2025-11-24 beta. Flipped JSON to "effort-2025-11-24" for bedrock + bedrock_converse and added an auto-attach branch in _process_tools_and_beta for non-adaptive Anthropic + output_config on Converse. - reasoning_effort=xhigh / =max on legacy budget-mode models (Haiku 4.5, Sonnet 4.5, Opus 4.5) now map to thinking.budget_tokens 8192 / 16384 instead of returning 400. Added two constants in litellm/constants.py. Tests updated for all four flips. Validated end-to-end via 306-cell live proxy matrix (6 model families x 3 routes x 17 effort cases), all pass. --- litellm/anthropic_beta_headers_config.json | 4 +- litellm/constants.py | 17 + litellm/llms/anthropic/chat/transformation.py | 88 ++-- .../messages/transformation.py | 20 +- .../bedrock/chat/converse_transformation.py | 56 ++- .../llms/databricks/chat/transformation.py | 9 +- ...odel_prices_and_context_window_backup.json | 392 +++++++++++++----- model_prices_and_context_window.json | 12 + .../test_anthropic_chat_transformation.py | 90 ++-- .../test_reasoning_effort_translation.py | 30 +- .../chat/test_converse_transformation.py | 45 +- 11 files changed, 559 insertions(+), 204 deletions(-) diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index 662b62cf20..d02afe3756 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -72,7 +72,7 @@ "computer-use-2025-11-24": "computer-use-2025-11-24", "context-1m-2025-08-07": "context-1m-2025-08-07", "context-management-2025-06-27": null, - "effort-2025-11-24": null, + "effort-2025-11-24": "effort-2025-11-24", "fast-mode-2026-02-01": null, "files-api-2025-04-14": null, "fine-grained-tool-streaming-2025-05-14": null, @@ -103,7 +103,7 @@ "computer-use-2025-11-24": "computer-use-2025-11-24", "context-1m-2025-08-07": "context-1m-2025-08-07", "context-management-2025-06-27": null, - "effort-2025-11-24": null, + "effort-2025-11-24": "effort-2025-11-24", "fast-mode-2026-02-01": null, "files-api-2025-04-14": null, "fine-grained-tool-streaming-2025-05-14": null, diff --git a/litellm/constants.py b/litellm/constants.py index 7bf94971a3..7708ba8b69 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -202,6 +202,23 @@ DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET = int( DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET = int( os.getenv("DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET", 4096) ) +# ``xhigh`` / ``max`` budget extrapolation for legacy ``thinking.budget_tokens`` +# models (Claude 4.5 series + haiku). Continues the 2× progression +# 1024 → 2048 → 4096 from the existing low/medium/high tiers. These tiers +# also exist as adaptive ``output_config.effort`` enum values on Claude 4.6+ +# / 4.7; this constant only governs the budget-tokens fallback for models +# that aren't on the adaptive path. Per +# https://platform.claude.com/docs/en/build-with-claude/effort the ``effort`` +# enum is gated by model, but the legacy ``budget_tokens`` knob accepts any +# integer up to the model's max_tokens — adopting #27051's mapping here lets +# ``reasoning_effort=xhigh|max`` Just Work as a unified OpenAI-format knob +# regardless of which Anthropic API surface implements it. +DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET = int( + os.getenv("DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET", 8192) +) +DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET = int( + os.getenv("DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET", 16384) +) MAX_TOKEN_TRIMMING_ATTEMPTS = int( os.getenv("MAX_TOKEN_TRIMMING_ATTEMPTS", 10) ) # Maximum number of attempts to trim the message diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 21edf01f82..9de89d8ad5 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -21,8 +21,10 @@ from litellm.constants import ( DEFAULT_ANTHROPIC_CHAT_MAX_TOKENS, DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET, + DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET, + DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, RESPONSE_FORMAT_TOOL_NAME, ) from litellm.litellm_core_utils.core_helpers import map_finish_reason @@ -869,7 +871,18 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _map_reasoning_effort( reasoning_effort: Optional[Union[REASONING_EFFORT, str]], model: str, + llm_provider: str = "anthropic", ) -> Optional[AnthropicThinkingParam]: + """Map an OpenAI-format ``reasoning_effort`` string to Anthropic's + ``thinking`` payload. + + Raises ``BadRequestError`` (clean 400) instead of ``ValueError`` (500) + on unmapped efforts so every caller — Anthropic native, Bedrock + Invoke/Converse, Databricks, Vertex Anthropic, Azure AI Anthropic, + and the experimental ``/v1/messages`` pass-through — surfaces a + consistent error to the user. Pass ``llm_provider`` so the + ``BadRequestError`` carries the right provider name in logs. + """ if reasoning_effort is None or reasoning_effort == "none": return None if AnthropicConfig._is_claude_4_6_model( @@ -893,6 +906,28 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): type="enabled", budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, ) + elif reasoning_effort == "xhigh": + # Continues the 2× progression of low/medium/high (1024/2048/4096). + # On adaptive models (Claude 4.6/4.7) the ``xhigh`` tier is + # already routed via ``output_config.effort=xhigh`` above; this + # branch only applies to budget-mode models (Claude 4.5 series + + # haiku) where the OpenAI-format ``reasoning_effort`` knob would + # otherwise 400 with ``Unmapped reasoning effort``. Keeps the + # cross-model UX uniform — ``reasoning_effort=xhigh`` Just Works + # regardless of which Anthropic API surface implements it. + return AnthropicThinkingParam( + type="enabled", + budget_tokens=DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, + ) + elif reasoning_effort == "max": + # Same rationale as ``xhigh`` above — ``max`` is the adaptive + # enum's top tier on Claude 4.6/4.7, but for budget-mode models + # we extend the 2× progression (8192 → 16384) so the OpenAI- + # format alias is usable on every Claude model. + return AnthropicThinkingParam( + type="enabled", + budget_tokens=DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET, + ) elif reasoning_effort == "minimal": # Anthropic Messages API rejects ``budget_tokens < 1024`` with a # 400. Floor at the provider minimum so ``minimal`` is a usable @@ -906,7 +941,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ), ) else: - raise ValueError(f"Unmapped reasoning effort: {reasoning_effort}") + raise litellm.exceptions.BadRequestError( + message=( + f"Unmapped reasoning effort: {reasoning_effort!r}. " + f"Must be one of: 'minimal', 'low', 'medium', 'high', " + f"'xhigh', 'max', 'none'." + ), + model=model, + llm_provider=llm_provider, + ) def _extract_json_schema_from_response_format( self, value: Optional[dict] @@ -1170,21 +1213,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): elif param == "thinking": optional_params["thinking"] = value elif param == "reasoning_effort" and isinstance(value, str): - # Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on - # unmapped efforts (``disabled`` / ``invalid`` / ``""`` / - # ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean - # 400 ``BadRequestError`` instead of letting it surface as - # 500. - try: - mapped_thinking = AnthropicConfig._map_reasoning_effort( - reasoning_effort=value, model=model - ) - except ValueError as e: - raise litellm.exceptions.BadRequestError( - message=str(e), - model=model, - llm_provider=self.custom_llm_provider or "anthropic", - ) + # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) + # directly on unmapped efforts (``disabled`` / ``invalid`` / + # ``""`` / ``xhigh``/``max`` on budget-mode Claude 4.5) so + # we no longer need to wrap a ``ValueError`` here. + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=value, + model=model, + llm_provider=self.custom_llm_provider or "anthropic", + ) if mapped_thinking is None: optional_params.pop("thinking", None) optional_params.pop("output_config", None) @@ -1673,15 +1710,18 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): model=model, llm_provider=self.custom_llm_provider or "anthropic", ) - # ``max`` is for Opus 4.6+ output effort (not Sonnet 4.6, not Opus 4.5). - # Accept known Opus 4.6/4.7 id patterns and/or ``supports_max_reasoning_effort`` - # in the model map (same pattern as ``xhigh`` below). The hardcoded - # patterns cover OpenRouter/GitHub Copilot/Vercel variants that don't - # carry the model-map flag yet — keep both checks until those provider - # entries are fully populated. + # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 + # adaptive-thinking models (per + # https://platform.claude.com/docs/en/build-with-claude/effort). + # Prefer the data-driven ``supports_max_reasoning_effort`` flag in + # ``model_prices_and_context_window.json`` so new variants only + # require a model-map update. Family-level ``_is_claude_4_6_model`` + # / ``_is_claude_4_7_model`` checks remain as a fallback for + # OpenRouter/GitHub Copilot/Vercel/Bedrock variants whose entries + # don't yet carry the flag. if effort == "max" and not ( - self._is_opus_4_6_model(model) - or self._is_opus_4_7_model(model) + self._is_claude_4_6_model(model) + or self._is_claude_4_7_model(model) or self._supports_effort_level(model, "max") ): raise litellm.exceptions.BadRequestError( diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 98004f647e..f756279877 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -201,12 +201,18 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if not isinstance(reasoning_effort, str): return + # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) directly + # on unmapped efforts. The /v1/messages pass-through surfaces errors + # as ``AnthropicError``; convert here so callers see a provider-shaped + # 400 rather than the LiteLLM-shaped one. + from litellm.exceptions import BadRequestError as _BadRequestError + try: mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort, model=model ) - except ValueError as e: - raise AnthropicError(message=str(e), status_code=400) + except _BadRequestError as e: + raise AnthropicError(message=str(e.message), status_code=400) if mapped_thinking is None: optional_params.pop("thinking", None) @@ -238,9 +244,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): # The chat completion path enforces this via # ``_apply_output_config``; mirror it here so /v1/messages # callers see a clean 400 instead of a provider-side error. + # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude + # 4.7 adaptive-thinking models. Prefer the data-driven + # ``supports_max_reasoning_effort`` flag in + # ``model_prices_and_context_window.json``; family-level checks + # are a fallback for variants whose entries don't yet carry the + # flag. if mapped_effort == "max" and not ( - AnthropicConfig._is_opus_4_6_model(model) - or AnthropicConfig._is_opus_4_7_model(model) + AnthropicConfig._is_claude_4_6_model(model) + or AnthropicConfig._is_claude_4_7_model(model) or AnthropicConfig._supports_effort_level(model, "max") ): raise AnthropicError( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index f711e88df6..14c7c3030c 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -463,20 +463,16 @@ class AmazonConverseConfig(BaseConfig): optional_params.update(reasoning_config) else: # Anthropic and other models: convert to thinking parameter. - # Wrap the ``ValueError`` ``_map_reasoning_effort`` raises on - # unmapped efforts (``disabled`` / ``invalid`` / ``""`` / - # ``xhigh``/``max`` on budget-mode Claude 4.5) into a clean 400 - # ``BadRequestError`` instead of letting it surface as 500. - try: - mapped_thinking = AnthropicConfig._map_reasoning_effort( - reasoning_effort=reasoning_effort, model=model - ) - except ValueError as e: - raise litellm.exceptions.BadRequestError( - message=str(e), - model=model, - llm_provider="bedrock_converse", - ) + # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) + # directly on unmapped efforts (``disabled`` / ``invalid`` / + # ``""`` / ``xhigh``/``max`` on budget-mode Claude 4.5); pass + # ``llm_provider="bedrock_converse"`` so the error carries the + # right provider name. + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=reasoning_effort, + model=model, + llm_provider="bedrock_converse", + ) if mapped_thinking is None: optional_params.pop("thinking", None) optional_params.pop("output_config", None) @@ -555,9 +551,15 @@ class AmazonConverseConfig(BaseConfig): model=model, llm_provider="bedrock_converse", ) + # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 + # adaptive-thinking models. Prefer the data-driven + # ``supports_max_reasoning_effort`` flag in + # ``model_prices_and_context_window.json`` so new variants only + # require a model-map update; family-level checks remain a fallback + # for Bedrock model ids whose entries don't yet carry the flag. if effort == "max" and not ( - AnthropicConfig._is_opus_4_6_model(model) - or AnthropicConfig._is_opus_4_7_model(model) + AnthropicConfig._is_claude_4_6_model(model) + or AnthropicConfig._is_claude_4_7_model(model) or AmazonConverseConfig._supports_effort_level_on_bedrock(model, "max") ): raise litellm.exceptions.BadRequestError( @@ -1535,9 +1537,29 @@ class AmazonConverseConfig(BaseConfig): # Append pre-formatted tools (systemTool etc.) after transformation bedrock_tools.extend(pre_formatted_tools) + # Auto-attach the effort beta header for non-adaptive Anthropic + # models on Bedrock Converse (i.e. Opus 4.5). Claude 4.6/4.7 accept + # ``output_config.effort`` as a stable, GA feature with no beta + # header; Opus 4.5 still gates it behind ``effort-2025-11-24``. The + # check mirrors ``AnthropicModelInfo.is_effort_used`` (which returns + # False for adaptive models) so we don't double-flag adaptive routes. + base_model = BedrockModelInfo.get_base_model(model) + if base_model.startswith("anthropic"): + output_config = additional_request_params.get("output_config") + if ( + isinstance(output_config, dict) + and output_config.get("effort") is not None + and not AnthropicConfig._is_adaptive_thinking_model(model) + ): + from litellm.types.llms.anthropic import ( + ANTHROPIC_EFFORT_BETA_HEADER, + ) + + if ANTHROPIC_EFFORT_BETA_HEADER not in anthropic_beta_list: + anthropic_beta_list.append(ANTHROPIC_EFFORT_BETA_HEADER) + # Set anthropic_beta in additional_request_params if we have any beta features # ONLY apply to Anthropic/Claude models - other models (e.g., Qwen, Llama) don't support this field - base_model = BedrockModelInfo.get_base_model(model) if anthropic_beta_list and base_model.startswith("anthropic"): additional_request_params["anthropic_beta"] = anthropic_beta_list diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index c086d4ad75..8ac438bbf0 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -330,8 +330,15 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): ) # unsupported for claude models - if json_schema -> convert to tool call if "reasoning_effort" in non_default_params and "claude" in model: + # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) + # directly on unmapped efforts; pass ``llm_provider="databricks"`` + # so the surfaced error carries the correct provider name (the + # default is ``"anthropic"``, which would mislead users routing + # via Databricks Foundation Model APIs). optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( - reasoning_effort=non_default_params.get("reasoning_effort"), model=model + reasoning_effort=non_default_params.get("reasoning_effort"), + model=model, + llm_provider="databricks", ) optional_params.pop("reasoning_effort", None) ## handle thinking tokens diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 392fafcfc2..7946e2dcee 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -984,21 +984,6 @@ "tool_use_system_prompt_tokens": 159, "supports_native_structured_output": true }, - "anthropic.claude-mythos-preview": { - "input_cost_per_token": 0, - "output_cost_per_token": 0, - "litellm_provider": "bedrock", - "max_input_tokens": 1000000, - "max_output_tokens": 128000, - "max_tokens": 128000, - "mode": "chat", - "supports_function_calling": true, - "supports_vision": true, - "supports_prompt_caching": false, - "supports_reasoning": true, - "supports_minimal_reasoning_effort": true, - "supports_tool_choice": true - }, "anthropic.claude-opus-4-6-v1": { "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -1178,6 +1163,21 @@ "supports_max_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, + "anthropic.claude-mythos-preview": { + "input_cost_per_token": 0, + "output_cost_per_token": 0, + "litellm_provider": "bedrock", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_vision": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_minimal_reasoning_effort": true, + "supports_tool_choice": true + }, "global.anthropic.claude-opus-4-7": { "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -1323,6 +1323,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1352,6 +1353,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1381,6 +1383,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1409,6 +1412,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1437,6 +1441,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -2055,6 +2060,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -9229,6 +9235,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -9496,7 +9503,6 @@ "us": 1.1, "fast": 6.0 }, - "supports_max_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, "claude-opus-4-7-20260416": { @@ -9531,7 +9537,6 @@ "us": 1.1, "fast": 6.0 }, - "supports_max_reasoning_effort": true, "supports_minimal_reasoning_effort": true }, "claude-sonnet-4-20250514": { @@ -15568,7 +15573,7 @@ "mode": "embedding", "output_cost_per_token": 0, "output_vector_size": 3072, - "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "source": "https://ai.google.dev/gemini-api/docs/embeddings#multimodal", "supports_multimodal": true, "uses_embed_content": true }, @@ -21164,7 +21169,7 @@ }, "gradient_ai/alibaba-qwen3-32b": { "litellm_provider": "gradient_ai", - "max_tokens": 2048, + "max_tokens": 40960, "mode": "chat", "supported_endpoints": [ "/v1/chat/completions" @@ -21172,7 +21177,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 131072, + "max_output_tokens": 40960 }, "gradient_ai/anthropic-claude-3-opus": { "input_cost_per_token": 1.5e-05, @@ -21186,7 +21193,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 1024 }, "gradient_ai/anthropic-claude-3.5-haiku": { "input_cost_per_token": 8e-07, @@ -21200,7 +21209,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 1024 }, "gradient_ai/anthropic-claude-3.5-sonnet": { "input_cost_per_token": 3e-06, @@ -21214,7 +21225,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 1024 }, "gradient_ai/anthropic-claude-3.7-sonnet": { "input_cost_per_token": 3e-06, @@ -21228,7 +21241,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 1024 }, "gradient_ai/deepseek-r1-distill-llama-70b": { "input_cost_per_token": 9.9e-07, @@ -21242,7 +21257,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 32768, + "max_output_tokens": 8000 }, "gradient_ai/llama3-8b-instruct": { "input_cost_per_token": 2e-07, @@ -21256,7 +21273,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 8192, + "max_output_tokens": 512 }, "gradient_ai/llama3.3-70b-instruct": { "input_cost_per_token": 6.5e-07, @@ -21270,7 +21289,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 128000, + "max_output_tokens": 2048 }, "gradient_ai/mistral-nemo-instruct-2407": { "input_cost_per_token": 3e-07, @@ -21284,7 +21305,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 128000, + "max_output_tokens": 512 }, "gradient_ai/openai-gpt-4o": { "litellm_provider": "gradient_ai", @@ -21296,7 +21319,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 128000, + "max_output_tokens": 16384 }, "gradient_ai/openai-gpt-4o-mini": { "litellm_provider": "gradient_ai", @@ -21308,7 +21333,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 128000, + "max_output_tokens": 16384 }, "gradient_ai/openai-o3": { "input_cost_per_token": 2e-06, @@ -21322,7 +21349,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 100000 }, "gradient_ai/openai-o3-mini": { "input_cost_per_token": 1.1e-06, @@ -21336,7 +21365,9 @@ "supported_modalities": [ "text" ], - "supports_tool_choice": false + "supports_tool_choice": false, + "max_input_tokens": 200000, + "max_output_tokens": 100000 }, "lemonade/Qwen3-Coder-30B-A3B-Instruct-GGUF": { "input_cost_per_token": 0, @@ -21636,11 +21667,13 @@ }, "heroku/claude-3-5-haiku": { "litellm_provider": "heroku", - "max_tokens": 4096, + "max_tokens": 8192, "mode": "chat", "supports_function_calling": true, "supports_system_messages": true, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 200000, + "max_output_tokens": 8192 }, "heroku/claude-3-5-sonnet-latest": { "litellm_provider": "heroku", @@ -21648,7 +21681,9 @@ "mode": "chat", "supports_function_calling": true, "supports_system_messages": true, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 200000, + "max_output_tokens": 8192 }, "heroku/claude-3-7-sonnet": { "litellm_provider": "heroku", @@ -21656,7 +21691,9 @@ "mode": "chat", "supports_function_calling": true, "supports_system_messages": true, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 200000, + "max_output_tokens": 8192 }, "heroku/claude-4-sonnet": { "litellm_provider": "heroku", @@ -21664,7 +21701,9 @@ "mode": "chat", "supports_function_calling": true, "supports_system_messages": true, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 200000, + "max_output_tokens": 8192 }, "high/1024-x-1024/gpt-image-1": { "input_cost_per_image": 0.167, @@ -22472,48 +22511,6 @@ "/v1/images/generations" ] }, - "luminous-base": { - "input_cost_per_token": 3e-05, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "completion", - "output_cost_per_token": 3.3e-05 - }, - "luminous-base-control": { - "input_cost_per_token": 3.75e-05, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "chat", - "output_cost_per_token": 4.125e-05 - }, - "luminous-extended": { - "input_cost_per_token": 4.5e-05, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "completion", - "output_cost_per_token": 4.95e-05 - }, - "luminous-extended-control": { - "input_cost_per_token": 5.625e-05, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "chat", - "output_cost_per_token": 6.1875e-05 - }, - "luminous-supreme": { - "input_cost_per_token": 0.000175, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "completion", - "output_cost_per_token": 0.0001925 - }, - "luminous-supreme-control": { - "input_cost_per_token": 0.00021875, - "litellm_provider": "aleph_alpha", - "max_tokens": 2048, - "mode": "chat", - "output_cost_per_token": 0.000240625 - }, "max-x-max/50-steps/stability.stable-diffusion-xl-v0": { "litellm_provider": "bedrock", "max_input_tokens": 77, @@ -25985,12 +25982,14 @@ "input_cost_per_image": 0.0004, "input_cost_per_token": 2.5e-07, "litellm_provider": "openrouter", - "max_tokens": 200000, + "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 1.25e-06, "supports_function_calling": true, "supports_tool_choice": true, - "supports_vision": true + "supports_vision": true, + "max_input_tokens": 200000, + "max_output_tokens": 4096 }, "openrouter/anthropic/claude-3.5-sonnet": { "input_cost_per_token": 3e-06, @@ -26109,6 +26108,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 159, @@ -26149,6 +26149,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -26197,6 +26198,29 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 346 }, + "openrouter/anthropic/claude-opus-4.7": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "supports_assistant_prefill": false, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_max_reasoning_effort": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_xhigh_reasoning_effort": true, + "tool_use_system_prompt_tokens": 346 + }, "openrouter/bytedance/ui-tars-1.5-7b": { "input_cost_per_token": 1e-07, "litellm_provider": "openrouter", @@ -26562,18 +26586,22 @@ "openrouter/mancer/weaver": { "input_cost_per_token": 5.625e-06, "litellm_provider": "openrouter", - "max_tokens": 8000, + "max_tokens": 2000, "mode": "chat", "output_cost_per_token": 5.625e-06, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 8000, + "max_output_tokens": 2000 }, "openrouter/meta-llama/llama-3-70b-instruct": { "input_cost_per_token": 5.9e-07, "litellm_provider": "openrouter", - "max_tokens": 8192, + "max_tokens": 8000, "mode": "chat", "output_cost_per_token": 7.9e-07, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 8192, + "max_output_tokens": 8000 }, "openrouter/minimax/minimax-m2": { "input_cost_per_token": 2.55e-07, @@ -26661,34 +26689,42 @@ "openrouter/mistralai/mistral-7b-instruct": { "input_cost_per_token": 1.3e-07, "litellm_provider": "openrouter", - "max_tokens": 8192, + "max_tokens": 8191, "mode": "chat", "output_cost_per_token": 1.3e-07, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 32768, + "max_output_tokens": 8191 }, "openrouter/mistralai/mistral-large": { "input_cost_per_token": 8e-06, "litellm_provider": "openrouter", - "max_tokens": 32000, + "max_tokens": 8191, "mode": "chat", "output_cost_per_token": 2.4e-05, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 128000, + "max_output_tokens": 8191 }, "openrouter/mistralai/mistral-small-3.1-24b-instruct": { "input_cost_per_token": 1e-07, "litellm_provider": "openrouter", - "max_tokens": 32000, + "max_tokens": 131072, "mode": "chat", "output_cost_per_token": 3e-07, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 131072, + "max_output_tokens": 131072 }, "openrouter/mistralai/mistral-small-3.2-24b-instruct": { "input_cost_per_token": 1e-07, "litellm_provider": "openrouter", - "max_tokens": 32000, + "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 3e-07, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 128000, + "max_output_tokens": 128000 }, "openrouter/mistralai/mixtral-8x22b-instruct": { "input_cost_per_token": 6.5e-07, @@ -26696,7 +26732,9 @@ "max_tokens": 65536, "mode": "chat", "output_cost_per_token": 6.5e-07, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 65536, + "max_output_tokens": 65536 }, "openrouter/moonshotai/kimi-k2.5": { "cache_read_input_token_cost": 1e-07, @@ -26716,26 +26754,32 @@ "openrouter/openai/gpt-3.5-turbo": { "input_cost_per_token": 1.5e-06, "litellm_provider": "openrouter", - "max_tokens": 4095, + "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 2e-06, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 16385, + "max_output_tokens": 4096 }, "openrouter/openai/gpt-3.5-turbo-16k": { "input_cost_per_token": 3e-06, "litellm_provider": "openrouter", - "max_tokens": 16383, + "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 4e-06, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 16385, + "max_output_tokens": 4096 }, "openrouter/openai/gpt-4": { "input_cost_per_token": 3e-05, "litellm_provider": "openrouter", - "max_tokens": 8192, + "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 6e-05, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 8191, + "max_output_tokens": 4096 }, "openrouter/openai/gpt-4.1": { "cache_read_input_token_cost": 5e-07, @@ -27243,10 +27287,12 @@ "openrouter/undi95/remm-slerp-l2-13b": { "input_cost_per_token": 1.875e-06, "litellm_provider": "openrouter", - "max_tokens": 6144, + "max_tokens": 4096, "mode": "chat", "output_cost_per_token": 1.875e-06, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 6144, + "max_output_tokens": 4096 }, "openrouter/x-ai/grok-4": { "input_cost_per_token": 3e-06, @@ -29868,14 +29914,16 @@ "together_ai/deepseek-ai/DeepSeek-V3.1": { "input_cost_per_token": 6e-07, "litellm_provider": "together_ai", - "max_tokens": 128000, + "max_tokens": 16384, "mode": "chat", "output_cost_per_token": 1.7e-06, "source": "https://www.together.ai/models/deepseek-v3-1", "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, - "supports_tool_choice": true + "supports_tool_choice": true, + "max_input_tokens": 128000, + "max_output_tokens": 16384 }, "together_ai/meta-llama/Llama-3.2-3B-Instruct-Turbo": { "litellm_provider": "together_ai", @@ -32552,6 +32600,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -34456,6 +34505,7 @@ "output_cost_per_token": 1.5e-05, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": false, "supports_tool_choice": true, "supports_web_search": true @@ -34471,6 +34521,7 @@ "output_cost_per_token": 1.5e-05, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": false, "supports_tool_choice": true, "supports_web_search": true @@ -34486,6 +34537,7 @@ "output_cost_per_token": 2.5e-05, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": false, "supports_tool_choice": true, "supports_web_search": true @@ -34501,6 +34553,7 @@ "output_cost_per_token": 2.5e-05, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": false, "supports_tool_choice": true, "supports_web_search": true @@ -34516,6 +34569,7 @@ "output_cost_per_token": 1.5e-05, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": false, "supports_tool_choice": true, "supports_web_search": true @@ -34532,6 +34586,7 @@ "output_cost_per_token": 5e-07, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34549,6 +34604,7 @@ "output_cost_per_token": 5e-07, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34565,6 +34621,7 @@ "output_cost_per_token": 4e-06, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34581,6 +34638,7 @@ "output_cost_per_token": 4e-06, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34597,6 +34655,7 @@ "output_cost_per_token": 4e-06, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34613,6 +34672,7 @@ "output_cost_per_token": 5e-07, "source": "https://x.ai/api#pricing", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": false, "supports_tool_choice": true, @@ -34628,38 +34688,41 @@ "output_cost_per_token": 1.5e-05, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_web_search": true }, "xai/grok-4-fast-reasoning": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "input_cost_per_token_above_128k_tokens": 4e-07, "litellm_provider": "xai", "max_input_tokens": 2000000.0, "max_output_tokens": 2000000.0, "max_tokens": 2000000.0, "mode": "chat", - "input_cost_per_token": 2e-07, - "input_cost_per_token_above_128k_tokens": 4e-07, "output_cost_per_token": 5e-07, "output_cost_per_token_above_128k_tokens": 1e-06, - "cache_read_input_token_cost": 5e-08, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_web_search": true }, "xai/grok-4-fast-non-reasoning": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "input_cost_per_token_above_128k_tokens": 4e-07, "litellm_provider": "xai", "max_input_tokens": 2000000.0, "max_output_tokens": 2000000.0, - "cache_read_input_token_cost": 5e-08, "max_tokens": 2000000.0, "mode": "chat", - "input_cost_per_token": 2e-07, - "input_cost_per_token_above_128k_tokens": 4e-07, "output_cost_per_token": 5e-07, "output_cost_per_token_above_128k_tokens": 1e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -34675,6 +34738,7 @@ "output_cost_per_token_above_128k_tokens": 3e-05, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -34690,6 +34754,7 @@ "output_cost_per_token_above_128k_tokens": 3e-05, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_web_search": true }, @@ -34707,6 +34772,7 @@ "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", "supports_audio_input": true, "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, @@ -34727,6 +34793,7 @@ "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", "supports_audio_input": true, "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, @@ -34747,6 +34814,7 @@ "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", "supports_audio_input": true, "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, "supports_tool_choice": true, @@ -34767,6 +34835,7 @@ "source": "https://docs.x.ai/docs/models/grok-4-1-fast-non-reasoning", "supports_audio_input": true, "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -34786,6 +34855,7 @@ "source": "https://docs.x.ai/docs/models/grok-4-1-fast-non-reasoning", "supports_audio_input": true, "supports_function_calling": true, + "supports_prompt_caching": true, "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true, @@ -34802,6 +34872,7 @@ "output_cost_per_token": 6e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true, "supports_vision": true, @@ -34818,6 +34889,7 @@ "output_cost_per_token": 6e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true, "supports_vision": true, @@ -34850,6 +34922,7 @@ "output_cost_per_token": 6e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_tool_choice": true, "supports_vision": true, "supports_web_search": true @@ -34878,6 +34951,7 @@ "output_cost_per_token": 1.5e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -34892,6 +34966,7 @@ "output_cost_per_token": 1.5e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -34906,6 +34981,7 @@ "output_cost_per_token": 1.5e-06, "source": "https://docs.x.ai/docs/models", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -34937,6 +35013,20 @@ "supports_tool_choice": true, "source": "https://aws.amazon.com/bedrock/pricing/" }, + "zai.glm-5": { + "input_cost_per_token": 1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "zai.glm-4.7-flash": { "input_cost_per_token": 7e-08, "litellm_provider": "bedrock_converse", @@ -39522,6 +39612,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -39746,6 +39837,87 @@ } ] }, + "zai.glm-5": { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3.2e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "bedrock/us-east-1/zai.glm-5": { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3.2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "bedrock/us-west-2/zai.glm-5": { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3.2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "minimax.minimax-m2.5": { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "bedrock/us-east-1/minimax.minimax-m2.5": { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "bedrock/us-west-2/minimax.minimax-m2.5": { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, "bedrock/us-gov-east-1/anthropic.claude-haiku-4-5-20251001-v1:0": { "cache_creation_input_token_cost": 1.5e-06, "cache_read_input_token_cost": 1.2e-07, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 03b13d743f..7946e2dcee 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -1323,6 +1323,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1352,6 +1353,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1381,6 +1383,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1409,6 +1412,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -1437,6 +1441,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -2055,6 +2060,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -9229,6 +9235,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -26101,6 +26108,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 159, @@ -26141,6 +26149,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -26206,6 +26215,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "supports_xhigh_reasoning_effort": true, @@ -32590,6 +32600,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, @@ -39601,6 +39612,7 @@ "supports_prompt_caching": true, "supports_reasoning": true, "supports_response_schema": true, + "supports_max_reasoning_effort": true, "supports_tool_choice": true, "supports_vision": true, "tool_use_system_prompt_tokens": 346, 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 c496df429e..1aa664f657 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 @@ -2078,13 +2078,17 @@ def test_get_config_does_not_leak_module_constants(): ("claude-opus-4-7", "xhigh", True), ("claude-opus-4-6", "max", True), ("claude-opus-4-6", "xhigh", False), - ("claude-sonnet-4-6", "max", False), + # ``max`` is documented as supported on Sonnet 4.6 (Claude 4.6 family). + # The model-map JSON now carries ``supports_max_reasoning_effort: true`` + # for every Sonnet 4.6 entry; ``_supports_effort_level`` should report + # ``True`` on every route prefix (anthropic, bedrock, vertex, azure). + ("claude-sonnet-4-6", "max", True), ("claude-sonnet-4-6", "xhigh", False), ("bedrock/invoke/us.anthropic.claude-opus-4-7", "max", True), ("bedrock/invoke/us.anthropic.claude-opus-4-7", "xhigh", True), ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "max", True), ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh", False), - ("bedrock/invoke/us.anthropic.claude-sonnet-4-6", "max", False), + ("bedrock/invoke/us.anthropic.claude-sonnet-4-6", "max", True), ("vertex_ai/claude-opus-4-7", "xhigh", True), ("azure_ai/claude-opus-4-7", "xhigh", True), ], @@ -2393,25 +2397,40 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): ), f"output_config should not be set for {model}" -def test_max_effort_rejected_for_sonnet_46(): - """Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level). +@pytest.mark.parametrize( + "model", + [ + "claude-sonnet-4-6", + "claude-sonnet-4-6-20260219", + "us.anthropic.claude-sonnet-4-6", + "bedrock/converse/us.anthropic.claude-sonnet-4-6", + "vertex_ai/claude-sonnet-4-6", + "openrouter/anthropic/claude-sonnet-4.6", + ], +) +def test_max_effort_accepted_for_sonnet_46_variants(model): + """``effort='max'`` is documented as supported on Claude 4.6 (Opus + Sonnet) + and Claude 4.7 (https://platform.claude.com/docs/en/build-with-claude/effort). - Surfaces as a clean 400 BadRequestError, not a 500 ValueError. + Earlier versions of this test asserted a 400 for Sonnet 4.6, mirroring an + Opus-only allow-list in ``_apply_output_config``. That gate has since been + widened to ``_is_claude_4_6_model`` (Opus + Sonnet) and the + ``supports_max_reasoning_effort`` JSON flag, matching Anthropic's published + matrix. Verify the param actually flows through every Sonnet 4.6 id + variant our routing layer might see. """ config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] - with pytest.raises( - litellm.exceptions.BadRequestError, - match="effort='max' is not supported by this model", - ): - config.transform_request( - model="claude-sonnet-4-6-20260219", - messages=messages, - optional_params={"output_config": {"effort": "max"}}, - litellm_params={}, - headers={}, - ) + result = config.transform_request( + model=model, + messages=messages, + optional_params={"output_config": {"effort": "max"}}, + litellm_params={}, + headers={}, + ) + + assert result["output_config"]["effort"] == "max" def test_max_effort_accepted_for_opus_46(): @@ -2510,24 +2529,37 @@ def test_reasoning_effort_garbage_raises_bad_request(effort): @pytest.mark.parametrize( - "effort", - ["xhigh", "max"], + "effort,expected_budget", + [("xhigh", 8192), ("max", 16384)], ) -def test_reasoning_effort_unsupported_tier_on_budget_model_raises_bad_request( - effort, +def test_reasoning_effort_xhigh_max_maps_to_budget_on_budget_model( + effort, expected_budget ): - """``xhigh`` / ``max`` aren't defined for budget-mode (4.5) Claude models; - surface as a clean 400 instead of 500. + """``xhigh`` / ``max`` extend the legacy ``thinking.budget_tokens`` + progression (low=1024 / medium=2048 / high=4096 → xhigh=8192 / max=16384) + on budget-mode Claude models (haiku / 4.5 series). + + Adopted from #27051. Keeps the OpenAI-format ``reasoning_effort`` knob + usable across the full Claude lineup — adaptive models (4.6/4.7) route + these tiers via ``output_config.effort``; budget-mode models use the + extended budget. Anthropic's "max only on Mythos / Opus 4.7 / Opus 4.6 / + Sonnet 4.6" gating applies to the *adaptive enum*, not to the legacy + ``budget_tokens`` knob, which accepts any integer up to ``max_tokens``. """ config = AnthropicConfig() - with pytest.raises(litellm.exceptions.BadRequestError): - config.map_openai_params( - non_default_params={"reasoning_effort": effort}, - optional_params={}, - model="claude-sonnet-4-5-20250929", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"reasoning_effort": effort}, + optional_params={}, + model="claude-sonnet-4-5-20250929", + drop_params=False, + ) + + assert result["thinking"]["type"] == "enabled" + assert result["thinking"]["budget_tokens"] == expected_budget + # Budget-mode models must NOT carry an ``output_config`` payload — that + # path is exclusively for adaptive (4.6+) models. + assert "output_config" not in result def test_output_config_effort_empty_string_raises_bad_request(): diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index 900bb15545..20b5f95839 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -132,11 +132,11 @@ def test_invalid_reasoning_effort_raises_400(bad_effort): @pytest.mark.parametrize( "model,bad_effort", [ + # ``xhigh`` is Opus-4.7-only on the public Anthropic effort matrix, + # so Opus 4.6 / Sonnet 4.6 must still 400 on it. ("claude-opus-4-6", "xhigh"), ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh"), ("claude-sonnet-4-6", "xhigh"), - ("claude-sonnet-4-6", "max"), - ("bedrock/invoke/us.anthropic.claude-sonnet-4-6", "max"), ], ) def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort): @@ -160,6 +160,32 @@ def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort assert "not supported by this model" in str(exc_info.value) +@pytest.mark.parametrize( + "model", + [ + # ``max`` is documented as supported on Claude 4.6 (Opus + Sonnet) + # and Claude 4.7. Verify the /v1/messages route accepts it for + # Sonnet 4.6 variants instead of 400-ing client-side. + "claude-sonnet-4-6", + "bedrock/invoke/us.anthropic.claude-sonnet-4-6", + ], +) +def test_reasoning_effort_max_accepted_on_sonnet_46_messages(model): + config = AnthropicMessagesConfig() + optional_params = {"max_tokens": 1024, "reasoning_effort": "max"} + + result = config.transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=optional_params, + litellm_params={}, + headers={}, + ) + + output_config = result.get("output_config") + assert isinstance(output_config, dict) and output_config.get("effort") == "max" + + def test_explicit_output_config_wins_over_reasoning_effort(): """ Explicit native ``output_config.effort`` is never overridden by the diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 9da7a04604..271498887f 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -395,24 +395,39 @@ def test_reasoning_effort_garbage_raises_bad_request_converse(effort): ) -def test_output_config_effort_unsupported_max_on_sonnet_46_raises_bad_request(): - """``effort='max'`` is Opus-only. On Sonnet 4.6 the explicit-output_config - path must surface a 400 (matching the chat-completion validation), not - silently forward an unsupported tier to Bedrock.""" +@pytest.mark.parametrize( + "model", + [ + "bedrock/converse/us.anthropic.claude-sonnet-4-6", + "bedrock/converse/global.anthropic.claude-sonnet-4-6", + "bedrock/converse/eu.anthropic.claude-sonnet-4-6", + "bedrock/converse/au.anthropic.claude-sonnet-4-6", + ], +) +def test_output_config_effort_max_passes_through_on_sonnet_46_variants(model): + """``effort='max'`` is supported on Claude 4.6 (Opus + Sonnet) per + https://platform.claude.com/docs/en/build-with-claude/effort. The earlier + Opus-only allow-list in ``_validate_anthropic_adaptive_effort`` has been + widened to ``_is_claude_4_6_model`` (Opus + Sonnet) plus the + ``supports_max_reasoning_effort`` JSON flag. Verify the param actually + flows through to ``additionalModelRequestFields.output_config.effort`` + for every Bedrock Converse Sonnet 4.6 id variant.""" config = AmazonConverseConfig() messages = [{"role": "user", "content": "hi"}] - with pytest.raises(litellm.exceptions.BadRequestError): - config._transform_request( - model="bedrock/converse/us.anthropic.claude-sonnet-4-6", - messages=messages, - optional_params={ - "maxTokens": 256, - "output_config": {"effort": "max"}, - }, - litellm_params={}, - headers={}, - ) + result = config._transform_request( + model=model, + messages=messages, + optional_params={ + "maxTokens": 256, + "output_config": {"effort": "max"}, + }, + litellm_params={}, + headers={}, + ) + + additional = result.get("additionalModelRequestFields", {}) + assert additional.get("output_config") == {"effort": "max"} def test_get_supported_openai_params(): From 1a10746e95ed021f3657b47207094f7660410516 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 3 May 2026 17:30:26 +0000 Subject: [PATCH 12/25] fix(databricks): validate reasoning_effort and set output_config on adaptive Claude The Databricks path called `AnthropicConfig._map_reasoning_effort` for Claude models but never validated the effort string nor set `output_config.effort` for adaptive models (Claude 4.6/4.7). Since `_map_reasoning_effort` returns `type=adaptive` for ANY non-None / non-"none" string on adaptive models (including "disabled", "invalid", ""), Databricks silently accepted garbage and emitted a request without an `output_config.effort`, collapsing every adaptive tier to identical behavior. Match the Anthropic native, Bedrock Converse, Bedrock Invoke, and /v1/messages paths: when the resolved `thinking` is non-None on a 4.6/4.7 model, look up the value in `REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT` and either raise a clean `BadRequestError` or set `optional_params["output_config"]`. --- .../llms/databricks/chat/transformation.py | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 8ac438bbf0..a0b1ec3d69 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -21,6 +21,7 @@ from typing import ( import httpx from pydantic import BaseModel +import litellm from litellm.constants import RESPONSE_FORMAT_TOOL_NAME from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( _handle_invalid_parallel_tool_calls, @@ -56,7 +57,10 @@ from litellm.types.utils import ( Usage, ) -from ...anthropic.chat.transformation import AnthropicConfig +from ...anthropic.chat.transformation import ( + REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, + AnthropicConfig, +) from ...openai_like.chat.transformation import OpenAILikeChatConfig from ..common_utils import DatabricksBase, DatabricksException @@ -335,11 +339,40 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): # so the surfaced error carries the correct provider name (the # default is ``"anthropic"``, which would mislead users routing # via Databricks Foundation Model APIs). - optional_params["thinking"] = AnthropicConfig._map_reasoning_effort( - reasoning_effort=non_default_params.get("reasoning_effort"), + reasoning_effort_value = non_default_params.get("reasoning_effort") + mapped_thinking = AnthropicConfig._map_reasoning_effort( + reasoning_effort=reasoning_effort_value, model=model, llm_provider="databricks", ) + if mapped_thinking is None: + optional_params.pop("thinking", None) + optional_params.pop("output_config", None) + else: + optional_params["thinking"] = mapped_thinking + # For Claude 4.6/4.7 adaptive models, ``_map_reasoning_effort`` + # returns ``type=adaptive`` for ANY non-None / non-"none" + # string without validating the value, so reject unmapped + # efforts here and set ``output_config.effort`` (matching the + # Anthropic native / Bedrock Converse / Bedrock Invoke / + # /v1/messages paths). + if AnthropicConfig._is_claude_4_6_model( + model + ) or AnthropicConfig._is_claude_4_7_model(model): + mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort_value + ) + if mapped_effort is None: + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid reasoning_effort: {reasoning_effort_value!r}. " + f"Must be one of: 'minimal', 'low', " + f"'medium', 'high', 'xhigh', 'max', 'none'" + ), + model=model, + llm_provider="databricks", + ) + optional_params["output_config"] = {"effort": mapped_effort} optional_params.pop("reasoning_effort", None) ## handle thinking tokens self.update_optional_params_with_thinking_tokens( From 4f9a3a5c9f9052e0ddd11866bf2ca94858931273 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sun, 3 May 2026 23:48:38 -0700 Subject: [PATCH 13/25] fix(azure_ai/anthropic): promote output_config out of extra_body so validation runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `azure_ai` is registered in `litellm.openai_compatible_providers`, so `add_provider_specific_params_to_optional_params` (litellm/utils.py) auto-stuffs any non-OpenAI kwarg (e.g. `output_config={"effort": "..."}`) into `optional_params["extra_body"]`. `AzureAnthropicConfig.transform_request` then strips `extra_body` entirely on the way out, silently dropping the param — and `AnthropicConfig._apply_output_config` never sees it, so `effort="invalid"` / `effort="xhigh"` on a non-supporting model quietly reaches the model with default behavior instead of returning a clean 400 (as the native `anthropic` provider does). Promote the keys back to top-level `optional_params` (using `setdefault` so explicit top-level values win) before delegating to the parent `AnthropicConfig`. Apply in both `validate_environment` and `transform_request` so flag detection (`is_mcp_server_used`, etc.) and output-config validation both run. Surfaced by the QA matrix expansion on PR #27074: 20 cells where Azure returned 200 while `anthropic` returned 400 — all `output_config` mode across haiku_4_5, sonnet_4_5, opus_4_5, sonnet_4_6, opus_4_6, opus_4_7 families with `effort` in {invalid, xhigh, max, low, medium, high}. Tests: * `test_output_config_promoted_from_extra_body`: valid effort reaches data * `test_invalid_output_config_effort_raises_via_extra_body`: 400 on bad effort * `test_unsupported_effort_xhigh_raises_via_extra_body`: 400 on xhigh-on-Sonnet-4.6 * `test_extra_body_promotion_does_not_clobber_top_level`: setdefault semantics --- .../llms/azure_ai/anthropic/transformation.py | 43 +++++++ .../test_azure_anthropic_transformation.py | 116 ++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/litellm/llms/azure_ai/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py index e935aa1c05..f3bd6012ec 100644 --- a/litellm/llms/azure_ai/anthropic/transformation.py +++ b/litellm/llms/azure_ai/anthropic/transformation.py @@ -12,6 +12,33 @@ if TYPE_CHECKING: pass +def _promote_extra_body_to_optional_params(optional_params: dict) -> None: + """Promote anthropic-native passthrough keys out of ``extra_body``. + + ``azure_ai`` is registered in ``litellm.openai_compatible_providers``, so + ``add_provider_specific_params_to_optional_params`` (litellm/utils.py) + auto-stuffs any non-OpenAI kwarg (e.g. ``output_config={"effort": "..."}``) + into ``optional_params["extra_body"]``. For the Azure→Anthropic route the + user's intent is to forward those params to Anthropic, so promote them to + the top level of ``optional_params`` so: + + * ``AnthropicConfig._apply_output_config`` validates ``effort`` values + (matching the native ``anthropic`` provider's 400 on bad efforts). + * Valid passthroughs (e.g. ``output_config``, ``thinking``) actually + reach the request body instead of being silently dropped by the + ``data.pop("extra_body", None)`` strip below. + + ``setdefault`` is used so an explicit top-level value is never clobbered + by a duplicate inside ``extra_body``. + """ + extra_body = optional_params.get("extra_body") + if not isinstance(extra_body, dict) or not extra_body: + return + for k, v in extra_body.items(): + optional_params.setdefault(k, v) + optional_params.pop("extra_body", None) + + class AzureAnthropicConfig(AnthropicConfig): """ Azure Anthropic configuration that extends AnthropicConfig. @@ -39,6 +66,11 @@ class AzureAnthropicConfig(AnthropicConfig): 1. API key via 'api-key' header 2. Azure AD token via 'Authorization: Bearer ' header """ + # Promote anthropic-native passthrough keys (``output_config``, + # ``thinking``, ``mcp_servers``, ...) out of ``extra_body`` so the + # flag detection below (``is_mcp_server_used``, etc.) sees them. + _promote_extra_body_to_optional_params(optional_params) + # Convert dict to GenericLiteLLMParams if needed if isinstance(litellm_params, dict): # Ensure api_key is included if provided @@ -101,6 +133,17 @@ class AzureAnthropicConfig(AnthropicConfig): Transform request using parent AnthropicConfig, then remove unsupported params. Azure Anthropic doesn't support extra_body, max_retries, or stream_options parameters. """ + # Promote anthropic-native passthrough keys (``output_config``, + # ``thinking``, ...) out of ``extra_body`` BEFORE delegating to + # ``AnthropicConfig.transform_request``. Without this: + # * ``output_config`` is silently dropped by the ``extra_body`` pop + # below, and + # * ``_apply_output_config`` never validates ``effort`` values, so + # ``effort="invalid"`` quietly reaches the model with default + # behavior instead of returning a clean 400 (as the native + # ``anthropic`` provider does). + _promote_extra_body_to_optional_params(optional_params) + # Call parent transform_request data = super().transform_request( model=model, diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py index 06ef04ca2c..2504e9d5b8 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py @@ -291,6 +291,122 @@ class TestAzureAnthropicConfig: assert "anthropic-beta" in headers assert "compact-2026-01-12" in headers["anthropic-beta"] + def test_output_config_promoted_from_extra_body(self): + """Anthropic-native ``output_config`` routed via ``extra_body`` (by the + openai-compatible kwarg stuffer in ``litellm/utils.py``) must be + promoted to top-level ``optional_params`` before delegating to + ``AnthropicConfig.transform_request``. Otherwise the value is + silently dropped by the ``extra_body`` pop and validation never + runs. + """ + config = AzureAnthropicConfig() + + messages = [{"role": "user", "content": "Hello"}] + # Simulate what ``add_provider_specific_params_to_optional_params`` + # produces for ``litellm.completion(model="azure_ai/claude-...", + # output_config={"effort": "low"})``. + optional_params = { + "max_tokens": 100, + "extra_body": {"output_config": {"effort": "low"}}, + } + litellm_params = {"api_key": "test-key"} + headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} + + result = config.transform_request( + model="claude-opus-4-6", # supports output_config.effort + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + + # output_config must reach the request body (not be silently dropped) + assert "output_config" in result + assert result["output_config"] == {"effort": "low"} + # extra_body should be stripped on the way out + assert "extra_body" not in result + + def test_invalid_output_config_effort_raises_via_extra_body(self): + """``effort="invalid"`` arriving via ``extra_body`` must still raise + ``BadRequestError`` (matching the native ``anthropic`` provider). + Previously this was silently dropped on Azure, so unsupported + efforts reached the model with default behavior instead of + returning a clean 400. + """ + import litellm + + config = AzureAnthropicConfig() + + messages = [{"role": "user", "content": "Hello"}] + optional_params = { + "max_tokens": 100, + "extra_body": {"output_config": {"effort": "invalid"}}, + } + litellm_params = {"api_key": "test-key"} + headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} + + with pytest.raises(litellm.exceptions.BadRequestError) as exc_info: + config.transform_request( + model="claude-opus-4-6", + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + assert "Invalid effort value" in str(exc_info.value) + + def test_unsupported_effort_xhigh_raises_via_extra_body(self): + """``effort="xhigh"`` on a model that does not support it (e.g. + Sonnet 4.6) must raise ``BadRequestError`` even when arriving via + ``extra_body`` on Azure.""" + import litellm + + config = AzureAnthropicConfig() + + messages = [{"role": "user", "content": "Hello"}] + optional_params = { + "max_tokens": 100, + "extra_body": {"output_config": {"effort": "xhigh"}}, + } + litellm_params = {"api_key": "test-key"} + headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} + + with pytest.raises(litellm.exceptions.BadRequestError) as exc_info: + config.transform_request( + model="claude-sonnet-4-6", + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + assert "xhigh" in str(exc_info.value) + + def test_extra_body_promotion_does_not_clobber_top_level(self): + """If a key exists at both top-level ``optional_params`` and + inside ``extra_body``, the top-level value wins (``setdefault`` + semantics). This protects against a caller who explicitly sets a + param at top-level and accidentally also has it in extra_body.""" + config = AzureAnthropicConfig() + + messages = [{"role": "user", "content": "Hello"}] + optional_params = { + "max_tokens": 100, + "output_config": {"effort": "low"}, # top-level wins + "extra_body": {"output_config": {"effort": "high"}}, # ignored + } + litellm_params = {"api_key": "test-key"} + headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} + + result = config.transform_request( + model="claude-opus-4-6", + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + + assert result["output_config"] == {"effort": "low"} + def test_context_management_mixed_edits_beta_headers(self): """Test that context_management with both compact and other edits adds both beta headers""" config = AzureAnthropicConfig() From f8f07c5cb7ac5d81458dd18f40ea1d630d2d0d05 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 00:47:55 -0700 Subject: [PATCH 14/25] refactor(anthropic): extract _validate_effort_for_model to prevent drift The chat completion path (`_apply_output_config`) and the /v1/messages pass-through (`AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic`) both gate `max` / `xhigh` per model. The two sites had diverged from near-identical copies into separately maintained blocks, creating a real drift risk when a new model tier (e.g. Claude 4.8) lands -- a contributor could update one site and miss the other. Centralise the gating in `AnthropicConfig._validate_effort_for_model`, which returns an error message string or `None`. Each call site keeps its own provider-appropriate exception type (`BadRequestError` for the chat path, `AnthropicError` for the /v1/messages pass-through) but the gating decision now comes from one place. Net -11 LOC. Adds a parametrised unit test exercising the helper directly across 4.5 / 4.6 / 4.7 model families and `max` / `xhigh` / lower-effort inputs. Existing tests at both call sites continue to pass unchanged. Addresses Greptile finding on PR #27074. --- litellm/llms/anthropic/chat/transformation.py | 71 +++++++++++-------- .../messages/transformation.py | 42 +++-------- .../test_anthropic_chat_transformation.py | 38 ++++++++++ 3 files changed, 89 insertions(+), 62 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 9de89d8ad5..a971bf1e96 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -275,6 +275,41 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): pass return False + @staticmethod + def _validate_effort_for_model(model: str, effort: Optional[str]) -> Optional[str]: + """Return ``None`` if ``effort`` is allowed on ``model``, else an error message. + + Centralises per-model gating for ``max`` and ``xhigh`` so the chat + completion path (``_apply_output_config``) and the /v1/messages + pass-through (``AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic``) + can't drift when a new model tier is added. Caller raises the + provider-appropriate exception type using the returned message. + + ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 + adaptive-thinking models per + https://platform.claude.com/docs/en/build-with-claude/effort. The + data-driven ``supports_max_reasoning_effort`` flag in + ``model_prices_and_context_window.json`` is the source of truth; + family-level ``_is_claude_4_6_model`` / ``_is_claude_4_7_model`` + checks remain as a fallback for OpenRouter / GitHub Copilot / + Vercel / Bedrock variants whose model-map entries don't yet carry + the flag. + + ``xhigh`` is purely data-driven via ``supports_xhigh_reasoning_effort`` + so enabling it for a new model is a model-map-only change. + """ + if effort == "max" and not ( + AnthropicConfig._is_claude_4_6_model(model) + or AnthropicConfig._is_claude_4_7_model(model) + or AnthropicConfig._supports_effort_level(model, "max") + ): + return f"effort='max' is not supported by this model. Got model: {model}" + if effort == "xhigh" and not AnthropicConfig._supports_effort_level( + model, "xhigh" + ): + return f"effort='xhigh' is not supported by this model. Got model: {model}" + return None + @staticmethod def _model_supports_effort_param(model: str) -> bool: """Whether the model accepts ``output_config.effort`` at all. @@ -1710,36 +1745,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): model=model, llm_provider=self.custom_llm_provider or "anthropic", ) - # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 - # adaptive-thinking models (per - # https://platform.claude.com/docs/en/build-with-claude/effort). - # Prefer the data-driven ``supports_max_reasoning_effort`` flag in - # ``model_prices_and_context_window.json`` so new variants only - # require a model-map update. Family-level ``_is_claude_4_6_model`` - # / ``_is_claude_4_7_model`` checks remain as a fallback for - # OpenRouter/GitHub Copilot/Vercel/Bedrock variants whose entries - # don't yet carry the flag. - if effort == "max" and not ( - self._is_claude_4_6_model(model) - or self._is_claude_4_7_model(model) - or self._supports_effort_level(model, "max") - ): + # Per-model gating for ``max`` / ``xhigh`` is centralised in + # ``_validate_effort_for_model`` so the chat path and the + # /v1/messages pass-through stay in lock-step when a new model + # tier lands. + gate_error = self._validate_effort_for_model(model, effort) + if gate_error is not None: raise litellm.exceptions.BadRequestError( - message=( - f"effort='max' is not supported by this model. " - f"Got model: {model}" - ), - model=model, - llm_provider=self.custom_llm_provider or "anthropic", - ) - # ``xhigh`` is data-driven via ``supports_xhigh_reasoning_effort`` so - # enabling it for a new model is a pure model-map change. - if effort == "xhigh" and not self._supports_effort_level(model, "xhigh"): - raise litellm.exceptions.BadRequestError( - message=( - f"effort='xhigh' is not supported by this model. " - f"Got model: {model}" - ), + message=gate_error, model=model, llm_provider=self.custom_llm_provider or "anthropic", ) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index f756279877..8f35e79f56 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -239,39 +239,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ), status_code=400, ) - # Per-model gating: ``xhigh`` and ``max`` are only valid on - # specific tiers (Opus 4.6/4.7 for max; data-driven for xhigh). - # The chat completion path enforces this via - # ``_apply_output_config``; mirror it here so /v1/messages - # callers see a clean 400 instead of a provider-side error. - # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude - # 4.7 adaptive-thinking models. Prefer the data-driven - # ``supports_max_reasoning_effort`` flag in - # ``model_prices_and_context_window.json``; family-level checks - # are a fallback for variants whose entries don't yet carry the - # flag. - if mapped_effort == "max" and not ( - AnthropicConfig._is_claude_4_6_model(model) - or AnthropicConfig._is_claude_4_7_model(model) - or AnthropicConfig._supports_effort_level(model, "max") - ): - raise AnthropicError( - message=( - f"effort='max' is not supported by this model. " - f"Got model: {model}" - ), - status_code=400, - ) - if mapped_effort == "xhigh" and not AnthropicConfig._supports_effort_level( - model, "xhigh" - ): - raise AnthropicError( - message=( - f"effort='xhigh' is not supported by this model. " - f"Got model: {model}" - ), - status_code=400, - ) + # Per-model gating for ``max`` / ``xhigh`` is centralised in + # ``AnthropicConfig._validate_effort_for_model`` so the chat + # completion path and this /v1/messages pass-through stay in + # lock-step when a new model tier lands. + gate_error = AnthropicConfig._validate_effort_for_model( + model, mapped_effort + ) + if gate_error is not None: + raise AnthropicError(message=gate_error, status_code=400) existing_output_config = optional_params.get("output_config") if not isinstance(existing_output_config, dict): existing_output_config = {} 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 1aa664f657..1aa753ff8f 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 @@ -2101,6 +2101,44 @@ def test_supports_effort_level_handles_provider_prefixes(model, level, expected) assert AnthropicConfig._supports_effort_level(model, level) is expected +@pytest.mark.parametrize( + "model,effort,expect_error", + [ + # ``max`` accepted on 4.6 / 4.7 (family fallback) and rejected on 4.5. + ("claude-opus-4-6", "max", False), + ("claude-sonnet-4-6", "max", False), + ("claude-opus-4-7", "max", False), + ("claude-opus-4-5-20251101", "max", True), + ("claude-sonnet-4-5", "max", True), + # ``xhigh`` data-driven; only 4.7 carries the flag in the model map. + ("claude-opus-4-7", "xhigh", False), + ("claude-opus-4-6", "xhigh", True), + ("claude-sonnet-4-6", "xhigh", True), + # Lower efforts and ``None`` always pass the gate. + ("claude-opus-4-5-20251101", "high", False), + ("claude-haiku-4-5", "low", False), + ("claude-opus-4-5-20251101", None, False), + ], +) +def test_validate_effort_for_model_centralises_per_model_gating( + model, effort, expect_error +): + """``_validate_effort_for_model`` is the single source of truth for the + per-model ``max`` / ``xhigh`` gating that ``_apply_output_config`` (chat + completion path) and ``AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic`` + (/v1/messages pass-through) both rely on. Both call sites raise their + own provider-appropriate exception, but the gating decision must come + from one place to prevent drift when a new model tier lands. + """ + err = AnthropicConfig._validate_effort_for_model(model, effort) + if expect_error: + assert err is not None + assert effort in err + assert model in err + else: + assert err is None + + def test_transform_request_uses_dynamic_max_tokens(): """ Test that transform_request uses dynamic max_tokens based on model From 2c9166c4f3ecc807432d0bcb544900e56ddd22e2 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 00:52:09 -0700 Subject: [PATCH 15/25] fix(databricks): narrow reasoning_effort_value to str for mypy `non_default_params.get("reasoning_effort")` returns `Any | None`, but `REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get()` expects `str`. Mypy flagged this on the strict pass. Narrow with `isinstance` before the lookup; non-strings fall through to the existing `BadRequestError` below with a clean validation message, so behavior is unchanged. Fixes a regression introduced by 1a10746e95 in this PR. --- litellm/llms/databricks/chat/transformation.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index a0b1ec3d69..34cf174c3f 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -359,9 +359,15 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): if AnthropicConfig._is_claude_4_6_model( model ) or AnthropicConfig._is_claude_4_7_model(model): - mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( - reasoning_effort_value - ) + # ``reasoning_effort_value`` comes from ``non_default_params`` + # so its static type is ``Any | None``. Narrow to ``str`` for + # the mapping lookup; non-strings fall through to the + # ``BadRequestError`` below with a clean validation message. + mapped_effort: Optional[str] = None + if isinstance(reasoning_effort_value, str): + mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( + reasoning_effort_value + ) if mapped_effort is None: raise litellm.exceptions.BadRequestError( message=( From f4d6d5953d6f6524fd66d92831a693c156f730d8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 09:24:59 -0700 Subject: [PATCH 16/25] test(anthropic/chat): force PR-local model_cost map via autouse fixture CI runs without LITELLM_LOCAL_MODEL_COST_MAP=True, so litellm.model_cost is loaded from main-branch JSON (default model_cost_map_url) instead of the PR's checked-out model_prices_and_context_window.json. Tests that assert per-model flags added in this PR (supports_max_reasoning_effort, supports_xhigh_reasoning_effort) therefore pass locally but fail in CI with 'AssertionError: assert False is True' on 5 cases: - test_anthropic_model_supports_effort_param_recognizes_supporting_models [anthropic.claude-mythos-preview, bedrock/.../mythos-preview, claude-opus-4-5-20251101] - test_supports_effort_level_handles_provider_prefixes [bedrock/invoke/us.anthropic.claude-sonnet-4-6-max-True, claude-sonnet-4-6-max-True] Add an autouse fixture at tests/test_litellm/llms/anthropic/chat/conftest.py that monkey-patches litellm.model_cost to the PR-local JSON for every test in this directory. The parent conftest already snapshots+restores litellm.model_cost per-function, so the mutation is contained. This is a scoped workaround. The proper fix is to set the env var globally in the test workflow once the ~10 inline self-set test files are audited; tracking that as a follow-up issue. --- .../llms/anthropic/chat/conftest.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_litellm/llms/anthropic/chat/conftest.py diff --git a/tests/test_litellm/llms/anthropic/chat/conftest.py b/tests/test_litellm/llms/anthropic/chat/conftest.py new file mode 100644 index 0000000000..7dc6aeeb90 --- /dev/null +++ b/tests/test_litellm/llms/anthropic/chat/conftest.py @@ -0,0 +1,54 @@ +""" +Local-conftest for ``tests/test_litellm/llms/anthropic/chat``. + +Why this exists +--------------- +``litellm.model_cost`` is loaded once at ``litellm.__init__`` time. By default +it fetches ``model_prices_and_context_window.json`` from the **main branch on +GitHub** (``litellm.model_cost_map_url``) — *not* from the PR-branch JSON in +the working tree. That works fine in production (operators get new models +without redeploying litellm) but is the wrong default for tests, which need +to validate the code in front of them against the data in front of them. + +Several anthropic chat transformation tests (``test_supports_effort_level_*``, +``test_anthropic_model_supports_effort_param_*``) assert per-model flags +like ``supports_max_reasoning_effort`` / ``supports_xhigh_reasoning_effort`` +that may exist in the PR-local JSON but not yet in main's JSON. Without this +fixture those tests pass locally (where AGENTS.md tells contributors to set +``LITELLM_LOCAL_MODEL_COST_MAP=True``) but fail in CI (which doesn't set the +env var) — the chicken-and-egg PR adds flag → CI fetches main without flag +→ test fails → flag never lands on main. + +The fixture force-loads ``litellm.model_cost`` from the local JSON for every +test in this directory. ``tests/test_litellm/conftest.py`` already snapshots +and restores ``litellm.model_cost`` per-function, so this mutation is safe +and contained. + +This is a scoped workaround. The proper fix is to set +``LITELLM_LOCAL_MODEL_COST_MAP=True`` globally in the test workflow once the +~10 inline-set test files have been audited and the few tests that exercise +the remote-fetch / integrity-validation path have been given carve-outs. +Tracked at https://github.com/BerriAI/litellm/issues/27122. +""" + +import os + +import pytest + +import litellm +from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map + + +@pytest.fixture(autouse=True) +def _use_pr_local_model_cost_map(monkeypatch): + """Force ``litellm.model_cost`` to the PR-branch JSON for the duration of + each test. ``monkeypatch`` reverts the env var after the test; the parent + conftest restores ``litellm.model_cost`` from its snapshot. + """ + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + monkeypatch.setattr( + litellm, + "model_cost", + get_model_cost_map(url=litellm.model_cost_map_url), + ) + yield From c1708ddbba21fcbc534df86fe6593f9e95736467 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 10:34:31 -0700 Subject: [PATCH 17/25] fix(xai): fold reasoning_tokens into completion_tokens to satisfy OpenAI invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xAI's chat completions API accounts reasoning_tokens separately from completion_tokens, but rolls them into total_tokens. This breaks the OpenAI invariant total_tokens == prompt_tokens + completion_tokens that downstream consumers (including litellm's own _usage_format_tests in tests/llm_translation/base_llm_unit_tests.py:58) rely on. Live capture (grok-3-mini-beta, 2026-05-04): prompt=14, completion=10, total=336, reasoning=312 14 + 10 = 24, NOT 336. OpenAI's o1/o3 reasoning models include reasoning_tokens in completion_tokens, leaving the prompt+completion=total invariant intact. xAI deviates. This patch aligns xAI to OpenAI semantics by folding reasoning_tokens into completion_tokens after the parent OpenAI parser runs. The fold is idempotent and defensive: - Only fires when total_tokens == prompt_tokens + completion_tokens + reasoning_tokens (the documented xAI shape). Refuses to fold if the gap doesn't match, guarding against silent corruption when xAI changes accounting. - Skips if completion_tokens already covers the gap (already normalised — e.g. cost calc replays a previously-folded Usage). xai.cost_calculator.cost_per_token already added reasoning_tokens to the visible completion count for billing. Post-fold the Usage block now satisfies that invariant directly, so the cost calc would double-bill. Updated cost_per_token to detect the OpenAI-normalised shape (total == prompt + completion) and skip the reasoning add-on in that case, falling through to the legacy raw-shape behaviour for callers that bypass the transformation (e.g. proxy log replay). Tests: - Adds TestXAIReasoningTokenFolding covering: gap-explained-fold, idempotent-no-double-fold, no-reasoning-skip, gap-mismatch-skip. - Adds test_already_normalised_usage_does_not_double_count_reasoning to lock the cost-calc idempotency. - Updates 7 pre-existing cost-calc tests whose total_tokens was internally inconsistent (used the OpenAI-normalised total but kept reasoning_tokens external) to use the documented xAI raw shape total = prompt + visible completion + reasoning. Pre-existing values masked the missing-fold by accident. Verified end-to-end against the live xAI API: LITELLM_LOCAL_MODEL_COST_MAP=False (CI default) + XAI_API_KEY set + pytest tests/llm_translation/test_xai.py::TestXAIChat::test_prompt_caching -> PASSED in 18.81s (was: AssertionError on usage.total_tokens == usage.prompt_tokens + usage.completion_tokens) 20/20 tests in tests/test_litellm/llms/xai/test_xai_cost_calculator.py and 8/8 in tests/test_litellm/llms/xai/test_xai_chat_transformation.py pass. --- litellm/llms/xai/chat/transformation.py | 49 ++++++++++ litellm/llms/xai/cost_calculator.py | 16 +++- .../llms/xai/test_xai_chat_transformation.py | 96 ++++++++++++++++++- .../llms/xai/test_xai_cost_calculator.py | 54 +++++++++-- 4 files changed, 203 insertions(+), 12 deletions(-) diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index 64b4a545ac..b1c0aaccf1 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -223,8 +223,57 @@ class XAIChatConfig(OpenAIGPTConfig): self._enhance_usage_with_xai_web_search_fields(response, raw_response_json) except Exception as e: verbose_logger.debug(f"Error extracting X.AI web search usage: {e}") + + # X.AI excludes reasoning_tokens from completion_tokens, breaking the + # OpenAI invariant total_tokens == prompt_tokens + completion_tokens. + # OpenAI o1/o3 fold reasoning into completion_tokens; align X.AI to + # match so downstream consumers (including litellm's own usage tests) + # see a self-consistent Usage object. Cost calc already accounts for + # reasoning tokens separately in xai.cost_calculator. + self._fold_reasoning_tokens_into_completion(response) return response + @staticmethod + def _fold_reasoning_tokens_into_completion(model_response: ModelResponse) -> None: + """Reconcile xAI Usage to the OpenAI invariant. + + xAI returns ``completion_tokens`` covering only visible output and + accounts ``reasoning_tokens`` separately, while still rolling them + into ``total_tokens``. OpenAI's published contract (o1/o3) includes + reasoning in ``completion_tokens``. Tests that assert + ``total_tokens == prompt_tokens + completion_tokens`` (e.g. + ``_usage_format_tests``) fail on the raw xAI shape. + + This helper is idempotent: if ``completion_tokens`` already covers + the gap, no change is made. + """ + usage = getattr(model_response, "usage", None) + if usage is None: + return + + details = getattr(usage, "completion_tokens_details", None) + reasoning_tokens = ( + int(getattr(details, "reasoning_tokens", 0) or 0) if details else 0 + ) + if reasoning_tokens <= 0: + return + + prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) + completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) + total_tokens = int(getattr(usage, "total_tokens", 0) or 0) + + # Already consistent — nothing to do. + if total_tokens == prompt_tokens + completion_tokens: + return + + # Only fold when xAI's accounting (total = prompt + completion + + # reasoning) explains the gap. This guards against double-counting + # if xAI ever changes their semantics. + if total_tokens != prompt_tokens + completion_tokens + reasoning_tokens: + return + + usage.completion_tokens = completion_tokens + reasoning_tokens + def _enhance_usage_with_xai_web_search_fields( self, model_response: ModelResponse, raw_response_json: dict ) -> None: diff --git a/litellm/llms/xai/cost_calculator.py b/litellm/llms/xai/cost_calculator.py index 0cfcfe9841..df5a7beffa 100644 --- a/litellm/llms/xai/cost_calculator.py +++ b/litellm/llms/xai/cost_calculator.py @@ -26,15 +26,27 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd """ # XAI-specific completion cost calculation - # For XAI models, completion is billed as (visible completion tokens + reasoning tokens) + # For XAI models, completion is billed as (visible completion tokens + reasoning tokens). + # The transformation layer normalises Usage to the OpenAI invariant + # (completion_tokens includes reasoning_tokens), so detect that and avoid + # double-counting. Fall back to the raw xAI shape (visible-only completion + + # reasoning kept in completion_tokens_details) for callers that bypass the + # transformation, e.g. proxy logs replayed straight into cost calc. + prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) + total_tokens = int(getattr(usage, "total_tokens", 0) or 0) reasoning_tokens = 0 if hasattr(usage, "completion_tokens_details") and usage.completion_tokens_details: reasoning_tokens = int( getattr(usage.completion_tokens_details, "reasoning_tokens", 0) or 0 ) - total_completion_tokens = completion_tokens + reasoning_tokens + already_normalised = total_tokens == prompt_tokens + completion_tokens + total_completion_tokens = ( + completion_tokens + if already_normalised + else completion_tokens + reasoning_tokens + ) modified_usage = Usage( prompt_tokens=usage.prompt_tokens, diff --git a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py index 5a236de900..a8bbb880c5 100644 --- a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py +++ b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py @@ -6,6 +6,98 @@ sys.path.insert( ) # Adds the parent directory to the system path from litellm.llms.xai.chat.transformation import XAIChatConfig +from litellm.types.utils import ( + CompletionTokensDetailsWrapper, + ModelResponse, + Usage, +) + + +class TestXAIReasoningTokenFolding: + """xAI breaks the OpenAI invariant total = prompt + completion by accounting + reasoning_tokens separately. ``_fold_reasoning_tokens_into_completion`` + re-aligns Usage so downstream consumers see the OpenAI shape (o1/o3 + semantics: completion_tokens includes reasoning).""" + + @staticmethod + def _make_response( + prompt_tokens: int, + completion_tokens: int, + total_tokens: int, + reasoning_tokens: int = 0, + ) -> ModelResponse: + details = ( + CompletionTokensDetailsWrapper(reasoning_tokens=reasoning_tokens) + if reasoning_tokens + else None + ) + usage = Usage( + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens, + completion_tokens_details=details, + ) + response = ModelResponse() + setattr(response, "usage", usage) + return response + + def test_should_fold_when_total_explained_by_reasoning_gap(self): + # Real xAI live shape captured 2026-05-04: prompt=14, completion=10, + # total=336, reasoning=312. 14+10+312 == 336. + response = self._make_response( + prompt_tokens=14, + completion_tokens=10, + total_tokens=336, + reasoning_tokens=312, + ) + + XAIChatConfig._fold_reasoning_tokens_into_completion(response) + + usage = response.usage + assert usage.completion_tokens == 322 + assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens + + def test_should_not_fold_when_already_normalised(self): + # OpenAI-normalised shape: completion already includes reasoning. + response = self._make_response( + prompt_tokens=14, + completion_tokens=322, + total_tokens=336, + reasoning_tokens=312, + ) + + XAIChatConfig._fold_reasoning_tokens_into_completion(response) + + # Idempotent — no double-fold. + assert response.usage.completion_tokens == 322 + + def test_should_skip_when_no_reasoning_tokens(self): + response = self._make_response( + prompt_tokens=14, + completion_tokens=10, + total_tokens=24, + reasoning_tokens=0, + ) + + XAIChatConfig._fold_reasoning_tokens_into_completion(response) + + assert response.usage.completion_tokens == 10 + + def test_should_skip_when_gap_does_not_match_reasoning(self): + # Defensive guard: if xAI ever changes accounting and the gap stops + # equalling reasoning_tokens, refuse to fold rather than corrupt. + response = self._make_response( + prompt_tokens=14, + completion_tokens=10, + total_tokens=999, + reasoning_tokens=312, + ) + + XAIChatConfig._fold_reasoning_tokens_into_completion(response) + + # No fold; original values preserved. + assert response.usage.completion_tokens == 10 + assert response.usage.total_tokens == 999 class TestXAIParallelToolCalls: @@ -14,9 +106,7 @@ class TestXAIParallelToolCalls: def test_get_supported_openai_params_includes_parallel_tool_calls(self): """Test that parallel_tool_calls is in supported parameters.""" config = XAIChatConfig() - supported_params = config.get_supported_openai_params( - "xai/grok-4.20" - ) + supported_params = config.get_supported_openai_params("xai/grok-4.20") assert "parallel_tool_calls" in supported_params def test_transform_request_preserves_parallel_tool_calls(self): diff --git a/tests/test_litellm/llms/xai/test_xai_cost_calculator.py b/tests/test_litellm/llms/xai/test_xai_cost_calculator.py index 0463199f7e..a8aae34622 100644 --- a/tests/test_litellm/llms/xai/test_xai_cost_calculator.py +++ b/tests/test_litellm/llms/xai/test_xai_cost_calculator.py @@ -107,10 +107,11 @@ class TestXAICostCalculator: def test_grok_4_cost_calculation(self): """Test cost calculation for grok-4 model.""" + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=10, completion_tokens=200, - total_tokens=210, + total_tokens=360, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -133,10 +134,11 @@ class TestXAICostCalculator: def test_grok_3_fast_beta_cost_calculation(self): """Test cost calculation for grok-3-fast-beta model.""" + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=20, completion_tokens=300, - total_tokens=320, + total_tokens=520, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -174,10 +176,11 @@ class TestXAICostCalculator: def test_edge_case_large_reasoning_tokens(self): """Test cost calculation when reasoning_tokens is larger than completion_tokens.""" + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=12, completion_tokens=50, # Less than reasoning_tokens - total_tokens=62, + total_tokens=162, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -201,10 +204,11 @@ class TestXAICostCalculator: def test_tiered_pricing_above_128k_tokens(self): """Test tiered pricing for tokens above 128k.""" # Test with grok-4-fast-reasoning which has tiered pricing + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=150000, # Above 128k threshold completion_tokens=100000, # Above 128k threshold - total_tokens=250000, + total_tokens=300000, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -230,10 +234,11 @@ class TestXAICostCalculator: def test_tiered_pricing_below_128k_tokens(self): """Test that regular pricing is used for tokens below 128k threshold.""" # Test with grok-4-fast-reasoning which has tiered pricing + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=100000, # Below 128k threshold completion_tokens=50000, - total_tokens=150000, + total_tokens=160000, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -258,10 +263,11 @@ class TestXAICostCalculator: def test_tiered_pricing_grok_4_latest(self): """Test tiered pricing for grok-4-latest model.""" + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=200000, # Above 128k threshold completion_tokens=100000, - total_tokens=300000, + total_tokens=350000, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -286,10 +292,11 @@ class TestXAICostCalculator: def test_tiered_pricing_output_tokens_below_128k(self): """Test that output tokens get tiered rate when input tokens > 128k, even if output tokens < 128k.""" + # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=150000, # Above 128k threshold completion_tokens=50000, # Below 128k threshold - total_tokens=200000, + total_tokens=210000, completion_tokens_details=CompletionTokensDetailsWrapper( accepted_prediction_tokens=0, audio_tokens=0, @@ -331,6 +338,39 @@ class TestXAICostCalculator: assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-10) assert math.isclose(completion_cost, expected_completion_cost, rel_tol=1e-10) + def test_already_normalised_usage_does_not_double_count_reasoning(self): + """Cost calc receives Usage post-transformation (OpenAI invariant). + + After XAIChatConfig.transform_response folds reasoning_tokens into + completion_tokens, the Usage block satisfies + ``total_tokens == prompt_tokens + completion_tokens``. Cost calc must + detect this and skip the reasoning_tokens add-on, otherwise it + double-bills the reasoning tokens. + """ + # OpenAI-normalised shape: completion_tokens already includes the + # 100 reasoning tokens (so 100 visible + 100 reasoning -> 200). + usage = Usage( + prompt_tokens=12, + completion_tokens=200, + total_tokens=212, + completion_tokens_details=CompletionTokensDetailsWrapper( + accepted_prediction_tokens=0, + audio_tokens=0, + reasoning_tokens=100, + rejected_prediction_tokens=0, + text_tokens=None, + ), + ) + + prompt_cost, completion_cost = cost_per_token(model="grok-3-mini", usage=usage) + + # Bill exactly what completion_tokens reports — no double-add. + expected_prompt_cost = 12 * 3e-7 + expected_completion_cost = 200 * 5e-7 + + assert math.isclose(prompt_cost, expected_prompt_cost, rel_tol=1e-10) + assert math.isclose(completion_cost, expected_completion_cost, rel_tol=1e-10) + def test_web_search_cost_calculation(self): """Test web search cost calculation for X.AI models.""" # Test with web_search_requests in prompt_tokens_details (primary path) From 5d124892d249004ce4de761c6b615375cfd2a0a4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 18:01:30 +0000 Subject: [PATCH 18/25] refactor(bedrock/converse): delegate effort gating to AnthropicConfig._validate_effort_for_model Removes the duplicated max/xhigh gating logic in _validate_anthropic_adaptive_effort and the now-unused _supports_effort_level_on_bedrock helper. Per-model gating now flows through the centralized AnthropicConfig._validate_effort_for_model (whose _supports_effort_level already strips Bedrock prefixes), so the chat completion, /v1/messages, and Bedrock Converse paths can't drift when a new gated effort tier is added. --- .../bedrock/chat/converse_transformation.py | 61 +++---------------- 1 file changed, 9 insertions(+), 52 deletions(-) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 14c7c3030c..d3dff398a6 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -511,34 +511,17 @@ class AmazonConverseConfig(BaseConfig): ) optional_params["output_config"] = {"effort": mapped_effort} - @staticmethod - def _supports_effort_level_on_bedrock(model: str, level: str) -> bool: - """Look up ``supports_{level}_reasoning_effort`` for a Bedrock-routed - model id directly in ``litellm.model_cost`` so the bedrock provider - prefix is irrelevant to the lookup. ``AnthropicConfig._supports_effort_level`` - hard-codes ``custom_llm_provider="anthropic"`` and returns False for - the same effort level on a Bedrock model id. - """ - try: - base_model = BedrockModelInfo.get_base_model(model) - for key in (model, base_model, f"bedrock/{base_model}"): - if key and key in litellm.model_cost: - if ( - litellm.model_cost[key].get( - f"supports_{level}_reasoning_effort" - ) - is True - ): - return True - except Exception: - pass - return False - @staticmethod def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None: """Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7 on Bedrock. Raises ``BadRequestError`` (clean 400) instead of letting a downstream ``ValueError`` surface as 500. + + Per-model gating for ``max``/``xhigh`` is delegated to + ``AnthropicConfig._validate_effort_for_model`` so the Bedrock Converse + path and the Anthropic chat / ``/v1/messages`` paths can't drift when + a new gated effort tier is added. ``_supports_effort_level`` on + ``AnthropicConfig`` already handles Bedrock-prefixed model ids. """ valid_efforts = {"high", "medium", "low", "xhigh", "max"} if effort not in valid_efforts: @@ -551,36 +534,10 @@ class AmazonConverseConfig(BaseConfig): model=model, llm_provider="bedrock_converse", ) - # ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 - # adaptive-thinking models. Prefer the data-driven - # ``supports_max_reasoning_effort`` flag in - # ``model_prices_and_context_window.json`` so new variants only - # require a model-map update; family-level checks remain a fallback - # for Bedrock model ids whose entries don't yet carry the flag. - if effort == "max" and not ( - AnthropicConfig._is_claude_4_6_model(model) - or AnthropicConfig._is_claude_4_7_model(model) - or AmazonConverseConfig._supports_effort_level_on_bedrock(model, "max") - ): + error = AnthropicConfig._validate_effort_for_model(model=model, effort=effort) + if error is not None: raise litellm.exceptions.BadRequestError( - message=( - f"effort='max' is not supported by this model. " - f"Got model: {model}" - ), - model=model, - llm_provider="bedrock_converse", - ) - if ( - effort == "xhigh" - and not AmazonConverseConfig._supports_effort_level_on_bedrock( - model, "xhigh" - ) - ): - raise litellm.exceptions.BadRequestError( - message=( - f"effort='xhigh' is not supported by this model. " - f"Got model: {model}" - ), + message=error, model=model, llm_provider="bedrock_converse", ) From 98ced0ae4371b47d6f70527be7054c18df755266 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 18:58:22 +0000 Subject: [PATCH 19/25] refactor(anthropic): drive adaptive-thinking gate via supports_adaptive_thinking flag Three of greptile's open comments on #27074 (P2 converse:512, P1 databricks:361, and the underlying capability-flag policy rule) flagged the same pattern: _is_claude_4_6_model(...) or _is_claude_4_7_model(...) used inline as a runtime 'is this an adaptive-thinking model?' check. That requires a code release each time a new adaptive Claude lands. Consolidate the inline gating to AnthropicModelInfo._is_adaptive_thinking_model, and switch the helper itself to read a new supports_adaptive_thinking flag from `model_prices_and_context_window.json` via `_supports_factory`, falling back to the family pattern only when the model-map entry doesn't carry the flag (preserves OpenRouter / Vercel / Bedrock-prefixed variants that route through the same code path with non-canonical ids). Adds `supports_adaptive_thinking: true` to the four 4.6/4.7 anthropic entries (opus-4-6 + dated, opus-4-7 + dated, sonnet-4-6). Bedrock-prefixed and Vertex-prefixed entries don't need the flag because both fall back through the family pattern (the helper short-circuits early on True from either path) and the bedrock/vertex Claude IDs all match the existing opus-4-{6,7} / sonnet-4-{6,7} pattern. Affected call sites: - `bedrock/chat/converse_transformation.py:_handle_reasoning_effort_parameter` - `anthropic/chat/transformation.py:_map_reasoning_effort` - `anthropic/chat/transformation.py:map_openai_params` (output_config branch) - `databricks/chat/transformation.py:map_openai_params` (output_config branch) The remaining `_is_claude_4_6_model` / `_is_claude_4_7_model` references in `AnthropicConfig._validate_effort_for_model` and `AnthropicConfig.get_supported_openai_params` are intentionally retained: they're per-model gating fallbacks for variants whose model-map entries don't yet carry the `supports_max_reasoning_effort` / `supports_reasoning` flag. Those are documented in-place. Tests: 537 anthropic/bedrock/databricks/vertex/messages tests pass. Co-authored-by: Mateo Wang --- litellm/llms/anthropic/chat/transformation.py | 15 +++++++------ litellm/llms/anthropic/common_utils.py | 21 ++++++++++++++++++- .../bedrock/chat/converse_transformation.py | 17 ++++++++------- .../llms/databricks/chat/transformation.py | 12 ++++++----- model_prices_and_context_window.json | 5 +++++ 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index a971bf1e96..7f16c69481 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -920,9 +920,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): """ if reasoning_effort is None or reasoning_effort == "none": return None - if AnthropicConfig._is_claude_4_6_model( - model - ) or AnthropicConfig._is_claude_4_7_model(model): + if AnthropicConfig._is_adaptive_thinking_model(model): return AnthropicThinkingParam( type="adaptive", ) @@ -1262,11 +1260,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # For Claude 4.6+ models, effort is controlled via output_config, - # not thinking budget_tokens. Map reasoning_effort to output_config. - if AnthropicConfig._is_claude_4_6_model( - model - ) or AnthropicConfig._is_claude_4_7_model(model): + # For Claude 4.6+ adaptive-thinking models, effort is + # controlled via ``output_config``, not + # ``thinking.budget_tokens``. Driven by + # ``supports_adaptive_thinking`` in the model map so + # adding a new adaptive Claude is a model-map-only change. + if AnthropicConfig._is_adaptive_thinking_model(model): # ``_map_reasoning_effort`` returns ``type=adaptive`` # for any string on adaptive models without checking # the value, so reject unmapped efforts here (matching diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index b095d40156..d711d05c68 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -273,7 +273,26 @@ class AnthropicModelInfo(BaseLLMModelInfo): @staticmethod def _is_adaptive_thinking_model(model: str) -> bool: - """Claude 4.6+ models use adaptive thinking with output_config effort.""" + """Claude 4.6+ models use adaptive thinking with ``output_config.effort``. + + Driven by the ``supports_adaptive_thinking`` flag in + ``model_prices_and_context_window.json`` so that adding a new + adaptive-thinking model is a pure model-map change. Falls back to + the family-pattern check for OpenRouter / Vercel / Bedrock / + provider-prefixed variants whose model-map entries don't (yet) + carry the flag. + """ + from litellm.utils import _supports_factory + + try: + if _supports_factory( + model=model, + custom_llm_provider=None, + key="supports_adaptive_thinking", + ): + return True + except Exception: + pass return AnthropicModelInfo._is_claude_4_6_model( model ) or AnthropicModelInfo._is_claude_4_7_model(model) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index d3dff398a6..5a0087a270 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -478,14 +478,15 @@ class AmazonConverseConfig(BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # Adaptive-thinking models (Claude 4.6 / 4.7) take the tier - # via output_config.effort. Mirror the mapping used by - # AnthropicConfig.map_openai_params and apply the same - # validation rules so unmapped/garbage efforts surface as a - # 400 instead of being silently flattened on the wire. - if AnthropicConfig._is_claude_4_6_model( - model - ) or AnthropicConfig._is_claude_4_7_model(model): + # Adaptive-thinking models (Claude 4.6 / 4.7+) take the + # tier via ``output_config.effort``. Mirror the mapping + # used by ``AnthropicConfig.map_openai_params`` and apply + # the same validation rules so unmapped/garbage efforts + # surface as a 400 instead of being silently flattened on + # the wire. Driven by ``supports_adaptive_thinking`` in + # ``model_prices_and_context_window.json`` so a future + # adaptive Claude release lands as a model-map change. + if AnthropicConfig._is_adaptive_thinking_model(model): # Use ``.get()`` without a fallback so unmapped efforts # (e.g. ``"disabled"``) surface as a clean 400 here # rather than leaking the raw garbage string through to diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 34cf174c3f..4cd3ad9e42 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -350,15 +350,17 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # For Claude 4.6/4.7 adaptive models, ``_map_reasoning_effort`` + # For Claude 4.6+ adaptive models, ``_map_reasoning_effort`` # returns ``type=adaptive`` for ANY non-None / non-"none" # string without validating the value, so reject unmapped # efforts here and set ``output_config.effort`` (matching the # Anthropic native / Bedrock Converse / Bedrock Invoke / - # /v1/messages paths). - if AnthropicConfig._is_claude_4_6_model( - model - ) or AnthropicConfig._is_claude_4_7_model(model): + # /v1/messages paths). Driven by ``supports_adaptive_thinking`` + # in the model map so future adaptive Claudes land via a + # model-map update rather than a code release — the + # Anthropic-native and Bedrock routes already use the same + # helper, so all three paths stay in lock-step. + if AnthropicConfig._is_adaptive_thinking_model(model): # ``reasoning_effort_value`` comes from ``non_default_params`` # so its static type is ``Any | None``. Narrow to ``str`` for # the mapping lookup; non-strings fall through to the diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 7946e2dcee..9c085c2c99 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -9228,6 +9228,7 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 }, + "supports_adaptive_thinking": true, "supports_assistant_prefill": true, "supports_computer_use": true, "supports_function_calling": true, @@ -9421,6 +9422,7 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 }, + "supports_adaptive_thinking": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -9454,6 +9456,7 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 }, + "supports_adaptive_thinking": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -9487,6 +9490,7 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 }, + "supports_adaptive_thinking": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -9521,6 +9525,7 @@ "search_context_size_low": 0.01, "search_context_size_medium": 0.01 }, + "supports_adaptive_thinking": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, From 56070b86a3ef7521457d45b4b5a75b7b7b67fd29 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:11:45 +0000 Subject: [PATCH 20/25] test(model_prices): add supports_adaptive_thinking to schema `test_aaamodel_prices_and_context_window_json_is_valid` validates the model-map JSON against an explicit schema with `additionalProperties`, so the new `supports_adaptive_thinking` flag added in 98ced0ae43 needs a matching schema entry. Co-authored-by: Mateo Wang --- tests/test_litellm/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index f28fe3ed25..7e840cc7ad 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -852,6 +852,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "supports_none_reasoning_effort": {"type": "boolean"}, "supports_xhigh_reasoning_effort": {"type": "boolean"}, "supports_max_reasoning_effort": {"type": "boolean"}, + "supports_adaptive_thinking": {"type": "boolean"}, "supports_service_tier": {"type": "boolean"}, "supports_preset": {"type": "boolean"}, "tool_use_system_prompt_tokens": {"type": "number"}, From 2cb3f0f027e866498e990f53b5e4a6197b3b7667 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 19:34:56 +0000 Subject: [PATCH 21/25] refactor: remove unnecessary comments from #27074 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strip out the explanatory and historical comments that don't carry business-logic justification. Comments that simply narrate what code does — or that explain prior behavior, what was changed, or which PR introduced a fix — are removed. Docstrings are reduced to a one-line summary where the long form repeated information already evident from the code or test data. No code-behavior changes. All 643 affected unit tests still pass. Co-authored-by: Mateo Wang --- litellm/constants.py | 20 +--- litellm/llms/anthropic/chat/transformation.py | 107 +----------------- litellm/llms/anthropic/common_utils.py | 10 +- .../messages/transformation.py | 37 +----- .../llms/azure_ai/anthropic/transformation.py | 33 +----- litellm/llms/base_llm/chat/transformation.py | 6 - .../bedrock/chat/converse_transformation.py | 101 ++--------------- .../anthropic_claude3_transformation.py | 5 - .../anthropic_claude3_transformation.py | 4 - .../llms/databricks/chat/transformation.py | 19 ---- .../transformation.py | 5 - .../anthropic/output_params_utils.py | 7 +- .../anthropic/transformation.py | 4 - litellm/llms/xai/chat/transformation.py | 24 +--- litellm/llms/xai/cost_calculator.py | 11 +- litellm/types/llms/anthropic.py | 6 - litellm/types/llms/bedrock.py | 8 -- .../llms/anthropic/chat/conftest.py | 43 +------ .../test_anthropic_chat_transformation.py | 93 +++------------ .../test_reasoning_effort_translation.py | 50 +------- .../test_azure_anthropic_transformation.py | 36 ++---- ...ations_anthropic_claude3_transformation.py | 14 +-- .../chat/test_converse_transformation.py | 29 +---- .../test_anthropic_claude3_transformation.py | 92 ++------------- ...partner_models_anthropic_transformation.py | 28 +---- .../llms/xai/test_xai_chat_transformation.py | 14 +-- .../llms/xai/test_xai_cost_calculator.py | 19 +--- tests/test_litellm/test_utils.py | 6 +- 28 files changed, 92 insertions(+), 739 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 7708ba8b69..6918e40cad 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -202,17 +202,6 @@ DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET = int( DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET = int( os.getenv("DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET", 4096) ) -# ``xhigh`` / ``max`` budget extrapolation for legacy ``thinking.budget_tokens`` -# models (Claude 4.5 series + haiku). Continues the 2× progression -# 1024 → 2048 → 4096 from the existing low/medium/high tiers. These tiers -# also exist as adaptive ``output_config.effort`` enum values on Claude 4.6+ -# / 4.7; this constant only governs the budget-tokens fallback for models -# that aren't on the adaptive path. Per -# https://platform.claude.com/docs/en/build-with-claude/effort the ``effort`` -# enum is gated by model, but the legacy ``budget_tokens`` knob accepts any -# integer up to the model's max_tokens — adopting #27051's mapping here lets -# ``reasoning_effort=xhigh|max`` Just Work as a unified OpenAI-format knob -# regardless of which Anthropic API surface implements it. DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET = int( os.getenv("DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET", 8192) ) @@ -416,14 +405,7 @@ BEDROCK_MAX_POLICY_SIZE = int(os.getenv("BEDROCK_MAX_POLICY_SIZE", 75)) BEDROCK_MIN_THINKING_BUDGET_TOKENS = int( os.getenv("BEDROCK_MIN_THINKING_BUDGET_TOKENS", 1024) ) -# Anthropic's Messages API rejects ``thinking.budget_tokens < 1024`` with a -# 400. ``reasoning_effort='minimal'`` historically mapped to 128 (the global -# default) which always 400'd against direct Anthropic, Azure AI Anthropic, -# Vertex AI Anthropic, and Bedrock Invoke. Floor at the provider minimum so -# ``minimal`` is a usable tier on every Anthropic-backed route; Bedrock -# Converse already clamps server-side, this just unifies the behavior. -# Constant — not env-overridable — because it tracks Anthropic's published -# wire-protocol minimum, not a tunable. +# Anthropic's Messages API rejects thinking.budget_tokens < 1024. ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = 1024 REPLICATE_POLLING_DELAY_SECONDS = float( os.getenv("REPLICATE_POLLING_DELAY_SECONDS", 0.5) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 7f16c69481..6e76a9c85e 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -224,11 +224,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _supports_effort_level(model: str, level: str) -> bool: """Check ``supports_{level}_reasoning_effort`` in the model map. - Mirrors the pattern used in ``openai/chat/gpt_5_transformation.py`` so - that adding support for a new effort level is a pure model-map change. - Handles bedrock-prefixed and vertex-prefixed model ids by stripping - the prefix and re-checking against ``litellm.model_cost`` directly, - so a Bedrock-routed Claude 4.6/4.7 keeps its model-map flag. + Strips bedrock/vertex prefixes so a provider-routed Claude still + resolves to the Anthropic model-map entry. """ key = f"supports_{level}_reasoning_effort" try: @@ -240,11 +237,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): return True except Exception: pass - # Bedrock and Vertex route the model id with a provider-prefix - # (e.g. ``bedrock/invoke/us.anthropic.claude-opus-4-7``). Strip - # known prefixes and look the resulting Anthropic-flavoured key - # up directly in ``litellm.model_cost`` so the lookup keeps - # working regardless of which route the request arrived on. candidates = [model] for prefix in ( "bedrock/converse/", @@ -277,27 +269,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): @staticmethod def _validate_effort_for_model(model: str, effort: Optional[str]) -> Optional[str]: - """Return ``None`` if ``effort`` is allowed on ``model``, else an error message. - - Centralises per-model gating for ``max`` and ``xhigh`` so the chat - completion path (``_apply_output_config``) and the /v1/messages - pass-through (``AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic``) - can't drift when a new model tier is added. Caller raises the - provider-appropriate exception type using the returned message. - - ``max`` is supported on Claude 4.6 (Opus + Sonnet) and Claude 4.7 - adaptive-thinking models per - https://platform.claude.com/docs/en/build-with-claude/effort. The - data-driven ``supports_max_reasoning_effort`` flag in - ``model_prices_and_context_window.json`` is the source of truth; - family-level ``_is_claude_4_6_model`` / ``_is_claude_4_7_model`` - checks remain as a fallback for OpenRouter / GitHub Copilot / - Vercel / Bedrock variants whose model-map entries don't yet carry - the flag. - - ``xhigh`` is purely data-driven via ``supports_xhigh_reasoning_effort`` - so enabling it for a new model is a model-map-only change. - """ + """Return ``None`` if ``effort`` is allowed on ``model``, else an error message.""" if effort == "max" and not ( AnthropicConfig._is_claude_4_6_model(model) or AnthropicConfig._is_claude_4_7_model(model) @@ -312,15 +284,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): @staticmethod def _model_supports_effort_param(model: str) -> bool: - """Whether the model accepts ``output_config.effort`` at all. - - Per https://platform.claude.com/docs/en/build-with-claude/effort the - ``output_config.effort`` parameter is supported on Opus 4.5+, Sonnet 4.6+ - and Mythos Preview; older Claude models reject it with a 400. Support is - encoded in ``model_prices_and_context_window.json`` via the - ``supports_*_reasoning_effort`` flags, so adding a new effort-capable - model is a pure model-map change. - """ + """Whether the model accepts ``output_config.effort`` at all.""" for level in ("low", "minimal", "medium", "high", "xhigh", "max"): if AnthropicConfig._supports_effort_level(model, level): return True @@ -908,16 +872,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): model: str, llm_provider: str = "anthropic", ) -> Optional[AnthropicThinkingParam]: - """Map an OpenAI-format ``reasoning_effort`` string to Anthropic's - ``thinking`` payload. - - Raises ``BadRequestError`` (clean 400) instead of ``ValueError`` (500) - on unmapped efforts so every caller — Anthropic native, Bedrock - Invoke/Converse, Databricks, Vertex Anthropic, Azure AI Anthropic, - and the experimental ``/v1/messages`` pass-through — surfaces a - consistent error to the user. Pass ``llm_provider`` so the - ``BadRequestError`` carries the right provider name in logs. - """ if reasoning_effort is None or reasoning_effort == "none": return None if AnthropicConfig._is_adaptive_thinking_model(model): @@ -940,32 +894,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): budget_tokens=DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, ) elif reasoning_effort == "xhigh": - # Continues the 2× progression of low/medium/high (1024/2048/4096). - # On adaptive models (Claude 4.6/4.7) the ``xhigh`` tier is - # already routed via ``output_config.effort=xhigh`` above; this - # branch only applies to budget-mode models (Claude 4.5 series + - # haiku) where the OpenAI-format ``reasoning_effort`` knob would - # otherwise 400 with ``Unmapped reasoning effort``. Keeps the - # cross-model UX uniform — ``reasoning_effort=xhigh`` Just Works - # regardless of which Anthropic API surface implements it. return AnthropicThinkingParam( type="enabled", budget_tokens=DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, ) elif reasoning_effort == "max": - # Same rationale as ``xhigh`` above — ``max`` is the adaptive - # enum's top tier on Claude 4.6/4.7, but for budget-mode models - # we extend the 2× progression (8192 → 16384) so the OpenAI- - # format alias is usable on every Claude model. return AnthropicThinkingParam( type="enabled", budget_tokens=DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET, ) elif reasoning_effort == "minimal": - # Anthropic Messages API rejects ``budget_tokens < 1024`` with a - # 400. Floor at the provider minimum so ``minimal`` is a usable - # tier on Anthropic / Azure AI Anthropic / Vertex AI Anthropic / - # Bedrock Invoke. Bedrock Converse already clamps server-side. return AnthropicThinkingParam( type="enabled", budget_tokens=max( @@ -1246,10 +1184,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): elif param == "thinking": optional_params["thinking"] = value elif param == "reasoning_effort" and isinstance(value, str): - # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) - # directly on unmapped efforts (``disabled`` / ``invalid`` / - # ``""`` / ``xhigh``/``max`` on budget-mode Claude 4.5) so - # we no longer need to wrap a ``ValueError`` here. mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=value, model=model, @@ -1260,20 +1194,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # For Claude 4.6+ adaptive-thinking models, effort is - # controlled via ``output_config``, not - # ``thinking.budget_tokens``. Driven by - # ``supports_adaptive_thinking`` in the model map so - # adding a new adaptive Claude is a model-map-only change. if AnthropicConfig._is_adaptive_thinking_model(model): - # ``_map_reasoning_effort`` returns ``type=adaptive`` - # for any string on adaptive models without checking - # the value, so reject unmapped efforts here (matching - # the /v1/messages path) instead of relying on the - # downstream ``_apply_output_config`` check. Co-locating - # validation with the mapping prevents garbage from - # leaking into ``optional_params`` if ``map_openai_params`` - # is ever called without a subsequent ``transform_request``. mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( value ) @@ -1703,22 +1624,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): def _apply_output_config( self, data: dict, model: str, optional_params: dict ) -> None: - """Validate and apply output_config to the request data. - - Validation errors raise ``BadRequestError`` (clean 400) so callers - passing ``effort="disabled"`` / ``effort=""`` / unsupported tiers - for the model see a client-side error rather than a 500. - """ + """Validate and apply output_config to the request data.""" if "output_config" not in optional_params: return output_config = optional_params.get("output_config") if not output_config or not isinstance(output_config, dict): return - # When ``drop_params`` is set, strip ``output_config`` for models that - # cannot accept it (e.g. proxy fronting Claude Code at haiku-3, where - # the client always sends effort but the model rejects it). The user - # opted into silent fixup via the global flag — log a warning so the - # strip is still visible in logs. if litellm.drop_params is True and not self._model_supports_effort_param(model): litellm.verbose_logger.warning( "Dropping unsupported `output_config` for model=%s " @@ -1730,10 +1641,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): data.pop("output_config", None) return effort = output_config.get("effort") - # ``effort=""`` (empty string) and unmapped strings should be treated - # as invalid, not silently passed through. We use ``effort is not None`` - # here so empty string fails the membership check below. (The legacy - # ``if effort and ...`` short-circuit silently accepted ``""``.) valid_efforts = ["high", "medium", "low", "xhigh", "max"] if effort is not None and effort not in valid_efforts: raise litellm.exceptions.BadRequestError( @@ -1744,10 +1651,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): model=model, llm_provider=self.custom_llm_provider or "anthropic", ) - # Per-model gating for ``max`` / ``xhigh`` is centralised in - # ``_validate_effort_for_model`` so the chat path and the - # /v1/messages pass-through stay in lock-step when a new model - # tier lands. gate_error = self._validate_effort_for_model(model, effort) if gate_error is not None: raise litellm.exceptions.BadRequestError( diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index d711d05c68..869a7c5fbc 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -273,15 +273,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): @staticmethod def _is_adaptive_thinking_model(model: str) -> bool: - """Claude 4.6+ models use adaptive thinking with ``output_config.effort``. - - Driven by the ``supports_adaptive_thinking`` flag in - ``model_prices_and_context_window.json`` so that adding a new - adaptive-thinking model is a pure model-map change. Falls back to - the family-pattern check for OpenRouter / Vercel / Bedrock / - provider-prefixed variants whose model-map entries don't (yet) - carry the flag. - """ + """Claude 4.6+ models use adaptive thinking with ``output_config.effort``.""" from litellm.utils import _supports_factory try: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 8f35e79f56..35495d5961 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -47,9 +47,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): "inference_geo", "speed", "output_config", - # OpenAI-style tier knob — translated to native ``thinking`` + - # ``output_config`` in ``transform_anthropic_messages_request`` - # and popped before the request is forwarded. "reasoning_effort", # TODO: Add Anthropic `metadata` support # "metadata", @@ -176,22 +173,10 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ) -> None: """Map OpenAI-style ``reasoning_effort`` to native Anthropic params. - The /v1/messages spec doesn't include ``reasoning_effort`` — without - this translation it gets silently dropped, leaving every adaptive - tier collapsed to the same behavior on Bedrock Invoke /v1/messages - (and on Anthropic / Azure AI / Vertex AI when callers pass it on - the messages route). Mirrors ``AnthropicConfig.map_openai_params`` - on the chat completion path so the two routes can't drift. - - - Pops ``reasoning_effort`` from ``optional_params`` so it never - reaches the wire. - - Caller-supplied ``thinking`` / ``output_config`` always win — we - don't override an explicit native value. - - Effort=``none`` clears thinking + output_config so callers can - opt out per request. - - Invalid efforts raise ``BadRequestError`` (clean 400) instead of - surfacing as 500s downstream. + Caller-supplied ``thinking`` / ``output_config`` win over the alias. + ``effort='none'`` clears both. Invalid efforts raise a 400. """ + from litellm.exceptions import BadRequestError as _BadRequestError from litellm.llms.anthropic.chat.transformation import ( REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, AnthropicConfig, @@ -201,12 +186,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if not isinstance(reasoning_effort, str): return - # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) directly - # on unmapped efforts. The /v1/messages pass-through surfaces errors - # as ``AnthropicError``; convert here so callers see a provider-shaped - # 400 rather than the LiteLLM-shaped one. - from litellm.exceptions import BadRequestError as _BadRequestError - try: mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort, model=model @@ -224,12 +203,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( reasoning_effort ) - # ``_map_reasoning_effort`` returns ``type=adaptive`` for any - # string on adaptive models without checking the value. The - # chat completion path validates the resolved effort downstream - # via ``_apply_output_config``; /v1/messages has no equivalent - # downstream check, so reject unmapped values here so callers - # see a clean 400 instead of a 500 from the provider. if mapped_effort is None: raise AnthropicError( message=( @@ -239,10 +212,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ), status_code=400, ) - # Per-model gating for ``max`` / ``xhigh`` is centralised in - # ``AnthropicConfig._validate_effort_for_model`` so the chat - # completion path and this /v1/messages pass-through stay in - # lock-step when a new model tier lands. gate_error = AnthropicConfig._validate_effort_for_model( model, mapped_effort ) diff --git a/litellm/llms/azure_ai/anthropic/transformation.py b/litellm/llms/azure_ai/anthropic/transformation.py index f3bd6012ec..e176a4d860 100644 --- a/litellm/llms/azure_ai/anthropic/transformation.py +++ b/litellm/llms/azure_ai/anthropic/transformation.py @@ -15,21 +15,11 @@ if TYPE_CHECKING: def _promote_extra_body_to_optional_params(optional_params: dict) -> None: """Promote anthropic-native passthrough keys out of ``extra_body``. - ``azure_ai`` is registered in ``litellm.openai_compatible_providers``, so - ``add_provider_specific_params_to_optional_params`` (litellm/utils.py) - auto-stuffs any non-OpenAI kwarg (e.g. ``output_config={"effort": "..."}``) - into ``optional_params["extra_body"]``. For the Azure→Anthropic route the - user's intent is to forward those params to Anthropic, so promote them to - the top level of ``optional_params`` so: - - * ``AnthropicConfig._apply_output_config`` validates ``effort`` values - (matching the native ``anthropic`` provider's 400 on bad efforts). - * Valid passthroughs (e.g. ``output_config``, ``thinking``) actually - reach the request body instead of being silently dropped by the - ``data.pop("extra_body", None)`` strip below. - - ``setdefault`` is used so an explicit top-level value is never clobbered - by a duplicate inside ``extra_body``. + ``azure_ai`` is an OpenAI-compatible provider, so non-OpenAI kwargs like + ``output_config`` get auto-routed into ``extra_body`` by + ``add_provider_specific_params_to_optional_params``. For the Azure→Anthropic + route those keys must reach the request body and be validated, so promote + them. ``setdefault`` keeps explicit top-level values authoritative. """ extra_body = optional_params.get("extra_body") if not isinstance(extra_body, dict) or not extra_body: @@ -66,9 +56,6 @@ class AzureAnthropicConfig(AnthropicConfig): 1. API key via 'api-key' header 2. Azure AD token via 'Authorization: Bearer ' header """ - # Promote anthropic-native passthrough keys (``output_config``, - # ``thinking``, ``mcp_servers``, ...) out of ``extra_body`` so the - # flag detection below (``is_mcp_server_used``, etc.) sees them. _promote_extra_body_to_optional_params(optional_params) # Convert dict to GenericLiteLLMParams if needed @@ -133,18 +120,8 @@ class AzureAnthropicConfig(AnthropicConfig): Transform request using parent AnthropicConfig, then remove unsupported params. Azure Anthropic doesn't support extra_body, max_retries, or stream_options parameters. """ - # Promote anthropic-native passthrough keys (``output_config``, - # ``thinking``, ...) out of ``extra_body`` BEFORE delegating to - # ``AnthropicConfig.transform_request``. Without this: - # * ``output_config`` is silently dropped by the ``extra_body`` pop - # below, and - # * ``_apply_output_config`` never validates ``effort`` values, so - # ``effort="invalid"`` quietly reaches the model with default - # behavior instead of returning a clean 400 (as the native - # ``anthropic`` provider does). _promote_extra_body_to_optional_params(optional_params) - # Call parent transform_request data = super().transform_request( model=model, messages=messages, diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py index e36ba0c14a..bec25916c4 100644 --- a/litellm/llms/base_llm/chat/transformation.py +++ b/litellm/llms/base_llm/chat/transformation.py @@ -84,12 +84,6 @@ class BaseConfig(ABC): @classmethod def get_config(cls): - # Subclasses lean on this to surface their public default settings - # (e.g. ``max_tokens``) as request params. Anything ``_``-prefixed is - # treated as private (lookup tables, ABC machinery, internal flags) - # and must not leak into the wire body — a tuple/dict/frozenset class - # attribute would otherwise serialise into the request as an extra - # top-level key and the provider would 400 it. return { k: v for k, v in cls.__dict__.items() diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 5a0087a270..dbd55e957f 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -189,8 +189,6 @@ class AmazonConverseConfig(BaseConfig): @classmethod def get_config(cls): - # ``_``-prefixed names are private (lookup tables, ABC machinery, - # internal flags) and must not leak into the wire body. return { k: v for k, v in cls.__dict__.items() @@ -415,59 +413,19 @@ class AmazonConverseConfig(BaseConfig): """ Handle the reasoning_effort parameter based on the model type. - Different model families handle reasoning effort differently: - - GPT-OSS models: Keep reasoning_effort as-is (passed to additionalModelRequestFields) - - Nova 2 models: Transform to reasoningConfig structure - - Other models (Anthropic, etc.): Convert to thinking parameter - - For Claude 4.6 / 4.7 (adaptive thinking) the tier is carried via - ``output_config.effort`` rather than ``thinking.budget_tokens``. We - validate the effort with the same rules ``AnthropicConfig._apply_output_config`` - uses (low/medium/high/xhigh/max + per-model gating) and stage the - validated dict on ``optional_params["output_config"]`` so it rides - along to ``additionalModelRequestFields`` on the Anthropic-on-Bedrock - wire path. Without this the silent strip in ``_prepare_request_params`` - collapsed every adaptive tier to identical behavior. - - Args: - model: The model identifier - reasoning_effort: The reasoning effort value - optional_params: Dictionary of optional parameters to update in-place - - Examples: - >>> config = AmazonConverseConfig() - >>> params = {} - >>> config._handle_reasoning_effort_parameter("gpt-oss-model", "high", params) - >>> params - {'reasoning_effort': 'high'} - - >>> params = {} - >>> config._handle_reasoning_effort_parameter("amazon.nova-2-lite-v1:0", "high", params) - >>> params - {'reasoningConfig': {'type': 'enabled', 'maxReasoningEffort': 'high'}} - - >>> params = {} - >>> config._handle_reasoning_effort_parameter("anthropic.claude-3", "high", params) - >>> params - {'thinking': {'type': 'enabled', 'budget_tokens': 10000}} + - GPT-OSS models: passed through unchanged via additionalModelRequestFields. + - Nova 2 models: transformed to reasoningConfig. + - Anthropic models: mapped to ``thinking`` (and ``output_config.effort`` on + adaptive Claude 4.6 / 4.7). """ if "gpt-oss" in model: - # GPT-OSS models: keep reasoning_effort as-is - # It will be passed through to additionalModelRequestFields optional_params["reasoning_effort"] = reasoning_effort elif self._is_nova_2_model(model): - # Nova 2 models: transform to reasoningConfig reasoning_config = self._transform_reasoning_effort_to_reasoning_config( reasoning_effort ) optional_params.update(reasoning_config) else: - # Anthropic and other models: convert to thinking parameter. - # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) - # directly on unmapped efforts (``disabled`` / ``invalid`` / - # ``""`` / ``xhigh``/``max`` on budget-mode Claude 4.5); pass - # ``llm_provider="bedrock_converse"`` so the error carries the - # right provider name. mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort, model=model, @@ -478,22 +436,7 @@ class AmazonConverseConfig(BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # Adaptive-thinking models (Claude 4.6 / 4.7+) take the - # tier via ``output_config.effort``. Mirror the mapping - # used by ``AnthropicConfig.map_openai_params`` and apply - # the same validation rules so unmapped/garbage efforts - # surface as a 400 instead of being silently flattened on - # the wire. Driven by ``supports_adaptive_thinking`` in - # ``model_prices_and_context_window.json`` so a future - # adaptive Claude release lands as a model-map change. if AnthropicConfig._is_adaptive_thinking_model(model): - # Use ``.get()`` without a fallback so unmapped efforts - # (e.g. ``"disabled"``) surface as a clean 400 here - # rather than leaking the raw garbage string through to - # ``_validate_anthropic_adaptive_effort`` (which does - # catch it, but only because validation happens to run). - # Matches the /v1/messages pattern where validation is - # co-located with the mapping. mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( reasoning_effort ) @@ -514,16 +457,7 @@ class AmazonConverseConfig(BaseConfig): @staticmethod def _validate_anthropic_adaptive_effort(model: str, effort: str) -> None: - """Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7 - on Bedrock. Raises ``BadRequestError`` (clean 400) instead of letting - a downstream ``ValueError`` surface as 500. - - Per-model gating for ``max``/``xhigh`` is delegated to - ``AnthropicConfig._validate_effort_for_model`` so the Bedrock Converse - path and the Anthropic chat / ``/v1/messages`` paths can't drift when - a new gated effort tier is added. ``_supports_effort_level`` on - ``AnthropicConfig`` already handles Bedrock-prefixed model ids. - """ + """Validate ``output_config.effort`` for adaptive-thinking Claude 4.6/4.7.""" valid_efforts = {"high", "medium", "low", "xhigh", "max"} if effort not in valid_efforts: raise litellm.exceptions.BadRequestError( @@ -1283,13 +1217,9 @@ class AmazonConverseConfig(BaseConfig): ) inference_params.pop("json_mode", None) # used for handling json_schema - # Anthropic-only ``output_config`` (snake_case) is the adaptive- - # thinking effort payload (e.g. ``{"effort": "max"}``) for Claude - # 4.6/4.7. On Bedrock Converse it must ride along inside - # ``additionalModelRequestFields`` so the model actually sees the - # tier; stripping it (the prior behavior) silently flattened every - # adaptive tier to identical thinking. Only the Bedrock-native - # ``outputConfig`` (camelCase) goes at the top level. + # Anthropic-only ``output_config`` (snake_case) — re-attached to + # ``additionalModelRequestFields`` for Anthropic models below. The + # Bedrock-native ``outputConfig`` (camelCase) is handled separately. anthropic_output_config = inference_params.pop("output_config", None) # Extract requestMetadata before processing other parameters @@ -1342,18 +1272,11 @@ class AmazonConverseConfig(BaseConfig): additional_request_params ) - # Re-attach the Anthropic ``output_config`` (e.g. adaptive thinking - # ``{"effort": "max"}``) onto additional_request_params for Anthropic - # Bedrock models so the wire request carries the requested tier. Other - # model families (Nova, GPT-OSS, ...) don't accept it; drop it for them. if anthropic_output_config is not None and isinstance( anthropic_output_config, dict ): base_model = BedrockModelInfo.get_base_model(model) if base_model.startswith("anthropic"): - # When ``drop_params`` is set, strip for models that don't - # accept effort (e.g. proxy routing Claude Code at haiku-3). - # Otherwise forward and let Bedrock surface the model's error. if ( litellm.drop_params is True and not AnthropicConfig._model_supports_effort_param(model) @@ -1495,12 +1418,8 @@ class AmazonConverseConfig(BaseConfig): # Append pre-formatted tools (systemTool etc.) after transformation bedrock_tools.extend(pre_formatted_tools) - # Auto-attach the effort beta header for non-adaptive Anthropic - # models on Bedrock Converse (i.e. Opus 4.5). Claude 4.6/4.7 accept - # ``output_config.effort`` as a stable, GA feature with no beta - # header; Opus 4.5 still gates it behind ``effort-2025-11-24``. The - # check mirrors ``AnthropicModelInfo.is_effort_used`` (which returns - # False for adaptive models) so we don't double-flag adaptive routes. + # Opus 4.5 gates ``output_config.effort`` behind a beta header; + # Claude 4.6/4.7 accept it without one. base_model = BedrockModelInfo.get_base_model(model) if base_model.startswith("anthropic"): output_config = additional_request_params.get("output_config") 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 7de05c977c..c883ab68df 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -169,11 +169,6 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): anthropic_request.pop("model", None) anthropic_request.pop("stream", None) anthropic_request.pop("output_format", None) - # ``output_config`` (e.g. ``{"effort": "max"}``) is the adaptive-thinking - # tier payload for Claude 4.6 / 4.7. Bedrock Invoke accepts it for - # those models — stripping it (the prior behavior) silently flattened - # every adaptive tier on this route. Forward it; if the model rejects - # it the surfaced error is correct, vs. swallowing the user's knob. if "anthropic_version" not in anthropic_request: anthropic_request["anthropic_version"] = self.anthropic_version 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 a717dfb78b..a44d5b0ba9 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -582,10 +582,6 @@ class AmazonAnthropicClaudeMessagesConfig( if filtered_betas: anthropic_messages_request["anthropic_beta"] = filtered_betas - # 6a. When ``drop_params`` is set, strip ``output_config`` for models - # that don't accept it (e.g. proxy fronting Claude Code at haiku-3). - # Without this, every Claude Code request to a pre-4.5 Anthropic model - # routes a forced 400 from Bedrock that the client can't fix. if ( litellm.drop_params is True and "output_config" in anthropic_messages_request diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 4cd3ad9e42..fedb3d1165 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -334,11 +334,6 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): ) # unsupported for claude models - if json_schema -> convert to tool call if "reasoning_effort" in non_default_params and "claude" in model: - # ``_map_reasoning_effort`` raises ``BadRequestError`` (400) - # directly on unmapped efforts; pass ``llm_provider="databricks"`` - # so the surfaced error carries the correct provider name (the - # default is ``"anthropic"``, which would mislead users routing - # via Databricks Foundation Model APIs). reasoning_effort_value = non_default_params.get("reasoning_effort") mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort_value, @@ -350,21 +345,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - # For Claude 4.6+ adaptive models, ``_map_reasoning_effort`` - # returns ``type=adaptive`` for ANY non-None / non-"none" - # string without validating the value, so reject unmapped - # efforts here and set ``output_config.effort`` (matching the - # Anthropic native / Bedrock Converse / Bedrock Invoke / - # /v1/messages paths). Driven by ``supports_adaptive_thinking`` - # in the model map so future adaptive Claudes land via a - # model-map update rather than a code release — the - # Anthropic-native and Bedrock routes already use the same - # helper, so all three paths stay in lock-step. if AnthropicConfig._is_adaptive_thinking_model(model): - # ``reasoning_effort_value`` comes from ``non_default_params`` - # so its static type is ``Any | None``. Narrow to ``str`` for - # the mapping lookup; non-strings fall through to the - # ``BadRequestError`` below with a clean validation message. mapped_effort: Optional[str] = None if isinstance(reasoning_effort_value, str): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get( 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 714877457f..4be4c2d5e7 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 @@ -159,11 +159,6 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert "model", None ) # do not pass model in request body to vertex ai - # Vertex AI Claude accepts ``output_config.format`` (structured outputs), - # ``output_format``, and ``output_config.effort`` (adaptive-thinking - # tier on Claude 4.6 / 4.7, verified end-to-end). The shared sanitize - # helper now no-ops for ``effort`` and remains the single hook for any - # future Vertex-only key drift. sanitize_vertex_anthropic_output_params(anthropic_messages_request) return anthropic_messages_request 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 891c8e4d05..a33ad67778 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 @@ -11,11 +11,8 @@ keeps the parent module's import surface narrow. """ # Keys inside ``output_config`` that Vertex AI Claude does not accept. -# Vertex now accepts ``output_config.effort`` for the adaptive-thinking -# Claude 4.6 / 4.7 models on direct ``:rawPredict`` (verified end-to-end -# against ``us-east5`` for ``opus-4-6`` and ``global`` for ``opus-4-7``). -# Keep this set narrow and only add a key here once a 400 "Extra inputs are -# not permitted" is reproducible against the live Vertex endpoint. +# Add an entry only when a 400 "Extra inputs are not permitted" is +# reproducible against the live Vertex endpoint. VERTEX_UNSUPPORTED_OUTPUT_CONFIG_KEYS: frozenset = frozenset() 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 48920402e2..4627d9f6df 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 @@ -106,10 +106,6 @@ class VertexAIAnthropicConfig(AnthropicConfig): data.pop("model", None) # vertex anthropic doesn't accept 'model' parameter - # Sanitize ``output_config`` / ``output_format`` for Vertex parity. - # Vertex now accepts ``output_config.effort`` for adaptive-thinking Claude - # 4.6 / 4.7 models, so the helper is a no-op for ``effort``; it remains - # the single hook for future Vertex-only sanitization. sanitize_vertex_anthropic_output_params(data) tools = optional_params.get("tools") diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index b1c0aaccf1..6300868a64 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -224,12 +224,6 @@ class XAIChatConfig(OpenAIGPTConfig): except Exception as e: verbose_logger.debug(f"Error extracting X.AI web search usage: {e}") - # X.AI excludes reasoning_tokens from completion_tokens, breaking the - # OpenAI invariant total_tokens == prompt_tokens + completion_tokens. - # OpenAI o1/o3 fold reasoning into completion_tokens; align X.AI to - # match so downstream consumers (including litellm's own usage tests) - # see a self-consistent Usage object. Cost calc already accounts for - # reasoning tokens separately in xai.cost_calculator. self._fold_reasoning_tokens_into_completion(response) return response @@ -237,15 +231,10 @@ class XAIChatConfig(OpenAIGPTConfig): def _fold_reasoning_tokens_into_completion(model_response: ModelResponse) -> None: """Reconcile xAI Usage to the OpenAI invariant. - xAI returns ``completion_tokens`` covering only visible output and - accounts ``reasoning_tokens`` separately, while still rolling them - into ``total_tokens``. OpenAI's published contract (o1/o3) includes - reasoning in ``completion_tokens``. Tests that assert - ``total_tokens == prompt_tokens + completion_tokens`` (e.g. - ``_usage_format_tests``) fail on the raw xAI shape. - - This helper is idempotent: if ``completion_tokens`` already covers - the gap, no change is made. + xAI accounts ``reasoning_tokens`` separately from + ``completion_tokens`` while still summing them into ``total_tokens``. + OpenAI's contract (o1/o3) folds reasoning into ``completion_tokens``, + so fold here to keep ``total = prompt + completion``. Idempotent. """ usage = getattr(model_response, "usage", None) if usage is None: @@ -262,13 +251,10 @@ class XAIChatConfig(OpenAIGPTConfig): completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) total_tokens = int(getattr(usage, "total_tokens", 0) or 0) - # Already consistent — nothing to do. if total_tokens == prompt_tokens + completion_tokens: return - # Only fold when xAI's accounting (total = prompt + completion + - # reasoning) explains the gap. This guards against double-counting - # if xAI ever changes their semantics. + # Guard against double-counting if xAI changes accounting. if total_tokens != prompt_tokens + completion_tokens + reasoning_tokens: return diff --git a/litellm/llms/xai/cost_calculator.py b/litellm/llms/xai/cost_calculator.py index df5a7beffa..8edfd0c27a 100644 --- a/litellm/llms/xai/cost_calculator.py +++ b/litellm/llms/xai/cost_calculator.py @@ -25,13 +25,10 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: Returns: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd """ - # XAI-specific completion cost calculation - # For XAI models, completion is billed as (visible completion tokens + reasoning tokens). - # The transformation layer normalises Usage to the OpenAI invariant - # (completion_tokens includes reasoning_tokens), so detect that and avoid - # double-counting. Fall back to the raw xAI shape (visible-only completion + - # reasoning kept in completion_tokens_details) for callers that bypass the - # transformation, e.g. proxy logs replayed straight into cost calc. + # XAI-specific completion cost: completion is billed as visible + reasoning + # tokens. Detect when the transformation layer already folded them so we + # don't double-count; fall back to raw xAI shape for callers that bypass + # the transformation (e.g. proxy logs replayed into cost calc). prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0) completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0) total_tokens = int(getattr(usage, "total_tokens", 0) or 0) diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 6c57d3dc07..1c4d31d21a 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -393,12 +393,6 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False): AnthropicOutputConfig ] # Configuration for Claude's output behavior cache_control: Optional[Dict[str, Any]] # Automatic prompt caching - # OpenAI-style ``reasoning_effort`` is accepted on /v1/messages so callers - # can drive adaptive/extended thinking with a single tier-name knob (the - # same vocabulary as the chat completion path). The transformation layer - # maps it to native Anthropic ``thinking`` + ``output_config`` and pops - # this key before the request is forwarded — no provider receives - # ``reasoning_effort`` on the wire. reasoning_effort: Optional[str] diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 72e509d178..64827db13f 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -1041,12 +1041,4 @@ class BedrockInvokeAnthropicMessagesRequest(TypedDict, total=False): # `metadata` is part of the common Anthropic Messages API shape. thinking: dict metadata: dict - - # ``output_config`` is the adaptive-thinking effort payload for - # Claude 4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke - # accepts it for these models when ``thinking={"type": "adaptive"}``. - # Without this field in the allowlist, the runtime filter in - # ``AmazonAnthropicClaudeMessagesConfig.transform_anthropic_messages_request`` - # silently drops it and every adaptive tier collapses to identical - # behavior on /v1/messages. output_config: dict diff --git a/tests/test_litellm/llms/anthropic/chat/conftest.py b/tests/test_litellm/llms/anthropic/chat/conftest.py index 7dc6aeeb90..7e93e3b539 100644 --- a/tests/test_litellm/llms/anthropic/chat/conftest.py +++ b/tests/test_litellm/llms/anthropic/chat/conftest.py @@ -1,37 +1,10 @@ +"""Force ``litellm.model_cost`` to load from the PR-local JSON for these tests. + +By default ``litellm.model_cost`` is fetched from the main branch on GitHub, +which lags behind PR-branch flag additions. This fixture loads the local +file so per-model flag tests pass in CI as well as locally. +See https://github.com/BerriAI/litellm/issues/27122. """ -Local-conftest for ``tests/test_litellm/llms/anthropic/chat``. - -Why this exists ---------------- -``litellm.model_cost`` is loaded once at ``litellm.__init__`` time. By default -it fetches ``model_prices_and_context_window.json`` from the **main branch on -GitHub** (``litellm.model_cost_map_url``) — *not* from the PR-branch JSON in -the working tree. That works fine in production (operators get new models -without redeploying litellm) but is the wrong default for tests, which need -to validate the code in front of them against the data in front of them. - -Several anthropic chat transformation tests (``test_supports_effort_level_*``, -``test_anthropic_model_supports_effort_param_*``) assert per-model flags -like ``supports_max_reasoning_effort`` / ``supports_xhigh_reasoning_effort`` -that may exist in the PR-local JSON but not yet in main's JSON. Without this -fixture those tests pass locally (where AGENTS.md tells contributors to set -``LITELLM_LOCAL_MODEL_COST_MAP=True``) but fail in CI (which doesn't set the -env var) — the chicken-and-egg PR adds flag → CI fetches main without flag -→ test fails → flag never lands on main. - -The fixture force-loads ``litellm.model_cost`` from the local JSON for every -test in this directory. ``tests/test_litellm/conftest.py`` already snapshots -and restores ``litellm.model_cost`` per-function, so this mutation is safe -and contained. - -This is a scoped workaround. The proper fix is to set -``LITELLM_LOCAL_MODEL_COST_MAP=True`` globally in the test workflow once the -~10 inline-set test files have been audited and the few tests that exercise -the remote-fetch / integrity-validation path have been given carve-outs. -Tracked at https://github.com/BerriAI/litellm/issues/27122. -""" - -import os import pytest @@ -41,10 +14,6 @@ from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map @pytest.fixture(autouse=True) def _use_pr_local_model_cost_map(monkeypatch): - """Force ``litellm.model_cost`` to the PR-branch JSON for the duration of - each test. ``monkeypatch`` reverts the env var after the test; the parent - conftest restores ``litellm.model_cost`` from its snapshot. - """ monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") monkeypatch.setattr( litellm, 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 1aa753ff8f..26ed8d29c1 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 @@ -1632,7 +1632,6 @@ def test_effort_validation(): ) assert result["output_config"]["effort"] == effort - # Invalid value should raise BadRequestError (clean 400, not a 500). with pytest.raises( litellm.exceptions.BadRequestError, match="Invalid effort value" ): @@ -1685,10 +1684,7 @@ def test_effort_validation_with_opus_46(): def test_max_effort_rejected_for_opus_45(): - """Test that effort='max' is rejected when using Claude Opus 4.5. - - Surfaces as a clean 400 BadRequestError, not a 500 ValueError. - """ + """Test that effort='max' is rejected when using Claude Opus 4.5.""" config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] @@ -1749,12 +1745,7 @@ def test_effort_with_other_features(): def test_anthropic_drop_params_strips_output_config_for_pre_4_5_models(): - """ - Proxies fronting Claude Code at pre-4.5 Anthropic models receive - ``output_config`` injected by the client; without ``drop_params`` Bedrock / - Anthropic 400s. With ``drop_params=True`` we strip it (logged) so the - request can succeed. - """ + """``drop_params=True`` strips unsupported ``output_config`` for pre-4.5 models.""" config = AnthropicConfig() messages = [{"role": "user", "content": "Hello"}] @@ -1796,10 +1787,7 @@ def test_anthropic_drop_params_keeps_output_config_for_supporting_models(): def test_anthropic_drop_params_false_forwards_to_unsupported_model(): - """ - Default behavior: forward ``output_config`` and let the provider 400. - This is the contract for users who want strict, debuggable failures. - """ + """Default ``drop_params=False`` forwards ``output_config`` and lets the provider 400.""" config = AnthropicConfig() messages = [{"role": "user", "content": "Hello"}] @@ -2059,10 +2047,7 @@ def test_get_config_without_model_uses_fallback(): def test_get_config_does_not_leak_module_constants(): - """``BaseConfig.get_config`` returns class attributes; the - reasoning-effort mapping must not be one of them or it ends up - serialised onto the wire as an extra request key. - """ + """``get_config`` must not leak the reasoning-effort lookup table onto the wire.""" cfg = AnthropicConfig.get_config(model="claude-opus-4-7") for forbidden in ( "REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT", @@ -2078,10 +2063,6 @@ def test_get_config_does_not_leak_module_constants(): ("claude-opus-4-7", "xhigh", True), ("claude-opus-4-6", "max", True), ("claude-opus-4-6", "xhigh", False), - # ``max`` is documented as supported on Sonnet 4.6 (Claude 4.6 family). - # The model-map JSON now carries ``supports_max_reasoning_effort: true`` - # for every Sonnet 4.6 entry; ``_supports_effort_level`` should report - # ``True`` on every route prefix (anthropic, bedrock, vertex, azure). ("claude-sonnet-4-6", "max", True), ("claude-sonnet-4-6", "xhigh", False), ("bedrock/invoke/us.anthropic.claude-opus-4-7", "max", True), @@ -2094,27 +2075,21 @@ def test_get_config_does_not_leak_module_constants(): ], ) def test_supports_effort_level_handles_provider_prefixes(model, level, expected): - """``_supports_effort_level`` must handle bedrock/ vertex_ai/ azure_ai/ - prefixed model ids so per-model gating works on every route, not just - the bare-Anthropic chat completion path. - """ + """``_supports_effort_level`` resolves bedrock/vertex/azure-prefixed model ids.""" assert AnthropicConfig._supports_effort_level(model, level) is expected @pytest.mark.parametrize( "model,effort,expect_error", [ - # ``max`` accepted on 4.6 / 4.7 (family fallback) and rejected on 4.5. ("claude-opus-4-6", "max", False), ("claude-sonnet-4-6", "max", False), ("claude-opus-4-7", "max", False), ("claude-opus-4-5-20251101", "max", True), ("claude-sonnet-4-5", "max", True), - # ``xhigh`` data-driven; only 4.7 carries the flag in the model map. ("claude-opus-4-7", "xhigh", False), ("claude-opus-4-6", "xhigh", True), ("claude-sonnet-4-6", "xhigh", True), - # Lower efforts and ``None`` always pass the gate. ("claude-opus-4-5-20251101", "high", False), ("claude-haiku-4-5", "low", False), ("claude-opus-4-5-20251101", None, False), @@ -2123,13 +2098,6 @@ 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 ): - """``_validate_effort_for_model`` is the single source of truth for the - per-model ``max`` / ``xhigh`` gating that ``_apply_output_config`` (chat - completion path) and ``AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic`` - (/v1/messages pass-through) both rely on. Both call sites raise their - own provider-appropriate exception, but the gating decision must come - from one place to prevent drift when a new model tier lands. - """ err = AnthropicConfig._validate_effort_for_model(model, effort) if expect_error: assert err is not None @@ -2342,14 +2310,12 @@ def test_reasoning_effort_maps_to_budget_thinking_for_non_opus_4_6(): """ config = AnthropicConfig() - # Test with Claude Sonnet 4.5 (non-Opus 4.6 model). - # ``minimal`` floors at the Anthropic provider minimum (1024) because - # Anthropic / Azure / Vertex / Bedrock Invoke 400 below that. + # ``minimal`` floors at ANTHROPIC_MIN_THINKING_BUDGET_TOKENS (1024). test_cases = [ - ("low", 1024), # DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET - ("medium", 2048), # DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET - ("high", 4096), # DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET - ("minimal", 1024), # ANTHROPIC_MIN_THINKING_BUDGET_TOKENS (provider floor) + ("low", 1024), + ("medium", 2048), + ("high", 4096), + ("minimal", 1024), ] for effort, expected_budget in test_cases: @@ -2447,16 +2413,7 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): ], ) def test_max_effort_accepted_for_sonnet_46_variants(model): - """``effort='max'`` is documented as supported on Claude 4.6 (Opus + Sonnet) - and Claude 4.7 (https://platform.claude.com/docs/en/build-with-claude/effort). - - Earlier versions of this test asserted a 400 for Sonnet 4.6, mirroring an - Opus-only allow-list in ``_apply_output_config``. That gate has since been - widened to ``_is_claude_4_6_model`` (Opus + Sonnet) and the - ``supports_max_reasoning_effort`` JSON flag, matching Anthropic's published - matrix. Verify the param actually flows through every Sonnet 4.6 id - variant our routing layer might see. - """ + """``effort='max'`` is supported on Claude 4.6 (Opus + Sonnet) and 4.7.""" config = AnthropicConfig() messages = [{"role": "user", "content": "Test"}] @@ -2552,9 +2509,7 @@ def test_reasoning_effort_none_omits_thinking_and_output_config(model): ["disabled", "invalid", ""], ) def test_reasoning_effort_garbage_raises_bad_request(effort): - """Unmapped / garbage / empty-string reasoning_effort surfaces as a clean - 400 ``BadRequestError`` instead of letting ``ValueError`` propagate as 500. - """ + """Unmapped reasoning_effort raises BadRequestError (clean 400, not a 500).""" config = AnthropicConfig() with pytest.raises(litellm.exceptions.BadRequestError): @@ -2573,17 +2528,7 @@ def test_reasoning_effort_garbage_raises_bad_request(effort): def test_reasoning_effort_xhigh_max_maps_to_budget_on_budget_model( effort, expected_budget ): - """``xhigh`` / ``max`` extend the legacy ``thinking.budget_tokens`` - progression (low=1024 / medium=2048 / high=4096 → xhigh=8192 / max=16384) - on budget-mode Claude models (haiku / 4.5 series). - - Adopted from #27051. Keeps the OpenAI-format ``reasoning_effort`` knob - usable across the full Claude lineup — adaptive models (4.6/4.7) route - these tiers via ``output_config.effort``; budget-mode models use the - extended budget. Anthropic's "max only on Mythos / Opus 4.7 / Opus 4.6 / - Sonnet 4.6" gating applies to the *adaptive enum*, not to the legacy - ``budget_tokens`` knob, which accepts any integer up to ``max_tokens``. - """ + """``xhigh`` / ``max`` extend the budget_tokens progression on budget-mode models.""" config = AnthropicConfig() result = config.map_openai_params( @@ -2595,16 +2540,11 @@ def test_reasoning_effort_xhigh_max_maps_to_budget_on_budget_model( assert result["thinking"]["type"] == "enabled" assert result["thinking"]["budget_tokens"] == expected_budget - # Budget-mode models must NOT carry an ``output_config`` payload — that - # path is exclusively for adaptive (4.6+) models. assert "output_config" not in result def test_output_config_effort_empty_string_raises_bad_request(): - """``output_config={"effort": ""}`` must be rejected with a 400 — the - legacy ``if effort and ...`` short-circuit silently let it pass - through (verified end-to-end on the QA sweep for PR #27039). - """ + """``output_config={"effort": ""}`` is rejected with a 400.""" config = AnthropicConfig() with pytest.raises(litellm.exceptions.BadRequestError, match="Invalid effort"): @@ -2618,10 +2558,7 @@ def test_output_config_effort_empty_string_raises_bad_request(): def test_reasoning_effort_minimal_floors_at_anthropic_provider_minimum(): - """Anthropic Messages API rejects ``budget_tokens < 1024``. ``minimal`` - must floor at the provider minimum so it's a usable tier on direct - Anthropic / Azure AI Anthropic / Vertex AI Anthropic / Bedrock Invoke. - """ + """``minimal`` floors at the Anthropic provider minimum (1024).""" config = AnthropicConfig() result = config.map_openai_params( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index 20b5f95839..83716b8c8d 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -1,18 +1,4 @@ -""" -Tests for OpenAI-style ``reasoning_effort`` translation on the Anthropic -/v1/messages route. - -The /v1/messages spec doesn't include ``reasoning_effort`` — without -translation it gets silently dropped at the filter step, leaving every -adaptive tier collapsed to the same behavior on Bedrock Invoke /v1/messages -(and on Anthropic / Azure AI / Vertex AI when callers pass it on the -messages route). - -These tests pin the translation and validation behavior at the shared -``AnthropicMessagesConfig`` level so all four /v1/messages routes -(direct Anthropic, Azure AI, Vertex AI, Bedrock Invoke) inherit the -same mapping. -""" +"""Tests for ``reasoning_effort`` translation on the Anthropic /v1/messages route.""" import pytest @@ -36,12 +22,6 @@ from litellm.llms.anthropic.experimental_pass_through.messages.transformation im def test_reasoning_effort_maps_to_output_config_for_adaptive_model( reasoning_effort, expected_effort ): - """ - For Claude 4.6 / 4.7, ``reasoning_effort`` is mapped to - ``thinking={"type": "adaptive"}`` plus ``output_config.effort=``, - using the same mapping table as the chat completion path so the two - routes can't drift. - """ config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": reasoning_effort} @@ -59,7 +39,6 @@ def test_reasoning_effort_maps_to_output_config_for_adaptive_model( def test_reasoning_effort_none_clears_thinking_and_output_config(): - """``reasoning_effort='none'`` opts out of extended thinking entirely.""" config = AnthropicMessagesConfig() optional_params = { "max_tokens": 1024, @@ -82,11 +61,6 @@ def test_reasoning_effort_none_clears_thinking_and_output_config(): def test_reasoning_effort_on_non_adaptive_model_uses_thinking_budget(): - """ - Non-adaptive models (Opus 4.5 / earlier) take ``thinking.budget_tokens`` - rather than ``output_config.effort``. The translation falls back to - the budget mapping in that case. - """ config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": "high"} @@ -109,11 +83,6 @@ def test_reasoning_effort_on_non_adaptive_model_uses_thinking_budget(): @pytest.mark.parametrize("bad_effort", ["invalid", "disabled", ""]) def test_invalid_reasoning_effort_raises_400(bad_effort): - """ - Garbage ``reasoning_effort`` values surface as a clean 400 instead of - silently passing through to the provider as an unknown - ``output_config.effort`` (which would 500). - """ config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": bad_effort} @@ -132,18 +101,12 @@ def test_invalid_reasoning_effort_raises_400(bad_effort): @pytest.mark.parametrize( "model,bad_effort", [ - # ``xhigh`` is Opus-4.7-only on the public Anthropic effort matrix, - # so Opus 4.6 / Sonnet 4.6 must still 400 on it. ("claude-opus-4-6", "xhigh"), ("bedrock/invoke/us.anthropic.claude-opus-4-6-v1", "xhigh"), ("claude-sonnet-4-6", "xhigh"), ], ) def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort): - """``xhigh`` and ``max`` are gated per-model. The /v1/messages route must - surface a clean 400 client-side instead of forwarding the unsupported - tier and letting the provider 500/400 it. - """ config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": bad_effort} @@ -163,9 +126,6 @@ def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort @pytest.mark.parametrize( "model", [ - # ``max`` is documented as supported on Claude 4.6 (Opus + Sonnet) - # and Claude 4.7. Verify the /v1/messages route accepts it for - # Sonnet 4.6 variants instead of 400-ing client-side. "claude-sonnet-4-6", "bedrock/invoke/us.anthropic.claude-sonnet-4-6", ], @@ -187,11 +147,6 @@ def test_reasoning_effort_max_accepted_on_sonnet_46_messages(model): def test_explicit_output_config_wins_over_reasoning_effort(): - """ - Explicit native ``output_config.effort`` is never overridden by the - OpenAI alias. Same precedence as - ``_translate_legacy_thinking_for_adaptive_model``. - """ config = AnthropicMessagesConfig() optional_params = { "max_tokens": 1024, @@ -212,7 +167,6 @@ def test_explicit_output_config_wins_over_reasoning_effort(): def test_explicit_thinking_wins_over_reasoning_effort(): - """Explicit native ``thinking`` is never overridden by the alias.""" config = AnthropicMessagesConfig() optional_params = { "max_tokens": 1024, @@ -233,8 +187,6 @@ def test_explicit_thinking_wins_over_reasoning_effort(): def test_reasoning_effort_in_supported_params(): - """``reasoning_effort`` is advertised as a supported messages param so - callers and validation paths can introspect the schema.""" config = AnthropicMessagesConfig() assert "reasoning_effort" in config.get_supported_anthropic_messages_params( "claude-opus-4-7" diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py index 2504e9d5b8..9dac914ca4 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py @@ -292,19 +292,10 @@ class TestAzureAnthropicConfig: assert "compact-2026-01-12" in headers["anthropic-beta"] def test_output_config_promoted_from_extra_body(self): - """Anthropic-native ``output_config`` routed via ``extra_body`` (by the - openai-compatible kwarg stuffer in ``litellm/utils.py``) must be - promoted to top-level ``optional_params`` before delegating to - ``AnthropicConfig.transform_request``. Otherwise the value is - silently dropped by the ``extra_body`` pop and validation never - runs. - """ + """Anthropic ``output_config`` routed via ``extra_body`` reaches the request body.""" config = AzureAnthropicConfig() messages = [{"role": "user", "content": "Hello"}] - # Simulate what ``add_provider_specific_params_to_optional_params`` - # produces for ``litellm.completion(model="azure_ai/claude-...", - # output_config={"effort": "low"})``. optional_params = { "max_tokens": 100, "extra_body": {"output_config": {"effort": "low"}}, @@ -313,26 +304,18 @@ class TestAzureAnthropicConfig: headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} result = config.transform_request( - model="claude-opus-4-6", # supports output_config.effort + model="claude-opus-4-6", messages=messages, optional_params=optional_params, litellm_params=litellm_params, headers=headers, ) - # output_config must reach the request body (not be silently dropped) - assert "output_config" in result assert result["output_config"] == {"effort": "low"} - # extra_body should be stripped on the way out assert "extra_body" not in result def test_invalid_output_config_effort_raises_via_extra_body(self): - """``effort="invalid"`` arriving via ``extra_body`` must still raise - ``BadRequestError`` (matching the native ``anthropic`` provider). - Previously this was silently dropped on Azure, so unsupported - efforts reached the model with default behavior instead of - returning a clean 400. - """ + """Invalid ``effort`` via ``extra_body`` raises BadRequestError.""" import litellm config = AzureAnthropicConfig() @@ -356,9 +339,7 @@ class TestAzureAnthropicConfig: assert "Invalid effort value" in str(exc_info.value) def test_unsupported_effort_xhigh_raises_via_extra_body(self): - """``effort="xhigh"`` on a model that does not support it (e.g. - Sonnet 4.6) must raise ``BadRequestError`` even when arriving via - ``extra_body`` on Azure.""" + """Unsupported ``effort='xhigh'`` via ``extra_body`` raises BadRequestError.""" import litellm config = AzureAnthropicConfig() @@ -382,17 +363,14 @@ class TestAzureAnthropicConfig: assert "xhigh" in str(exc_info.value) def test_extra_body_promotion_does_not_clobber_top_level(self): - """If a key exists at both top-level ``optional_params`` and - inside ``extra_body``, the top-level value wins (``setdefault`` - semantics). This protects against a caller who explicitly sets a - param at top-level and accidentally also has it in extra_body.""" + """Top-level ``optional_params`` wins over duplicates in ``extra_body``.""" config = AzureAnthropicConfig() messages = [{"role": "user", "content": "Hello"}] optional_params = { "max_tokens": 100, - "output_config": {"effort": "low"}, # top-level wins - "extra_body": {"output_config": {"effort": "high"}}, # ignored + "output_config": {"effort": "low"}, + "extra_body": {"output_config": {"effort": "high"}}, } litellm_params = {"api_key": "test-key"} headers = {"api-key": "test-key", "anthropic-version": "2023-06-01"} diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index 8689a05e8e..4495e3f410 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -407,18 +407,7 @@ def test_opus_4_5_model_detection(): def test_output_config_forwarded_for_bedrock_chat_invoke_request(): - """ - Bedrock Invoke (chat/completions route) must forward - ``output_config`` for Anthropic adaptive-thinking models. The earlier - behavior stripped it unconditionally, which silently flattened every - adaptive tier (``low``/``medium``/``high``/``xhigh``/``max``) to identical - behavior on the wire. - - The wire QA at https://github.com/BerriAI/litellm/pull/27039 showed - ``thinking.type: adaptive`` was forwarded but ``output_config.effort`` - was always missing, even though direct curls to Anthropic's Bedrock - Invoke endpoint accept it. - """ + """Bedrock Invoke chat path forwards ``output_config`` for adaptive Claude models.""" config = AmazonAnthropicClaudeConfig() messages = [{"role": "user", "content": "test"}] @@ -437,7 +426,6 @@ def test_output_config_forwarded_for_bedrock_chat_invoke_request(): ) assert result.get("output_config") == {"effort": "high"} - # Verify normal params survive assert result["max_tokens"] == 100 diff --git a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py index 271498887f..5f2ed3dc00 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py @@ -326,10 +326,7 @@ def test_reasoning_effort_none_omits_thinking_for_anthropic_converse(model): def test_reasoning_effort_sets_output_config_for_adaptive_models_converse( model, effort, expected_effort ): - """Adaptive-thinking Claude 4.6 / 4.7 on Bedrock Converse must carry the - requested tier via ``output_config.effort``. The prior strip silently - flattened every adaptive tier on the wire (verified in the PR #27039 - QA sweep).""" + """Adaptive Claude 4.6 / 4.7 on Bedrock Converse routes the tier via ``output_config.effort``.""" config = AmazonConverseConfig() optional_params = config.map_openai_params( @@ -352,10 +349,7 @@ def test_reasoning_effort_sets_output_config_for_adaptive_models_converse( ], ) def test_output_config_effort_forwarded_into_additional_request_fields(model): - """``output_config`` must ride along inside ``additionalModelRequestFields`` - so the Anthropic-on-Bedrock wire request actually carries the effort - tier. The prior ``inference_params.pop("output_config")`` dropped it - on the floor for every adaptive tier.""" + """``output_config`` rides along inside ``additionalModelRequestFields``.""" config = AmazonConverseConfig() messages = [{"role": "user", "content": "hi"}] @@ -380,10 +374,7 @@ def test_output_config_effort_forwarded_into_additional_request_fields(model): ["disabled", "invalid", ""], ) def test_reasoning_effort_garbage_raises_bad_request_converse(effort): - """Garbage / empty-string reasoning_effort on Bedrock Converse Anthropic - must surface as a clean 400 ``BadRequestError`` instead of 500. The - earlier ``ValueError`` from ``_map_reasoning_effort`` propagated up as - a generic 500 in the proxy and ate the request.""" + """Unmapped reasoning_effort on Bedrock Converse Anthropic raises BadRequestError.""" config = AmazonConverseConfig() with pytest.raises(litellm.exceptions.BadRequestError): @@ -405,13 +396,7 @@ def test_reasoning_effort_garbage_raises_bad_request_converse(effort): ], ) def test_output_config_effort_max_passes_through_on_sonnet_46_variants(model): - """``effort='max'`` is supported on Claude 4.6 (Opus + Sonnet) per - https://platform.claude.com/docs/en/build-with-claude/effort. The earlier - Opus-only allow-list in ``_validate_anthropic_adaptive_effort`` has been - widened to ``_is_claude_4_6_model`` (Opus + Sonnet) plus the - ``supports_max_reasoning_effort`` JSON flag. Verify the param actually - flows through to ``additionalModelRequestFields.output_config.effort`` - for every Bedrock Converse Sonnet 4.6 id variant.""" + """``effort='max'`` flows through for every Bedrock Converse Sonnet 4.6 id.""" config = AmazonConverseConfig() messages = [{"role": "user", "content": "hi"}] @@ -3450,9 +3435,7 @@ def test_transform_request_strips_anthropic_output_config(): def test_converse_drop_params_strips_output_config_for_pre_4_5_anthropic(): - """``drop_params=True`` strips ``output_config`` for pre-4.5 Anthropic - models on Bedrock Converse so a proxy fronting Claude Code at haiku doesn't - force a 400 on every request.""" + """``drop_params=True`` strips unsupported ``output_config`` on Bedrock Converse.""" config = AmazonConverseConfig() messages = [{"role": "user", "content": "hi"}] @@ -3477,7 +3460,7 @@ def test_converse_drop_params_strips_output_config_for_pre_4_5_anthropic(): def test_converse_drop_params_keeps_output_config_for_supporting_anthropic(): - """``drop_params=True`` must not strip on supporting models.""" + """``drop_params=True`` does not strip on models that support ``output_config``.""" config = AmazonConverseConfig() messages = [{"role": "user", "content": "hi"}] 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 644ff81f6f..c98f784034 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 @@ -593,16 +593,7 @@ def test_remove_scope_from_cache_control(): def test_bedrock_messages_forwards_output_config(): - """ - ``output_config`` is the adaptive-thinking effort payload for Claude - 4.6 / 4.7 (e.g. ``{"effort": "max"}``). Bedrock Invoke accepts it for - those models — the prior behavior of stripping it silently flattened - every adaptive tier on /v1/messages so ``low`` / ``medium`` / ``high`` / - ``xhigh`` / ``max`` all collapsed to identical thinking with no tier - differentiation. - - Regression coverage for the QA bug listed on PR #27039. - """ + """Bedrock Invoke /v1/messages forwards ``output_config`` for adaptive Claude models.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -628,12 +619,7 @@ def test_bedrock_messages_forwards_output_config(): def test_bedrock_messages_forwards_output_config_with_output_format(): - """ - When both output_config and output_format are present, output_format is - converted to inline schema (Bedrock Invoke doesn't accept output_format - natively), and output_config is forwarded for Claude 4.6/4.7 adaptive - thinking. - """ + """``output_config`` is forwarded; ``output_format`` is converted to inline schema.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -663,19 +649,7 @@ def test_bedrock_messages_forwards_output_config_with_output_format(): def test_bedrock_messages_forwards_output_config_for_non_adaptive_model(): - """ - ``output_config`` is forwarded for non-adaptive models too (e.g. haiku). - Bedrock will reject the unsupported key for those models — surfacing the - provider error is the correct behavior, since silently swallowing the - knob would hide caller bugs. - - Restores coverage previously asserted by - ``test_bedrock_messages_strips_output_config`` (renamed to the - ``forwards`` variant on Opus 4.7); the strip path no longer exists, - but the non-adaptive pass-through path needs its own explicit test - so a future regression that silently re-adds the strip can't sneak - through. - """ + """``output_config`` is forwarded for non-adaptive models so the provider's error surfaces.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -698,14 +672,7 @@ def test_bedrock_messages_forwards_output_config_for_non_adaptive_model(): def test_bedrock_messages_drop_params_strips_output_config_for_pre_4_5(): - """ - ``drop_params=True`` is the operator opt-in for "silently fix up" - behavior. When a proxy fronts Claude Code at a pre-4.5 Anthropic model - (haiku-3, sonnet-3.5, ...) on the /v1/messages route, the client always - sends ``output_config.effort`` and the model rejects it. Stripping under - ``drop_params`` lets those requests succeed; otherwise we forward and - surface the model's 400 as designed. - """ + """``drop_params=True`` strips ``output_config`` for pre-4.5 Anthropic on /v1/messages.""" import litellm from litellm.types.router import GenericLiteLLMParams @@ -733,8 +700,7 @@ def test_bedrock_messages_drop_params_strips_output_config_for_pre_4_5(): def test_bedrock_messages_drop_params_keeps_output_config_for_4_7(): - """``drop_params=True`` must not strip on supporting models — opus-4-7 - accepts effort, so the client's tier knob has to land on the wire.""" + """``drop_params=True`` does not strip on opus-4-7 (supports effort).""" import litellm from litellm.types.router import GenericLiteLLMParams @@ -775,18 +741,7 @@ def test_bedrock_messages_drop_params_keeps_output_config_for_4_7(): def test_bedrock_messages_maps_reasoning_effort_for_adaptive_model( reasoning_effort, expected_effort ): - """ - OpenAI-style ``reasoning_effort`` is mapped to native Anthropic - ``thinking`` + ``output_config.effort`` on the /v1/messages route so - callers can drive adaptive thinking with the same tier vocabulary as - the chat completion path. ``reasoning_effort`` itself is popped — the - /v1/messages spec doesn't define it and Bedrock rejects unknown - top-level fields. - - Closes the QA-sweep gap on PR #27074 where Bedrock Invoke /v1/messages - silently dropped ``reasoning_effort`` and every effort tier collapsed - to the same behavior. - """ + """``reasoning_effort`` maps to ``thinking`` + ``output_config.effort`` on /v1/messages.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -810,16 +765,7 @@ def test_bedrock_messages_maps_reasoning_effort_for_adaptive_model( def test_bedrock_messages_reasoning_effort_on_non_adaptive_uses_thinking_budget(): - """ - For non-adaptive thinking models (e.g. Opus 4.5), ``reasoning_effort`` - is mapped to ``thinking.type=enabled`` with a budget_tokens value - instead of ``output_config.effort``. ``output_config`` is not set on - these models because they don't accept it. - - Mirrors ``AnthropicConfig._map_reasoning_effort`` behavior for the - non-adaptive branch on Opus 4.5 / earlier Claude 4 models, applied to - the /v1/messages route. - """ + """Non-adaptive models map ``reasoning_effort`` to ``thinking.budget_tokens``.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -847,11 +793,7 @@ def test_bedrock_messages_reasoning_effort_on_non_adaptive_uses_thinking_budget( def test_bedrock_messages_reasoning_effort_none_clears_thinking(): - """ - ``reasoning_effort='none'`` opts out — both ``thinking`` and - ``output_config`` are cleared so the request goes out without - extended thinking. Mirrors the chat completion path's behavior. - """ + """``reasoning_effort='none'`` clears both ``thinking`` and ``output_config``.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -877,11 +819,7 @@ def test_bedrock_messages_reasoning_effort_none_clears_thinking(): def test_bedrock_messages_invalid_reasoning_effort_raises_400(): - """ - Garbage ``reasoning_effort`` values (``invalid`` / ``disabled`` / ``""``) - surface as a clean 400 ``AnthropicError`` instead of silently passing - an invalid string through to Bedrock as ``output_config.effort``. - """ + """Garbage ``reasoning_effort`` raises AnthropicError (400).""" from litellm.llms.anthropic.common_utils import AnthropicError from litellm.types.router import GenericLiteLLMParams @@ -903,12 +841,7 @@ def test_bedrock_messages_invalid_reasoning_effort_raises_400(): def test_bedrock_messages_explicit_output_config_wins_over_reasoning_effort(): - """ - Caller-supplied native ``output_config.effort`` wins over the OpenAI - ``reasoning_effort`` knob. Same precedence as - ``_translate_legacy_thinking_for_adaptive_model``: explicit native - Anthropic params are never overridden by the alias. - """ + """Explicit ``output_config.effort`` wins over the ``reasoning_effort`` alias.""" from litellm.types.router import GenericLiteLLMParams cfg = AmazonAnthropicClaudeMessagesConfig() @@ -1006,11 +939,6 @@ def test_bedrock_messages_allowlist_filters_anthropic_only_fields(): ): assert bad not in result, f"{bad} should be stripped by the allowlist" - # ``output_config`` rides along — Bedrock Invoke accepts it for Claude - # 4.6/4.7 adaptive thinking and stripping it silently flattens every - # adaptive tier. (Bedrock will reject it for non-adaptive models, which - # is the correct behavior — surface the model error rather than swallow - # the knob.) assert result.get("output_config") == {"effort": "low"} # Supported fields pass through. assert result["max_tokens"] == 4096 diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py index 0a5ba12f18..d89d09a4e6 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py @@ -499,14 +499,7 @@ def test_vertex_ai_partner_models_anthropic_remove_prompt_caching_scope_beta_hea def test_vertex_ai_anthropic_output_config_effort_only_forwarded(): - """ - Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct - ``:rawPredict`` (verified end-to-end against ``us-east5`` for - ``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). The earlier - strip silently flattened every adaptive tier on Vertex, so ``low`` / - ``medium`` / ``high`` / ``xhigh`` / ``max`` all produced identical - thinking with no tier differentiation. - """ + """Vertex AI Claude 4.6/4.7 accept ``output_config.effort`` on rawPredict.""" config = VertexAIAnthropicConfig() messages = [{"role": "user", "content": "What is 2+2?"}] @@ -568,15 +561,7 @@ def test_vertex_ai_anthropic_output_config_format_passes_through(): def test_vertex_ai_anthropic_output_config_format_plus_effort_preserved(): - """ - Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct - ``:rawPredict`` (verified end-to-end against ``us-east5`` for - ``claude-opus-4-6`` and ``global`` for ``claude-opus-4-7``). Since the - strip was unjustified, ``effort`` must now ride along with ``format``. - - We use a Claude 4.6 model id here because ``_apply_output_config`` only - accepts ``effort`` on adaptive-thinking 4.6/4.7 model ids. - """ + """Both ``format`` and ``effort`` ride along on Vertex Claude 4.6/4.7.""" config = VertexAIAnthropicConfig() messages = [{"role": "user", "content": "Return a person object."}] @@ -625,14 +610,7 @@ def test_vertex_ai_anthropic_output_config_non_dict_dropped(): def test_vertex_ai_anthropic_output_format_and_output_config_effort_preserved(): - """ - Vertex AI Claude 4.6 / 4.7 accept ``output_config.effort`` on direct - ``:rawPredict`` (verified end-to-end). When both ``output_format`` and - ``output_config: {effort}`` are present, both must be forwarded — the - earlier ``effort`` strip caused silent loss of the requested adaptive - thinking tier on Vertex routes (``low``/``medium``/``high``/``xhigh``/``max`` - all collapsed to identical adaptive thinking with no tier differentiation). - """ + """Both ``output_format`` and ``output_config.effort`` are forwarded on Vertex 4.6/4.7.""" config = VertexAIAnthropicConfig() messages = [{"role": "user", "content": "Extract structured data"}] diff --git a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py index a8bbb880c5..3ae8dfc3c0 100644 --- a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py +++ b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py @@ -14,10 +14,7 @@ from litellm.types.utils import ( class TestXAIReasoningTokenFolding: - """xAI breaks the OpenAI invariant total = prompt + completion by accounting - reasoning_tokens separately. ``_fold_reasoning_tokens_into_completion`` - re-aligns Usage so downstream consumers see the OpenAI shape (o1/o3 - semantics: completion_tokens includes reasoning).""" + """``_fold_reasoning_tokens_into_completion`` re-aligns xAI Usage to the OpenAI invariant.""" @staticmethod def _make_response( @@ -42,8 +39,7 @@ class TestXAIReasoningTokenFolding: return response def test_should_fold_when_total_explained_by_reasoning_gap(self): - # Real xAI live shape captured 2026-05-04: prompt=14, completion=10, - # total=336, reasoning=312. 14+10+312 == 336. + # xAI live shape: 14 + 10 + 312 == 336. response = self._make_response( prompt_tokens=14, completion_tokens=10, @@ -58,7 +54,6 @@ class TestXAIReasoningTokenFolding: assert usage.total_tokens == usage.prompt_tokens + usage.completion_tokens def test_should_not_fold_when_already_normalised(self): - # OpenAI-normalised shape: completion already includes reasoning. response = self._make_response( prompt_tokens=14, completion_tokens=322, @@ -68,7 +63,6 @@ class TestXAIReasoningTokenFolding: XAIChatConfig._fold_reasoning_tokens_into_completion(response) - # Idempotent — no double-fold. assert response.usage.completion_tokens == 322 def test_should_skip_when_no_reasoning_tokens(self): @@ -84,8 +78,7 @@ class TestXAIReasoningTokenFolding: assert response.usage.completion_tokens == 10 def test_should_skip_when_gap_does_not_match_reasoning(self): - # Defensive guard: if xAI ever changes accounting and the gap stops - # equalling reasoning_tokens, refuse to fold rather than corrupt. + # Refuse to fold if xAI accounting changes (gap != reasoning_tokens). response = self._make_response( prompt_tokens=14, completion_tokens=10, @@ -95,7 +88,6 @@ class TestXAIReasoningTokenFolding: XAIChatConfig._fold_reasoning_tokens_into_completion(response) - # No fold; original values preserved. assert response.usage.completion_tokens == 10 assert response.usage.total_tokens == 999 diff --git a/tests/test_litellm/llms/xai/test_xai_cost_calculator.py b/tests/test_litellm/llms/xai/test_xai_cost_calculator.py index a8aae34622..02fe7c8e68 100644 --- a/tests/test_litellm/llms/xai/test_xai_cost_calculator.py +++ b/tests/test_litellm/llms/xai/test_xai_cost_calculator.py @@ -107,7 +107,6 @@ class TestXAICostCalculator: def test_grok_4_cost_calculation(self): """Test cost calculation for grok-4 model.""" - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=10, completion_tokens=200, @@ -134,7 +133,6 @@ class TestXAICostCalculator: def test_grok_3_fast_beta_cost_calculation(self): """Test cost calculation for grok-3-fast-beta model.""" - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=20, completion_tokens=300, @@ -176,7 +174,6 @@ class TestXAICostCalculator: def test_edge_case_large_reasoning_tokens(self): """Test cost calculation when reasoning_tokens is larger than completion_tokens.""" - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=12, completion_tokens=50, # Less than reasoning_tokens @@ -204,7 +201,6 @@ class TestXAICostCalculator: def test_tiered_pricing_above_128k_tokens(self): """Test tiered pricing for tokens above 128k.""" # Test with grok-4-fast-reasoning which has tiered pricing - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=150000, # Above 128k threshold completion_tokens=100000, # Above 128k threshold @@ -234,7 +230,6 @@ class TestXAICostCalculator: def test_tiered_pricing_below_128k_tokens(self): """Test that regular pricing is used for tokens below 128k threshold.""" # Test with grok-4-fast-reasoning which has tiered pricing - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=100000, # Below 128k threshold completion_tokens=50000, @@ -263,7 +258,6 @@ class TestXAICostCalculator: def test_tiered_pricing_grok_4_latest(self): """Test tiered pricing for grok-4-latest model.""" - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=200000, # Above 128k threshold completion_tokens=100000, @@ -292,7 +286,6 @@ class TestXAICostCalculator: def test_tiered_pricing_output_tokens_below_128k(self): """Test that output tokens get tiered rate when input tokens > 128k, even if output tokens < 128k.""" - # xAI raw API shape: total_tokens = prompt + visible completion + reasoning usage = Usage( prompt_tokens=150000, # Above 128k threshold completion_tokens=50000, # Below 128k threshold @@ -339,16 +332,7 @@ class TestXAICostCalculator: assert math.isclose(completion_cost, expected_completion_cost, rel_tol=1e-10) def test_already_normalised_usage_does_not_double_count_reasoning(self): - """Cost calc receives Usage post-transformation (OpenAI invariant). - - After XAIChatConfig.transform_response folds reasoning_tokens into - completion_tokens, the Usage block satisfies - ``total_tokens == prompt_tokens + completion_tokens``. Cost calc must - detect this and skip the reasoning_tokens add-on, otherwise it - double-bills the reasoning tokens. - """ - # OpenAI-normalised shape: completion_tokens already includes the - # 100 reasoning tokens (so 100 visible + 100 reasoning -> 200). + """Cost calc must not double-bill when Usage is already OpenAI-normalised.""" usage = Usage( prompt_tokens=12, completion_tokens=200, @@ -364,7 +348,6 @@ class TestXAICostCalculator: prompt_cost, completion_cost = cost_per_token(model="grok-3-mini", usage=usage) - # Bill exactly what completion_tokens reports — no double-add. expected_prompt_cost = 12 * 3e-7 expected_completion_cost = 200 * 5e-7 diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 7e840cc7ad..65305e1a81 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -2905,9 +2905,9 @@ def test_gemini_embedding_2_ga_in_cost_map(): assert info.get("input_cost_per_audio_per_second") == 0.00016 assert info.get("input_cost_per_video_per_second") == 0.00079 if provider in ("vertex_ai-embedding-models", "vertex_ai"): - assert info.get("uses_embed_content") is True, ( - f"{key} must have uses_embed_content=true for correct Vertex AI routing" - ) + assert ( + info.get("uses_embed_content") is True + ), f"{key} must have uses_embed_content=true for correct Vertex AI routing" def test_gemini_lyria_3_preview_models_in_cost_map(): From 1ac430cfeafdbae9d8d960d67c7cf161a9919358 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 13:01:34 -0700 Subject: [PATCH 22/25] style: make _model_supports_effort_param more concise --- litellm/llms/anthropic/chat/transformation.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 6e76a9c85e..de021ed0c4 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -285,10 +285,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): @staticmethod def _model_supports_effort_param(model: str) -> bool: """Whether the model accepts ``output_config.effort`` at all.""" - for level in ("low", "minimal", "medium", "high", "xhigh", "max"): - if AnthropicConfig._supports_effort_level(model, level): - return True - return False + return any(AnthropicConfig._supports_effort_level(model, level) + for level in ("low", "minimal", "medium", "high", "xhigh", "max")) def get_supported_openai_params(self, model: str): params = [ From 37ae24dbebc582180feec84afb995f2822d6d976 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 21:05:25 +0000 Subject: [PATCH 23/25] refactor(anthropic,bedrock): hoist drop_params output_config warning to module constant Three call sites (anthropic chat, bedrock converse, bedrock invoke messages) emitted the same '...Effort is only supported on Opus 4.5+, Sonnet 4.6+, and Mythos Preview' warning verbatim. Extract DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING in litellm/llms/anthropic/chat/transformation.py and import it from the bedrock sites so future copy edits live in one place. Addresses Michael's review on PR #27074. Co-authored-by: Mateo Wang --- litellm/llms/anthropic/chat/transformation.py | 10 +++++++--- litellm/llms/bedrock/chat/converse_transformation.py | 5 ++--- .../anthropic_claude3_transformation.py | 9 +++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index de021ed0c4..00147b2ec8 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -113,6 +113,12 @@ REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Dict[str, str] = { "max": "max", } +DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING = ( + "Dropping unsupported `output_config` for model=%s " + "(drop_params=True). Effort is only supported on Opus 4.5+, " + "Sonnet 4.6+, and Mythos Preview." +) + class AnthropicConfig(AnthropicModelInfo, BaseConfig): """ @@ -1630,9 +1636,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): return if litellm.drop_params is True and not self._model_supports_effort_param(model): litellm.verbose_logger.warning( - "Dropping unsupported `output_config` for model=%s " - "(drop_params=True). Effort is only supported on Opus 4.5+, " - "Sonnet 4.6+, and Mythos Preview.", + DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model, ) optional_params.pop("output_config", None) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index dbd55e957f..3b79d5b5db 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -32,6 +32,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( _bedrock_tools_pt, ) from litellm.llms.anthropic.chat.transformation import ( + DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT, AnthropicConfig, ) @@ -1282,9 +1283,7 @@ class AmazonConverseConfig(BaseConfig): and not AnthropicConfig._model_supports_effort_param(model) ): litellm.verbose_logger.warning( - "Dropping unsupported `output_config` for model=%s " - "(drop_params=True). Effort is only supported on " - "Opus 4.5+, Sonnet 4.6+, and Mythos Preview.", + DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model, ) else: 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 a44d5b0ba9..aae2bc5e28 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -16,7 +16,10 @@ import litellm from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers from litellm.constants import BEDROCK_MIN_THINKING_BUDGET_TOKENS from litellm.litellm_core_utils.litellm_logging import verbose_logger -from litellm.llms.anthropic.chat.transformation import AnthropicConfig +from litellm.llms.anthropic.chat.transformation import ( + DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, + AnthropicConfig, +) from litellm.llms.anthropic.common_utils import AnthropicModelInfo from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( AnthropicMessagesConfig, @@ -588,9 +591,7 @@ class AmazonAnthropicClaudeMessagesConfig( and not AnthropicConfig._model_supports_effort_param(model) ): verbose_logger.warning( - "Dropping unsupported `output_config` for model=%s " - "(drop_params=True). Effort is only supported on Opus 4.5+, " - "Sonnet 4.6+, and Mythos Preview.", + DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING, model, ) anthropic_messages_request.pop("output_config", None) From 0f04132cf865756f71bc7421eb8211f35a008616 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 21:16:40 +0000 Subject: [PATCH 24/25] refactor(anthropic,bedrock,databricks): factor BadRequestError for unknown reasoning_effort Three call sites raised the same BadRequestError("Invalid reasoning_effort: ... Must be one of 'minimal', 'low', ...") block when REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT returned None: anthropic chat map_openai_params, bedrock converse _handle_reasoning_effort_parameter, and databricks chat reasoning_effort path. Extract AnthropicConfig._raise_invalid_reasoning_effort(model, value, llm_provider) so future copy edits / valid-set changes happen in one place. Typed as NoReturn so type-checkers correctly narrow control flow at call sites. Addresses Michael's review on PR #27074. Co-authored-by: Mateo Wang --- litellm/llms/anthropic/chat/transformation.py | 40 +++++++++++++++---- .../bedrock/chat/converse_transformation.py | 8 +--- .../llms/databricks/chat/transformation.py | 8 +--- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 00147b2ec8..345558a69f 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -6,6 +6,7 @@ from typing import ( Any, Dict, List, + NoReturn, Optional, Tuple, Union, @@ -291,8 +292,35 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): @staticmethod def _model_supports_effort_param(model: str) -> bool: """Whether the model accepts ``output_config.effort`` at all.""" - return any(AnthropicConfig._supports_effort_level(model, level) - for level in ("low", "minimal", "medium", "high", "xhigh", "max")) + return any( + AnthropicConfig._supports_effort_level(model, level) + for level in ("low", "minimal", "medium", "high", "xhigh", "max") + ) + + @staticmethod + def _raise_invalid_reasoning_effort( + model: str, value: Any, llm_provider: str + ) -> NoReturn: + """Raise a ``BadRequestError`` for an unrecognised ``reasoning_effort``. + + Args: + model: The model id the request was routed to (surfaced in the error). + value: The offending ``reasoning_effort`` value supplied by the caller. + llm_provider: Provider tag for the raised exception (``"anthropic"``, + ``"bedrock_converse"``, ``"databricks"``, ...). + + Raises: + litellm.exceptions.BadRequestError: Always. + """ + raise litellm.exceptions.BadRequestError( + message=( + f"Invalid reasoning_effort: {value!r}. " + f"Must be one of: 'minimal', 'low', 'medium', " + f"'high', 'xhigh', 'max', 'none'" + ), + model=model, + llm_provider=llm_provider, + ) def get_supported_openai_params(self, model: str): params = [ @@ -1203,13 +1231,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): value ) if mapped_effort is None: - raise litellm.exceptions.BadRequestError( - message=( - f"Invalid reasoning_effort: {value!r}. " - f"Must be one of: 'minimal', 'low', " - f"'medium', 'high', 'xhigh', 'max', 'none'" - ), + AnthropicConfig._raise_invalid_reasoning_effort( model=model, + value=value, llm_provider=self.custom_llm_provider or "anthropic", ) optional_params["output_config"] = {"effort": mapped_effort} diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 3b79d5b5db..efc890d9ee 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -442,13 +442,9 @@ class AmazonConverseConfig(BaseConfig): reasoning_effort ) if mapped_effort is None: - raise litellm.exceptions.BadRequestError( - message=( - f"Invalid reasoning_effort: {reasoning_effort!r}. " - f"Must be one of: 'minimal', 'low', 'medium', " - f"'high', 'xhigh', 'max', 'none'" - ), + AnthropicConfig._raise_invalid_reasoning_effort( model=model, + value=reasoning_effort, llm_provider="bedrock_converse", ) self._validate_anthropic_adaptive_effort( diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index fedb3d1165..bd10e929e1 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -352,13 +352,9 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): reasoning_effort_value ) if mapped_effort is None: - raise litellm.exceptions.BadRequestError( - message=( - f"Invalid reasoning_effort: {reasoning_effort_value!r}. " - f"Must be one of: 'minimal', 'low', " - f"'medium', 'high', 'xhigh', 'max', 'none'" - ), + AnthropicConfig._raise_invalid_reasoning_effort( model=model, + value=reasoning_effort_value, llm_provider="databricks", ) optional_params["output_config"] = {"effort": mapped_effort} From cdd777fb214e35fbc909cb561d8b83bc132f8ff7 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 4 May 2026 14:23:05 -0700 Subject: [PATCH 25/25] fix: remove unused import --- litellm/llms/databricks/chat/transformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index bd10e929e1..09c782a475 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -21,7 +21,6 @@ from typing import ( import httpx from pydantic import BaseModel -import litellm from litellm.constants import RESPONSE_FORMAT_TOOL_NAME from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( _handle_invalid_parallel_tool_calls,