Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,7 @@
"openai.gpt-oss-120b-1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-fable-5-1",
"anthropic.claude-fable-5",
"anthropic.claude-sonnet-5",
"anthropic.claude-opus-5",
Expand Down
4 changes: 3 additions & 1 deletion litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,9 @@ def map_openai_params(
)

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
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structured output still forces tool use

Medium Severity

The new forced-tool gate only wraps caller tool_choice. Tool-based response_format still writes {type: "tool"} without that check. Azure and Vertex claude-fable-5-1 entries also omit supports_native_structured_output (present on the Anthropic and Bedrock rows), and Vertex/Invoke stub the model name for response_format, so structured-output calls on those routes still take the forced-tool path and 400.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d6005a1. Configure here.

elif param == "stream" and value is True:
optional_params["stream"] = value
elif param == "stop" and (isinstance(value, str) or isinstance(value, list)):
Expand Down
40 changes: 40 additions & 0 deletions litellm/llms/anthropic/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@
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 "
Expand Down Expand Up @@ -320,6 +325,41 @@ def _apply_sampling_param(
status_code=400,
)

@staticmethod
def forced_tool_use_downgraded(model: str, drop_params: bool) -> bool:
"""True when the model map flags the model with
``supports_forced_tool_use: false`` (Fable 5.1 / Mythos 5.1 400 on
``any``/``tool``) and ``drop_params`` asks for the ``auto`` downgrade;
raises a clean client-side 400 for such models without ``drop_params``."""
if AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is not False:
return False
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)
return True

@staticmethod
def _apply_forced_tool_choice(
model: str,
tool_choice: AnthropicMessagesToolChoice,
drop_params: bool,
) -> AnthropicMessagesToolChoice:
if tool_choice["type"] not in ("any", "tool"):
return tool_choice
if not AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params):
return tool_choice
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)
Comment thread
cursor[bot] marked this conversation as resolved.

@staticmethod
def _strip_version_suffix(model: str) -> str:
at: Final = model.rfind("@")
Expand Down
10 changes: 9 additions & 1 deletion litellm/llms/bedrock/chat/converse_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,10 @@ def get_supported_openai_params(self, model: str) -> list[str]:
supported_params.append("context_management")
return supported_params

@staticmethod
def _auto_tool_choice() -> ToolChoiceValuesBlock:
return ToolChoiceValuesBlock(auto={})

def map_tool_choice_values(
self, model: str, tool_choice: str | dict, drop_params: bool
) -> ToolChoiceValuesBlock | None:
Expand All @@ -600,10 +604,14 @@ def map_tool_choice_values(
status_code=400,
)
elif tool_choice == "required":
if AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params):
return self._auto_tool_choice()
return ToolChoiceValuesBlock(any={})
elif tool_choice == "auto":
return ToolChoiceValuesBlock(auto={})
return self._auto_tool_choice()
elif isinstance(tool_choice, dict):
if AnthropicModelInfo.forced_tool_use_downgraded(model, drop_params):
return self._auto_tool_choice()
# only supported for anthropic + mistral models - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html
specific_tool: Final = SpecificToolChoiceBlock(
name=make_valid_bedrock_tool_name(tool_choice.get("function", {}).get("name", ""))
Expand Down
Loading
Loading