fix(anthropic/passthrough): drop incompatible temperature when downgrading adaptive thinking for pre-4.6 models (#33244)

* fix(anthropic/passthrough): drop temperature and cap thinking budget when downgrading adaptive thinking for pre-4.6 models

* test(anthropic/passthrough): use sufficient max_tokens for reasoning_effort thinking mapping

* fix(anthropic/passthrough): drop incompatible temperature when downgrading adaptive thinking for pre-4.6 models

Narrow the fix to the temperature reconciliation; the reasoning_effort
budget cap is reverted because the live translation grid relies on
budget_tokens >= max_tokens to reject unsupported effort tiers
(xhigh/max) on budget-mode models, so capping turned those 400s into
200s.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
(cherry picked from commit 71dffc1e9a)
This commit is contained in:
devin-ai-integration[bot] 2026-07-14 12:31:28 -07:00 committed by Yuneng Jiang
parent b3086ccd74
commit 6fec38dabd
No known key found for this signature in database
2 changed files with 112 additions and 0 deletions

View file

@ -386,6 +386,36 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
return thinking
return {**thinking, "budget_tokens": max_tokens - 1}
@staticmethod
def _drop_incompatible_temperature_for_thinking(
model: str, optional_params: dict, custom_llm_provider: str
) -> None:
"""Anthropic rejects any ``temperature`` other than 1 while extended thinking
is enabled ("temperature may only be set to 1 when thinking is enabled").
Clients like Claude Code send ``thinking``/``output_config.effort`` together
with a pinned ``temperature`` (e.g. the safety classifier uses ``temperature=0``
for determinism). When the request lands on a non-adaptive model, the effort
interface is reshaped above into legacy ``thinking={type: enabled}`` (or kept
as ``output_config.effort`` on Opus 4.5), and the leftover ``temperature`` would
400. Preserving the thinking the caller asked for wins over an unhonorable
sampling value (Anthropic forces ``temperature=1`` under thinking regardless),
so drop it and let the API default apply.
Adaptive models (4.6+) own this natively and are left untouched.
"""
if AnthropicModelInfo._is_adaptive_thinking_model(model, custom_llm_provider):
return
temperature = optional_params.get("temperature")
if temperature is None or temperature == 1:
return
thinking = optional_params.get("thinking")
output_config = optional_params.get("output_config")
thinking_enabled = isinstance(thinking, dict) and thinking.get("type") == "enabled"
effort_enabled = isinstance(output_config, dict) and output_config.get("effort") is not None
if thinking_enabled or effort_enabled:
optional_params.pop("temperature", None)
def transform_anthropic_messages_request(
self,
model: str,
@ -426,6 +456,12 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
custom_llm_provider=self._resolved_provider,
)
self._drop_incompatible_temperature_for_thinking(
model=model,
optional_params=anthropic_messages_optional_request_params,
custom_llm_provider=self._resolved_provider,
)
system_param = anthropic_messages_optional_request_params.get("system")
if self.should_strip_billing_metadata() and system_param is not None:
filtered_system = self._filter_billing_headers_from_system(system_param)

View file

@ -2,6 +2,7 @@ import pytest
from litellm.constants import (
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET,
)
@ -174,6 +175,81 @@ def test_unrecognized_effort_raises_clean_400():
assert exc_info.value.status_code == 400
def test_pinned_temperature_dropped_when_adaptive_downgraded_to_enabled():
"""Regression (#33203): Claude Code's safety classifier sends adaptive thinking +
temperature=0 to Haiku 4.5. The adaptive interface is downgraded to legacy enabled
thinking, but Anthropic rejects "temperature may only be set to 1 when thinking is
enabled". The pinned temperature must be dropped so the request succeeds while the
downgraded thinking is preserved."""
params = _claude_code_payload(effort="medium")
params["temperature"] = 0
result = _transform("claude-haiku-4-5", params)
assert result["thinking"] == {
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET,
}
assert "temperature" not in result
def test_temperature_one_preserved_with_enabled_thinking():
"""temperature=1 is compatible with extended thinking, so it must be kept."""
params = _claude_code_payload(effort="medium")
params["temperature"] = 1
result = _transform("claude-haiku-4-5", params)
assert result["thinking"]["type"] == "enabled"
assert result["temperature"] == 1
def test_pinned_temperature_preserved_when_thinking_dropped():
"""When thinking is dropped entirely (non-reasoning model), there is no thinking
conflict, so a pinned temperature must survive untouched."""
params = _claude_code_payload(effort="medium")
params["temperature"] = 0
result = _transform("claude-3-5-haiku-latest", params)
assert "thinking" not in result
assert result["temperature"] == 0
def test_pinned_temperature_preserved_for_adaptive_model():
"""Adaptive models (4.6+) own the thinking/temperature relationship natively, so
the passthrough must not strip a pinned temperature for them."""
params = _claude_code_payload(effort="high")
params["temperature"] = 0
result = _transform("claude-sonnet-4-6", params)
assert result["thinking"] == {"type": "adaptive"}
assert result["temperature"] == 0
def test_pinned_temperature_dropped_for_opus_4_5_effort():
"""Opus 4.5 keeps native output_config.effort (extended thinking), which is equally
incompatible with a pinned non-1 temperature, so the temperature must be dropped."""
params = _claude_code_payload(effort="medium")
params["temperature"] = 0
result = _transform("claude-opus-4-5", params)
assert result["output_config"] == {"effort": "medium"}
assert "temperature" not in result
def test_reasoning_effort_with_pinned_temperature_drops_temperature():
"""The reasoning_effort alias synthesizes legacy enabled thinking on a non-adaptive
model; a co-pinned non-1 temperature must be dropped to avoid the Anthropic 400."""
result = _transform(
"claude-haiku-4-5",
{"max_tokens": 8192, "reasoning_effort": "low", "temperature": 0},
)
assert result["thinking"] == {
"type": "enabled",
"budget_tokens": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
}
assert "temperature" not in result
def test_non_adaptive_request_without_effort_is_untouched():
"""A non-adaptive model receiving a request with no adaptive interface (no
effort, no adaptive thinking) must pass through untouched."""