"tests/vscode:/vscode.git/clone" did not exist on "d6c49779de32ec9e6b58723547e73b2bb086f534"
Unverified Commit 8317cedc authored by Flora Feng's avatar Flora Feng Committed by GitHub
Browse files

[Responses] Add tool_choice/tools validation to match OpenAI behavior (#40399)


Signed-off-by: default avatarsfeng33 <4florafeng@gmail.com>
Co-authored-by: default avatarmergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent 98a242ff
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
from pydantic import ValidationError
from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
SAMPLE_TOOL = {
"type": "function",
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string", "description": "City name"}},
"required": ["location"],
},
}
NAMED_TOOL_CHOICE = {
"type": "function",
"name": "get_weather",
}
def test_responses_request_with_no_tools():
# tools key is not present — defaults tool_choice to "none"
request = ResponsesRequest.model_validate({"input": "Hello", "model": "test-model"})
assert request.tool_choice == "none"
# tools key present but empty
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tools": []}
)
assert request.tool_choice == "none"
def test_responses_request_no_tools_tool_choice_none():
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tool_choice": "none"}
)
assert request.tool_choice == "none"
def test_responses_request_no_tools_tool_choice_auto():
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tool_choice": "auto"}
)
assert request.tool_choice == "none"
@pytest.mark.parametrize("tools", [None, []])
def test_responses_request_required_without_tools(tools):
kwargs = {"input": "Hello", "model": "test-model", "tool_choice": "required"}
if tools is not None:
kwargs["tools"] = tools
with pytest.raises(
ValidationError, match="Tool choice 'required' must be specified"
):
ResponsesRequest.model_validate(kwargs)
def test_responses_request_named_tool_choice_without_tools():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tool_choice": NAMED_TOOL_CHOICE,
}
)
def test_responses_request_with_tools_default_tool_choice():
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tools": [SAMPLE_TOOL]}
)
assert request.tool_choice == "auto"
def test_responses_request_with_tools_tool_choice_none():
request = ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": "none",
}
)
assert request.tool_choice == "none"
def test_responses_request_named_tool_choice_matching():
request = ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": NAMED_TOOL_CHOICE,
}
)
assert request.tool_choice.type == "function"
assert request.tool_choice.name == "get_weather"
def test_responses_request_named_tool_choice_not_matching():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": {"type": "function", "name": "nonexistent"},
}
)
def test_responses_request_with_tools_tool_choice_auto():
request = ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": "auto",
}
)
assert request.tool_choice == "auto"
def test_responses_request_with_tools_tool_choice_required():
request = ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": "required",
}
)
assert request.tool_choice == "required"
def test_responses_request_empty_tools_tool_choice_none():
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tools": [], "tool_choice": "none"}
)
assert request.tool_choice == "none"
def test_responses_request_empty_tools_tool_choice_auto():
request = ResponsesRequest.model_validate(
{"input": "Hello", "model": "test-model", "tools": [], "tool_choice": "auto"}
)
assert request.tool_choice == "none"
@pytest.mark.parametrize(
"tool_choice",
[
{"type": "function"},
{"type": "function", "name": ""},
],
)
def test_responses_request_named_tool_choice_missing_name(tool_choice):
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [SAMPLE_TOOL],
"tool_choice": tool_choice,
}
)
def test_responses_request_empty_tools_named_tool_choice():
with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
ResponsesRequest.model_validate(
{
"input": "Hello",
"model": "test-model",
"tools": [],
"tool_choice": NAMED_TOOL_CHOICE,
}
)
......@@ -492,6 +492,46 @@ class ResponsesRequest(OpenAIBaseModel):
data["input"] = processed_input
return data
@model_validator(mode="before")
@classmethod
def check_tool_usage(cls, data):
if not isinstance(data, dict):
return data
tools = data.get("tools")
tool_choice = data.get("tool_choice", "auto")
has_tools = tools is not None and len(tools) > 0
is_named_tool_choice = (
isinstance(tool_choice, dict) and tool_choice.get("type") == "function"
)
if not has_tools:
if tool_choice in ("auto", "none"):
data["tool_choice"] = "none"
elif tool_choice == "required":
raise VLLMValidationError(
"Tool choice 'required' must be specified with 'tools' parameter.",
parameter="tool_choice",
)
elif is_named_tool_choice:
raise VLLMValidationError(
"Tool choice 'function' not found in 'tools' parameter.",
parameter="tool_choice",
)
elif is_named_tool_choice and tools is not None:
tool_name = tool_choice.get("name")
tool_names = {
t.get("name") if isinstance(t, dict) else getattr(t, "name", None)
for t in tools
}
if not tool_name or tool_name not in tool_names:
raise VLLMValidationError(
"Tool choice 'function' not found in 'tools' parameter.",
parameter="tool_choice",
)
return data
class ResponsesResponse(OpenAIBaseModel):
id: str = Field(default_factory=lambda: f"resp_{random_uuid()}")
......
......@@ -718,9 +718,10 @@ class OpenAIServingResponses(OpenAIServing):
request: ResponsesRequest,
prev_response: ResponsesResponse | None,
):
if request.tool_choice != "auto":
if request.tool_choice not in ("auto", "none"):
raise NotImplementedError(
"Only 'auto' tool_choice is supported in response API with Harmony"
"Only 'auto' or 'none' tool_choice is supported "
"in response API with Harmony"
)
arrival_time = time.time()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment