fix(anthropic_messages): gate sampling params on /v1/messages like /chat/completions (#37868)

* 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>
This commit is contained in:
tin-berri 2026-08-21 18:23:25 -07:00 committed by GitHub
parent 7cb100af63
commit 770bd40f5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 0 deletions

View file

@ -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)

View file

@ -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,
)