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 aec5d9ebc27..4292752f1c5 100644 --- a/litellm-rust/crates/llms/src/anthropic/common_utils.rs +++ b/litellm-rust/crates/llms/src/anthropic/common_utils.rs @@ -1882,6 +1882,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 df1ae13fd28..fd0c07cd9a4 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,60 @@ 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 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) + }; + MessagesOptionalParams { + thinking: None, + context_management, + ..params + } +} + fn translate_reasoning_effort( request: MessagesRequest, context: &ThinkingContext, @@ -171,9 +225,8 @@ fn translate_reasoning_effort( ) else { return Ok(MessagesRequest { params: MessagesOptionalParams { - thinking: None, output_config: None, - ..request.params + ..without_thinking(request.params, context) }, ..request }); @@ -199,7 +252,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 { @@ -277,15 +336,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 +349,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 }) @@ -458,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 { @@ -625,6 +692,120 @@ 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::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)] + #[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 + )] + #[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, + ) { + 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 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() + .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 && !thinks_without_thinking_param + ); + } + + #[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-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/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index d773e31c733..0ef4dbf5f70 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 @@ -48,7 +48,7 @@ from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import place_mid_conversation_system, split_leading_system_run, ) -from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_non_string_reasoning_effort +from litellm.litellm_core_utils.reasoning_effort_utils import warn_non_string_reasoning_effort from litellm.llms.base_llm.base_utils import ( ResponseFormatSchema, as_json_object, @@ -123,6 +123,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, @@ -183,6 +184,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" @@ -1835,6 +1846,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))) @@ -1948,11 +1963,9 @@ 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): - warn_non_string_reasoning_effort(OPAQUE_VALUE.validate_python(value), model) + effort_value = _requested_effort(requested_params[param]) + if effort_value is None: + warn_non_string_reasoning_effort(requested_params[param], model) continue mapped_thinking = AnthropicConfig._map_reasoning_effort( reasoning_effort=effort_value, @@ -2004,6 +2017,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 @@ -2290,7 +2311,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 e1f9a7d540f..7a55dece712 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1975,17 +1975,67 @@ 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 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) + + 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/messages/native/transformation.py b/litellm/llms/anthropic/messages/native/transformation.py index 5281ad99620..1c3c66a64dd 100644 --- a/litellm/llms/anthropic/messages/native/transformation.py +++ b/litellm/llms/anthropic/messages/native/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 @@ -46,6 +46,7 @@ from ...common_utils import ( AnthropicError, AnthropicModelInfo, context_management_betas, + drop_clear_thinking_edits_for_model, is_claude_model_target, merge_anthropic_beta_headers, optionally_handle_anthropic_oauth, @@ -115,6 +116,16 @@ 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, + max_tokens: int | None, + ) -> 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", @@ -464,9 +475,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. @@ -495,13 +505,15 @@ 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, max_tokens) optional_params.pop("output_config", None) return 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, max_tokens) return optional_params.setdefault("thinking", fitted_thinking) @@ -525,9 +537,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 @@ -590,7 +601,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, max_tokens) return supports_thinking: Final = AnthropicModelInfo.supports_model_capability( @@ -618,7 +629,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, 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"} @@ -703,7 +714,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, ) @@ -727,7 +738,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_claude_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py index e5d6a168d57..a001a634066 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude_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 @@ -100,6 +100,18 @@ 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, + max_tokens: int | None, + ) -> 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..5b5bc7b230f 100644 --- a/litellm/llms/bedrock_mantle/messages/transformation.py +++ b/litellm/llms/bedrock_mantle/messages/transformation.py @@ -1,8 +1,9 @@ -from collections.abc import Mapping +from collections.abc import Mapping, MutableMapping 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,26 @@ class BedrockMantleAnthropicMessagesConfig(BedrockMantleAuthMixin, AmazonMantleM } ) + def _drop_thinking( + self, + 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, 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, max_tokens: int | None + ) -> bool: + trial_request: Final = { + "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) + def __init__(self, aws_signer: BaseAWSLLM | None = None) -> None: AmazonMantleMessagesConfig.__init__(self) self._aws_signer = aws_signer or self diff --git a/litellm/llms/bigmodel/messages/transformation.py b/litellm/llms/bigmodel/messages/transformation.py index 82ce59631f1..5197e18e908 100644 --- a/litellm/llms/bigmodel/messages/transformation.py +++ b/litellm/llms/bigmodel/messages/transformation.py @@ -154,15 +154,13 @@ class BigModelMessagesConfig(OpenAILikeAnthropicMessagesConfig): model, normalized, request_body, litellm_logging_obj ) - @staticmethod def _translate_reasoning_effort_to_anthropic( - model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str + self, model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str ) -> None: return None - @staticmethod def _translate_adaptive_effort_for_non_adaptive_model( - model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str + self, model: str, optional_params: Mapping[str, object], max_tokens: int | None, custom_llm_provider: str ) -> None: return None diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 1d18903bb07..3f7ca4194a6 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, @@ -16169,6 +16183,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, @@ -16702,6 +16717,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, @@ -21562,6 +21578,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, @@ -21719,6 +21736,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, @@ -33155,6 +33173,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -33222,6 +33241,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -33265,6 +33285,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -49386,6 +49407,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, @@ -49463,6 +49485,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, @@ -51863,6 +51886,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, @@ -51901,6 +51925,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, @@ -52144,6 +52169,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, @@ -59497,6 +59523,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, @@ -63607,6 +63634,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))?)", @@ -65707,6 +65742,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, @@ -66022,6 +66058,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, @@ -68035,6 +68072,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, @@ -68110,6 +68148,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, @@ -68323,6 +68362,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, @@ -68388,6 +68428,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, @@ -69225,6 +69266,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, @@ -69248,6 +69290,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, @@ -71324,6 +71367,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 }, @@ -71344,6 +71388,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": { @@ -74024,6 +74069,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, @@ -74138,6 +74184,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, @@ -75628,6 +75675,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, @@ -75680,6 +75728,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, @@ -75724,6 +75773,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, @@ -75771,6 +75821,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, @@ -75807,6 +75858,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, @@ -75863,6 +75915,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, @@ -75909,6 +75962,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, @@ -75949,6 +76003,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, @@ -75990,6 +76045,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, @@ -76036,6 +76092,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, @@ -76087,6 +76144,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, @@ -76133,6 +76191,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, @@ -76160,6 +76219,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, @@ -76190,6 +76250,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, @@ -76241,6 +76302,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, @@ -76291,6 +76353,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, @@ -76335,6 +76398,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, @@ -78094,6 +78158,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, @@ -78308,6 +78373,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, @@ -78472,6 +78538,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, @@ -80454,6 +80521,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 db865c7488a..1b09e9441c5 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -223,6 +223,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 560d0c31e4a..0d8433311de 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7353,6 +7353,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 1d18903bb07..3f7ca4194a6 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, @@ -16169,6 +16183,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, @@ -16702,6 +16717,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, @@ -21562,6 +21578,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, @@ -21719,6 +21736,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, @@ -33155,6 +33173,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -33222,6 +33241,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -33265,6 +33285,7 @@ "/v1/messages" ], "supports_adaptive_thinking": true, + "thinking_on_by_default": true, "supports_function_calling": true, "supports_parallel_function_calling": true, "supports_reasoning": true, @@ -49386,6 +49407,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, @@ -49463,6 +49485,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, @@ -51863,6 +51886,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, @@ -51901,6 +51925,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, @@ -52144,6 +52169,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, @@ -59497,6 +59523,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, @@ -63607,6 +63634,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))?)", @@ -65707,6 +65742,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, @@ -66022,6 +66058,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, @@ -68035,6 +68072,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, @@ -68110,6 +68148,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, @@ -68323,6 +68362,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, @@ -68388,6 +68428,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, @@ -69225,6 +69266,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, @@ -69248,6 +69290,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, @@ -71324,6 +71367,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 }, @@ -71344,6 +71388,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": { @@ -74024,6 +74069,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, @@ -74138,6 +74184,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, @@ -75628,6 +75675,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, @@ -75680,6 +75728,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, @@ -75724,6 +75773,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, @@ -75771,6 +75821,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, @@ -75807,6 +75858,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, @@ -75863,6 +75915,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, @@ -75909,6 +75962,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, @@ -75949,6 +76003,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, @@ -75990,6 +76045,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, @@ -76036,6 +76092,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, @@ -76087,6 +76144,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, @@ -76133,6 +76191,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, @@ -76160,6 +76219,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, @@ -76190,6 +76250,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, @@ -76241,6 +76302,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, @@ -76291,6 +76353,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, @@ -76335,6 +76398,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, @@ -78094,6 +78158,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, @@ -78308,6 +78373,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, @@ -78472,6 +78538,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, @@ -80454,6 +80521,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/chat/test_anthropic_chat_transformation.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py index cbb60caa57b..ed67f5221d6 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -9849,3 +9849,108 @@ 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: 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: Final = 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: 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, + "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: pytest.MonkeyPatch, model: str, modify_params: bool, keeps_thinking: bool, keeps_edit: bool +) -> None: + monkeypatch.setattr(litellm, "modify_params", modify_params) + result: Final = 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/messages/native/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/messages/native/test_anthropic_messages_effort.py index 94476139313..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 @@ -1,3 +1,5 @@ +from typing import Final + import pytest from litellm.constants import ( @@ -334,3 +336,70 @@ 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-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"), + 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: str, params: dict[str, object], keeps_thinking: bool +) -> None: + result: Final = _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: 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", + 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]}}, + ) + + 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 0808ce0ec3c..d321980f2b8 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -1612,6 +1612,52 @@ class TestAnthropicThinkingSignatureSelfHeal: assert "thinking" not in data assert data["messages"] == [] + 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: Final[dict[str, object]] = { + "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: object, expected: object) -> None: + from litellm.llms.anthropic.common_utils import ( + drop_clear_thinking_edits, + ) + + params: Final[dict[str, object]] = {"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_claude_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude_transformation.py index 0bd480dcdbc..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 @@ -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: dict[str, object], +) -> None: + from litellm.types.router import GenericLiteLLMParams + + 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={ + "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 @@ -4488,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 432d0912248..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 @@ -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,86 @@ 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-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"}}, + {"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-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", + {}, + 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_dropped_only_with_dropped_thinking( + 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={ + "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) -> 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"}}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + assert body["thinking"] == {"type": "disabled"} + def test_body_hoists_wrapped_defer_loading_and_drops_custom(self): schema: Final = {"type": "object", "properties": {}} 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 8ca678a94ad..26323295176 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 5cf872f4ea7..d6346768748 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1211,6 +1211,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"},