From 6e64c39ddccfac7cbb61bef90a9bf15a2bb37934 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Fri, 2 Oct 2026 20:44:51 -0400 Subject: [PATCH 1/5] fix(anthropic): remap disabled thinking to between_tools on Sonnet 5.5 maybe_drop_disabled_thinking dropped thinking={type: disabled} for every always-on-thinking model, including Sonnet 5.5, which silently left thinking on at whatever effort the caller set. Sonnet 5.5 has a real off-equivalent, between_tools, valid at low/medium/high effort. Remap to that instead of dropping, and keep the existing drop-and-warn path for xhigh/max effort (where between_tools 400s) and for every other always-on-thinking family, whose own off-equivalent is not confirmed yet. Fixes #44298 --- litellm/llms/anthropic/common_utils.py | 43 +++++++++++++-- ...odel_prices_and_context_window_backup.json | 16 ++++++ litellm/types/llms/anthropic.py | 2 +- litellm/types/utils.py | 1 + model_prices_and_context_window.json | 16 ++++++ model_prices_and_context_window.schema.json | 3 ++ .../anthropic/test_anthropic_common_utils.py | 53 +++++++++++++++++++ tests/unit/test_claude_sonnet_5_config.py | 28 ++++++++-- 8 files changed, 155 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 65c2fccceeb..dff8a1de385 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -57,6 +57,11 @@ DROP_DISABLED_THINKING_WARNING: Final = ( "disabled (the alternative is a provider 400). The model will still think adaptively, its response can contain " "thinking blocks, and those thinking tokens are billed as output tokens." ) +REMAP_DISABLED_TO_BETWEEN_TOOLS_WARNING: Final = ( + "Replacing `thinking={'type': 'disabled'}` with `thinking={'type': 'between_tools'}` for model=%s: this model " + "400s on `disabled`, and `between_tools` is its lowest thinking setting. The model will not think before " + "responding; progress notes it writes between tool calls still come back as `thinking` blocks." +) # Anthropic error `type` (both the JSON error body and SSE `event: error` # payloads use this field) mapped to the HTTP status code it corresponds to. @@ -713,6 +718,28 @@ class AnthropicModelInfo(BaseLLMModelInfo): """ return AnthropicModelInfo._supports_model_capability(model, "thinking_always_on", custom_llm_provider) + @staticmethod + def _supports_between_tools_thinking(model: str, custom_llm_provider: str) -> bool: + """Whether ``model`` accepts ``thinking.type=between_tools`` as its off-equivalent + setting for an always-on-thinking model. The model cost map is authoritative: an + explicit ``supports_between_tools_thinking`` entry resolved under + ``custom_llm_provider``. Sonnet 5.5 is the only family with this entry today; + other always-on-thinking families fall back to the drop-and-warn path in + ``maybe_drop_disabled_thinking`` until their own off-equivalent is confirmed. + """ + return AnthropicModelInfo._supports_model_capability( + model, "supports_between_tools_thinking", custom_llm_provider + ) + + @staticmethod + def _effort_rejects_between_tools(optional_params: Mapping[str, object]) -> bool: + """``thinking.type=between_tools`` 400s at ``xhigh``/``max`` effort; those levels + keep adaptive thinking as their only valid off-equivalent.""" + output_config: Final = optional_params.get("output_config") + if not isinstance(output_config, dict): + return False + return output_config.get("effort") in ("xhigh", "max") + @staticmethod def _supports_legacy_thinking(model: str, custom_llm_provider: str) -> bool: """Whether ``model`` is an adaptive-thinking model that still accepts legacy @@ -729,14 +756,24 @@ class AnthropicModelInfo(BaseLLMModelInfo): optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param, as in _maybe_drop_speed_param custom_llm_provider: str, ) -> None: - """Omit ``thinking={'type': 'disabled'}`` for always-on-thinking models - (Fable 5 / Mythos 5), which 400 on it; omission is the API-documented - remedy and yields the model's default adaptive thinking.""" + """Replace ``thinking={'type': 'disabled'}`` for always-on-thinking models + (Fable 5 / Mythos 5, Sonnet 5.5, Opus 5.5), which 400 on it. Sonnet 5.5 has a + real off-equivalent, ``between_tools``, valid at `low`/`medium`/`high` effort; + remap to that instead of silently leaving thinking on at whatever effort the + caller set. Every other always-on-thinking family falls back to omitting + ``thinking``, the API-documented remedy, which yields the model's default + adaptive thinking.""" thinking: Final = optional_params.get("thinking") if not isinstance(thinking, dict) or thinking.get("type") != "disabled": return if not AnthropicModelInfo._is_always_on_thinking_model(model, custom_llm_provider): return + if AnthropicModelInfo._supports_between_tools_thinking( + model, custom_llm_provider + ) and not AnthropicModelInfo._effort_rejects_between_tools(optional_params): + litellm.verbose_logger.warning(REMAP_DISABLED_TO_BETWEEN_TOOLS_WARNING, model) + optional_params["thinking"] = {"type": "between_tools"} + return litellm.verbose_logger.warning( DROP_DISABLED_THINKING_WARNING, model, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 12e4760ed3a..800e4de6c74 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -15057,6 +15057,7 @@ "supports_output_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://platform.claude.com/docs/en/models/sonnet-5-5/overview", "supports_web_search": true @@ -59065,6 +59066,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5-5.html" }, @@ -65798,6 +65800,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78708,6 +78711,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json" }, @@ -78739,6 +78743,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "cache_creation_input_token_cost_above_1hr": 4.4e-06, "cache_creation_input_token_cost": 2.75e-06, @@ -78785,6 +78790,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -78820,6 +78826,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512 }, "bedrock/us-gov-east-1/anthropic.claude-sonnet-5-5": { @@ -78838,6 +78845,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78873,6 +78881,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78930,6 +78939,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -78971,6 +78981,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79012,6 +79023,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79134,6 +79146,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, @@ -79192,6 +79205,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79232,6 +79246,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, @@ -79272,6 +79287,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index ee357cd6581..67cb38d2bcb 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -731,7 +731,7 @@ ANTHROPIC_API_ONLY_HEADERS: Final = { # fails if calling anthropic on vertex ai class AnthropicThinkingParam(TypedDict, total=False): - type: ReadOnly[Literal["enabled", "adaptive", "disabled"]] + type: ReadOnly[Literal["enabled", "adaptive", "disabled", "between_tools"]] budget_tokens: int display: ReadOnly[Literal["summarized", "omitted", "updates"]] diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8c10b9e3497..7910983c4b7 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -193,6 +193,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False): supports_adaptive_thinking: bool | None supports_legacy_thinking: ReadOnly[bool | None] thinking_always_on: ReadOnly[bool | None] + supports_between_tools_thinking: ReadOnly[bool | None] supports_tool_search: bool | None supports_mid_conversation_system: bool | None supports_url_context: bool | None diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 12e4760ed3a..800e4de6c74 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -15057,6 +15057,7 @@ "supports_output_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://platform.claude.com/docs/en/models/sonnet-5-5/overview", "supports_web_search": true @@ -59065,6 +59066,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-sonnet-5-5.html" }, @@ -65798,6 +65800,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78708,6 +78711,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json" }, @@ -78739,6 +78743,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "cache_creation_input_token_cost_above_1hr": 4.4e-06, "cache_creation_input_token_cost": 2.75e-06, @@ -78785,6 +78790,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -78820,6 +78826,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512 }, "bedrock/us-gov-east-1/anthropic.claude-sonnet-5-5": { @@ -78838,6 +78845,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78873,6 +78881,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, "supports_assistant_prefill": false, @@ -78930,6 +78939,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -78971,6 +78981,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79012,6 +79023,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79134,6 +79146,7 @@ "output_cost_per_token": 1.2e-05, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, @@ -79192,6 +79205,7 @@ "supports_parallel_tool_use_config": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/" }, @@ -79232,6 +79246,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, @@ -79272,6 +79287,7 @@ "supports_max_reasoning_effort": true, "supports_forced_tool_use": false, "thinking_always_on": true, + "supports_between_tools_thinking": true, "prompt_cache_min_tokens": 512, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing" }, diff --git a/model_prices_and_context_window.schema.json b/model_prices_and_context_window.schema.json index cdf023e71ef..104380c0fe3 100644 --- a/model_prices_and_context_window.schema.json +++ b/model_prices_and_context_window.schema.json @@ -990,6 +990,9 @@ "supports_audio_output": { "type": "boolean" }, + "supports_between_tools_thinking": { + "type": "boolean" + }, "supports_computer_use": { "type": "boolean" }, diff --git a/tests/unit/llms/anthropic/test_anthropic_common_utils.py b/tests/unit/llms/anthropic/test_anthropic_common_utils.py index 68e2e9650d5..9f9133dfbc9 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -2504,3 +2504,56 @@ def test_tool_changes_beta_requires_system_tool_reference(role: str, content: ob ) assert ANTHROPIC_MID_CONVERSATION_TOOL_CHANGES_BETA_HEADER not in headers.get("anthropic-beta", "").split(",") + + +@pytest.mark.parametrize("effort", (None, "low", "medium", "high")) +def test_maybe_drop_disabled_thinking_remaps_sonnet_5_5_to_between_tools( + local_model_cost_map: None, effort: str | None +) -> None: + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + optional_params: Final[dict[str, object]] = {"thinking": {"type": "disabled"}} + if effort is not None: + optional_params["output_config"] = {"effort": effort} + + AnthropicModelInfo.maybe_drop_disabled_thinking( + model="claude-sonnet-5-5", + optional_params=optional_params, + custom_llm_provider="anthropic", + ) + + assert optional_params["thinking"] == {"type": "between_tools"} + + +@pytest.mark.parametrize("effort", ("xhigh", "max")) +def test_maybe_drop_disabled_thinking_drops_for_sonnet_5_5_at_rejecting_effort( + local_model_cost_map: None, effort: str +) -> None: + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + optional_params: Final[dict[str, object]] = { + "thinking": {"type": "disabled"}, + "output_config": {"effort": effort}, + } + + 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_maybe_drop_disabled_thinking_still_drops_for_opus_5_5(local_model_cost_map: None) -> None: + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + optional_params: Final[dict[str, object]] = {"thinking": {"type": "disabled"}} + + AnthropicModelInfo.maybe_drop_disabled_thinking( + model="claude-opus-5-5", + optional_params=optional_params, + custom_llm_provider="anthropic", + ) + + assert "thinking" not in optional_params diff --git a/tests/unit/test_claude_sonnet_5_config.py b/tests/unit/test_claude_sonnet_5_config.py index 36078ff494f..0f6eeb81ad9 100644 --- a/tests/unit/test_claude_sonnet_5_config.py +++ b/tests/unit/test_claude_sonnet_5_config.py @@ -10,8 +10,10 @@ populate ``litellm.anthropic_models`` at import, which is what lets a bare ``anthropic/*`` wildcard deployment). Sonnet 5.5 (``claude-sonnet-5-5``) is covered here too. It carries the same -gen-5 profile but thinking cannot be turned off and forced tool use is not -supported, same as Opus 5.5 +gen-5 profile, and forced tool use is not supported, same as Opus 5.5. Thinking +cannot be turned off via ``thinking.type=disabled``, the Sonnet 5 shape; the +off-equivalent is ``thinking.type=between_tools``, which ``maybe_drop_disabled_thinking`` +remaps a caller's ``disabled`` request to. """ import json @@ -79,9 +81,29 @@ def test_sonnet_5_5_present_in_bundled_backup(model_name): ) def test_sonnet_5_5_thinking_profile(local_model_cost_map, model, provider): """Sonnet 5.5 has thinking always on with the adaptive thinking surface, and - no forced tool use, same as Opus 5.5.""" + no forced tool use, same as Opus 5.5. Unlike the rest of that family, it also + has a real off-equivalent for thinking: between_tools.""" from litellm.llms.anthropic.common_utils import AnthropicModelInfo assert AnthropicModelInfo._is_adaptive_thinking_model(model, provider) is True assert AnthropicModelInfo._is_always_on_thinking_model(model, provider) is True + assert AnthropicModelInfo._supports_between_tools_thinking(model, provider) is True assert AnthropicModelInfo.forced_tool_use_unsupported(model.removeprefix("anthropic/")) is True + + +@pytest.mark.parametrize( + ("model", "provider"), + [ + ("claude-opus-5-5", "anthropic"), + ("claude-fable-5", "anthropic"), + ("claude-mythos-5", "anthropic"), + ], +) +def test_other_always_on_thinking_models_lack_between_tools(local_model_cost_map, model, provider): + """Sonnet 5.5 is the only always-on-thinking family with a confirmed + between_tools off-equivalent today; the rest must keep falling back to the + drop-and-warn path in maybe_drop_disabled_thinking.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + assert AnthropicModelInfo._is_always_on_thinking_model(model, provider) is True + assert AnthropicModelInfo._supports_between_tools_thinking(model, provider) is False From 2927cbd1e3172b2f7690080e97694982b0574102 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Fri, 2 Oct 2026 21:13:24 -0400 Subject: [PATCH 2/5] fix(anthropic): add supports_between_tools_thinking to the test_utils.py cost-map schema test_aaamodel_prices_and_context_window_json_is_valid validates the cost map against its own copy of the schema, separate from model_prices_and_context_window.schema.json. Missed updating it in the previous commit. --- tests/unit/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ab7ecfcda05..ed94b54d138 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -975,6 +975,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "supports_anthropic_thinking_payload": {"type": "boolean"}, "supports_legacy_thinking": {"type": "boolean"}, "thinking_always_on": {"type": "boolean"}, + "supports_between_tools_thinking": {"type": "boolean"}, "supports_mid_conversation_system": {"type": "boolean"}, "supports_sampling_params": {"type": "boolean"}, "supports_output_config": {"type": "boolean"}, From 601a4092d09a50202178786e806816ee7cbdeb55 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Mon, 5 Oct 2026 02:54:58 -0400 Subject: [PATCH 3/5] fix(anthropic): keep disabled thinking through the missing-thinking-blocks guard With modify_params=True, a tool-result turn whose assistant history has no thinking_blocks dropped thinking={'type': 'disabled'} before maybe_drop_disabled_thinking could remap it to between_tools on Sonnet 5.5, so the provider fell back to adaptive thinking. Disabled thinking needs no thinking_blocks, so both the chat and Converse guards now skip it --- litellm/llms/anthropic/chat/transformation.py | 2 +- .../bedrock/chat/converse_transformation.py | 2 +- .../test_anthropic_chat_transformation.py | 26 ++++++++++++++++++ .../chat/test_converse_transformation.py | 27 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 490912d42eb..d8dc9186b39 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1921,7 +1921,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # Anthropic errors with: "When thinking is disabled, an assistant message cannot contain thinking" # Related issue: https://github.com/BerriAI/litellm/issues/18926 if ( - optional_params.get("thinking") is not None + optional_params.get("thinking") not in (None, {"type": "disabled"}) and messages is not None and last_assistant_with_tool_calls_has_no_thinking_blocks(messages) and not any_assistant_message_has_thinking_blocks(messages) diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 48a8b1b44bb..bbefd851801 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1875,7 +1875,7 @@ class AmazonConverseConfig(BaseConfig): # If any message has thinking_blocks, we must keep thinking enabled, otherwise # Related issues: https://github.com/BerriAI/litellm/issues/14194 if ( - optional_params.get("thinking") is not None + optional_params.get("thinking") not in (None, {"type": "disabled"}) and messages is not None and last_assistant_with_tool_calls_has_no_thinking_blocks(messages) and not any_assistant_message_has_thinking_blocks(messages) diff --git a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py index 332153b4c7d..2fa71e63292 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -6798,3 +6798,29 @@ def test_chat_dummy_tool_result_for_an_orphaned_tool_call_replays_a_byte_identic _assert_prefix_stable(requests) assert [m["role"] for m in requests[0]["messages"]] == ["user", "assistant", "user"] assert requests[0]["messages"][2]["content"][0]["type"] == "tool_result" + + +def test_disabled_thinking_on_sonnet_5_5_tool_result_turn_remaps_to_between_tools(local_model_cost_map, monkeypatch): + monkeypatch.setattr(litellm, "modify_params", True) + messages: Final = [ + {"role": "user", "content": "Look it up"}, + { + "role": "assistant", + "content": None, + "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "lookup", "arguments": "{}"}}], + }, + {"role": "tool", "tool_call_id": "call_1", "content": "found"}, + ] + tools: Final = [ + {"name": "lookup", "description": "Look something up", "input_schema": {"type": "object", "properties": {}}} + ] + + request: Final = AnthropicConfig().transform_request( + model="claude-sonnet-5-5", + messages=messages, + optional_params={"tools": tools, "thinking": {"type": "disabled"}}, + litellm_params={}, + headers={}, + ) + + assert request["thinking"] == {"type": "between_tools"} diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index f6f98e3b9bd..95c0a6b78a7 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -7347,6 +7347,33 @@ def test_disabled_thinking_omitted_for_always_on_models_converse( else: assert additional.get("thinking") == {"type": "disabled"} + +def test_disabled_thinking_on_sonnet_5_5_tool_result_turn_remaps_to_between_tools_converse( + local_model_cost_map, monkeypatch +): + monkeypatch.setattr(litellm, "modify_params", True) + messages: Final = [ + {"role": "user", "content": "Look it up"}, + { + "role": "assistant", + "content": None, + "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "lookup", "arguments": "{}"}}], + }, + {"role": "tool", "tool_call_id": "call_1", "content": "found"}, + ] + tools: Final = [{"type": "function", "function": {"name": "lookup", "parameters": {"type": "object"}}}] + + result: Final = AmazonConverseConfig()._transform_request( + model="us.anthropic.claude-sonnet-5-5", + messages=messages, + optional_params={"maxTokens": 64, "tools": tools, "thinking": {"type": "disabled"}}, + litellm_params={}, + headers={}, + ) + + assert result.get("additionalModelRequestFields", {}).get("thinking") == {"type": "between_tools"} + + @pytest.mark.parametrize( "model", ["anthropic.claude-fable-5-1", "us.anthropic.claude-fable-5-1"], From 4142eace346996d9a99b593712326907c8d90c6b Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Mon, 12 Oct 2026 01:49:17 -0400 Subject: [PATCH 4/5] chore(anthropic): mark the between_tools remap as an intended in-place write maybe_drop_disabled_thinking mutates optional_params by design (its signature already carries mutable-ok); annotate the new store so the type-discipline gate's LIT011 ceiling doesn't grow. Co-Authored-By: Claude Opus 5.5 --- litellm/llms/anthropic/common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 65ef0cc6c7d..77702146790 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -944,7 +944,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): model, custom_llm_provider ) and not AnthropicModelInfo._effort_rejects_between_tools(optional_params): litellm.verbose_logger.warning(REMAP_DISABLED_TO_BETWEEN_TOOLS_WARNING, model) - optional_params["thinking"] = {"type": "between_tools"} + optional_params["thinking"] = {"type": "between_tools"} # rebind-ok: in-place out-param, see signature return litellm.verbose_logger.warning( DROP_DISABLED_THINKING_WARNING, From c572b9910fec8419a69fd2fa6f7d92d4e869ed67 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Mon, 12 Oct 2026 02:17:56 -0400 Subject: [PATCH 5/5] chore(anthropic): read output_config.effort without an untyped dict.get The bare dict narrowed from optional_params has unknown key/value types, so .get() added a reportUnknownMemberType error past the basedpyright ceiling. A membership check plus subscript reads the same value without the member access. Co-Authored-By: Claude Opus 5.5 --- litellm/llms/anthropic/common_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 77702146790..c6e1546f53a 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -910,7 +910,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): output_config: Final = optional_params.get("output_config") if not isinstance(output_config, dict): return False - return output_config.get("effort") in ("xhigh", "max") + return "effort" in output_config and output_config["effort"] in ("xhigh", "max") @staticmethod def _supports_legacy_thinking(model: str, custom_llm_provider: str) -> bool: