diff --git a/litellm/utils.py b/litellm/utils.py index a7d7447d7f3..14d53a6a327 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8340,6 +8340,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", [