From a7594196cdf31eaf4715510ad62f75196d970ce3 Mon Sep 17 00:00:00 2001 From: Cole McIntosh <82463175+colesmcintosh@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:39:58 -0600 Subject: [PATCH] fix: support Cursor IDE tool_choice format {"type": "auto"} (#12168) * fix: support Cursor IDE tool_choice format {"type": "auto"} - Update validate_chat_completion_tool_choice to normalize {"type": "auto"} to "auto" - Handles Cursor IDE sending non-standard tool_choice format - Add comprehensive tests for tool choice validation Fixes #12098 * fix: return full tool_choice object for Cursor IDE format Based on PR feedback, updated validate_chat_completion_tool_choice to return the full tool_choice dictionary instead of just extracting the type string. This maintains consistency with downstream code that expects the full object structure. - Changed behavior: {"type": "auto"} now returns {"type": "auto"} instead of "auto" - Updated tests to reflect the new expected behavior - Ensures compatibility with code that passes tool_choice to optional_params Addresses feedback from PR #12168 --- litellm/utils.py | 8 +++ .../test_validate_tool_choice.py | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/litellm_utils_tests/test_validate_tool_choice.py diff --git a/litellm/utils.py b/litellm/utils.py index 43f2b6c3f9f..621221be1bb 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6571,6 +6571,14 @@ def validate_chat_completion_tool_choice( elif isinstance(tool_choice, str): return tool_choice elif isinstance(tool_choice, dict): + # Handle Cursor IDE format: {"type": "auto"} -> return as-is + if ( + tool_choice.get("type") in ["auto", "none", "required"] + and "function" not in tool_choice + ): + return tool_choice + + # Standard OpenAI format: {"type": "function", "function": {...}} if tool_choice.get("type") is None or tool_choice.get("function") is None: raise Exception( f"Invalid tool choice, tool_choice={tool_choice}. Please ensure tool_choice follows the OpenAI spec" diff --git a/tests/litellm_utils_tests/test_validate_tool_choice.py b/tests/litellm_utils_tests/test_validate_tool_choice.py new file mode 100644 index 00000000000..00f82712aa2 --- /dev/null +++ b/tests/litellm_utils_tests/test_validate_tool_choice.py @@ -0,0 +1,63 @@ +import pytest +import sys +import os + +sys.path.insert(0, os.path.abspath("../..")) + +from litellm.utils import validate_chat_completion_tool_choice + + +def test_validate_tool_choice_none(): + """Test that None is returned as-is.""" + result = validate_chat_completion_tool_choice(None) + assert result is None + + +def test_validate_tool_choice_string(): + """Test that string values are returned as-is.""" + assert validate_chat_completion_tool_choice("auto") == "auto" + assert validate_chat_completion_tool_choice("none") == "none" + assert validate_chat_completion_tool_choice("required") == "required" + + +def test_validate_tool_choice_standard_dict(): + """Test standard OpenAI format with function.""" + tool_choice = {"type": "function", "function": {"name": "my_function"}} + result = validate_chat_completion_tool_choice(tool_choice) + assert result == tool_choice + + +def test_validate_tool_choice_cursor_format(): + """Test Cursor IDE format: {"type": "auto"} -> {"type": "auto"}.""" + assert validate_chat_completion_tool_choice({"type": "auto"}) == {"type": "auto"} + assert validate_chat_completion_tool_choice({"type": "none"}) == {"type": "none"} + assert validate_chat_completion_tool_choice({"type": "required"}) == {"type": "required"} + + +def test_validate_tool_choice_invalid_dict(): + """Test that invalid dict formats raise exceptions.""" + # Missing both type and function + with pytest.raises(Exception) as exc_info: + validate_chat_completion_tool_choice({}) + assert "Invalid tool choice" in str(exc_info.value) + + # Invalid type value + with pytest.raises(Exception) as exc_info: + validate_chat_completion_tool_choice({"type": "invalid"}) + assert "Invalid tool choice" in str(exc_info.value) + + # Has type but missing function when type is "function" + with pytest.raises(Exception) as exc_info: + validate_chat_completion_tool_choice({"type": "function"}) + assert "Invalid tool choice" in str(exc_info.value) + + +def test_validate_tool_choice_invalid_type(): + """Test that invalid types raise exceptions.""" + with pytest.raises(Exception) as exc_info: + validate_chat_completion_tool_choice(123) + assert "Got=" in str(exc_info.value) + + with pytest.raises(Exception) as exc_info: + validate_chat_completion_tool_choice([]) + assert "Got=" in str(exc_info.value) \ No newline at end of file