litellm/tests/litellm_utils_tests/test_validate_tool_choice.py
Scott Wilson 80d8e95228 fix(responses): unwrap object-form tool_choice before calling the Responses API
Clients send tool_choice as {"type": "auto"} (Cursor on chat completions,
Claude Code's Anthropic tool_choice shape). validate_chat_completion_tool_choice
recognized that shape but returned it verbatim, and the chat -> Responses API
bridge only normalized {"type": "function"}, so the wrapper reached OpenAI and
the whole call failed with:

  Invalid value: 'auto'. Supported values are: 'code_interpreter', ...,
  'web_search_preview', ... (param: tool_choice.type)

That broke every tool call, web search included, on responses-mode models.

Unwrap {"type": "auto"|"none"|"required"} to the bare string at both layers:
the chat completions validation boundary where the shape is first accepted,
and the Responses API bridge that owns the Responses tool_choice contract.
No OpenAI surface accepts the object form for these values, so the previous
passthrough only deferred the 400 to the provider.
2026-08-05 22:41:18 -04:00

68 lines
2.5 KiB
Python

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():
"""Cursor IDE format {"type": "auto"} must be unwrapped to the bare string.
No OpenAI surface accepts the object form of these values. Forwarding it
verbatim makes the provider reject the call with
"Invalid value: 'auto' ... param: tool_choice.type".
"""
assert validate_chat_completion_tool_choice({"type": "auto"}) == "auto"
assert validate_chat_completion_tool_choice({"type": "none"}) == "none"
assert validate_chat_completion_tool_choice({"type": "required"}) == "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=<class 'int'>" in str(exc_info.value)
with pytest.raises(Exception) as exc_info:
validate_chat_completion_tool_choice([])
assert "Got=<class 'list'>" in str(exc_info.value)