From 770bd40f5de3f38ccc37d1b5bf5b942aecdf77c7 Mon Sep 17 00:00:00 2001 From: tin-berri Date: Fri, 21 Aug 2026 18:23:25 -0700 Subject: [PATCH] fix(anthropic_messages): gate sampling params on /v1/messages like /chat/completions (#37868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(anthropic_messages): gate sampling params on /v1/messages like /chat/completions /v1/messages forwarded temperature/top_p/top_k raw to models that removed sampling params (supports_sampling_params: false — Claude 4.7+/Fable 5), producing provider 400s that router fallbacks mask as silent model downgrades. The chat path already gates these via AnthropicModelInfo._apply_sampling_param; reuse it in get_requested_anthropic_messages_optional_param so both endpoints agree: drop under drop_params, else raise the clean client-side 400. Fixes #35053 * test(anthropic): drive new sampling-param tests off the kwarg, not the global The five tests added here set `litellm.drop_params = True` under a manual try/finally. That trips TQ005 (module-global mutation, 10 new violations over the ceiling) and it leaks process-wide if the finally is ever skipped, which is what the save/restore conftest exists to paper over. `get_requested_anthropic_messages_optional_param` already takes `drop_params` as a kwarg, and that is the path /v1/messages actually uses, so pass it directly. `monkeypatch.setattr` pins the global to False so each test proves the per-request flag alone is sufficient and cannot pass on a leaked global. Verified: TQ gate clean, all 10 tests pass, and the 3 that assert the new gating still fail with the fix in utils.py reverted. --------- Co-authored-by: Mihidum Hettiyahandi <55163074+mihidumh@users.noreply.github.com> --- .../messages/utils.py | 11 ++++ .../test_request_optional_param_utils.py | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py index d0dc5d527fe..02d82887dde 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py @@ -44,6 +44,7 @@ class AnthropicMessagesRequestUtils: filtered_params: Final = {k: v for k, v in params.items() if k in valid_keys and v is not None} if model is not None: from litellm.llms.anthropic.chat.transformation import AnthropicConfig + from litellm.llms.anthropic.common_utils import AnthropicModelInfo AnthropicConfig._maybe_drop_speed_param( model=model, @@ -51,6 +52,16 @@ class AnthropicMessagesRequestUtils: drop_params=drop_params, custom_llm_provider=custom_llm_provider, ) + for param in ("temperature", "top_p", "top_k"): + if param in filtered_params: + AnthropicModelInfo._apply_sampling_param( # pyright: ignore[reportPrivateUsage] # same gating the /chat/completions path applies; forking it would drift + optional_params=filtered_params, + model=model, + param=param, + value=filtered_params.pop(param), + drop_params=drop_params, + output_key=param, + ) return cast(AnthropicMessagesRequestOptionalParams, filtered_params) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py index f0252e13336..dc2e107928f 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py @@ -6,6 +6,8 @@ Regression tests for the /v1/messages request-parse fast paths: while resolving the (static) type hints only once per process. """ +import pytest + import litellm from litellm.llms.anthropic.experimental_pass_through.messages.utils import ( AnthropicMessagesRequestUtils, @@ -88,3 +90,61 @@ def test_drop_params_keeps_speed_for_supporting_model(): litellm.drop_params = original assert result == {"speed": "fast"} + + +def test_drop_params_strips_sampling_params_for_unsupported_model(monkeypatch): + # claude-opus-4-7 has supports_sampling_params: false in the model map; the + # API 400s on these rather than ignoring them. + monkeypatch.setattr(litellm, "drop_params", False) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"temperature": 0.3, "top_p": 0.9, "top_k": 40, "stream": True}, + model="claude-opus-4-7", + drop_params=True, + ) + + assert result == {"stream": True} + + +def test_drop_params_strips_sampling_params_for_provider_prefixed_model(monkeypatch): + # Vertex-routed ids must resolve the same capability flag. + monkeypatch.setattr(litellm, "drop_params", False) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"temperature": 0.3, "top_p": 0.9, "top_k": 40}, + model="vertex_ai/claude-opus-4-7", + drop_params=True, + ) + + assert result == {} + + +def test_sampling_params_kept_for_supporting_model(monkeypatch): + monkeypatch.setattr(litellm, "drop_params", False) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"temperature": 0.3, "top_p": 0.9, "top_k": 40}, + model="claude-sonnet-4-6", + drop_params=True, + ) + + assert result == {"temperature": 0.3, "top_p": 0.9, "top_k": 40} + + +def test_temperature_1_kept_for_unsupported_model(monkeypatch): + # temperature=1 is the one value these models still accept. + monkeypatch.setattr(litellm, "drop_params", False) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"temperature": 1}, + model="claude-opus-4-7", + drop_params=True, + ) + + assert result == {"temperature": 1} + + +def test_sampling_param_raises_clean_400_without_drop_params(monkeypatch): + monkeypatch.setattr(litellm, "drop_params", False) + with pytest.raises(litellm.utils.UnsupportedParamsError, match="does not support temperature"): + AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"temperature": 0.3}, + model="claude-opus-4-7", + drop_params=False, + )