mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
Merge pull request #41234 from BerriAI/litellm_invalid_tool_choice_400
fix(utils): reject an untranslatable tool_choice with a 400 instead of a 500
This commit is contained in:
commit
c39ec34553
6 changed files with 91 additions and 41 deletions
|
|
@ -5108,7 +5108,7 @@ def completion(
|
|||
messages = validate_and_fix_openai_messages(messages=messages)
|
||||
tools = validate_and_fix_openai_tools(tools=tools)
|
||||
# validate tool_choice
|
||||
tool_choice = validate_chat_completion_tool_choice(tool_choice=tool_choice)
|
||||
tool_choice = validate_chat_completion_tool_choice(tool_choice=tool_choice, model=model)
|
||||
# validate optional params
|
||||
stop = validate_openai_optional_params(stop=stop)
|
||||
thinking = validate_and_fix_thinking_param(thinking=thinking)
|
||||
|
|
|
|||
|
|
@ -8110,6 +8110,7 @@ def validate_chat_completion_user_messages(messages: list[AllMessageValues]):
|
|||
|
||||
def validate_chat_completion_tool_choice(
|
||||
tool_choice: dict | str | None,
|
||||
model: str = "",
|
||||
) -> dict | str | None:
|
||||
"""
|
||||
Confirm the tool choice is passed in the OpenAI format.
|
||||
|
|
@ -8125,12 +8126,19 @@ def validate_chat_completion_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"
|
||||
raise BadRequestError(
|
||||
message=f"Invalid tool choice, tool_choice={tool_choice}. Please ensure tool_choice follows the OpenAI spec",
|
||||
model=model,
|
||||
llm_provider="",
|
||||
)
|
||||
return tool_choice
|
||||
raise Exception(
|
||||
f"Invalid tool choice, tool_choice={tool_choice}. Got={type(tool_choice)}. Expecting str, or dict. Please ensure tool_choice follows the OpenAI tool_choice spec"
|
||||
raise BadRequestError(
|
||||
message=(
|
||||
f"Invalid tool choice, tool_choice={tool_choice}. Got={type(tool_choice)}. Expecting str, or dict. "
|
||||
"Please ensure tool_choice follows the OpenAI tool_choice spec"
|
||||
),
|
||||
model=model,
|
||||
llm_provider="",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1180,10 +1180,10 @@ def test_validate_chat_completion_tool_choice(tool_choice, expected_bool):
|
|||
from litellm.utils import validate_chat_completion_tool_choice
|
||||
|
||||
if expected_bool:
|
||||
validate_chat_completion_tool_choice(tool_choice=tool_choice)
|
||||
validate_chat_completion_tool_choice(tool_choice=tool_choice, model="gpt-5.6-sol")
|
||||
else:
|
||||
with pytest.raises(Exception, match="Invalid tool choice"):
|
||||
validate_chat_completion_tool_choice(tool_choice=tool_choice)
|
||||
with pytest.raises(litellm.BadRequestError, match="Invalid tool choice"):
|
||||
validate_chat_completion_tool_choice(tool_choice=tool_choice, model="gpt-5.6-sol")
|
||||
|
||||
|
||||
def test_models_by_provider():
|
||||
|
|
|
|||
|
|
@ -1,60 +1,74 @@
|
|||
import re
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
import litellm
|
||||
from litellm.utils import validate_chat_completion_tool_choice
|
||||
|
||||
MODEL: Final = "anthropic/claude-haiku-4-5"
|
||||
|
||||
|
||||
def test_validate_tool_choice_none():
|
||||
"""Test that None is returned as-is."""
|
||||
result = validate_chat_completion_tool_choice(None)
|
||||
result = validate_chat_completion_tool_choice(None, model=MODEL)
|
||||
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"
|
||||
assert validate_chat_completion_tool_choice("auto", model=MODEL) == "auto"
|
||||
assert validate_chat_completion_tool_choice("none", model=MODEL) == "none"
|
||||
assert validate_chat_completion_tool_choice("required", model=MODEL) == "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)
|
||||
result = validate_chat_completion_tool_choice(tool_choice, model=MODEL)
|
||||
assert result == tool_choice
|
||||
|
||||
|
||||
def test_validate_tool_choice_cursor_format():
|
||||
"""Cursor IDE format {"type": "auto"} is unwrapped to the bare string."""
|
||||
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"
|
||||
assert validate_chat_completion_tool_choice({"type": "auto"}, model=MODEL) == "auto"
|
||||
assert validate_chat_completion_tool_choice({"type": "none"}, model=MODEL) == "none"
|
||||
assert validate_chat_completion_tool_choice({"type": "required"}, model=MODEL) == "required"
|
||||
|
||||
|
||||
def test_validate_tool_choice_invalid_dict():
|
||||
"""Test that invalid dict formats raise exceptions."""
|
||||
# Missing both type and function
|
||||
with pytest.raises(Exception, match='Invalid tool choice, tool_choice=\\{\\}\\. Please ensure') as exc_info:
|
||||
validate_chat_completion_tool_choice({})
|
||||
assert "Invalid tool choice" in str(exc_info.value)
|
||||
|
||||
# Invalid type value
|
||||
with pytest.raises(Exception, match="Invalid tool choice, tool_choice=\\{'type': 'invalid'\\}\\.") 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, match="Invalid tool choice, tool_choice=\\{'type': 'function'\\}\\.") as exc_info:
|
||||
validate_chat_completion_tool_choice({"type": "function"})
|
||||
assert "Invalid tool choice" in str(exc_info.value)
|
||||
@pytest.mark.parametrize(
|
||||
"tool_choice",
|
||||
[
|
||||
{},
|
||||
{"type": "invalid"},
|
||||
{"type": "function"},
|
||||
{"name": "lookup_fruit"},
|
||||
{"type": "file_search"},
|
||||
],
|
||||
)
|
||||
def test_validate_tool_choice_invalid_dict_is_a_400(tool_choice):
|
||||
"""A dict shape chat completions cannot carry is the caller's mistake: a 400 that names the field, never a 500."""
|
||||
with pytest.raises(
|
||||
litellm.BadRequestError, match=f"Invalid tool choice, tool_choice={re.escape(str(tool_choice))}\\. Please ensure"
|
||||
) as exc_info:
|
||||
validate_chat_completion_tool_choice(tool_choice, model=MODEL)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.model == MODEL
|
||||
|
||||
|
||||
def test_validate_tool_choice_invalid_type():
|
||||
"""Test that invalid types raise exceptions."""
|
||||
with pytest.raises(Exception, match="<class 'int'>\\. Expecting str, or dict\\. Please ensure") as exc_info:
|
||||
validate_chat_completion_tool_choice(123)
|
||||
assert "Got=<class 'int'>" in str(exc_info.value)
|
||||
@pytest.mark.parametrize("tool_choice", [123, []])
|
||||
def test_validate_tool_choice_invalid_type_is_a_400(tool_choice):
|
||||
"""A non-str, non-dict tool_choice is rejected as a 400 that names the type it got."""
|
||||
with pytest.raises(
|
||||
litellm.BadRequestError, match=f"Got={re.escape(str(type(tool_choice)))}\\. Expecting str, or dict\\."
|
||||
) as exc_info:
|
||||
validate_chat_completion_tool_choice(tool_choice, model=MODEL)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
with pytest.raises(Exception, match="Invalid tool choice, tool_choice=\\[\\]\\. Got=<class 'list'>\\.") as exc_info:
|
||||
validate_chat_completion_tool_choice([])
|
||||
assert "Got=<class 'list'>" in str(exc_info.value)
|
||||
|
||||
def test_validate_tool_choice_without_model_is_still_a_400():
|
||||
"""Callers that predate the model argument keep getting a 400, with an empty model on the error."""
|
||||
with pytest.raises(litellm.BadRequestError, match="Invalid tool choice") as exc_info:
|
||||
validate_chat_completion_tool_choice({"type": "bogus"})
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.model == ""
|
||||
|
|
|
|||
|
|
@ -5033,3 +5033,17 @@ def test_transform_chat_completion_response_incomplete_details():
|
|||
assert result_existing.status == "incomplete"
|
||||
assert result_existing.incomplete_details == existing_details
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", [True, False])
|
||||
async def test_bridge_rejects_untranslatable_tool_choice_with_a_400(stream: bool):
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
await litellm.aresponses(
|
||||
model="anthropic/claude-haiku-4-5",
|
||||
input="Which fruit is red?",
|
||||
tools=[{"type": "function", "name": "lookup_fruit", "parameters": {"type": "object"}}],
|
||||
tool_choice={"type": "file_search"},
|
||||
stream=stream,
|
||||
api_key="sk-unused",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "tool_choice={'type': 'file_search'}" in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -3967,3 +3967,17 @@ def test_aiohttp_openai_warns_only_when_http2_enabled(
|
|||
assert handler_completion.called
|
||||
warned: Final = "aiohttp_openai/ always uses aiohttp" in caplog.text
|
||||
assert warned is http2_on
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_choice", [{"type": "bogus"}, {"name": "lookup_fruit"}, {"type": "file_search"}])
|
||||
def test_completion_rejects_untranslatable_tool_choice_with_a_400(tool_choice):
|
||||
with pytest.raises(litellm.BadRequestError) as exc_info:
|
||||
litellm.completion(
|
||||
model="anthropic/claude-haiku-4-5",
|
||||
messages=[{"role": "user", "content": "Which fruit is red?"}],
|
||||
tools=[{"type": "function", "function": {"name": "lookup_fruit", "parameters": {"type": "object"}}}],
|
||||
tool_choice=tool_choice,
|
||||
api_key="sk-unused",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert f"tool_choice={tool_choice}" in str(exc_info.value)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue