From f4baab638380e478a692bdde4b5bdfe5f0869340 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Sat, 10 Oct 2026 12:54:55 +0530 Subject: [PATCH 1/2] fix(anthropic): drop sampling params Anthropic rejects under adaptive thinking on /v1/messages --- .../pass_through/messages/transformation.py | 54 ++++++++++--------- .../test_anthropic_messages_effort.py | 42 ++++++++++++--- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/messages/transformation.py b/litellm/llms/anthropic/pass_through/messages/transformation.py index f1eb30b63c1..827763cb27f 100644 --- a/litellm/llms/anthropic/pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/pass_through/messages/transformation.py @@ -40,6 +40,7 @@ from .mid_conversation_system import ( DEFAULT_ANTHROPIC_API_VERSION: Final = "2023-06-01" _CALLER_CREDENTIAL_HEADERS: Final = frozenset({"x-api-key", "authorization"}) +_MIN_TOP_P_WITH_THINKING: Final = 0.95 def _carries_caller_credential(headers: Mapping[str, str]) -> bool: @@ -540,34 +541,37 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): optional_params.pop("output_config", None) @staticmethod - def _drop_incompatible_temperature_for_thinking( - model: str, optional_params: dict, custom_llm_provider: str + def _drop_sampling_params_incompatible_with_thinking( + model: str, optional_params: dict[str, object], 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"). + """While thinking is on, Anthropic rejects ``temperature`` other than 1, any + ``top_k``, and ``top_p`` below 0.95, for legacy ``enabled`` and ``adaptive`` + thinking alike ("temperature may only be set to 1 when thinking is enabled or + in adaptive mode"). - 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. + Clients like Claude Code send thinking together with a pinned ``temperature`` + (the safety classifier uses ``temperature=0``). Preserving the thinking the + caller asked for wins over a sampling value the API would refuse, so drop it + and let the API default apply. On a non-adaptive model a bare + ``output_config.effort`` (kept natively on Opus 4.5) also means thinking is on. """ - if AnthropicModelInfo.is_adaptive_thinking_model(model, custom_llm_provider): - return - temperature: Final = optional_params.get("temperature") - if temperature is None or temperature == 1: - return thinking: Final = optional_params.get("thinking") output_config: Final = optional_params.get("output_config") - thinking_enabled: Final = isinstance(thinking, dict) and thinking.get("type") == "enabled" - effort_enabled: Final = isinstance(output_config, dict) and output_config.get("effort") is not None - if thinking_enabled or effort_enabled: - optional_params.pop("temperature", None) + thinking_on: Final = isinstance(thinking, dict) and thinking.get("type") in ("enabled", "adaptive") + effort_on: Final = ( + isinstance(output_config, dict) + and output_config.get("effort") is not None + and not AnthropicModelInfo.is_adaptive_thinking_model(model, custom_llm_provider) + ) + if not (thinking_on or effort_on): + return + temperature: Final = optional_params.get("temperature") + top_p: Final = optional_params.get("top_p") + if temperature is not None and temperature != 1: + optional_params.pop("temperature") + if isinstance(top_p, int | float) and top_p < _MIN_TOP_P_WITH_THINKING: + optional_params.pop("top_p") + optional_params.pop("top_k", None) def transform_anthropic_messages_request( self, @@ -616,9 +620,9 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): custom_llm_provider=self._resolved_provider, ) - self._drop_incompatible_temperature_for_thinking( + self._drop_sampling_params_incompatible_with_thinking( model=model, - optional_params=anthropic_messages_optional_request_params, + optional_params=anthropic_messages_optional_request_params, # pyright: ignore[reportUnknownArgumentType] # override signature fixes this param as a bare dict custom_llm_provider=self._resolved_provider, ) diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py index 7885cc69b19..07675d540b0 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py @@ -248,15 +248,43 @@ def test_pinned_temperature_preserved_when_thinking_dropped(): 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) +@pytest.mark.parametrize( + ("sampling_params", "kept"), + [ + ({"temperature": 0}, {}), + ({"temperature": 1}, {"temperature": 1}), + ({"top_k": 5}, {}), + ({"top_p": 0.5}, {}), + ({"top_p": 0.97}, {"top_p": 0.97}), + ({"temperature": 0, "top_k": 5, "top_p": 0.5}, {}), + ], +) +def test_adaptive_thinking_keeps_only_sampling_values_anthropic_accepts(sampling_params, kept): + """#42886: Anthropic rejects temperature other than 1, any top_k, and top_p below 0.95 + "when thinking is enabled or in adaptive mode", so adaptive thinking gets the same + reconciliation as legacy enabled thinking while the thinking itself is preserved.""" + result = _transform("claude-sonnet-4-6", {**_claude_code_payload(effort="high"), **sampling_params}) assert result["thinking"] == {"type": "adaptive"} - assert result["temperature"] == 0 + assert {key: result[key] for key in ("temperature", "top_k", "top_p") if key in result} == kept + + +def test_sampling_params_survive_on_an_adaptive_model_without_thinking(): + """Without thinking there is no conflict, so the caller's sampling values pass through.""" + result = _transform("claude-sonnet-4-6", {"max_tokens": 1024, "temperature": 0, "top_k": 5, "top_p": 0.5}) + + assert "thinking" not in result + assert (result["temperature"], result["top_k"], result["top_p"]) == (0, 5, 0.5) + + +def test_top_k_dropped_with_legacy_enabled_thinking(): + """The legacy enabled-thinking path drops top_k too, not only a pinned temperature.""" + params = _claude_code_payload(effort="medium") + params["top_k"] = 5 + result = _transform("claude-haiku-4-5", params) + + assert result["thinking"]["type"] == "enabled" + assert "top_k" not in result def test_pinned_temperature_dropped_for_opus_4_5_effort(): From 7a2c7110d5060af1ab8b8bd8f65c64cc991c1b48 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Sun, 11 Oct 2026 13:02:39 +0530 Subject: [PATCH 2/2] fix(anthropic): keep sampling params when thinking is explicitly disabled On a non-adaptive model a bare output_config.effort counts as thinking on, but an explicit thinking type disabled now wins, so the caller's sampling values pass through. Also pins the top_p=0.95 boundary as allowed. --- .../pass_through/messages/transformation.py | 6 ++++-- .../test_anthropic_messages_effort.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/messages/transformation.py b/litellm/llms/anthropic/pass_through/messages/transformation.py index 827763cb27f..b1aa12ae91b 100644 --- a/litellm/llms/anthropic/pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/pass_through/messages/transformation.py @@ -553,13 +553,15 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): (the safety classifier uses ``temperature=0``). Preserving the thinking the caller asked for wins over a sampling value the API would refuse, so drop it and let the API default apply. On a non-adaptive model a bare - ``output_config.effort`` (kept natively on Opus 4.5) also means thinking is on. + ``output_config.effort`` (kept natively on Opus 4.5) also means thinking is on, + unless the caller explicitly disabled thinking. """ thinking: Final = optional_params.get("thinking") output_config: Final = optional_params.get("output_config") thinking_on: Final = isinstance(thinking, dict) and thinking.get("type") in ("enabled", "adaptive") effort_on: Final = ( - isinstance(output_config, dict) + thinking != {"type": "disabled"} + and isinstance(output_config, dict) and output_config.get("effort") is not None and not AnthropicModelInfo.is_adaptive_thinking_model(model, custom_llm_provider) ) diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py index 07675d540b0..bef373bbbec 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_messages_effort.py @@ -255,6 +255,7 @@ def test_pinned_temperature_preserved_when_thinking_dropped(): ({"temperature": 1}, {"temperature": 1}), ({"top_k": 5}, {}), ({"top_p": 0.5}, {}), + ({"top_p": 0.95}, {"top_p": 0.95}), ({"top_p": 0.97}, {"top_p": 0.97}), ({"temperature": 0, "top_k": 5, "top_p": 0.5}, {}), ], @@ -298,6 +299,24 @@ def test_pinned_temperature_dropped_for_opus_4_5_effort(): assert "temperature" not in result +def test_explicitly_disabled_thinking_keeps_sampling_params_for_opus_4_5_effort(): + """Effort alone counts as thinking on Opus 4.5, but not when the caller turned + thinking off, so their sampling values must pass through.""" + result = _transform( + "claude-opus-4-5", + { + "max_tokens": 1024, + "thinking": {"type": "disabled"}, + "output_config": {"effort": "medium"}, + "temperature": 0, + "top_k": 5, + "top_p": 0.5, + }, + ) + + assert (result["temperature"], result["top_k"], result["top_p"]) == (0, 5, 0.5) + + 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."""