mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(anthropic): disable optional default thinking for reasoning effort none (#50541)
This commit is contained in:
parent
8d9ddb349e
commit
02d610f1a9
14 changed files with 158 additions and 17 deletions
|
|
@ -144,6 +144,7 @@ mod tests {
|
|||
supports_reasoning: true,
|
||||
supports_adaptive_thinking: true,
|
||||
thinking_always_on: false,
|
||||
thinking_on_by_default: false,
|
||||
supports_legacy_thinking: false,
|
||||
supports_output_config: true,
|
||||
supports_sampling_params: false,
|
||||
|
|
|
|||
|
|
@ -1913,6 +1913,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,
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ use litellm_llms_types::{
|
|||
formats::{
|
||||
chat_completions::ReasoningEffort,
|
||||
messages::{
|
||||
AdaptiveThinking, EffortLevel, MessagesOptionalParams, MessagesRequest, OutputConfig,
|
||||
ThinkingConfig, ThinkingDisplay,
|
||||
AdaptiveThinking, DisabledThinking, EffortLevel, MessagesOptionalParams,
|
||||
MessagesRequest, OutputConfig, ThinkingConfig, ThinkingDisplay,
|
||||
},
|
||||
},
|
||||
recognized::Recognized,
|
||||
|
|
@ -172,7 +172,11 @@ fn translate_reasoning_effort(
|
|||
) else {
|
||||
return Ok(MessagesRequest {
|
||||
params: MessagesOptionalParams {
|
||||
thinking: None,
|
||||
thinking: (context.capabilities.thinking_on_by_default
|
||||
&& !context.capabilities.thinking_always_on)
|
||||
.then(|| {
|
||||
Recognized::Known(ThinkingConfig::Disabled(DisabledThinking::default()))
|
||||
}),
|
||||
output_config: None,
|
||||
..request.params
|
||||
},
|
||||
|
|
@ -748,6 +752,33 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default_thinking(true, false, Some(serde_json::json!({"type": "disabled"}))) ]
|
||||
#[case::legacy(false, false, None)]
|
||||
#[case::always_on(true, true, None)]
|
||||
fn none_disables_only_optional_default_thinking(
|
||||
#[case] thinking_on_by_default: bool,
|
||||
#[case] thinking_always_on: bool,
|
||||
#[case] thinking: Option<Value>,
|
||||
) {
|
||||
let capabilities = MessagesModelCapabilities {
|
||||
thinking_on_by_default,
|
||||
thinking_always_on,
|
||||
..opus_4_7()
|
||||
};
|
||||
let expected = match thinking {
|
||||
Some(thinking) => serde_json::json!({"max_tokens": 1024, "thinking": thinking}),
|
||||
None => serde_json::json!({"max_tokens": 1024}),
|
||||
};
|
||||
assert_eq!(
|
||||
translate(
|
||||
capabilities,
|
||||
serde_json::json!({"max_tokens": 1024, "reasoning_effort": "none"})
|
||||
),
|
||||
Ok(request(expected))
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::adaptive_model(opus_4_7())]
|
||||
#[case::effort_capable_model(opus_4_5())]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1718,7 +1718,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
llm_provider: str = "anthropic",
|
||||
) -> AnthropicThinkingParam | None:
|
||||
"""Capability probes read the cost map under ``custom_llm_provider``; ``llm_provider`` only tags raised exceptions."""
|
||||
if reasoning_effort is None or reasoning_effort == "none":
|
||||
if reasoning_effort is None:
|
||||
return None
|
||||
if reasoning_effort == "none":
|
||||
if AnthropicModelInfo.thinks_by_default(
|
||||
model, custom_llm_provider
|
||||
) and not AnthropicModelInfo._is_always_on_thinking_model(model, custom_llm_provider):
|
||||
return AnthropicThinkingParam(type="disabled")
|
||||
return None
|
||||
if AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider):
|
||||
# without display, Anthropic defaults adaptive thinking to
|
||||
|
|
@ -2126,12 +2132,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
custom_llm_provider=self._resolved_provider,
|
||||
llm_provider=self._resolved_provider,
|
||||
)
|
||||
if mapped_thinking is None:
|
||||
if mapped_thinking is None or mapped_thinking.get("type") == "disabled":
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
else:
|
||||
if mapped_thinking is not None:
|
||||
optional_params["thinking"] = mapped_thinking
|
||||
if AnthropicConfig._is_adaptive_thinking_model(model, self._resolved_provider):
|
||||
if mapped_thinking.get("type") != "disabled" and AnthropicConfig._is_adaptive_thinking_model(
|
||||
model, self._resolved_provider
|
||||
):
|
||||
mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(effort_value)
|
||||
if mapped_effort is None:
|
||||
AnthropicConfig._raise_invalid_reasoning_effort(
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
"""Map OpenAI-style ``reasoning_effort`` to native Anthropic params.
|
||||
|
||||
Caller-supplied ``thinking`` / ``output_config`` win over the alias.
|
||||
``effort='none'`` clears both. Invalid efforts raise a 400. A mapped
|
||||
``effort='none'`` disables optional default thinking and clears effort. Invalid efforts raise a 400. A mapped
|
||||
thinking budget is capped below ``max_tokens`` and dropped when even
|
||||
the minimum budget cannot fit.
|
||||
"""
|
||||
|
|
@ -485,6 +485,10 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
if not isinstance(reasoning_effort, str):
|
||||
return
|
||||
|
||||
if reasoning_effort == "none":
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
|
||||
try:
|
||||
mapped_thinking: Final = AnthropicConfig.map_reasoning_effort(
|
||||
reasoning_effort=reasoning_effort,
|
||||
|
|
@ -495,8 +499,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
raise AnthropicError(message=str(e.message), status_code=400)
|
||||
|
||||
if mapped_thinking is None:
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
return
|
||||
|
||||
fitted_thinking: Final = AnthropicConfig.cap_thinking_budget_to_max_tokens(mapped_thinking, max_tokens)
|
||||
|
|
@ -505,7 +507,9 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
return
|
||||
|
||||
optional_params.setdefault("thinking", fitted_thinking)
|
||||
if AnthropicModelInfo.is_adaptive_thinking_model(model, custom_llm_provider):
|
||||
if fitted_thinking.get("type") != "disabled" and AnthropicModelInfo.is_adaptive_thinking_model(
|
||||
model, custom_llm_provider
|
||||
):
|
||||
mapped_effort: Final = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort)
|
||||
if mapped_effort is None:
|
||||
raise AnthropicError(
|
||||
|
|
|
|||
|
|
@ -583,12 +583,14 @@ class AmazonConverseConfig(BaseConfig):
|
|||
custom_llm_provider="bedrock",
|
||||
llm_provider="bedrock_converse",
|
||||
)
|
||||
if mapped_thinking is None:
|
||||
if mapped_thinking is None or mapped_thinking.get("type") == "disabled":
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
else:
|
||||
if mapped_thinking is not None:
|
||||
optional_params["thinking"] = mapped_thinking
|
||||
if AnthropicConfig.is_adaptive_thinking_model(model, "bedrock"):
|
||||
if mapped_thinking.get("type") != "disabled" and AnthropicConfig.is_adaptive_thinking_model(
|
||||
model, "bedrock"
|
||||
):
|
||||
mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort)
|
||||
if mapped_effort is None:
|
||||
AnthropicConfig.raise_invalid_reasoning_effort(
|
||||
|
|
|
|||
|
|
@ -629,12 +629,14 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
|
|||
custom_llm_provider="databricks",
|
||||
llm_provider="databricks",
|
||||
)
|
||||
if mapped_thinking is None:
|
||||
if mapped_thinking is None or mapped_thinking.get("type") == "disabled":
|
||||
optional_params.pop("thinking", None)
|
||||
optional_params.pop("output_config", None)
|
||||
else:
|
||||
if mapped_thinking is not None:
|
||||
optional_params["thinking"] = mapped_thinking
|
||||
if AnthropicConfig._is_adaptive_thinking_model(model, "databricks"):
|
||||
if mapped_thinking.get("type") != "disabled" and AnthropicConfig._is_adaptive_thinking_model(
|
||||
model, "databricks"
|
||||
):
|
||||
mapped_effort: str | None = None
|
||||
if isinstance(reasoning_effort_value, str):
|
||||
mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort_value)
|
||||
|
|
|
|||
|
|
@ -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": AnthropicModelInfo.thinks_by_default(model, provider),
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -721,3 +721,17 @@ def test_a_first_party_messages_request_never_fetches_the_remote_beta_headers_co
|
|||
|
||||
assert request["messages"][1] == {"role": "system", "content": "be terse"}
|
||||
assert not remote_config.called
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected",
|
||||
[
|
||||
("claude-sonnet-5", {"type": "disabled"}),
|
||||
("claude-haiku-4-5", None),
|
||||
("claude-sonnet-5-5", None),
|
||||
],
|
||||
)
|
||||
def test_none_disables_only_optional_default_thinking(model: str, expected: dict[str, str] | None) -> None:
|
||||
params: Final[dict[str, object]] = {"reasoning_effort": "none", "output_config": {"effort": "high"}}
|
||||
AnthropicMessagesConfig._translate_reasoning_effort_to_anthropic(model, params, 1024, "anthropic")
|
||||
assert params == ({"thinking": expected} if expected is not None else {})
|
||||
|
|
|
|||
|
|
@ -5,11 +5,42 @@ Verifies that reasoning_effort=None returns None for all models,
|
|||
including Claude Opus 4.6.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.exceptions import BadRequestError
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected",
|
||||
[
|
||||
("claude-sonnet-5", {"type": "disabled"}),
|
||||
("claude-opus-5", {"type": "disabled"}),
|
||||
("claude-haiku-5-5", {"type": "disabled"}),
|
||||
("claude-haiku-4-5", None),
|
||||
("claude-sonnet-5-5", None),
|
||||
("claude-opus-5-5", None),
|
||||
("claude-fable-5", None),
|
||||
("claude-mythos-5", None),
|
||||
],
|
||||
)
|
||||
def test_none_disables_only_optional_default_thinking(model: str, expected: dict[str, str] | None) -> None:
|
||||
mapped: Final = AnthropicConfig().map_openai_params({"reasoning_effort": "none"}, {}, model, False)
|
||||
assert mapped.get("thinking") == expected
|
||||
assert "output_config" not in mapped
|
||||
|
||||
|
||||
@pytest.mark.parametrize("effort", [None, "bogus"])
|
||||
def test_default_thinking_preserves_absent_and_invalid_effort(effort: str | None) -> None:
|
||||
if effort is None:
|
||||
assert AnthropicConfig.map_reasoning_effort(effort, "claude-sonnet-5", "anthropic") is None
|
||||
else:
|
||||
with pytest.raises(BadRequestError, match="Invalid reasoning_effort"):
|
||||
AnthropicConfig().map_openai_params({"reasoning_effort": effort}, {}, "claude-sonnet-5", False)
|
||||
|
||||
|
||||
class TestMapReasoningEffort:
|
||||
def test_none_returns_none_for_opus_4_6(self):
|
||||
"""reasoning_effort=None should return None for Opus 4.6, not adaptive."""
|
||||
|
|
|
|||
|
|
@ -12203,6 +12203,21 @@ def test_converse_claude_request_over_the_cache_control_cap_drops_the_oldest_hin
|
|||
assert messages == messages_before
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected",
|
||||
[
|
||||
("us.anthropic.claude-sonnet-5", {"type": "disabled"}),
|
||||
("us.anthropic.claude-haiku-4-5", None),
|
||||
("us.anthropic.claude-sonnet-5-5", None),
|
||||
],
|
||||
)
|
||||
def test_none_disables_only_optional_default_thinking(model: str, expected: dict[str, str] | None) -> None:
|
||||
params: Final[dict[str, object]] = {}
|
||||
AmazonConverseConfig()._handle_reasoning_effort_parameter(model, "none", params)
|
||||
assert params.get("thinking") == expected
|
||||
assert "output_config" not in params
|
||||
|
||||
|
||||
def test_converse_claude_tool_message_with_several_marked_blocks_counts_as_one_cache_point_against_the_cap(
|
||||
monkeypatch: pytest.MonkeyPatch, local_model_cost_map: None
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -3220,3 +3220,18 @@ def test_databricks_claude_cache_control_cap_keeps_room_for_a_top_level_cache_co
|
|||
|
||||
assert "cache_control" not in json.dumps(body["messages"][0])
|
||||
assert json.dumps(body["messages"]).count('"cache_control"') == 3
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected",
|
||||
[
|
||||
("databricks-claude-sonnet-5", {"type": "disabled"}),
|
||||
("databricks-claude-haiku-4-5", None),
|
||||
("databricks-claude-sonnet-5-5", None),
|
||||
],
|
||||
)
|
||||
def test_none_disables_only_optional_default_thinking(model: str, expected: dict[str, str] | None) -> None:
|
||||
params: Final = _map_reasoning_effort(model, "none")
|
||||
assert params.get("thinking") == expected
|
||||
assert "output_config" not in params
|
||||
assert "reasoning_effort" not in params
|
||||
|
|
|
|||
|
|
@ -73,3 +73,16 @@ def test_capability_source_observes_runtime_registration_changes(monkeypatch: py
|
|||
assert before["supports_output_config"] is True
|
||||
assert after["supports_reasoning"] is False
|
||||
assert after["supports_output_config"] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model, expected",
|
||||
[
|
||||
("claude-sonnet-5", True),
|
||||
("claude-haiku-4-5", False),
|
||||
("claude-sonnet-5-5", True),
|
||||
],
|
||||
)
|
||||
def test_default_thinking_capability_is_projected(model: str, expected: bool) -> None:
|
||||
capabilities: Final = anthropic_model_capabilities(model, "anthropic")
|
||||
assert capabilities["thinking_on_by_default"] is expected
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue