From 2e6d8bff5b7cad50d877030c6e44775d9daa918f Mon Sep 17 00:00:00 2001 From: Mrun25 <165297479+Mrun25@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:26:47 +0530 Subject: [PATCH 1/2] fix(anthropic): map reasoning_effort "none" to disabled thinking on default-on Claude models On Claude 5-generation models that default thinking on (Opus 5, Sonnet 5, Haiku 5.5), reasoning_effort="none" was conflated with an omitted parameter: the mapper returned None and the request went out without a thinking field, leaving the model's default in effect and billing unexpected reasoning tokens. Translate an explicit "none" to thinking.type="disabled" for those models, which accept it. Models that default thinking off keep omission, and always-on-thinking models remain handled by maybe_drop_disabled_thinking. --- litellm/llms/anthropic/chat/transformation.py | 25 ++++++++- .../test_anthropic_reasoning_effort.py | 55 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 9a63afd5cc0..1062e2812ab 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -376,6 +376,8 @@ def _build_anthropic_tool_name_maps( return forward, reverse +_CLAUDE_MAJOR_VERSION: Final = re.compile(r"claude-[a-z]+-(\d+)") + REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT: Final[dict[str, str]] = { "low": "low", "minimal": "low", @@ -1564,7 +1566,23 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): llm_provider: str = "anthropic", ) -> AnthropicThinkingParam | None: """Capability probes read the cost map under ``custom_llm_provider``; ``llm_provider`` only tags raised exceptions.""" - if reasoning_effort is None or reasoning_effort == "none": + if reasoning_effort is None: + return None + if reasoning_effort == "none": + # An explicit "none" asks for thinking off. On Claude 5-generation + # models that default thinking on (Opus 5, Sonnet 5, Haiku 5.5), + # omission leaves the model's default in effect, so translate + # "none" to thinking.type="disabled", which those models accept. + # Models that default thinking off keep omission, which already + # honors "none"; always-on-thinking models reject "disabled" and + # are handled downstream by maybe_drop_disabled_thinking. + major: Final = _CLAUDE_MAJOR_VERSION.search(model) + if ( + major is not None + and int(major.group(1)) >= 5 + and AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider) + ): + return AnthropicThinkingParam(type="disabled") return None if AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider): # without display, Anthropic defaults adaptive thinking to @@ -1961,7 +1979,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - if AnthropicConfig._is_adaptive_thinking_model(model, self._resolved_provider): + # "none" disables thinking outright; there is no matching output_config effort. + if effort_value != "none" and AnthropicConfig._is_adaptive_thinking_model( + model, self._resolved_provider + ): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(effort_value) if mapped_effort is None: AnthropicConfig._raise_invalid_reasoning_effort( diff --git a/tests/unit/llms/anthropic/test_anthropic_reasoning_effort.py b/tests/unit/llms/anthropic/test_anthropic_reasoning_effort.py index 573f586de1e..a7581652a29 100644 --- a/tests/unit/llms/anthropic/test_anthropic_reasoning_effort.py +++ b/tests/unit/llms/anthropic/test_anthropic_reasoning_effort.py @@ -74,3 +74,58 @@ class TestMapReasoningEffort: reasoning_effort="none", model="claude-4-sonnet-20250514", custom_llm_provider="anthropic" ) assert result is None + + def test_none_string_returns_disabled_for_haiku_5_5(self): + """reasoning_effort='none' must explicitly disable thinking on Claude + models that default thinking on (Haiku 5.5); omission leaves the + model's default in effect.""" + result = AnthropicConfig.map_reasoning_effort( + reasoning_effort="none", model="claude-haiku-5-5", custom_llm_provider="anthropic" + ) + assert result == {"type": "disabled"} + + def test_none_string_returns_disabled_for_opus_5(self): + result = AnthropicConfig.map_reasoning_effort( + reasoning_effort="none", model="claude-opus-5", custom_llm_provider="anthropic" + ) + assert result == {"type": "disabled"} + + def test_none_string_returns_disabled_for_always_on_model(self): + """Always-on models map to 'disabled' too; transform_request drops it + via maybe_drop_disabled_thinking (the API-documented remedy).""" + result = AnthropicConfig.map_reasoning_effort( + reasoning_effort="none", model="claude-sonnet-5-5", custom_llm_provider="anthropic" + ) + assert result == {"type": "disabled"} + + def test_none_string_map_openai_params_sets_thinking_without_output_config(self): + params = AnthropicConfig().map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="claude-haiku-5-5", + drop_params=False, + ) + assert params.get("thinking") == {"type": "disabled"} + assert "output_config" not in params + + def test_disabled_thinking_dropped_for_always_on_model(self): + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + optional_params = {"thinking": {"type": "disabled"}} + AnthropicModelInfo.maybe_drop_disabled_thinking( + model="claude-sonnet-5-5", + optional_params=optional_params, + custom_llm_provider="anthropic", + ) + assert "thinking" not in optional_params + + def test_disabled_thinking_kept_for_haiku_5_5(self): + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + optional_params = {"thinking": {"type": "disabled"}} + AnthropicModelInfo.maybe_drop_disabled_thinking( + model="claude-haiku-5-5", + optional_params=optional_params, + custom_llm_provider="anthropic", + ) + assert optional_params["thinking"] == {"type": "disabled"} From 2b3cde3526aa8ce38cb42377e5bb3793bdbba7f9 Mon Sep 17 00:00:00 2001 From: Mrun25 <165297479+Mrun25@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:28:10 +0530 Subject: [PATCH 2/2] fix(bedrock): skip output_config effort lookup for reasoning_effort "none" Bedrock's converse handler has its own copy of the adaptive-effort lookup; with reasoning_effort="none" now mapping to thinking disabled for Claude 5-gen models, that path raised Invalid reasoning_effort. Guard it the same way as the anthropic path. --- litellm/llms/bedrock/chat/converse_transformation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index cd3727401ef..b6b455d9e30 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -542,7 +542,8 @@ class AmazonConverseConfig(BaseConfig): optional_params.pop("output_config", None) else: optional_params["thinking"] = mapped_thinking - if AnthropicConfig.is_adaptive_thinking_model(model, "bedrock"): + # "none" disables thinking outright; there is no matching output_config effort. + if reasoning_effort != "none" and AnthropicConfig.is_adaptive_thinking_model(model, "bedrock"): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort) if mapped_effort is None: AnthropicConfig.raise_invalid_reasoning_effort(