mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
feat(anthropic): gate forced tool_choice for Fable 5.1 behind supports_forced_tool_use
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0610331aa1
commit
3c9ce458fd
6 changed files with 159 additions and 1 deletions
|
|
@ -1466,7 +1466,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
)
|
||||
|
||||
if _tool_choice is not None:
|
||||
optional_params["tool_choice"] = _tool_choice
|
||||
optional_params["tool_choice"] = AnthropicConfig._apply_forced_tool_choice(
|
||||
model=model, tool_choice=_tool_choice, drop_params=drop_params
|
||||
)
|
||||
elif param == "stream" and value is True:
|
||||
optional_params["stream"] = value
|
||||
elif param == "stop" and (isinstance(value, str) or isinstance(value, list)):
|
||||
|
|
|
|||
|
|
@ -28,10 +28,15 @@ from litellm.types.llms.anthropic import (
|
|||
ANTHROPIC_OAUTH_TOKEN_PREFIX,
|
||||
AllAnthropicToolsValues,
|
||||
AnthropicMcpServerTool,
|
||||
AnthropicMessagesToolChoice,
|
||||
)
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.proxy.model_listing import ModelInfoResponse
|
||||
|
||||
DROP_FORCED_TOOL_CHOICE_WARNING: Final = (
|
||||
"Downgrading forced tool_choice to 'auto' for model=%s (drop_params=True): this model rejects tool_choice type "
|
||||
"'any'/'tool' with a 400 because thinking is always on and a forced call would skip it."
|
||||
)
|
||||
DROP_DISABLED_THINKING_WARNING: Final = (
|
||||
"Dropping `thinking={'type': 'disabled'}` for model=%s: thinking is always on for this model and cannot be "
|
||||
"disabled (the alternative is a provider 400). The model will still think adaptively, its response can contain "
|
||||
|
|
@ -320,6 +325,35 @@ class AnthropicModelInfo(BaseLLMModelInfo):
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _apply_forced_tool_choice(
|
||||
model: str,
|
||||
tool_choice: AnthropicMessagesToolChoice,
|
||||
drop_params: bool,
|
||||
) -> AnthropicMessagesToolChoice:
|
||||
"""Forward ``tool_choice`` unless the model map flags the model with
|
||||
``supports_forced_tool_use: false`` (Fable 5.1 / Mythos 5.1 400 on
|
||||
``any``/``tool``), in which case downgrade to ``auto`` (with
|
||||
drop_params) or raise a clean client-side 400."""
|
||||
if tool_choice["type"] not in ("any", "tool"):
|
||||
return tool_choice
|
||||
if AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is not False:
|
||||
return tool_choice
|
||||
if not (litellm.drop_params or drop_params):
|
||||
raise litellm.utils.UnsupportedParamsError(
|
||||
message=(
|
||||
f"{model} does not support forced tool use (tool_choice='required' or a named tool). "
|
||||
"Use tool_choice='auto' and tell the model in the prompt when to call the tool, or set "
|
||||
"`litellm.drop_params = True` to downgrade to 'auto' automatically."
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
litellm.verbose_logger.warning(DROP_FORCED_TOOL_CHOICE_WARNING, model)
|
||||
disable_parallel: Final = tool_choice.get("disable_parallel_tool_use")
|
||||
if disable_parallel is None:
|
||||
return AnthropicMessagesToolChoice(type="auto")
|
||||
return AnthropicMessagesToolChoice(type="auto", disable_parallel_tool_use=disable_parallel)
|
||||
|
||||
@staticmethod
|
||||
def _strip_version_suffix(model: str) -> str:
|
||||
at: Final = model.rfind("@")
|
||||
|
|
|
|||
|
|
@ -1472,6 +1472,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1546,6 +1547,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1620,6 +1622,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1694,6 +1697,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -3248,6 +3252,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -13231,6 +13236,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -42204,6 +42210,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -42273,6 +42280,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
|
|||
|
|
@ -1472,6 +1472,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1546,6 +1547,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1620,6 +1622,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -1694,6 +1697,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -3248,6 +3252,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -13231,6 +13236,7 @@
|
|||
"supports_mid_conversation_system": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -42204,6 +42210,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
@ -42273,6 +42280,7 @@
|
|||
"thinking_always_on": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_computer_use": true,
|
||||
"supports_forced_tool_use": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
|
|
|
|||
|
|
@ -6176,6 +6176,7 @@ def test_is_anthropic_usage_object_rejects_responses_api_usage():
|
|||
[
|
||||
# always-on-thinking models reject thinking.type=disabled with a 400
|
||||
("claude-fable-5", True),
|
||||
("claude-fable-5-1", True),
|
||||
("claude-mythos-5", True),
|
||||
# unmapped future family member -> claude-always-on-thinking fallback rule
|
||||
("claude-fable-6-1", True),
|
||||
|
|
@ -6207,3 +6208,107 @@ def test_disabled_thinking_omitted_only_for_always_on_models(
|
|||
assert "thinking" not in request
|
||||
else:
|
||||
assert request["thinking"] == {"type": "disabled"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
["required", {"type": "required"}, {"type": "function", "function": {"name": "get_weather"}}],
|
||||
)
|
||||
def test_forced_tool_choice_raises_clean_error_on_fable_5_1_without_drop_params(
|
||||
local_model_cost_map, tool_choice, monkeypatch
|
||||
):
|
||||
"""Fable 5.1 400s on tool_choice type any/tool (thinking is always on and a
|
||||
forced call would skip it); without drop_params the caller gets a clean
|
||||
client-side 400 that explains the workaround, not a provider error."""
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
config = AnthropicConfig()
|
||||
|
||||
with pytest.raises(litellm.utils.UnsupportedParamsError, match="forced tool use"):
|
||||
config.map_openai_params(
|
||||
non_default_params={"tool_choice": tool_choice},
|
||||
optional_params={},
|
||||
model="claude-fable-5-1",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
["required", {"type": "required"}, {"type": "function", "function": {"name": "get_weather"}}],
|
||||
)
|
||||
def test_forced_tool_choice_downgraded_to_auto_on_fable_5_1_with_drop_params(
|
||||
local_model_cost_map, tool_choice
|
||||
):
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"tool_choice": tool_choice},
|
||||
optional_params={},
|
||||
model="claude-fable-5-1",
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert result["tool_choice"] == {"type": "auto"}
|
||||
|
||||
|
||||
def test_forced_tool_choice_downgrade_keeps_parallel_tool_calls_flag(local_model_cost_map):
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"tool_choice": "required", "parallel_tool_calls": False},
|
||||
optional_params={},
|
||||
model="claude-fable-5-1",
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert result["tool_choice"] == {"type": "auto", "disable_parallel_tool_use": True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_choice, expected_type", [("auto", "auto"), ("none", "none")])
|
||||
def test_unforced_tool_choice_forwarded_on_fable_5_1(
|
||||
local_model_cost_map, tool_choice, expected_type, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"tool_choice": tool_choice},
|
||||
optional_params={},
|
||||
model="claude-fable-5-1",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["tool_choice"]["type"] == expected_type
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", ["claude-fable-5", "claude-opus-5", "claude-sonnet-5"])
|
||||
def test_forced_tool_choice_forwarded_on_models_that_support_it(
|
||||
local_model_cost_map, model, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(litellm, "drop_params", False)
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"tool_choice": "required"},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert result["tool_choice"] == {"type": "any"}
|
||||
|
||||
|
||||
def test_forced_tool_choice_gating_driven_by_model_map_flag(local_model_cost_map, monkeypatch):
|
||||
"""The gate must read ``supports_forced_tool_use`` from the model map, not
|
||||
the model name: a flagged entry gates a model whose name says nothing."""
|
||||
monkeypatch.setitem(litellm.model_cost, "claude-zeta-9", {"supports_forced_tool_use": False})
|
||||
config = AnthropicConfig()
|
||||
|
||||
result = config.map_openai_params(
|
||||
non_default_params={"tool_choice": "required"},
|
||||
optional_params={},
|
||||
model="claude-zeta-9",
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert result["tool_choice"] == {"type": "auto"}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ def test_fable_5_1_model_pricing_and_capabilities():
|
|||
assert "output_cost_per_token_above_200k_tokens" not in info
|
||||
|
||||
assert info["supports_assistant_prefill"] is False
|
||||
assert info["supports_forced_tool_use"] is False
|
||||
assert info["supports_function_calling"] is True
|
||||
assert info["supports_prompt_caching"] is True
|
||||
assert info["supports_reasoning"] is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue