fix(anthropic): upgrade legacy thinking for adaptive models

This commit is contained in:
Devin AI 2026-07-12 01:16:16 +00:00
parent a5b0f32a84
commit fecec625e4
5 changed files with 98 additions and 47 deletions

View file

@ -1241,6 +1241,35 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
return thinking
return AnthropicThinkingParam(type=thinking.get("type", "enabled"), budget_tokens=max_tokens - 1)
@staticmethod
def _translate_legacy_thinking_for_adaptive_model(
model: str, optional_params: Dict, custom_llm_provider: str
) -> None:
if not AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider):
return
thinking = optional_params.get("thinking")
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
return
budget = 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"}
output_config = optional_params.get("output_config")
if not isinstance(output_config, dict):
output_config = {}
output_config.setdefault("effort", effort)
optional_params["output_config"] = output_config
def _extract_json_schema_from_response_format(self, value: Optional[dict]) -> Optional[dict]:
if value is None:
return None
@ -1484,6 +1513,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
):
optional_params["metadata"] = {"user_id": value}
elif param == "thinking":
optional_params["thinking"] = value
AnthropicConfig._translate_legacy_thinking_for_adaptive_model(
model=model,
optional_params=optional_params,
custom_llm_provider=self._resolved_provider,
)
if (
isinstance(value, dict)
and value.get("type") == "adaptive"
@ -1514,8 +1549,6 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
model,
)
optional_params.pop("thinking", None)
else:
optional_params["thinking"] = value
elif param == "reasoning_effort":
# Accept both string ("low") and dict ({"effort": "low",
# "summary": "concise"}). The Responses->Chat parser keeps the

View file

@ -2,13 +2,9 @@ from typing import Any, AsyncIterator, Dict, List, Optional, Tuple
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.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.litellm_logging import verbose_logger
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.base_llm.anthropic_messages.transformation import (
BaseAnthropicMessagesConfig,
)
@ -239,40 +235,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 4.6/4.7.
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
thinking = optional_params.get("thinking")
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
return
budget = 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: Optional[int], custom_llm_provider: str
@ -316,7 +278,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
subclasses to handle.
"""
from litellm.exceptions import BadRequestError as _BadRequestError
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
if AnthropicConfig._is_adaptive_thinking_model(model, custom_llm_provider):
return
@ -397,7 +358,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
custom_llm_provider=self._resolved_provider,
)
self._translate_legacy_thinking_for_adaptive_model(
AnthropicConfig._translate_legacy_thinking_for_adaptive_model(
model=model,
optional_params=anthropic_messages_optional_request_params,
custom_llm_provider=self._resolved_provider,
@ -421,8 +382,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
# Transform context_management from OpenAI format to Anthropic format if needed
context_management_param = anthropic_messages_optional_request_params.get("context_management")
if context_management_param is not None:
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
transformed_context_management = AnthropicConfig.map_openai_context_management_to_anthropic(
context_management_param
)

View file

@ -900,6 +900,12 @@ class AmazonConverseConfig(BaseConfig):
"tool_choice": {"disable_parallel_tool_use": disable_parallel}
}
if param == "thinking":
optional_params["thinking"] = value
AnthropicConfig._translate_legacy_thinking_for_adaptive_model(
model=model,
optional_params=optional_params,
custom_llm_provider="bedrock",
)
if (
isinstance(value, dict)
and value.get("type") == "adaptive"
@ -920,8 +926,7 @@ class AmazonConverseConfig(BaseConfig):
optional_params["thinking"] = capped
else:
litellm.verbose_logger.warning(DROP_UNSUPPORTED_ADAPTIVE_THINKING_WARNING, model)
else:
optional_params["thinking"] = value
optional_params.pop("thinking", None)
elif param == "reasoning_effort" and isinstance(value, str):
self._handle_reasoning_effort_parameter(
model=model, reasoning_effort=value, optional_params=optional_params

View file

@ -22,6 +22,12 @@ from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import (
AmazonAnthropicClaudeConfig,
)
from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation import (
VertexAIAnthropicConfig,
)
from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES
from litellm.types.utils import ServerToolUse
@ -2516,6 +2522,35 @@ def test_raw_adaptive_thinking_untouched_for_46_plus_model():
assert result["thinking"] == {"type": "adaptive"}
@pytest.mark.parametrize(
"config,model",
[
(AnthropicConfig(), "claude-opus-4-8"),
(
AmazonAnthropicClaudeConfig(),
"global.anthropic.claude-opus-4-8",
),
(VertexAIAnthropicConfig(), "claude-opus-4-8"),
],
ids=["anthropic", "bedrock-invoke", "vertex"],
)
def test_legacy_thinking_maps_to_adaptive_thinking_for_chat_routes(config, model):
result = config.map_openai_params(
non_default_params={
"thinking": {
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
}
},
optional_params={},
model=model,
drop_params=False,
)
assert result["thinking"] == {"type": "adaptive"}
assert result["output_config"] == {"effort": "high"}
@pytest.fixture
def local_model_cost_map(monkeypatch):
original_model_cost = litellm.model_cost

View file

@ -341,6 +341,25 @@ def test_reasoning_effort_sets_output_config_for_adaptive_models_converse(
assert optional_params["output_config"] == {"effort": expected_effort}
def test_legacy_thinking_maps_to_adaptive_thinking_for_converse():
config = AmazonConverseConfig()
optional_params = config.map_openai_params(
non_default_params={
"thinking": {
"type": "enabled",
"budget_tokens": 4096,
}
},
optional_params={"output_config": {"effort": "low"}},
model="bedrock/converse/global.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",
[