mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-26 06:03:19 +00:00
fix(anthropic): apply the forced tool_choice gate on the /v1/messages pass-through
Models flagged supports_forced_tool_use: false (claude-opus-5-5, claude-sonnet-5-5, claude-fable-5-1) reject tool_choice type 'any'/'tool' with a 400. The chat path already downgrades a forced tool_choice to 'auto' under drop_params (or raises a clean client-side error without it) via AnthropicConfig.map_openai_params, but the /v1/messages pass-through forwarded the forced value verbatim, so the same request 400ed on /v1/messages while succeeding on /chat/completions. Reuse the shared AnthropicModelInfo._apply_forced_tool_choice gate in transform_anthropic_messages_request so both paths behave identically: downgrade to 'auto' (keeping disable_parallel_tool_use) when dropping params, raise the clean UnsupportedParamsError otherwise, and leave supporting models untouched. Fixes #46074
This commit is contained in:
parent
4bebcdf757
commit
fd2301fdc0
2 changed files with 105 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue