From 7dea050090c78111a26658673ce78a7d28630f4b Mon Sep 17 00:00:00 2001 From: Sami Jawhar Date: Mon, 26 Oct 2026 23:04:24 +0000 Subject: [PATCH 1/4] fix(anthropic): drop clear_thinking edits whenever thinking is dropped Anthropic rejects a context_management clear_thinking_* edit unless thinking is enabled or adaptive. Several paths remove thinking from a request (the invalid-signature retry, reasoning_effort="none", adaptive thinking on a model that cannot take it, a tool turn without thinking blocks) and kept the edit, so the request failed with a 400. Remove the edit with thinking on those paths. Always-on thinking models keep it, Bedrock Invoke keeps it to add thinking back, and Bedrock Mantle drops only the edit, unless the model always thinks. The Rust /v1/messages path gets the same change. Omp-Session: 01a14c3d-9d7f-7079-97aa-f810a884c050 --- .../llms/src/anthropic/messages/thinking.rs | 176 ++++++++++++++++-- litellm/llms/anthropic/chat/transformation.py | 37 +++- litellm/llms/anthropic/common_utils.py | 48 ++++- .../pass_through/messages/transformation.py | 28 ++- .../anthropic_claude3_transformation.py | 13 +- .../bedrock_mantle/messages/transformation.py | 17 ++ .../test_anthropic_chat_transformation.py | 103 ++++++++++ .../test_anthropic_messages_effort.py | 44 +++++ .../anthropic/test_anthropic_common_utils.py | 46 +++++ .../test_anthropic_claude3_transformation.py | 30 +++ ..._bedrock_mantle_messages_transformation.py | 63 +++++++ 11 files changed, 569 insertions(+), 36 deletions(-) diff --git a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs index df1ae13fd28..f964c797815 100644 --- a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs +++ b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs @@ -2,8 +2,8 @@ use litellm_llms_types::{ formats::{ chat_completions::ReasoningEffort, messages::{ - EffortLevel, MessagesOptionalParams, MessagesRequest, OutputConfig, ThinkingConfig, - ThinkingDisplay, + ContextEdit, ContextManagement, EffortLevel, MessagesOptionalParams, MessagesRequest, + OutputConfig, ThinkingConfig, ThinkingDisplay, }, }, recognized::Recognized, @@ -144,6 +144,58 @@ fn legacy_reasoning_effort( } } +fn is_clear_thinking_edit(edit: &Recognized) -> bool { + match edit { + Recognized::Known(ContextEdit::ClearThinking { .. }) => true, + Recognized::Known(_) => false, + Recognized::Unrecognized(value) => value + .get("type") + .and_then(Value::as_str) + .is_some_and(|edit_type| edit_type.starts_with("clear_thinking_")), + } +} + +/// Anthropic rejects `clear_thinking_*` edits without thinking. `context_management` goes +/// only when such an edit was removed and nothing else is left in it. +fn without_clear_thinking_edits( + context_management: Option>, +) -> Option> { + match context_management { + Some(Recognized::Known(ContextManagement { + edits: Some(edits), + extra, + })) if edits.iter().any(is_clear_thinking_edit) => { + let kept: Vec<_> = edits + .into_iter() + .filter(|edit| !is_clear_thinking_edit(edit)) + .collect(); + (!kept.is_empty() || !extra.is_empty()).then(|| { + Recognized::Known(ContextManagement { + edits: Some(kept), + extra, + }) + }) + } + other => other, + } +} + +fn without_thinking( + params: MessagesOptionalParams, + context: &ThinkingContext, +) -> MessagesOptionalParams { + let context_management = if context.capabilities.thinking_always_on { + params.context_management + } else { + without_clear_thinking_edits(params.context_management) + }; + MessagesOptionalParams { + thinking: None, + context_management, + ..params + } +} + fn translate_reasoning_effort( request: MessagesRequest, context: &ThinkingContext, @@ -171,9 +223,8 @@ fn translate_reasoning_effort( ) else { return Ok(MessagesRequest { params: MessagesOptionalParams { - thinking: None, output_config: None, - ..request.params + ..without_thinking(request.params, context) }, ..request }); @@ -277,15 +328,11 @@ fn translate_adaptive_effort_for_non_adaptive_model( _ => true, }; if supports_effort_param(capabilities) && (!adaptive_thinking || level_accepted) { + if !adaptive_thinking { + return Ok(request); + } return Ok(MessagesRequest { - params: MessagesOptionalParams { - thinking: if adaptive_thinking { - None - } else { - request.params.thinking - }, - ..request.params - }, + params: without_thinking(request.params, context), ..request }); } @@ -294,13 +341,17 @@ fn translate_adaptive_effort_for_non_adaptive_model( } else { None }; + let thinking = budget + .and_then(|budget| fit_budget_to_max_tokens(budget, request.params.max_tokens)) + .map(|budget| Recognized::Known(ThinkingConfig::enabled(budget))); + let params = MessagesOptionalParams { + output_config: without_effort(request.params.output_config), + ..request.params + }; Ok(MessagesRequest { - params: MessagesOptionalParams { - thinking: budget - .and_then(|budget| fit_budget_to_max_tokens(budget, request.params.max_tokens)) - .map(|budget| Recognized::Known(ThinkingConfig::enabled(budget))), - output_config: without_effort(request.params.output_config), - ..request.params + params: match thinking { + Some(_) => MessagesOptionalParams { thinking, ..params }, + None => without_thinking(params, context), }, ..request }) @@ -625,6 +676,95 @@ mod tests { assert_eq!(translate(capabilities, input), Ok(request(expected))); } + #[rstest] + #[case::reasoning_effort_none( + haiku_4_5(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none"}), + false + )] + #[case::reasoning_effort_high( + haiku_4_5(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "high"}), + true + )] + #[case::effort_model_drops_adaptive(opus_4_5(), claude_code_payload("high", 8192), false)] + #[case::adaptive_model_keeps_adaptive(sonnet_4_6(), claude_code_payload("high", 8192), true)] + #[case::non_reasoning_model(haiku_3_5(), claude_code_payload("high", 8192), false)] + #[case::legacy_thinking_model(haiku_4_5(), claude_code_payload("high", 8192), true)] + #[case::always_on_model_keeps_edit( + fable_5_1(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none"}), + true + )] + fn clear_thinking_edit_is_dropped_with_thinking_except_on_always_on_models( + #[case] capabilities: MessagesModelCapabilities, + #[case] fields: Value, + #[case] keeps_edit: bool, + ) { + let mut fields = fields; + fields.as_object_mut().unwrap().insert( + "context_management".into(), + serde_json::json!({"edits": [{"type": "clear_thinking_20251015", "keep": "all"}, {"type": "clear_tool_uses_20250919"}]}), + ); + let always_on = capabilities.thinking_always_on; + let translated = serde_json::to_value(translate(capabilities, fields).unwrap()).unwrap(); + let edit_types: Vec<&str> = translated["context_management"]["edits"] + .as_array() + .unwrap() + .iter() + .filter_map(|edit| edit["type"].as_str()) + .collect(); + let expected: &[&str] = if keeps_edit { + &["clear_thinking_20251015", "clear_tool_uses_20250919"] + } else { + &["clear_tool_uses_20250919"] + }; + assert_eq!(edit_types, expected); + assert_eq!( + translated.get("thinking").is_some(), + keeps_edit && !always_on + ); + } + + #[rstest] + #[case::only_clear_thinking_is_removed_whole( + serde_json::json!({"edits": [{"type": "clear_thinking_20251015"}]}), + None + )] + #[case::any_clear_thinking_version( + serde_json::json!({"edits": [{"type": "clear_thinking_20990101"}, {"type": "compact_20260112"}]}), + Some(serde_json::json!({"edits": [{"type": "compact_20260112"}]})) + )] + #[case::other_keys_kept( + serde_json::json!({"edits": [{"type": "clear_thinking_20251015"}], "extra": 1}), + Some(serde_json::json!({"edits": [], "extra": 1})) + )] + #[case::no_clear_thinking_untouched( + serde_json::json!({"edits": []}), + Some(serde_json::json!({"edits": []})) + )] + #[case::openai_list_untouched( + serde_json::json!([{"type": "compaction"}]), + Some(serde_json::json!([{"type": "compaction"}])) + )] + fn clear_thinking_edits_are_removed( + #[case] context_management: Value, + #[case] expected: Option, + ) { + let translated = translate( + haiku_4_5(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none", "context_management": context_management}), + ) + .unwrap(); + assert_eq!( + serde_json::to_value(translated) + .unwrap() + .get("context_management") + .cloned(), + expected + ); + } + #[rstest] #[case::minimal_floors_at_min_budget("minimal", 1024)] #[case::low("low", 1024)] diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 92e7c84031c..03d8b728ce0 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -2,7 +2,7 @@ import copy import json import re import time -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Callable, Mapping, MutableMapping, Sequence from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, Final, NoReturn, cast @@ -121,6 +121,7 @@ from ..common_utils import ( REFUSAL_STOP_FIELDS, AnthropicError, AnthropicModelInfo, + drop_clear_thinking_edits_for_model, eager_input_streaming_flag, process_anthropic_headers, refusal_provider_specific_fields, @@ -181,6 +182,16 @@ def _tool_type(tool: object) -> str | None: return None +def _requested_effort(reasoning_effort: object) -> str | None: + match reasoning_effort: + case str(): + return reasoning_effort + case {"effort": str(effort)}: + return effort + case _: + return None + + def _is_openai_tool_search_tool(tool: object) -> bool: return _tool_type(tool) == "tool_search" @@ -1833,6 +1844,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # via ``data = {**optional_params}`` and would 400 with # ``Extra inputs are not permitted``). + requested_params: Final = cast( # cast-ok: non_default_params is the untyped dict of the shared signature + Mapping[str, object], non_default_params + ) + for param, value in non_default_params.items(): if param == "max_tokens" or param == "max_completion_tokens": optional_params["max_tokens"] = value if isinstance(value, int) else max(1, int(round(value))) @@ -1946,10 +1961,8 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # send. Coerce to the effort string before mapping — same # shape-tolerance the GPT-5 path already implements in # `_normalize_reasoning_effort_for_chat_completion`. - effort_value = value - if isinstance(effort_value, dict): - effort_value = effort_value.get("effort") - if not isinstance(effort_value, str): + effort_value = _requested_effort(requested_params[param]) + if effort_value is None: continue mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=effort_value, @@ -2001,6 +2014,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ): optional_params["service_tier"] = anthropic_tier # rebind-ok: in-place fill is this method's contract + mapped_params: Final = cast( # cast-ok: optional_params is the untyped dict of the shared signature + MutableMapping[str, object], optional_params + ) + if "thinking" not in mapped_params and ( + "thinking" in requested_params or _requested_effort(requested_params.get("reasoning_effort")) is not None + ): + drop_clear_thinking_edits_for_model(mapped_params, model, self._resolved_provider) + ## handle thinking tokens self.update_optional_params_with_thinking_tokens( non_default_params=non_default_params, optional_params=optional_params @@ -2287,7 +2308,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): and not any_assistant_message_has_thinking_blocks(messages) ): if litellm.modify_params: - optional_params.pop("thinking", None) + mapped_params: Final = cast( # cast-ok: optional_params is the untyped dict of the shared signature + MutableMapping[str, object], optional_params + ) + mapped_params.pop("thinking", None) + drop_clear_thinking_edits_for_model(mapped_params, model, self._resolved_provider) litellm.verbose_logger.warning( "Dropping 'thinking' param because the last assistant message with tool_calls " "has no thinking_blocks. The model won't use extended thinking for this turn." diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 1f5e4e7de39..4f609c7759a 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1780,17 +1780,63 @@ def strip_encrypted_reasoning_blocks_from_anthropic_messages( return [m for m in stripped if m is not None] +def _is_clear_thinking_edit(edit: object) -> bool: + match edit: + case {"type": str(edit_type)}: + return edit_type.startswith("clear_thinking_") + case _: + return False + + +def drop_clear_thinking_edits(params: MutableMapping[str, object]) -> None: # mutable-ok: edits in place + """ + Remove the ``clear_thinking_*`` context-management edits, which Anthropic + rejects without ``thinking``. ``context_management`` itself goes only when + such an edit was removed and nothing else is left in it. + """ + raw_context_management: Final = params.get("context_management") + if not isinstance(raw_context_management, dict): + return + context_management: Final = OBJECT_MAPPING_ADAPTER.validate_python(raw_context_management) + raw_edits: Final = context_management.get("edits") + if not isinstance(raw_edits, list): + return + edits: Final = OBJECT_LIST_ADAPTER.validate_python(raw_edits) + kept_edits: Final = [edit for edit in edits if not _is_clear_thinking_edit(edit)] + if len(kept_edits) == len(edits): + return + if not kept_edits and len(context_management) == 1: + params.pop("context_management") + return + params["context_management"] = { # rebind-ok: callers mutate their request params in place + **context_management, + "edits": kept_edits, + } + + +def drop_clear_thinking_edits_for_model( + params: MutableMapping[str, object], # mutable-ok: edits in place + model: str, + custom_llm_provider: str, +) -> None: + """Remove the clear_thinking edits unless the model always thinks anyway.""" + if not AnthropicModelInfo.supports_model_capability(model, "thinking_always_on", custom_llm_provider): + drop_clear_thinking_edits(params) + + def strip_thinking_blocks_from_anthropic_messages_request_dict( data: dict[str, object], ) -> None: """ Mutate an Anthropic Messages-style request dict: strip thinking blocks from - ``messages`` and remove the top-level ``thinking`` extended-thinking param. + ``messages`` and remove ``thinking`` with its dependent context-management + edits (see :func:`drop_clear_thinking_edits`). """ msgs: Final = data.get("messages") if isinstance(msgs, list): data["messages"] = strip_thinking_blocks_from_anthropic_messages(msgs) data.pop("thinking", None) + drop_clear_thinking_edits(data) _DROP_ANTHROPIC_MESSAGE: Final = object() diff --git a/litellm/llms/anthropic/pass_through/messages/transformation.py b/litellm/llms/anthropic/pass_through/messages/transformation.py index 35db54147c5..654fbb182ef 100644 --- a/litellm/llms/anthropic/pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/pass_through/messages/transformation.py @@ -1,4 +1,4 @@ -from collections.abc import AsyncIterator, Mapping, Sequence +from collections.abc import AsyncIterator, Mapping, MutableMapping, Sequence from typing import ClassVar, Final import httpx @@ -31,6 +31,7 @@ from litellm.types.router import GenericLiteLLMParams from ...common_utils import ( AnthropicError, AnthropicModelInfo, + drop_clear_thinking_edits_for_model, merge_anthropic_beta_headers, optionally_handle_anthropic_oauth, requires_native_compaction_beta, @@ -92,6 +93,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): def _resolved_provider(self) -> str: return self.custom_llm_provider or "anthropic" + def _drop_thinking( + self, + optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place + model: str, + custom_llm_provider: str, + ) -> None: + optional_params.pop("thinking", None) + drop_clear_thinking_edits_for_model(optional_params, model, custom_llm_provider) + def get_supported_anthropic_messages_params(self, model: str) -> list: return [ "messages", @@ -412,9 +422,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): messages=messages, ) - @staticmethod def _translate_reasoning_effort_to_anthropic( - model: str, optional_params: dict, max_tokens: int | None, custom_llm_provider: str + self, model: str, optional_params: dict[str, object], max_tokens: int | None, custom_llm_provider: str ) -> None: """Map OpenAI-style ``reasoning_effort`` to native Anthropic params. @@ -443,7 +452,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): raise AnthropicError(message=str(e.message), status_code=400) if mapped_thinking is None: - optional_params.pop("thinking", None) + self._drop_thinking(optional_params, model, custom_llm_provider) optional_params.pop("output_config", None) return @@ -473,9 +482,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): existing_output_config.setdefault("effort", mapped_effort) optional_params["output_config"] = existing_output_config - @staticmethod def _translate_adaptive_effort_for_non_adaptive_model( - model: str, optional_params: dict, max_tokens: int | None, custom_llm_provider: str + self, model: str, optional_params: dict[str, object], max_tokens: int | None, custom_llm_provider: str ) -> None: """Translate the 4.6+ adaptive-thinking interface (``thinking.type=adaptive`` and/or ``output_config.effort``) down to what an older Anthropic model @@ -538,7 +546,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): or AnthropicConfig.validate_effort_for_model(model, effort, custom_llm_provider) is None ): if adaptive_thinking: - optional_params.pop("thinking", None) + self._drop_thinking(optional_params, model, custom_llm_provider) return supports_thinking: Final = AnthropicModelInfo.supports_model_capability( @@ -566,7 +574,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params["thinking"] = capped_thinking else: verbose_logger.warning(DROP_UNSUPPORTED_ADAPTIVE_EFFORT_WARNING, model) - optional_params.pop("thinking", None) + self._drop_thinking(optional_params, model, custom_llm_provider) if isinstance(output_config, dict) and "effort" in output_config: residual: Final = {k: v for k, v in output_config.items() if k != "effort"} @@ -642,7 +650,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): self._translate_reasoning_effort_to_anthropic( model=model, - optional_params=anthropic_messages_optional_request_params, + optional_params=anthropic_messages_optional_request_params, # pyright: ignore[reportUnknownArgumentType] # the request params arrive as a bare dict max_tokens=max_tokens, custom_llm_provider=self._resolved_provider, ) @@ -662,7 +670,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): self._translate_adaptive_effort_for_non_adaptive_model( model=model, - optional_params=anthropic_messages_optional_request_params, + optional_params=anthropic_messages_optional_request_params, # pyright: ignore[reportUnknownArgumentType] # the request params arrive as a bare dict max_tokens=max_tokens, custom_llm_provider=self._resolved_provider, ) 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 805652630ed..402285b2848 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -1,4 +1,4 @@ -from collections.abc import AsyncIterator, Mapping, Sequence +from collections.abc import AsyncIterator, Mapping, MutableMapping, Sequence from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final, cast @@ -98,6 +98,17 @@ class AmazonAnthropicClaudeMessagesConfig( def beta_headers_provider(self) -> str: return self.custom_llm_provider or "bedrock" + def _drop_thinking( + self, + optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place + model: str, + custom_llm_provider: str, + ) -> None: + """Keep the clear_thinking edit: it makes + ``_ensure_thinking_for_clear_thinking_context_management`` add thinking back, + and ``_filter_context_management_for_bedrock_invoke`` strips it before sending.""" + optional_params.pop("thinking", None) + def should_strip_billing_metadata(self) -> bool: return True diff --git a/litellm/llms/bedrock_mantle/messages/transformation.py b/litellm/llms/bedrock_mantle/messages/transformation.py index aa6510f0491..2ce1a7a4e81 100644 --- a/litellm/llms/bedrock_mantle/messages/transformation.py +++ b/litellm/llms/bedrock_mantle/messages/transformation.py @@ -3,6 +3,7 @@ from types import MappingProxyType from typing import Final from litellm.litellm_core_utils.url_utils import join_api_base +from litellm.llms.anthropic.common_utils import drop_clear_thinking_edits_for_model from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock.common_utils import MANTLE_MESSAGES_PATH from litellm.llms.bedrock.messages.mantle_transformation import AmazonMantleMessagesConfig @@ -45,6 +46,22 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM } ) + def _ensure_thinking_for_clear_thinking_context_management( + self, + anthropic_messages_request: dict[str, object], # mutable-ok: edits the request in place like the base + model: str, + drop_params: bool, + ) -> bool: + injected_thinking: Final = super()._ensure_thinking_for_clear_thinking_context_management( + anthropic_messages_request, model, drop_params + ) + match anthropic_messages_request.get("thinking"): + case {"type": "enabled" | "adaptive"}: + pass + case _: + drop_clear_thinking_edits_for_model(anthropic_messages_request, model, self._resolved_provider) + return injected_thinking + def __init__(self, aws_signer: BaseAWSLLM | None = None) -> None: AmazonMantleMessagesConfig.__init__(self) self._aws_signer = aws_signer or self 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 32f18b952ac..857eae8593d 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -9816,3 +9816,106 @@ def test_transform_request_tool_config_point_without_tools_drops_the_key(): assert "cache_control_injection_points" not in data assert "tools" not in data assert litellm_params["metadata"] == {} + + +CLEAR_THINKING_EDIT: Final = {"type": "clear_thinking_20251015", "keep": "all"} +CLEAR_TOOL_USES_EDIT: Final = {"type": "clear_tool_uses_20250919"} + + +def _edits_for(keeps_thinking: bool) -> list[dict[str, str]]: + return [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT] if keeps_thinking else [CLEAR_TOOL_USES_EDIT] + + +@pytest.mark.parametrize( + ("model", "params", "keeps_thinking"), + [ + pytest.param( + "claude-haiku-4-5", + {"thinking": {"type": "adaptive"}, "max_tokens": 1000}, + False, + id="adaptive-unfit-dropped", + ), + pytest.param( + "claude-haiku-4-5", {"thinking": {"type": "adaptive"}, "max_tokens": 8192}, True, id="adaptive-translated" + ), + pytest.param("claude-sonnet-4-5", {"reasoning_effort": "none"}, False, id="reasoning-effort-none"), + pytest.param( + "claude-sonnet-4-5", {"reasoning_effort": {"effort": "none"}}, False, id="reasoning-effort-dict-none" + ), + pytest.param("claude-sonnet-4-5", {"reasoning_effort": "high"}, True, id="reasoning-effort-high"), + ], +) +@pytest.mark.parametrize("context_management_first", [True, False]) +def test_map_openai_params_drops_clear_thinking_edit_exactly_when_thinking_is( + model, params, keeps_thinking, context_management_first +): + context_management: Final = {"context_management": {"edits": [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT]}} + rest: Final = {"max_tokens": 8192, **params} + result = AnthropicConfig().map_openai_params( + non_default_params={**context_management, **rest} + if context_management_first + else {**rest, **context_management}, + optional_params={}, + model=model, + drop_params=False, + ) + + assert ("thinking" in result) is keeps_thinking + assert result["context_management"] == {"edits": _edits_for(keeps_thinking)} + + +@pytest.mark.parametrize( + ("model", "reasoning_effort"), + [ + pytest.param("claude-fable-5", "none", id="always-on-model"), + pytest.param("claude-sonnet-4-5", {"summary": "concise"}, id="effort-less-dict"), + ], +) +def test_map_openai_params_keeps_clear_thinking_edit_when_thinking_was_not_dropped(model, reasoning_effort): + result = AnthropicConfig().map_openai_params( + non_default_params={ + "context_management": {"edits": [CLEAR_THINKING_EDIT]}, + "max_tokens": 8192, + "reasoning_effort": reasoning_effort, + }, + optional_params={}, + model=model, + drop_params=False, + ) + + assert result["context_management"] == {"edits": [CLEAR_THINKING_EDIT]} + + +@pytest.mark.parametrize( + ("model", "modify_params", "keeps_thinking", "keeps_edit"), + [ + pytest.param("claude-sonnet-4-5", True, False, False, id="dropped"), + pytest.param("claude-sonnet-4-5", False, True, True, id="modify-params-off"), + pytest.param("claude-fable-5", True, False, True, id="always-on-keeps-edit"), + ], +) +def test_transform_request_drops_clear_thinking_edit_with_thinking_for_tool_turn_without_thinking_blocks( + monkeypatch, model, modify_params, keeps_thinking, keeps_edit +): + monkeypatch.setattr(litellm, "modify_params", modify_params) + result = AnthropicConfig().transform_request( + model=model, + messages=[ + {"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"}, + ], + optional_params={ + "thinking": {"type": "enabled", "budget_tokens": 1024}, + "context_management": {"edits": [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT]}, + }, + litellm_params={}, + headers={}, + ) + + assert ("thinking" in result) is keeps_thinking + assert result["context_management"] == {"edits": _edits_for(keeps_edit)} diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py index 7885cc69b19..b6f2c4f830c 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py @@ -1,3 +1,5 @@ +from typing import Final + import pytest from litellm.constants import ( @@ -334,3 +336,45 @@ def test_reasoning_effort_budget_capped_for_openai_like_messages_upstream(): assert result["thinking"] == {"type": "enabled", "budget_tokens": 3999} assert result["max_tokens"] == 4000 + + +CLEAR_THINKING_EDIT: Final = {"type": "clear_thinking_20251015", "keep": "all"} +CLEAR_TOOL_USES_EDIT: Final = {"type": "clear_tool_uses_20250919"} + + +@pytest.mark.parametrize( + ("model", "params", "keeps_thinking"), + [ + pytest.param("claude-sonnet-4-5", {"reasoning_effort": "none"}, False, id="reasoning-effort-none"), + pytest.param("claude-haiku-4-5", {"reasoning_effort": "high"}, True, id="reasoning-effort-high"), + pytest.param("claude-opus-4-5", _claude_code_payload(effort="high"), False, id="effort-model-drops-adaptive"), + pytest.param("claude-sonnet-4-6", _claude_code_payload(effort="high"), True, id="adaptive-model"), + pytest.param("claude-3-5-haiku-latest", _claude_code_payload(), False, id="non-reasoning-model"), + pytest.param("claude-haiku-4-5", _claude_code_payload(), True, id="legacy-thinking-model"), + ], +) +def test_clear_thinking_edit_dropped_exactly_when_thinking_is(model, params, keeps_thinking): + """Anthropic rejects a clear_thinking edit without thinking, so every path that drops + thinking must drop the edit too, and leave other edits alone""" + result = _transform( + model, + { + "max_tokens": 8192, + **params, + "context_management": {"edits": [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT]}, + }, + ) + + assert ("thinking" in result) is keeps_thinking + expected_edits = [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT] if keeps_thinking else [CLEAR_TOOL_USES_EDIT] + assert result["context_management"] == {"edits": expected_edits} + + +def test_clear_thinking_edit_kept_for_always_on_thinking_model_without_thinking(): + """An always-on model keeps thinking adaptively after reasoning_effort='none' drops the param""" + result = _transform( + "claude-fable-5", + {"max_tokens": 8192, "reasoning_effort": "none", "context_management": {"edits": [CLEAR_THINKING_EDIT]}}, + ) + + assert result["context_management"] == {"edits": [CLEAR_THINKING_EDIT]} diff --git a/tests/unit/llms/anthropic/test_anthropic_common_utils.py b/tests/unit/llms/anthropic/test_anthropic_common_utils.py index 093b4d6cce8..28b7b2eb826 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -1610,6 +1610,52 @@ class TestAnthropicThinkingSignatureSelfHeal: assert "thinking" not in data assert data["messages"] == [] + def test_strip_thinking_request_dict_drops_clear_thinking_edits(self): + from litellm.llms.anthropic.common_utils import ( + strip_thinking_blocks_from_anthropic_messages_request_dict, + ) + + data = { + "messages": [{"role": "assistant", "content": [{"type": "thinking", "thinking": "unsigned"}]}], + "thinking": {"type": "adaptive"}, + "context_management": {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]}, + } + strip_thinking_blocks_from_anthropic_messages_request_dict(data) + assert "context_management" not in data + + @pytest.mark.parametrize( + ("context_management", "expected"), + [ + pytest.param( + {"edits": [{"type": "clear_thinking_20990101"}, {"type": "clear_tool_uses_20250919"}]}, + {"edits": [{"type": "clear_tool_uses_20250919"}]}, + id="any-clear-thinking-version-dropped-others-kept", + ), + pytest.param({"edits": [{"type": "clear_thinking_20251015"}]}, None, id="emptied-removed"), + pytest.param( + {"edits": [{"type": "clear_thinking_20251015"}], "extra": 1}, + {"edits": [], "extra": 1}, + id="other-keys-kept", + ), + pytest.param({"edits": []}, {"edits": []}, id="empty-edits-untouched"), + pytest.param("clear_thinking_20251015", "clear_thinking_20251015", id="non-dict-untouched"), + pytest.param({"edits": "clear_thinking"}, {"edits": "clear_thinking"}, id="non-list-edits-untouched"), + pytest.param( + {"edits": ["clear_thinking_20251015", {"type": "clear_thinking_20251015"}]}, + {"edits": ["clear_thinking_20251015"]}, + id="non-dict-edit-kept", + ), + ], + ) + def test_drop_clear_thinking_edits(self, context_management, expected): + from litellm.llms.anthropic.common_utils import ( + drop_clear_thinking_edits, + ) + + params = {"context_management": context_management} + drop_clear_thinking_edits(params) + assert params == ({} if expected is None else {"context_management": expected}) + def test_strip_empty_content_blocks_from_anthropic_messages(self): """Covers #22930. The core regression scenario: an assistant message with an empty text block alongside ``tool_use`` loses the empty block diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 6fe32785281..995729f10a3 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -2195,6 +2195,36 @@ def test_bedrock_invoke_transform_emits_adaptive_thinking_for_opus_4_8(): assert result["output_config"]["effort"] == "low" +@pytest.mark.parametrize( + "thinking_params", + [ + pytest.param({"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}}, id="adaptive-effort"), + pytest.param({"reasoning_effort": "none"}, id="reasoning-effort-none"), + ], +) +def test_bedrock_invoke_keeps_thinking_for_clear_thinking_edit_when_base_drops_thinking(thinking_params): + """The base messages transform drops thinking for Opus 4.5 here. Bedrock keeps the + clear_thinking edit long enough to add the minimum enabled thinking back, then strips it""" + from litellm.types.router import GenericLiteLLMParams + + result = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( + model="anthropic.claude-opus-4-5-20251101-v1:0", + messages=[{"role": "user", "content": "hi"}], + anthropic_messages_optional_request_params={ + "max_tokens": 32000, + **thinking_params, + "context_management": { + "edits": [{"type": "clear_thinking_20251015", "keep": "all"}, {"type": "clear_tool_uses_20250919"}] + }, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert result["thinking"] == {"type": "enabled", "budget_tokens": BEDROCK_MIN_THINKING_BUDGET_TOKENS} + assert result["context_management"] == {"edits": [{"type": "clear_tool_uses_20250919"}]} + + def test_bedrock_invoke_transform_normalizes_system_role_message_into_system(): """Bedrock Invoke rejects ``role: "system"`` entries in ``messages`` on some Claude aliases; they must be moved into the top-level ``system`` field diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py index 3236efbbfba..5cca57744b0 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py @@ -19,6 +19,7 @@ import respx import litellm from litellm.caching.llm_caching_handler import LLMClientCache +from litellm.constants import BEDROCK_MIN_THINKING_BUDGET_TOKENS from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock_mantle.messages.transformation import ( BedrockMantleAnthropicMessagesConfig, @@ -224,6 +225,68 @@ class TestRequestBody: ) assert "stream" not in body + @pytest.mark.parametrize( + ("model", "thinking_params", "expected_thinking", "expected_edit_types"), + [ + pytest.param( + "anthropic.claude-3-5-haiku-20241022-v1:0", + {"thinking": {"type": "adaptive"}}, + None, + ["clear_tool_uses_20250919"], + id="no-thinking-support-drops-edit", + ), + pytest.param( + "anthropic.claude-opus-4-5-20251101-v1:0", + {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}}, + {"type": "enabled", "budget_tokens": BEDROCK_MIN_THINKING_BUDGET_TOKENS}, + ["clear_thinking_20251015", "clear_tool_uses_20250919"], + id="thinking-added-back-keeps-edit", + ), + pytest.param( + "anthropic.claude-mythos-preview", + {}, + None, + ["clear_thinking_20251015", "clear_tool_uses_20250919"], + id="always-on-model-keeps-edit", + ), + pytest.param( + "anthropic.claude-sonnet-4-6", + {"thinking": {"type": "adaptive"}}, + {"type": "adaptive"}, + ["clear_thinking_20251015", "clear_tool_uses_20250919"], + id="adaptive-thinking-keeps-edit", + ), + ], + ) + def test_clear_thinking_edit_forwarded_only_with_thinking( + self, model, thinking_params, expected_thinking, expected_edit_types + ): + body = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( + model=model, + messages=[{"role": "user", "content": "ping"}], + anthropic_messages_optional_request_params={ + "max_tokens": 32000, + **thinking_params, + "context_management": { + "edits": [{"type": "clear_thinking_20251015", "keep": "all"}, {"type": "clear_tool_uses_20250919"}] + }, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + assert body.get("thinking") == expected_thinking + assert [edit["type"] for edit in body["context_management"]["edits"]] == expected_edit_types + + def test_disabled_thinking_without_edits_is_untouched(self): + body = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( + model="anthropic.claude-3-5-haiku-20241022-v1:0", + messages=[{"role": "user", "content": "ping"}], + anthropic_messages_optional_request_params={"max_tokens": 8, "thinking": {"type": "disabled"}}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + assert body["thinking"] == {"type": "disabled"} + class TestAuth: def test_bearer_from_api_key_skips_aws_credentials(self): From 7bb865014027a502edfa88cd36e2dc2970edc91b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:41:31 -0700 Subject: [PATCH 2/4] fix(anthropic): drop the clear_thinking edit when the reasoning budget cannot fit When reasoning_effort maps to a budget that max_tokens cannot hold, thinking is dropped. The clear_thinking edit now goes with it on native /v1/messages and in the Rust path, unless the caller sent thinking themselves. --- .../llms/src/anthropic/messages/thinking.rs | 22 ++++++++++++++++++- .../messages/native/transformation.py | 2 ++ .../native/test_anthropic_messages_effort.py | 12 ++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs index f964c797815..e21711dc40d 100644 --- a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs +++ b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs @@ -250,7 +250,13 @@ fn translate_reasoning_effort( }); } let Some(budget) = fit_budget_to_max_tokens(budget, request.params.max_tokens) else { - return Ok(request); + if request.params.thinking.is_some() { + return Ok(request); + } + return Ok(MessagesRequest { + params: without_thinking(request.params, context), + ..request + }); }; let enabled = ThinkingConfig::enabled(budget); Ok(MessagesRequest { @@ -687,6 +693,20 @@ mod tests { serde_json::json!({"max_tokens": 8192, "reasoning_effort": "high"}), true )] + #[case::reasoning_effort_budget_cannot_fit( + haiku_4_5(), + serde_json::json!({"max_tokens": 1024, "reasoning_effort": "high"}), + false + )] + #[case::caller_thinking_kept_when_budget_cannot_fit( + haiku_4_5(), + serde_json::json!({ + "max_tokens": 1024, + "reasoning_effort": "high", + "thinking": {"type": "enabled", "budget_tokens": 1000} + }), + true + )] #[case::effort_model_drops_adaptive(opus_4_5(), claude_code_payload("high", 8192), false)] #[case::adaptive_model_keeps_adaptive(sonnet_4_6(), claude_code_payload("high", 8192), true)] #[case::non_reasoning_model(haiku_3_5(), claude_code_payload("high", 8192), false)] diff --git a/litellm/llms/anthropic/messages/native/transformation.py b/litellm/llms/anthropic/messages/native/transformation.py index 6869c3d4095..51c9621b7ff 100644 --- a/litellm/llms/anthropic/messages/native/transformation.py +++ b/litellm/llms/anthropic/messages/native/transformation.py @@ -459,6 +459,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): fitted_thinking: Final = AnthropicConfig.cap_thinking_budget_to_max_tokens(mapped_thinking, max_tokens) if fitted_thinking is None: verbose_logger.warning(DROP_UNFITTING_REASONING_EFFORT_WARNING, reasoning_effort, model, max_tokens) + if "thinking" not in optional_params: + self._drop_thinking(optional_params, model, custom_llm_provider) return optional_params.setdefault("thinking", fitted_thinking) diff --git a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py index 37592a66c9f..b1eed12b4a3 100644 --- a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py @@ -347,6 +347,18 @@ CLEAR_TOOL_USES_EDIT: Final = {"type": "clear_tool_uses_20250919"} [ pytest.param("claude-sonnet-4-5", {"reasoning_effort": "none"}, False, id="reasoning-effort-none"), pytest.param("claude-haiku-4-5", {"reasoning_effort": "high"}, True, id="reasoning-effort-high"), + pytest.param( + "claude-haiku-4-5", + {"max_tokens": 1024, "reasoning_effort": "high"}, + False, + id="reasoning-effort-budget-cannot-fit", + ), + pytest.param( + "claude-haiku-4-5", + {"max_tokens": 1024, "reasoning_effort": "high", "thinking": {"type": "enabled", "budget_tokens": 1000}}, + True, + id="caller-thinking-kept-when-budget-cannot-fit", + ), pytest.param("claude-opus-4-5", _claude_code_payload(effort="high"), False, id="effort-model-drops-adaptive"), pytest.param("claude-sonnet-4-6", _claude_code_payload(effort="high"), True, id="adaptive-model"), pytest.param("claude-3-5-haiku-latest", _claude_code_payload(), False, id="non-reasoning-model"), From dc172438b99c5df296445886b3202c1d5f849dd3 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:39 -0700 Subject: [PATCH 3/4] fix(anthropic): keep the clear_thinking edit on models that think by default Sonnet 5, Opus 5, and Haiku 5.5 still think when thinking is left out, so dropping their clear_thinking edit on effort none only stopped old thinking from being cleared. A new thinking_on_by_default cost map key marks them, and both the Python and Rust paths keep the edit for those models. Python Bedrock Mantle now drops the edit only when LiteLLM dropped thinking and Bedrock cannot turn it back on, so a client's own edit is forwarded as before. --- .../crates/inference-messages/src/types.rs | 2 + .../crates/llms/src/anthropic/common_utils.rs | 1 + .../llms/src/anthropic/messages/thinking.rs | 29 +++++++-- .../llms/src/base_llm/messages/context.rs | 3 + .../crates/model-catalog/src/model_info.rs | 1 + .../model-catalog/src/wire/attributes.rs | 1 + litellm/llms/anthropic/common_utils.py | 8 ++- .../bedrock_mantle/messages/transformation.py | 28 ++++----- ...odel_prices_and_context_window_backup.json | 60 +++++++++++++++++++ .../rust_bridge/host/model_capabilities.py | 1 + litellm/types/utils.py | 1 + litellm/utils.py | 1 + model_prices_and_context_window.json | 60 +++++++++++++++++++ .../native/test_anthropic_messages_effort.py | 11 ++-- .../test_anthropic_claude_transformation.py | 2 - ..._bedrock_mantle_messages_transformation.py | 9 ++- .../test_model_prices_and_context_window.py | 2 + tests/unit/test_utils.py | 1 + 18 files changed, 193 insertions(+), 28 deletions(-) diff --git a/litellm-rust/crates/inference-messages/src/types.rs b/litellm-rust/crates/inference-messages/src/types.rs index ae346259559..638f709f265 100644 --- a/litellm-rust/crates/inference-messages/src/types.rs +++ b/litellm-rust/crates/inference-messages/src/types.rs @@ -122,6 +122,7 @@ mod tests { "supports_reasoning": true, "supports_adaptive_thinking": true, "thinking_always_on": false, + "thinking_on_by_default": true, "supports_legacy_thinking": false, "supports_output_config": true, "supports_sampling_params": false, @@ -144,6 +145,7 @@ mod tests { supports_reasoning: true, supports_adaptive_thinking: true, thinking_always_on: false, + thinking_on_by_default: true, supports_legacy_thinking: false, supports_output_config: true, supports_sampling_params: false, diff --git a/litellm-rust/crates/llms/src/anthropic/common_utils.rs b/litellm-rust/crates/llms/src/anthropic/common_utils.rs index 7991a57353e..f2d73363644 100644 --- a/litellm-rust/crates/llms/src/anthropic/common_utils.rs +++ b/litellm-rust/crates/llms/src/anthropic/common_utils.rs @@ -1880,6 +1880,7 @@ mod tests { supports_reasoning: false, supports_adaptive_thinking: false, thinking_always_on: false, + thinking_on_by_default: false, supports_legacy_thinking: false, supports_output_config: false, supports_sampling_params: true, diff --git a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs index e21711dc40d..fd0c07cd9a4 100644 --- a/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs +++ b/litellm-rust/crates/llms/src/anthropic/messages/thinking.rs @@ -184,7 +184,9 @@ fn without_thinking( params: MessagesOptionalParams, context: &ThinkingContext, ) -> MessagesOptionalParams { - let context_management = if context.capabilities.thinking_always_on { + let thinks_without_thinking_param = + context.capabilities.thinking_always_on || context.capabilities.thinking_on_by_default; + let context_management = if thinks_without_thinking_param { params.context_management } else { without_clear_thinking_edits(params.context_management) @@ -515,6 +517,14 @@ mod tests { } } + #[fixture] + fn sonnet_5() -> MessagesModelCapabilities { + MessagesModelCapabilities { + thinking_on_by_default: true, + ..opus_4_7() + } + } + #[fixture] fn newfamily_6() -> MessagesModelCapabilities { MessagesModelCapabilities { @@ -716,7 +726,17 @@ mod tests { serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none"}), true )] - fn clear_thinking_edit_is_dropped_with_thinking_except_on_always_on_models( + #[case::default_thinking_model_keeps_edit( + sonnet_5(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none"}), + true + )] + #[case::adaptive_model_without_default_thinking_drops_edit( + opus_4_7(), + serde_json::json!({"max_tokens": 8192, "reasoning_effort": "none"}), + false + )] + fn clear_thinking_edit_is_dropped_with_thinking_except_on_models_that_think_by_default( #[case] capabilities: MessagesModelCapabilities, #[case] fields: Value, #[case] keeps_edit: bool, @@ -726,7 +746,8 @@ mod tests { "context_management".into(), serde_json::json!({"edits": [{"type": "clear_thinking_20251015", "keep": "all"}, {"type": "clear_tool_uses_20250919"}]}), ); - let always_on = capabilities.thinking_always_on; + let thinks_without_thinking_param = + capabilities.thinking_always_on || capabilities.thinking_on_by_default; let translated = serde_json::to_value(translate(capabilities, fields).unwrap()).unwrap(); let edit_types: Vec<&str> = translated["context_management"]["edits"] .as_array() @@ -742,7 +763,7 @@ mod tests { assert_eq!(edit_types, expected); assert_eq!( translated.get("thinking").is_some(), - keeps_edit && !always_on + keeps_edit && !thinks_without_thinking_param ); } diff --git a/litellm-rust/crates/llms/src/base_llm/messages/context.rs b/litellm-rust/crates/llms/src/base_llm/messages/context.rs index e59a63813b0..1af17f9b975 100644 --- a/litellm-rust/crates/llms/src/base_llm/messages/context.rs +++ b/litellm-rust/crates/llms/src/base_llm/messages/context.rs @@ -32,6 +32,8 @@ pub struct MessagesModelCapabilities { #[serde(default)] pub thinking_always_on: bool, #[serde(default)] + pub thinking_on_by_default: bool, + #[serde(default)] pub supports_legacy_thinking: bool, #[serde(default)] pub supports_output_config: bool, @@ -55,6 +57,7 @@ impl Default for MessagesModelCapabilities { supports_reasoning: false, supports_adaptive_thinking: false, thinking_always_on: false, + thinking_on_by_default: false, supports_legacy_thinking: false, supports_output_config: false, supports_sampling_params: true, diff --git a/litellm-rust/crates/model-catalog/src/model_info.rs b/litellm-rust/crates/model-catalog/src/model_info.rs index 8f7549c1c6d..6e0ab177d1e 100644 --- a/litellm-rust/crates/model-catalog/src/model_info.rs +++ b/litellm-rust/crates/model-catalog/src/model_info.rs @@ -155,6 +155,7 @@ pub struct ModelInfoFields { pub supports_adaptive_thinking: Option, pub supports_legacy_thinking: Option, pub thinking_always_on: Option, + pub thinking_on_by_default: Option, pub supports_tool_search: Option, pub supports_mid_conversation_system: Option, pub supports_anthropic_thinking_payload: Option, diff --git a/litellm-rust/crates/model-catalog/src/wire/attributes.rs b/litellm-rust/crates/model-catalog/src/wire/attributes.rs index 93c10a81524..2d47c1fdb0d 100644 --- a/litellm-rust/crates/model-catalog/src/wire/attributes.rs +++ b/litellm-rust/crates/model-catalog/src/wire/attributes.rs @@ -104,6 +104,7 @@ attributes! { /// Cloud regions the model is available in ('global' or region ids). supported_regions: Vec, thinking_always_on: bool, + thinking_on_by_default: bool, token_count_framing: u64 as Count, token_count_model: TokenCountModel, token_count_tokenizer: TokenCountTokenizer, diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 4f609c7759a..59fdc9a5791 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1819,8 +1819,12 @@ def drop_clear_thinking_edits_for_model( model: str, custom_llm_provider: str, ) -> None: - """Remove the clear_thinking edits unless the model always thinks anyway.""" - if not AnthropicModelInfo.supports_model_capability(model, "thinking_always_on", custom_llm_provider): + """Remove the clear_thinking edits unless the model still thinks without a ``thinking`` param.""" + thinks_without_thinking_param: Final = any( + AnthropicModelInfo.supports_model_capability(model, capability, custom_llm_provider) + for capability in ("thinking_always_on", "thinking_on_by_default") + ) + if not thinks_without_thinking_param: drop_clear_thinking_edits(params) diff --git a/litellm/llms/bedrock_mantle/messages/transformation.py b/litellm/llms/bedrock_mantle/messages/transformation.py index 2ce1a7a4e81..68f75d3c206 100644 --- a/litellm/llms/bedrock_mantle/messages/transformation.py +++ b/litellm/llms/bedrock_mantle/messages/transformation.py @@ -1,4 +1,4 @@ -from collections.abc import Mapping +from collections.abc import Mapping, MutableMapping from types import MappingProxyType from typing import Final @@ -46,21 +46,21 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM } ) - def _ensure_thinking_for_clear_thinking_context_management( + def _drop_thinking( self, - anthropic_messages_request: dict[str, object], # mutable-ok: edits the request in place like the base + optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place model: str, - drop_params: bool, - ) -> bool: - injected_thinking: Final = super()._ensure_thinking_for_clear_thinking_context_management( - anthropic_messages_request, model, drop_params - ) - match anthropic_messages_request.get("thinking"): - case {"type": "enabled" | "adaptive"}: - pass - case _: - drop_clear_thinking_edits_for_model(anthropic_messages_request, model, self._resolved_provider) - return injected_thinking + custom_llm_provider: str, + ) -> None: + super()._drop_thinking(optional_params, model, custom_llm_provider) + if not self._adds_thinking_back_for_clear_thinking(optional_params, model): + drop_clear_thinking_edits_for_model(optional_params, model, custom_llm_provider) + + def _adds_thinking_back_for_clear_thinking(self, optional_params: Mapping[str, object], model: str) -> bool: + trial_request: Final = { + key: optional_params[key] for key in ("context_management", "max_tokens") if key in optional_params + } + return self._ensure_thinking_for_clear_thinking_context_management(trial_request, model, drop_params=False) def __init__(self, aws_signer: BaseAWSLLM | None = None) -> None: AmazonMantleMessagesConfig.__init__(self) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 23c6ccf9c0a..8d0a063dab5 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -1939,6 +1939,7 @@ "anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -2025,6 +2026,7 @@ "global.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -2111,6 +2113,7 @@ "us.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2197,6 +2200,7 @@ "eu.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2278,6 +2282,7 @@ "au.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2364,6 +2369,7 @@ "jp.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2760,6 +2766,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2804,6 +2811,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2848,6 +2856,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2892,6 +2901,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2936,6 +2946,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2980,6 +2991,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -3827,6 +3839,7 @@ "deprecation_date": "2027-07-08", "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "input_cost_per_token": 5e-06, "output_cost_per_token": 2.5e-05, "litellm_provider": "azure_ai", @@ -3964,6 +3977,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -15928,6 +15942,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -16461,6 +16476,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -21268,6 +21284,7 @@ "prompt_cache_min_tokens": 512, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_function_calling": true, "supports_mid_conversation_system": true, @@ -21425,6 +21442,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_function_calling": true, "supports_mid_conversation_system": true, @@ -32864,6 +32882,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -32931,6 +32950,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -32974,6 +32994,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -49051,6 +49072,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49128,6 +49150,7 @@ "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -51528,6 +51551,7 @@ "regional_endpoint_uplift_multiplier": 1.1, "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, "cache_creation_input_token_cost_batches": 3.125e-06, @@ -51566,6 +51590,7 @@ "regional_endpoint_uplift_multiplier": 1.1, "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, "cache_creation_input_token_cost_batches": 3.125e-06, @@ -51809,6 +51834,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -59133,6 +59159,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -65209,6 +65236,7 @@ "prompt_cache_min_tokens": 512, "source": "https://deepinfra.com/pricing", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -65524,6 +65552,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://deepinfra.com/pricing", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -67521,6 +67550,7 @@ "output_cost_per_token": 1.2e-05, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67596,6 +67626,7 @@ "output_cost_per_token": 3e-05, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67809,6 +67840,7 @@ "output_cost_per_token": 1.2e-05, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67874,6 +67906,7 @@ "output_cost_per_token": 3e-05, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -68711,6 +68744,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "prompt_cache_min_tokens": 512, "supports_web_search": true, "supports_function_calling": true, @@ -68734,6 +68768,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_web_search": true, "supports_function_calling": true, "input_cost_per_token": 2e-06, @@ -70810,6 +70845,7 @@ "supports_vision": true, "supports_web_search": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "prompt_cache_min_tokens": 512, "supports_sampling_params": false }, @@ -70830,6 +70866,7 @@ "supports_vision": true, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_sampling_params": false }, "aihubmix/coding-glm-5.3": { @@ -73453,6 +73490,7 @@ "apac.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "litellm_provider": "bedrock_converse", "supports_tool_search": true, @@ -73567,6 +73605,7 @@ "max_output_tokens": 128000, "mode": "chat", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75054,6 +75093,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75106,6 +75146,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75150,6 +75191,7 @@ "thinking_always_on": false, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75197,6 +75239,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75233,6 +75276,7 @@ "max_output_tokens": 128000, "mode": "chat", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75289,6 +75333,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75335,6 +75380,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75375,6 +75421,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75416,6 +75463,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75462,6 +75510,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75513,6 +75562,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75559,6 +75609,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75586,6 +75637,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_web_search": true, "supports_function_calling": true, "input_cost_per_token": 1e-07, @@ -75616,6 +75668,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75667,6 +75720,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75717,6 +75771,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75761,6 +75816,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -77516,6 +77572,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -77730,6 +77787,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -77894,6 +77952,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -78362,6 +78421,7 @@ "prompt_cache_min_tokens": 512.0, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_anthropic_thinking_payload": true, "supports_assistant_prefill": false, "supports_function_calling": true, diff --git a/litellm/rust_bridge/host/model_capabilities.py b/litellm/rust_bridge/host/model_capabilities.py index cbf59dfb97b..321a6de8581 100644 --- a/litellm/rust_bridge/host/model_capabilities.py +++ b/litellm/rust_bridge/host/model_capabilities.py @@ -27,6 +27,7 @@ def anthropic_model_capabilities(model: str, custom_llm_provider: str | None) -> "supports_reasoning": supports("supports_reasoning"), "supports_adaptive_thinking": supports("supports_adaptive_thinking"), "thinking_always_on": supports("thinking_always_on"), + "thinking_on_by_default": supports("thinking_on_by_default"), "supports_legacy_thinking": supports("supports_legacy_thinking"), "supports_output_config": supports("supports_output_config"), "supports_sampling_params": AnthropicModelInfo._supports_sampling_params(resolved_model), # pyright: ignore[reportPrivateUsage] # same gate the handler applies diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 70ca05904ef..eb2bc6ff89f 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -216,6 +216,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False): supports_adaptive_thinking: bool | None supports_legacy_thinking: ReadOnly[bool | None] thinking_always_on: ReadOnly[bool | None] + thinking_on_by_default: ReadOnly[bool | None] supports_tool_search: bool | None supports_mid_conversation_system: bool | None supports_url_context: bool | None diff --git a/litellm/utils.py b/litellm/utils.py index 72c37c3760e..a02031a26f3 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7022,6 +7022,7 @@ def get_model_info_helper( supports_adaptive_thinking=_model_info.get("supports_adaptive_thinking", None), supports_legacy_thinking=_model_info.get("supports_legacy_thinking", None), thinking_always_on=_model_info.get("thinking_always_on", None), + thinking_on_by_default=_model_info.get("thinking_on_by_default", None), supports_tool_search=_model_info.get("supports_tool_search", None), supports_mid_conversation_system=_model_info.get("supports_mid_conversation_system", None), supports_anthropic_thinking_payload=_model_info.get("supports_anthropic_thinking_payload", None), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 23c6ccf9c0a..8d0a063dab5 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -1939,6 +1939,7 @@ "anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -2025,6 +2026,7 @@ "global.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, @@ -2111,6 +2113,7 @@ "us.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2197,6 +2200,7 @@ "eu.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2278,6 +2282,7 @@ "au.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2364,6 +2369,7 @@ "jp.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "cache_creation_input_token_cost": 6.875e-06, "cache_creation_input_token_cost_above_1hr": 1.1e-05, @@ -2760,6 +2766,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2804,6 +2811,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2848,6 +2856,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2892,6 +2901,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2936,6 +2946,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -2980,6 +2991,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -3827,6 +3839,7 @@ "deprecation_date": "2027-07-08", "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "input_cost_per_token": 5e-06, "output_cost_per_token": 2.5e-05, "litellm_provider": "azure_ai", @@ -3964,6 +3977,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -15928,6 +15942,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -16461,6 +16476,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -21268,6 +21284,7 @@ "prompt_cache_min_tokens": 512, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_function_calling": true, "supports_mid_conversation_system": true, @@ -21425,6 +21442,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_function_calling": true, "supports_mid_conversation_system": true, @@ -32864,6 +32882,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -32931,6 +32950,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -32974,6 +32994,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -49051,6 +49072,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -49128,6 +49150,7 @@ "prompt_cache_min_tokens": 512, "source": "https://aws.amazon.com/bedrock/pricing/", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -51528,6 +51551,7 @@ "regional_endpoint_uplift_multiplier": 1.1, "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, "cache_creation_input_token_cost_batches": 3.125e-06, @@ -51566,6 +51590,7 @@ "regional_endpoint_uplift_multiplier": 1.1, "supports_mid_conversation_system": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "cache_creation_input_token_cost": 6.25e-06, "cache_creation_input_token_cost_above_1hr": 1e-05, "cache_creation_input_token_cost_batches": 3.125e-06, @@ -51809,6 +51834,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -59133,6 +59159,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -65209,6 +65236,7 @@ "prompt_cache_min_tokens": 512, "source": "https://deepinfra.com/pricing", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -65524,6 +65552,7 @@ "prompt_cache_min_tokens": 1024, "source": "https://deepinfra.com/pricing", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_reasoning": true, "supports_response_schema": true, @@ -67521,6 +67550,7 @@ "output_cost_per_token": 1.2e-05, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67596,6 +67626,7 @@ "output_cost_per_token": 3e-05, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67809,6 +67840,7 @@ "output_cost_per_token": 1.2e-05, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -67874,6 +67906,7 @@ "output_cost_per_token": 3e-05, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -68711,6 +68744,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "prompt_cache_min_tokens": 512, "supports_web_search": true, "supports_function_calling": true, @@ -68734,6 +68768,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_web_search": true, "supports_function_calling": true, "input_cost_per_token": 2e-06, @@ -70810,6 +70845,7 @@ "supports_vision": true, "supports_web_search": true, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "prompt_cache_min_tokens": 512, "supports_sampling_params": false }, @@ -70830,6 +70866,7 @@ "supports_vision": true, "prompt_cache_min_tokens": 1024, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_sampling_params": false }, "aihubmix/coding-glm-5.3": { @@ -73453,6 +73490,7 @@ "apac.anthropic.claude-opus-5": { "bedrock_converse_supports_strict_tools": false, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "litellm_provider": "bedrock_converse", "supports_tool_search": true, @@ -73567,6 +73605,7 @@ "max_output_tokens": 128000, "mode": "chat", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75054,6 +75093,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75106,6 +75146,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75150,6 +75191,7 @@ "thinking_always_on": false, "prompt_cache_min_tokens": 512, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75197,6 +75239,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75233,6 +75276,7 @@ "max_output_tokens": 128000, "mode": "chat", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75289,6 +75333,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75335,6 +75380,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75375,6 +75421,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75416,6 +75463,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75462,6 +75510,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75513,6 +75562,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75559,6 +75609,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75586,6 +75637,7 @@ "litellm_provider": "perplexity", "mode": "responses", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_web_search": true, "supports_function_calling": true, "input_cost_per_token": 1e-07, @@ -75616,6 +75668,7 @@ "prompt_cache_min_tokens": 512, "source": "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/index.json", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75667,6 +75720,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -75717,6 +75771,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -75761,6 +75816,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, @@ -77516,6 +77572,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -77730,6 +77787,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -77894,6 +77952,7 @@ "search_context_size_medium": 0.01 }, "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_mid_conversation_system": true, "supports_assistant_prefill": false, "supports_computer_use": true, @@ -78362,6 +78421,7 @@ "prompt_cache_min_tokens": 512.0, "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_anthropic_thinking_payload": true, "supports_assistant_prefill": false, "supports_function_calling": true, diff --git a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py index b1eed12b4a3..c31b94aacd9 100644 --- a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py @@ -363,11 +363,12 @@ CLEAR_TOOL_USES_EDIT: Final = {"type": "clear_tool_uses_20250919"} pytest.param("claude-sonnet-4-6", _claude_code_payload(effort="high"), True, id="adaptive-model"), pytest.param("claude-3-5-haiku-latest", _claude_code_payload(), False, id="non-reasoning-model"), pytest.param("claude-haiku-4-5", _claude_code_payload(), True, id="legacy-thinking-model"), + pytest.param( + "claude-opus-4-8", {"reasoning_effort": "none"}, False, id="adaptive-model-without-default-thinking" + ), ], ) def test_clear_thinking_edit_dropped_exactly_when_thinking_is(model, params, keeps_thinking): - """Anthropic rejects a clear_thinking edit without thinking, so every path that drops - thinking must drop the edit too, and leave other edits alone""" result = _transform( model, { @@ -382,10 +383,10 @@ def test_clear_thinking_edit_dropped_exactly_when_thinking_is(model, params, kee assert result["context_management"] == {"edits": expected_edits} -def test_clear_thinking_edit_kept_for_always_on_thinking_model_without_thinking(): - """An always-on model keeps thinking adaptively after reasoning_effort='none' drops the param""" +@pytest.mark.parametrize("model", ["claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-haiku-5-5"]) +def test_clear_thinking_edit_kept_when_model_thinks_without_thinking_param(model): result = _transform( - "claude-fable-5", + model, {"max_tokens": 8192, "reasoning_effort": "none", "context_management": {"edits": [CLEAR_THINKING_EDIT]}}, ) diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index cf31bdec96c..cdabb36c8d9 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py @@ -2203,8 +2203,6 @@ def test_bedrock_invoke_transform_emits_adaptive_thinking_for_opus_4_8(): ], ) def test_bedrock_invoke_keeps_thinking_for_clear_thinking_edit_when_base_drops_thinking(thinking_params): - """The base messages transform drops thinking for Opus 4.5 here. Bedrock keeps the - clear_thinking edit long enough to add the minimum enabled thinking back, then strips it""" from litellm.types.router import GenericLiteLLMParams result = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py index 5cca57744b0..03700c0672c 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py @@ -235,6 +235,13 @@ class TestRequestBody: ["clear_tool_uses_20250919"], id="no-thinking-support-drops-edit", ), + pytest.param( + "anthropic.claude-3-5-haiku-20241022-v1:0", + {}, + None, + ["clear_thinking_20251015", "clear_tool_uses_20250919"], + id="caller-edit-without-thinking-forwarded", + ), pytest.param( "anthropic.claude-opus-4-5-20251101-v1:0", {"thinking": {"type": "adaptive"}, "output_config": {"effort": "high"}}, @@ -258,7 +265,7 @@ class TestRequestBody: ), ], ) - def test_clear_thinking_edit_forwarded_only_with_thinking( + def test_clear_thinking_edit_dropped_only_with_dropped_thinking( self, model, thinking_params, expected_thinking, expected_edit_types ): body = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( diff --git a/tests/unit/test_model_prices_and_context_window.py b/tests/unit/test_model_prices_and_context_window.py index 2aceed493a7..585ff294ffa 100644 --- a/tests/unit/test_model_prices_and_context_window.py +++ b/tests/unit/test_model_prices_and_context_window.py @@ -60,6 +60,7 @@ class _CopilotThinkingCapabilities(BaseModel): supports_adaptive_thinking: bool | None = None supports_legacy_thinking: bool | None = None thinking_always_on: bool | None = None + thinking_on_by_default: bool | None = None def _copilot_endpoint_rows(path: Path) -> Mapping[str, _CopilotEndpointRow]: @@ -87,6 +88,7 @@ _THINKING_KEYS: Final[tuple[str, ...]] = ( "supports_adaptive_thinking", "supports_legacy_thinking", "thinking_always_on", + "thinking_on_by_default", ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 665a540e462..9aaf1bf07e1 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1205,6 +1205,7 @@ _COST_MAP_ROW_SCHEMA: Final = { "supports_anthropic_thinking_payload": {"type": "boolean"}, "supports_legacy_thinking": {"type": "boolean"}, "thinking_always_on": {"type": "boolean"}, + "thinking_on_by_default": {"type": "boolean"}, "supports_mid_conversation_system": {"type": "boolean"}, "supports_sampling_params": {"type": "boolean"}, "supports_output_config": {"type": "boolean"}, From f353aabb2a897da670457ee4e414a5c53855cfbb Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:32:55 -0700 Subject: [PATCH 4/4] fix(anthropic): pass max_tokens to the Mantle clear_thinking check and keep the edit on unmapped Claude 5 ids Bedrock Mantle now drops the clear_thinking edit when max_tokens is too small for it to add thinking back, instead of sending the edit without thinking. A cost-map fallback rule marks every Claude model at major version 5 or higher as thinking_on_by_default, so new minors and dated ids keep the edit. --- .../messages/native/transformation.py | 9 +++---- .../anthropic_claude_transformation.py | 1 + .../bedrock_mantle/messages/transformation.py | 12 ++++++---- ...odel_prices_and_context_window_backup.json | 8 +++++++ model_prices_and_context_window.json | 8 +++++++ .../test_anthropic_chat_transformation.py | 18 +++++++------- .../native/test_anthropic_messages_effort.py | 24 ++++++++++++++----- .../anthropic/test_anthropic_common_utils.py | 8 +++---- .../test_anthropic_claude_transformation.py | 11 ++++++--- ..._bedrock_mantle_messages_transformation.py | 21 ++++++++++++---- 10 files changed, 86 insertions(+), 34 deletions(-) diff --git a/litellm/llms/anthropic/messages/native/transformation.py b/litellm/llms/anthropic/messages/native/transformation.py index e7fabd946b2..44ac584f187 100644 --- a/litellm/llms/anthropic/messages/native/transformation.py +++ b/litellm/llms/anthropic/messages/native/transformation.py @@ -118,6 +118,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place model: str, custom_llm_provider: str, + max_tokens: int | None, ) -> None: optional_params.pop("thinking", None) drop_clear_thinking_edits_for_model(optional_params, model, custom_llm_provider) @@ -501,7 +502,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): raise AnthropicError(message=str(e.message), status_code=400) if mapped_thinking is None: - self._drop_thinking(optional_params, model, custom_llm_provider) + self._drop_thinking(optional_params, model, custom_llm_provider, max_tokens) optional_params.pop("output_config", None) return @@ -509,7 +510,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): if fitted_thinking is None: verbose_logger.warning(DROP_UNFITTING_REASONING_EFFORT_WARNING, reasoning_effort, model, max_tokens) if "thinking" not in optional_params: - self._drop_thinking(optional_params, model, custom_llm_provider) + self._drop_thinking(optional_params, model, custom_llm_provider, max_tokens) return optional_params.setdefault("thinking", fitted_thinking) @@ -597,7 +598,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): or AnthropicConfig.validate_effort_for_model(model, effort, custom_llm_provider) is None ): if adaptive_thinking: - self._drop_thinking(optional_params, model, custom_llm_provider) + self._drop_thinking(optional_params, model, custom_llm_provider, max_tokens) return supports_thinking: Final = AnthropicModelInfo.supports_model_capability( @@ -625,7 +626,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params["thinking"] = capped_thinking else: verbose_logger.warning(DROP_UNSUPPORTED_ADAPTIVE_EFFORT_WARNING, model) - self._drop_thinking(optional_params, model, custom_llm_provider) + self._drop_thinking(optional_params, model, custom_llm_provider, max_tokens) if isinstance(output_config, dict) and "effort" in output_config: residual: Final = {k: v for k, v in output_config.items() if k != "effort"} diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py index 12fdaf11da4..a001a634066 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py @@ -105,6 +105,7 @@ class AmazonAnthropicClaudeMessagesConfig( optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place model: str, custom_llm_provider: str, + max_tokens: int | None, ) -> None: """Keep the clear_thinking edit: it makes ``_ensure_thinking_for_clear_thinking_context_management`` add thinking back, diff --git a/litellm/llms/bedrock_mantle/messages/transformation.py b/litellm/llms/bedrock_mantle/messages/transformation.py index 68f75d3c206..5b5bc7b230f 100644 --- a/litellm/llms/bedrock_mantle/messages/transformation.py +++ b/litellm/llms/bedrock_mantle/messages/transformation.py @@ -51,14 +51,18 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM optional_params: MutableMapping[str, object], # mutable-ok: edits optional params in place model: str, custom_llm_provider: str, + max_tokens: int | None, ) -> None: - super()._drop_thinking(optional_params, model, custom_llm_provider) - if not self._adds_thinking_back_for_clear_thinking(optional_params, model): + super()._drop_thinking(optional_params, model, custom_llm_provider, max_tokens) + if not self._adds_thinking_back_for_clear_thinking(optional_params, model, max_tokens): drop_clear_thinking_edits_for_model(optional_params, model, custom_llm_provider) - def _adds_thinking_back_for_clear_thinking(self, optional_params: Mapping[str, object], model: str) -> bool: + def _adds_thinking_back_for_clear_thinking( + self, optional_params: Mapping[str, object], model: str, max_tokens: int | None + ) -> bool: trial_request: Final = { - key: optional_params[key] for key in ("context_management", "max_tokens") if key in optional_params + "context_management": optional_params.get("context_management"), + "max_tokens": max_tokens, } return self._ensure_thinking_for_clear_thinking_context_management(trial_request, model, drop_params=False) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f7237cbdd8b..b049f9b41df 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -63583,6 +63583,14 @@ "thinking_always_on": true } }, + { + "name": "claude-thinking-on-by-default", + "pattern": "claude-[a-z]+-[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?", + "description": "Claude at major version 5 or higher, in any id shape that contains claude--, so new minors such as claude-sonnet-5-1 and provider-namespaced ids match. Every Claude 5 model thinks when a request carries no thinking param, so a clear_thinking context-management edit stays valid when LiteLLM drops thinking. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41.", + "model_info": { + "thinking_on_by_default": true + } + }, { "name": "claude-mid-conversation-system", "pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f7237cbdd8b..b049f9b41df 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -63583,6 +63583,14 @@ "thinking_always_on": true } }, + { + "name": "claude-thinking-on-by-default", + "pattern": "claude-[a-z]+-[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?", + "description": "Claude at major version 5 or higher, in any id shape that contains claude--, so new minors such as claude-sonnet-5-1 and provider-namespaced ids match. Every Claude 5 model thinks when a request carries no thinking param, so a clear_thinking context-management edit stays valid when LiteLLM drops thinking. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41.", + "model_info": { + "thinking_on_by_default": true + } + }, { "name": "claude-mid-conversation-system", "pattern": "claude-[a-z]+-(?:4[-._](?:[89]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", 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 d90337709a0..efe45966e4a 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -9838,11 +9838,11 @@ def _edits_for(keeps_thinking: bool) -> list[dict[str, str]]: ) @pytest.mark.parametrize("context_management_first", [True, False]) def test_map_openai_params_drops_clear_thinking_edit_exactly_when_thinking_is( - model, params, keeps_thinking, context_management_first -): + model: str, params: dict[str, object], keeps_thinking: bool, context_management_first: bool +) -> None: context_management: Final = {"context_management": {"edits": [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT]}} rest: Final = {"max_tokens": 8192, **params} - result = AnthropicConfig().map_openai_params( + result: Final = AnthropicConfig().map_openai_params( non_default_params={**context_management, **rest} if context_management_first else {**rest, **context_management}, @@ -9862,8 +9862,10 @@ def test_map_openai_params_drops_clear_thinking_edit_exactly_when_thinking_is( pytest.param("claude-sonnet-4-5", {"summary": "concise"}, id="effort-less-dict"), ], ) -def test_map_openai_params_keeps_clear_thinking_edit_when_thinking_was_not_dropped(model, reasoning_effort): - result = AnthropicConfig().map_openai_params( +def test_map_openai_params_keeps_clear_thinking_edit_when_thinking_was_not_dropped( + model: str, reasoning_effort: str | dict[str, str] +) -> None: + result: Final = AnthropicConfig().map_openai_params( non_default_params={ "context_management": {"edits": [CLEAR_THINKING_EDIT]}, "max_tokens": 8192, @@ -9886,10 +9888,10 @@ def test_map_openai_params_keeps_clear_thinking_edit_when_thinking_was_not_dropp ], ) def test_transform_request_drops_clear_thinking_edit_with_thinking_for_tool_turn_without_thinking_blocks( - monkeypatch, model, modify_params, keeps_thinking, keeps_edit -): + monkeypatch: pytest.MonkeyPatch, model: str, modify_params: bool, keeps_thinking: bool, keeps_edit: bool +) -> None: monkeypatch.setattr(litellm, "modify_params", modify_params) - result = AnthropicConfig().transform_request( + result: Final = AnthropicConfig().transform_request( model=model, messages=[ {"role": "user", "content": "Look it up"}, diff --git a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py index c31b94aacd9..6220c31ba04 100644 --- a/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py @@ -368,8 +368,10 @@ CLEAR_TOOL_USES_EDIT: Final = {"type": "clear_tool_uses_20250919"} ), ], ) -def test_clear_thinking_edit_dropped_exactly_when_thinking_is(model, params, keeps_thinking): - result = _transform( +def test_clear_thinking_edit_dropped_exactly_when_thinking_is( + model: str, params: dict[str, object], keeps_thinking: bool +) -> None: + result: Final = _transform( model, { "max_tokens": 8192, @@ -379,13 +381,23 @@ def test_clear_thinking_edit_dropped_exactly_when_thinking_is(model, params, kee ) assert ("thinking" in result) is keeps_thinking - expected_edits = [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT] if keeps_thinking else [CLEAR_TOOL_USES_EDIT] + expected_edits: Final = [CLEAR_THINKING_EDIT, CLEAR_TOOL_USES_EDIT] if keeps_thinking else [CLEAR_TOOL_USES_EDIT] assert result["context_management"] == {"edits": expected_edits} -@pytest.mark.parametrize("model", ["claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-haiku-5-5"]) -def test_clear_thinking_edit_kept_when_model_thinks_without_thinking_param(model): - result = _transform( +@pytest.mark.parametrize( + "model", + [ + "claude-fable-5", + "claude-sonnet-5", + "claude-opus-5", + "claude-haiku-5-5", + pytest.param("claude-sonnet-5-1", id="unmapped-claude-5-minor"), + pytest.param("claude-opus-5-2-20270101", id="unmapped-dated-claude-5-minor"), + ], +) +def test_clear_thinking_edit_kept_when_model_thinks_without_thinking_param(model: str) -> None: + result: Final = _transform( model, {"max_tokens": 8192, "reasoning_effort": "none", "context_management": {"edits": [CLEAR_THINKING_EDIT]}}, ) diff --git a/tests/unit/llms/anthropic/test_anthropic_common_utils.py b/tests/unit/llms/anthropic/test_anthropic_common_utils.py index 75c358200df..5fdca18970b 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -1611,12 +1611,12 @@ class TestAnthropicThinkingSignatureSelfHeal: assert "thinking" not in data assert data["messages"] == [] - def test_strip_thinking_request_dict_drops_clear_thinking_edits(self): + def test_strip_thinking_request_dict_drops_clear_thinking_edits(self) -> None: from litellm.llms.anthropic.common_utils import ( strip_thinking_blocks_from_anthropic_messages_request_dict, ) - data = { + data: Final[dict[str, object]] = { "messages": [{"role": "assistant", "content": [{"type": "thinking", "thinking": "unsigned"}]}], "thinking": {"type": "adaptive"}, "context_management": {"edits": [{"type": "clear_thinking_20251015", "keep": "all"}]}, @@ -1648,12 +1648,12 @@ class TestAnthropicThinkingSignatureSelfHeal: ), ], ) - def test_drop_clear_thinking_edits(self, context_management, expected): + def test_drop_clear_thinking_edits(self, context_management: object, expected: object) -> None: from litellm.llms.anthropic.common_utils import ( drop_clear_thinking_edits, ) - params = {"context_management": context_management} + params: Final[dict[str, object]] = {"context_management": context_management} drop_clear_thinking_edits(params) assert params == ({} if expected is None else {"context_management": expected}) diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index cadeaf6f3d6..1491525f6ce 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py @@ -2202,10 +2202,12 @@ def test_bedrock_invoke_transform_emits_adaptive_thinking_for_opus_4_8(): pytest.param({"reasoning_effort": "none"}, id="reasoning-effort-none"), ], ) -def test_bedrock_invoke_keeps_thinking_for_clear_thinking_edit_when_base_drops_thinking(thinking_params): +def test_bedrock_invoke_keeps_thinking_for_clear_thinking_edit_when_base_drops_thinking( + thinking_params: dict[str, object], +) -> None: from litellm.types.router import GenericLiteLLMParams - result = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( + result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( model="anthropic.claude-opus-4-5-20251101-v1:0", messages=[{"role": "user", "content": "hi"}], anthropic_messages_optional_request_params={ @@ -4516,7 +4518,10 @@ async def test_bedrock_messages_fallback_to_anthropic_sends_the_tool_names_bedro "aws_region_name": "us-east-1", }, }, - {"model_name": "anthropic-claude", "litellm_params": {"model": "anthropic/claude-opus-4-8", "api_key": "test"}}, + { + "model_name": "anthropic-claude", + "litellm_params": {"model": "anthropic/claude-opus-4-8", "api_key": "test"}, + }, ], fallbacks=[{"bedrock-claude": ["anthropic-claude"]}], num_retries=0, diff --git a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py index df549c127ad..34f0f7d2e0c 100644 --- a/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py +++ b/tests/unit/llms/bedrock_mantle/test_bedrock_mantle_messages_transformation.py @@ -249,6 +249,13 @@ class TestRequestBody: ["clear_thinking_20251015", "clear_tool_uses_20250919"], id="thinking-added-back-keeps-edit", ), + pytest.param( + "anthropic.claude-opus-4-5-20251101-v1:0", + {"max_tokens": BEDROCK_MIN_THINKING_BUDGET_TOKENS, "reasoning_effort": "none"}, + None, + ["clear_tool_uses_20250919"], + id="max-tokens-too-small-to-add-thinking-back-drops-edit", + ), pytest.param( "anthropic.claude-mythos-preview", {}, @@ -266,9 +273,13 @@ class TestRequestBody: ], ) def test_clear_thinking_edit_dropped_only_with_dropped_thinking( - self, model, thinking_params, expected_thinking, expected_edit_types - ): - body = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( + self, + model: str, + thinking_params: dict[str, object], + expected_thinking: dict[str, object] | None, + expected_edit_types: list[str], + ) -> None: + body: Final = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( model=model, messages=[{"role": "user", "content": "ping"}], anthropic_messages_optional_request_params={ @@ -284,8 +295,8 @@ class TestRequestBody: assert body.get("thinking") == expected_thinking assert [edit["type"] for edit in body["context_management"]["edits"]] == expected_edit_types - def test_disabled_thinking_without_edits_is_untouched(self): - body = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( + def test_disabled_thinking_without_edits_is_untouched(self) -> None: + body: Final = BedrockMantleAnthropicMessagesConfig().transform_anthropic_messages_request( model="anthropic.claude-3-5-haiku-20241022-v1:0", messages=[{"role": "user", "content": "ping"}], anthropic_messages_optional_request_params={"max_tokens": 8, "thinking": {"type": "disabled"}},