This commit is contained in:
Mrunmayee Daware 2026-10-19 21:11:26 +00:00 • committed by GitHub
commit ddb88fbcc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 105 additions and 0 deletions

View file

@ -678,6 +678,21 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
if transformed_context_management is not None:
anthropic_messages_optional_request_params["context_management"] = transformed_context_management
# Downgrade a forced tool_choice for models flagged supports_forced_tool_use: false
# (e.g. claude-opus-5-5), matching the chat path's AnthropicConfig.map_openai_params;
# otherwise /v1/messages 400s where /chat/completions downgrades to auto (or raises
# the clean UnsupportedParamsError without drop_params).
tool_choice_param: Final = anthropic_messages_optional_request_params.get("tool_choice") # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType] # the request params arrive as a bare dict
if isinstance(tool_choice_param, dict) and tool_choice_param.get("type") in ("any", "tool"): # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
gated_tool_choice: Final = AnthropicModelInfo._apply_forced_tool_choice( # pyright: ignore[reportPrivateUsage] # the same gate the chat path applies from AnthropicConfig
model=model,
tool_choice=tool_choice_param, # pyright: ignore[reportArgumentType] # the request params arrive as a bare dict
drop_params=litellm_params.drop_params is True,
)
anthropic_messages_optional_request_params["tool_choice"] = ( # rebind-ok: write the gated value back
gated_tool_choice
)
####### get required params for all anthropic messages requests ######
# Lazy %s: the f-string previously stringified the entire messages
# payload on every request regardless of log level (a full scan of the

View file

@ -300,3 +300,93 @@ async def test_a_json_provider_gets_safeguards_and_the_beta_together_unless_drop
assert sent.headers["anthropic-beta"] == forwarded_beta
assert "safeguard_results" not in response
assert response["content"][0]["text"] == "hello"
def _tool_choice_request(
model: str,
tool_choice: Mapping[str, object],
litellm_params: GenericLiteLLMParams | None = None,
) -> Mapping[str, object]:
return AnthropicMessagesConfig().transform_anthropic_messages_request( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # transform returns an untyped provider dict
model=model,
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
anthropic_messages_optional_request_params={
"max_tokens": 64,
"tools": [
{
"name": "get_weather",
"description": "Get the weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
],
"tool_choice": tool_choice,
},
litellm_params=litellm_params or GenericLiteLLMParams(),
headers={},
)
def test_forced_tool_choice_downgraded_to_auto_with_drop_params(
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
):
"""Regression test for #46074: /v1/messages forwarded a forced tool_choice verbatim to
models flagged supports_forced_tool_use: false (Opus/Sonnet 5.5) and 400ed, while the
chat path downgraded it. The pass-through now applies the same gate."""
monkeypatch.setattr(litellm, "drop_params", False)
result: Final = _tool_choice_request(
model="claude-opus-5-5",
tool_choice={"type": "tool", "name": "get_weather"},
litellm_params=GenericLiteLLMParams(drop_params=True),
)
assert result["tool_choice"] == {"type": "auto"}
def test_forced_tool_choice_downgrade_keeps_disable_parallel_tool_use(
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setattr(litellm, "drop_params", False)
result: Final = _tool_choice_request(
model="claude-opus-5-5",
tool_choice={"type": "any", "disable_parallel_tool_use": True},
litellm_params=GenericLiteLLMParams(drop_params=True),
)
assert result["tool_choice"] == {"type": "auto", "disable_parallel_tool_use": True}
def test_forced_tool_choice_raises_clean_error_without_drop_params(
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setattr(litellm, "drop_params", False)
with pytest.raises(litellm.utils.UnsupportedParamsError, match="forced tool use"):
_tool_choice_request(
model="claude-opus-5-5",
tool_choice={"type": "tool", "name": "get_weather"},
)
def test_forced_tool_choice_forwarded_on_models_that_support_it(
local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
):
monkeypatch.setattr(litellm, "drop_params", False)
tool_choice: Final[Mapping[str, object]] = {"type": "tool", "name": "get_weather"}
result: Final = _tool_choice_request(model="claude-sonnet-4-6", tool_choice=tool_choice)
assert result["tool_choice"] == tool_choice
def test_non_forced_tool_choice_forwarded_on_opus_5_5(local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(litellm, "drop_params", False)
result: Final = _tool_choice_request(model="claude-opus-5-5", tool_choice={"type": "auto"})
assert result["tool_choice"] == {"type": "auto"}