@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 8317cedc77
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 4, 18:45 UTC

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

Signed-off-by: sfeng33 <4florafeng@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>

Flora Feng committed Apr 23, 2026 at 01:46 UTC 8317cedc7718301866963400646b1d624a6471ea
3 files changed +227 -2
tests/tool_use/test_responses_request_validations.py new
+184
@@ -0,0 +1,184 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import pytest
5 +from pydantic import ValidationError
6 +
7 +from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
8 +
9 +SAMPLE_TOOL = {
10 + "type": "function",
11 + "name": "get_weather",
12 + "description": "Get current weather",
13 + "parameters": {
14 + "type": "object",
15 + "properties": {"location": {"type": "string", "description": "City name"}},
16 + "required": ["location"],
17 + },
18 +}
19 +
20 +NAMED_TOOL_CHOICE = {
21 + "type": "function",
22 + "name": "get_weather",
23 +}
24 +
25 +
26 +def test_responses_request_with_no_tools():
27 + # tools key is not present — defaults tool_choice to "none"
28 + request = ResponsesRequest.model_validate({"input": "Hello", "model": "test-model"})
29 + assert request.tool_choice == "none"
30 +
31 + # tools key present but empty
32 + request = ResponsesRequest.model_validate(
33 + {"input": "Hello", "model": "test-model", "tools": []}
34 + )
35 + assert request.tool_choice == "none"
36 +
37 +
38 +def test_responses_request_no_tools_tool_choice_none():
39 + request = ResponsesRequest.model_validate(
40 + {"input": "Hello", "model": "test-model", "tool_choice": "none"}
41 + )
42 + assert request.tool_choice == "none"
43 +
44 +
45 +def test_responses_request_no_tools_tool_choice_auto():
46 + request = ResponsesRequest.model_validate(
47 + {"input": "Hello", "model": "test-model", "tool_choice": "auto"}
48 + )
49 + assert request.tool_choice == "none"
50 +
51 +
52 +@pytest.mark.parametrize("tools", [None, []])
53 +def test_responses_request_required_without_tools(tools):
54 + kwargs = {"input": "Hello", "model": "test-model", "tool_choice": "required"}
55 + if tools is not None:
56 + kwargs["tools"] = tools
57 + with pytest.raises(
58 + ValidationError, match="Tool choice 'required' must be specified"
59 + ):
60 + ResponsesRequest.model_validate(kwargs)
61 +
62 +
63 +def test_responses_request_named_tool_choice_without_tools():
64 + with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
65 + ResponsesRequest.model_validate(
66 + {
67 + "input": "Hello",
68 + "model": "test-model",
69 + "tool_choice": NAMED_TOOL_CHOICE,
70 + }
71 + )
72 +
73 +
74 +def test_responses_request_with_tools_default_tool_choice():
75 + request = ResponsesRequest.model_validate(
76 + {"input": "Hello", "model": "test-model", "tools": [SAMPLE_TOOL]}
77 + )
78 + assert request.tool_choice == "auto"
79 +
80 +
81 +def test_responses_request_with_tools_tool_choice_none():
82 + request = ResponsesRequest.model_validate(
83 + {
84 + "input": "Hello",
85 + "model": "test-model",
86 + "tools": [SAMPLE_TOOL],
87 + "tool_choice": "none",
88 + }
89 + )
90 + assert request.tool_choice == "none"
91 +
92 +
93 +def test_responses_request_named_tool_choice_matching():
94 + request = ResponsesRequest.model_validate(
95 + {
96 + "input": "Hello",
97 + "model": "test-model",
98 + "tools": [SAMPLE_TOOL],
99 + "tool_choice": NAMED_TOOL_CHOICE,
100 + }
101 + )
102 + assert request.tool_choice.type == "function"
103 + assert request.tool_choice.name == "get_weather"
104 +
105 +
106 +def test_responses_request_named_tool_choice_not_matching():
107 + with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
108 + ResponsesRequest.model_validate(
109 + {
110 + "input": "Hello",
111 + "model": "test-model",
112 + "tools": [SAMPLE_TOOL],
113 + "tool_choice": {"type": "function", "name": "nonexistent"},
114 + }
115 + )
116 +
117 +
118 +def test_responses_request_with_tools_tool_choice_auto():
119 + request = ResponsesRequest.model_validate(
120 + {
121 + "input": "Hello",
122 + "model": "test-model",
123 + "tools": [SAMPLE_TOOL],
124 + "tool_choice": "auto",
125 + }
126 + )
127 + assert request.tool_choice == "auto"
128 +
129 +
130 +def test_responses_request_with_tools_tool_choice_required():
131 + request = ResponsesRequest.model_validate(
132 + {
133 + "input": "Hello",
134 + "model": "test-model",
135 + "tools": [SAMPLE_TOOL],
136 + "tool_choice": "required",
137 + }
138 + )
139 + assert request.tool_choice == "required"
140 +
141 +
142 +def test_responses_request_empty_tools_tool_choice_none():
143 + request = ResponsesRequest.model_validate(
144 + {"input": "Hello", "model": "test-model", "tools": [], "tool_choice": "none"}
145 + )
146 + assert request.tool_choice == "none"
147 +
148 +
149 +def test_responses_request_empty_tools_tool_choice_auto():
150 + request = ResponsesRequest.model_validate(
151 + {"input": "Hello", "model": "test-model", "tools": [], "tool_choice": "auto"}
152 + )
153 + assert request.tool_choice == "none"
154 +
155 +
156 +@pytest.mark.parametrize(
157 + "tool_choice",
158 + [
159 + {"type": "function"},
160 + {"type": "function", "name": ""},
161 + ],
162 +)
163 +def test_responses_request_named_tool_choice_missing_name(tool_choice):
164 + with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
165 + ResponsesRequest.model_validate(
166 + {
167 + "input": "Hello",
168 + "model": "test-model",
169 + "tools": [SAMPLE_TOOL],
170 + "tool_choice": tool_choice,
171 + }
172 + )
173 +
174 +
175 +def test_responses_request_empty_tools_named_tool_choice():
176 + with pytest.raises(ValidationError, match="not found in 'tools' parameter"):
177 + ResponsesRequest.model_validate(
178 + {
179 + "input": "Hello",
180 + "model": "test-model",
181 + "tools": [],
182 + "tool_choice": NAMED_TOOL_CHOICE,
183 + }
184 + )
vllm/entrypoints/openai/responses/protocol.py
+40
@@ -492,6 +492,46 @@ class ResponsesRequest(OpenAIBaseModel):
492 data["input"] = processed_input
493 return data
494
495 + @model_validator(mode="before")
496 + @classmethod
497 + def check_tool_usage(cls, data):
498 + if not isinstance(data, dict):
499 + return data
500 +
501 + tools = data.get("tools")
502 + tool_choice = data.get("tool_choice", "auto")
503 + has_tools = tools is not None and len(tools) > 0
504 + is_named_tool_choice = (
505 + isinstance(tool_choice, dict) and tool_choice.get("type") == "function"
506 + )
507 +
508 + if not has_tools:
509 + if tool_choice in ("auto", "none"):
510 + data["tool_choice"] = "none"
511 + elif tool_choice == "required":
512 + raise VLLMValidationError(
513 + "Tool choice 'required' must be specified with 'tools' parameter.",
514 + parameter="tool_choice",
515 + )
516 + elif is_named_tool_choice:
517 + raise VLLMValidationError(
518 + "Tool choice 'function' not found in 'tools' parameter.",
519 + parameter="tool_choice",
520 + )
521 + elif is_named_tool_choice and tools is not None:
522 + tool_name = tool_choice.get("name")
523 + tool_names = {
524 + t.get("name") if isinstance(t, dict) else getattr(t, "name", None)
525 + for t in tools
526 + }
527 + if not tool_name or tool_name not in tool_names:
528 + raise VLLMValidationError(
529 + "Tool choice 'function' not found in 'tools' parameter.",
530 + parameter="tool_choice",
531 + )
532 +
533 + return data
534 +
535
536 class ResponsesResponse(OpenAIBaseModel):
537 id: str = Field(default_factory=lambda: f"resp_{random_uuid()}")
vllm/entrypoints/openai/responses/serving.py
+3 -2
@@ -718,9 +718,10 @@ class OpenAIServingResponses(OpenAIServing):
718 request: ResponsesRequest,
719 prev_response: ResponsesResponse | None,
720 ):
721 - if request.tool_choice != "auto":
721 + if request.tool_choice not in ("auto", "none"):
722 raise NotImplementedError(
723 - "Only 'auto' tool_choice is supported in response API with Harmony"
723 + "Only 'auto' or 'none' tool_choice is supported "
724 + "in response API with Harmony"
725 )
726
727 arrival_time = time.time()