mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(anthropic): upgrade legacy thinking to adaptive on adaptive-only models for chat and Bedrock Converse
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
77813ae906
commit
2063c29f5d
6 changed files with 154 additions and 47 deletions
|
|
@ -1544,6 +1544,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
optional_params.pop("thinking", None)
|
||||
else:
|
||||
optional_params["thinking"] = value
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=optional_params, custom_llm_provider=self._resolved_provider
|
||||
)
|
||||
elif param == "reasoning_effort":
|
||||
# Accept both string ("low") and dict ({"effort": "low",
|
||||
# "summary": "concise"}). The Responses->Chat parser keeps the
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ import httpx
|
|||
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
|
||||
|
||||
import litellm
|
||||
from litellm.constants import DEFAULT_MODEL_CREATED_AT_TIME
|
||||
from litellm.constants import (
|
||||
DEFAULT_MODEL_CREATED_AT_TIME,
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_file_ids_from_messages,
|
||||
)
|
||||
|
|
@ -490,6 +495,46 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
)
|
||||
optional_params.pop("thinking", None)
|
||||
|
||||
@staticmethod
|
||||
def translate_legacy_thinking_for_adaptive_model(
|
||||
model: str,
|
||||
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param, as in maybe_drop_disabled_thinking
|
||||
custom_llm_provider: str,
|
||||
) -> None:
|
||||
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
|
||||
adaptive-thinking models that reject it (4.7+ and the 5 families).
|
||||
Models flagged ``supports_legacy_thinking`` (the 4.6 family) accept the
|
||||
legacy shape natively, so it is forwarded verbatim and the caller's
|
||||
``budget_tokens`` cap keeps applying. Caller-provided
|
||||
``output_config.effort`` is never overridden.
|
||||
"""
|
||||
if not AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider):
|
||||
return
|
||||
if AnthropicModelInfo._supports_legacy_thinking(model, custom_llm_provider):
|
||||
return
|
||||
thinking: Final = optional_params.get("thinking")
|
||||
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
|
||||
return
|
||||
|
||||
budget: Final = int(thinking.get("budget_tokens") or 0)
|
||||
if budget >= DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET and (
|
||||
AnthropicModelInfo._supports_model_capability(model, "supports_xhigh_reasoning_effort", custom_llm_provider)
|
||||
):
|
||||
effort = "xhigh"
|
||||
elif budget >= DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET:
|
||||
effort = "high"
|
||||
elif budget >= DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET:
|
||||
effort = "medium"
|
||||
else:
|
||||
effort = "low"
|
||||
|
||||
optional_params["thinking"] = {"type": "adaptive"}
|
||||
existing_output_config = optional_params.get("output_config")
|
||||
if not isinstance(existing_output_config, dict):
|
||||
existing_output_config = {}
|
||||
existing_output_config.setdefault("effort", effort)
|
||||
optional_params["output_config"] = existing_output_config
|
||||
|
||||
def is_effort_used(
|
||||
self,
|
||||
optional_params: dict | None,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@ from typing import Any, Final
|
|||
|
||||
import httpx
|
||||
|
||||
from litellm.constants import (
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
|
||||
)
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.litellm_logging import verbose_logger
|
||||
|
|
@ -400,46 +395,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
existing_output_config.setdefault("effort", mapped_effort)
|
||||
optional_params["output_config"] = existing_output_config
|
||||
|
||||
@staticmethod
|
||||
def _translate_legacy_thinking_for_adaptive_model(
|
||||
model: str, optional_params: dict, custom_llm_provider: str
|
||||
) -> None:
|
||||
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
|
||||
adaptive-thinking models that reject it (4.7+ and the 5 families).
|
||||
Models flagged ``supports_legacy_thinking`` (the 4.6 family) accept the
|
||||
legacy shape natively, so it is forwarded verbatim and the caller's
|
||||
``budget_tokens`` cap keeps applying. Caller-provided
|
||||
``output_config.effort`` is never overridden.
|
||||
"""
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
if not AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider):
|
||||
return
|
||||
if AnthropicModelInfo._supports_legacy_thinking(model, custom_llm_provider):
|
||||
return
|
||||
thinking: Final = optional_params.get("thinking")
|
||||
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
|
||||
return
|
||||
|
||||
budget: Final = int(thinking.get("budget_tokens") or 0)
|
||||
if budget >= DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET and (
|
||||
AnthropicConfig._supports_effort_level(model, "xhigh", custom_llm_provider)
|
||||
):
|
||||
effort = "xhigh"
|
||||
elif budget >= DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET:
|
||||
effort = "high"
|
||||
elif budget >= DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET:
|
||||
effort = "medium"
|
||||
else:
|
||||
effort = "low"
|
||||
|
||||
optional_params["thinking"] = {"type": "adaptive"}
|
||||
existing_output_config = optional_params.get("output_config")
|
||||
if not isinstance(existing_output_config, dict):
|
||||
existing_output_config = {}
|
||||
existing_output_config.setdefault("effort", 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
|
||||
|
|
@ -606,7 +561,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
custom_llm_provider=self._resolved_provider,
|
||||
)
|
||||
|
||||
self._translate_legacy_thinking_for_adaptive_model(
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model,
|
||||
optional_params=anthropic_messages_optional_request_params,
|
||||
custom_llm_provider=self._resolved_provider,
|
||||
|
|
|
|||
|
|
@ -934,6 +934,9 @@ class AmazonConverseConfig(BaseConfig):
|
|||
litellm.verbose_logger.warning(DROP_UNSUPPORTED_ADAPTIVE_THINKING_WARNING, model)
|
||||
else:
|
||||
optional_params["thinking"] = value
|
||||
AnthropicModelInfo.translate_legacy_thinking_for_adaptive_model(
|
||||
model=model, optional_params=optional_params, custom_llm_provider="bedrock"
|
||||
)
|
||||
elif param == "reasoning_effort" and isinstance(value, str):
|
||||
self._handle_reasoning_effort_parameter(
|
||||
model=model, reasoning_effort=value, optional_params=optional_params
|
||||
|
|
|
|||
|
|
@ -3127,6 +3127,31 @@ def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,budget_tokens,expected",
|
||||
[
|
||||
("claude-opus-4-8", 4096, ({"type": "adaptive"}, {"effort": "high"})),
|
||||
("claude-opus-4-7", 24000, ({"type": "adaptive"}, {"effort": "xhigh"})),
|
||||
("claude-opus-4-6", 4096, ({"type": "enabled", "budget_tokens": 4096}, None)),
|
||||
("claude-sonnet-4-5-20250929", 4096, ({"type": "enabled", "budget_tokens": 4096}, None)),
|
||||
],
|
||||
)
|
||||
def test_legacy_thinking_translated_to_adaptive_on_adaptive_only_models(model, budget_tokens, expected):
|
||||
"""Adaptive-only models reject thinking={type: enabled} with a 400, so the
|
||||
legacy shape must be upgraded to adaptive + output_config.effort on
|
||||
/chat/completions too, while models that accept it keep the caller's budget."""
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"thinking": {"type": "enabled", "budget_tokens": budget_tokens}, "max_tokens": 64000},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert (result["thinking"], result.get("output_config")) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_value",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -6269,6 +6269,82 @@ def test_adaptive_thinking_passes_through_on_46_plus_converse(model):
|
|||
assert optional_params.get("thinking") == {"type": "adaptive"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,budget_tokens,expected_effort",
|
||||
[
|
||||
("anthropic.claude-opus-4-8", 4096, "high"),
|
||||
("us.anthropic.claude-opus-4-8", 2000, "low"),
|
||||
("global.anthropic.claude-opus-4-8", 12000, "xhigh"),
|
||||
("us.anthropic.claude-opus-4-7", 3000, "medium"),
|
||||
("anthropic.claude-fable-5", 4096, "high"),
|
||||
],
|
||||
)
|
||||
def test_legacy_thinking_translated_to_adaptive_on_adaptive_only_converse(model, budget_tokens, expected_effort):
|
||||
"""Adaptive-only models (4.7+, 5 families) reject thinking={type: enabled}
|
||||
with a 400 on Bedrock Converse, so the legacy shape from callers like Claude
|
||||
Code must be upgraded to thinking={type: adaptive} + output_config.effort
|
||||
derived from budget_tokens, matching the /v1/messages passthrough."""
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
optional_params = config.map_openai_params(
|
||||
non_default_params={"thinking": {"type": "enabled", "budget_tokens": budget_tokens}, "max_tokens": 64000},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
request = config.transform_request(
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request["additionalModelRequestFields"]["thinking"] == {"type": "adaptive"}
|
||||
assert request["additionalModelRequestFields"]["output_config"] == {"effort": expected_effort}
|
||||
|
||||
|
||||
def test_legacy_thinking_translation_keeps_caller_output_config_effort_converse():
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
optional_params = config.map_openai_params(
|
||||
non_default_params={
|
||||
"output_config": {"effort": "low"},
|
||||
"thinking": {"type": "enabled", "budget_tokens": 12000},
|
||||
"max_tokens": 64000,
|
||||
},
|
||||
optional_params={},
|
||||
model="anthropic.claude-opus-4-8",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert optional_params["thinking"] == {"type": "adaptive"}
|
||||
assert optional_params["output_config"] == {"effort": "low"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"us.anthropic.claude-opus-4-6",
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
],
|
||||
)
|
||||
def test_legacy_thinking_forwarded_verbatim_when_model_accepts_it_converse(model):
|
||||
"""The 4.6 family and pre-adaptive models accept thinking={type: enabled}
|
||||
natively, so the caller's budget_tokens cap must keep applying."""
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
optional_params = config.map_openai_params(
|
||||
non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}, "max_tokens": 8192},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert optional_params["thinking"] == {"type": "enabled", "budget_tokens": 4096}
|
||||
assert "output_config" not in optional_params
|
||||
|
||||
|
||||
def test_adaptive_thinking_dropped_when_max_tokens_too_small_converse():
|
||||
"""When max_tokens can't fit even the minimum thinking budget, the raw
|
||||
adaptive block must be dropped entirely rather than translated, so the
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue