From d2831ab70901de885e57a084581b916c86a56bf6 Mon Sep 17 00:00:00 2001 From: ege-arhan Date: Fri, 25 Sep 2026 06:04:11 +0000 Subject: [PATCH] fix(utils): pass allowed_tools tool_choice through to providers Chat completions rejected the allowed_tools shape before it reached the provider, even though OpenAI accepts it. Keep the nested payload intact. --- litellm/utils.py | 6 ++++++ .../test_validate_tool_choice.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/litellm/utils.py b/litellm/utils.py index 4ea0769ea11..b751b9c0e1d 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8328,6 +8328,12 @@ def validate_chat_completion_tool_choice( if tool_choice_type in ("auto", "none", "required") and "function" not in tool_choice: return tool_choice_type + # OpenAI also allows constraining which tools the model may call: + # {"type": "allowed_tools", "allowed_tools": {"mode": ..., "tools": [...]}}. + # Pass it through untouched so downstream providers see the full payload. + if tool_choice_type == "allowed_tools" and isinstance(tool_choice.get("allowed_tools"), dict): + return tool_choice + # Standard OpenAI format: {"type": "function", "function": {...}} if tool_choice.get("type") is None or tool_choice.get("function") is None: raise BadRequestError( diff --git a/tests/litellm_utils_tests/test_validate_tool_choice.py b/tests/litellm_utils_tests/test_validate_tool_choice.py index a9dacf9fa15..a28aee87c48 100644 --- a/tests/litellm_utils_tests/test_validate_tool_choice.py +++ b/tests/litellm_utils_tests/test_validate_tool_choice.py @@ -36,6 +36,25 @@ def test_validate_tool_choice_cursor_format(): assert validate_chat_completion_tool_choice({"type": "required"}, model=MODEL) == "required" +def test_validate_tool_choice_allowed_tools_passthrough(): + """Chat completions accepts the allowed_tools shape and keeps its payload intact.""" + tool_choice = { + "type": "allowed_tools", + "allowed_tools": { + "mode": "required", + "tools": [{"type": "function", "function": {"name": "get_time"}}], + }, + } + assert validate_chat_completion_tool_choice(tool_choice, model=MODEL) is tool_choice + + +def test_validate_tool_choice_allowed_tools_without_payload_is_a_400(): + """allowed_tools without its nested object stays a caller error.""" + tool_choice = {"type": "allowed_tools"} + with pytest.raises(litellm.BadRequestError, match="Invalid tool choice"): + validate_chat_completion_tool_choice(tool_choice, model=MODEL) + + @pytest.mark.parametrize( "tool_choice", [