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"),