Skip to content

Commit 07f494e

Browse files
committed
[Bugfix] Add validation for tool requests when tool_parser is unavailable
When `tool_choice` is set to `"required"`, a named tool, or `"auto"`, but the server was not started with `--tool-call-parser`, vLLM would silently degrade instead of returning a clear error. This PR adds validation to return an appropriate error message: - For `tool_choice="auto"` without `--enable-auto-tool-choice`: requires both flags - For `tool_choice="auto"` with `--enable-auto-tool-choice` but no parser: requires `--tool-call-parser` - For `tool_choice="required"` or named tool: requires `--tool-call-parser` Fixes #29432 Signed-off-by: majiayu000 <[email protected]>
1 parent ace34e3 commit 07f494e

File tree

2 files changed

+120
-9
lines changed

2 files changed

+120
-9
lines changed

tests/entrypoints/openai/test_serving_chat.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,3 +1372,95 @@ async def test_non_tool_reasoning_empty_content_list(self, serving_chat):
13721372
},
13731373
],
13741374
)
1375+
1376+
1377+
@pytest.mark.asyncio
1378+
async def test_tool_choice_validation_without_parser():
1379+
"""Test that tool_choice='required' or named tool without tool_parser
1380+
returns an appropriate error message."""
1381+
mock_engine = MagicMock(spec=AsyncLLM)
1382+
mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
1383+
mock_engine.errored = False
1384+
mock_engine.model_config = MockModelConfig()
1385+
mock_engine.input_processor = MagicMock()
1386+
mock_engine.io_processor = MagicMock()
1387+
1388+
models = OpenAIServingModels(
1389+
engine_client=mock_engine,
1390+
base_model_paths=BASE_MODEL_PATHS,
1391+
)
1392+
# Create serving_chat without tool_parser (enable_auto_tools=False)
1393+
serving_chat = OpenAIServingChat(
1394+
mock_engine,
1395+
models,
1396+
response_role="assistant",
1397+
chat_template=CHAT_TEMPLATE,
1398+
chat_template_content_format="auto",
1399+
request_logger=None,
1400+
enable_auto_tools=False, # No tool parser
1401+
)
1402+
1403+
tools = [
1404+
{
1405+
"type": "function",
1406+
"function": {
1407+
"name": "get_weather",
1408+
"description": "Get the weather in a given location",
1409+
"parameters": {
1410+
"type": "object",
1411+
"properties": {"location": {"type": "string"}},
1412+
"required": ["location"],
1413+
},
1414+
},
1415+
}
1416+
]
1417+
1418+
# Test tool_choice="required" without tool_parser
1419+
req_required = ChatCompletionRequest(
1420+
model=MODEL_NAME,
1421+
messages=[{"role": "user", "content": "What's the weather?"}],
1422+
tools=tools,
1423+
tool_choice="required",
1424+
)
1425+
response_required = await serving_chat.create_chat_completion(req_required)
1426+
assert hasattr(response_required, "body")
1427+
error_body = response_required.body.decode()
1428+
assert "tool_choice" in error_body
1429+
assert "--tool-call-parser" in error_body
1430+
1431+
# Test named tool_choice without tool_parser
1432+
req_named = ChatCompletionRequest(
1433+
model=MODEL_NAME,
1434+
messages=[{"role": "user", "content": "What's the weather?"}],
1435+
tools=tools,
1436+
tool_choice={"type": "function", "function": {"name": "get_weather"}},
1437+
)
1438+
response_named = await serving_chat.create_chat_completion(req_named)
1439+
assert hasattr(response_named, "body")
1440+
error_body = response_named.body.decode()
1441+
assert "tool_choice" in error_body
1442+
assert "--tool-call-parser" in error_body
1443+
1444+
# Test tool_choice="auto" with enable_auto_tools=True but no tool_parser
1445+
# This is a regression test: enable_auto_tools is set but tool_parser is None
1446+
serving_chat_auto_enabled = OpenAIServingChat(
1447+
mock_engine,
1448+
models,
1449+
response_role="assistant",
1450+
chat_template=CHAT_TEMPLATE,
1451+
chat_template_content_format="auto",
1452+
request_logger=None,
1453+
enable_auto_tools=True, # Enabled but no tool_parser configured
1454+
)
1455+
1456+
req_auto = ChatCompletionRequest(
1457+
model=MODEL_NAME,
1458+
messages=[{"role": "user", "content": "What's the weather?"}],
1459+
tools=tools,
1460+
tool_choice="auto",
1461+
)
1462+
response_auto = await serving_chat_auto_enabled.create_chat_completion(req_auto)
1463+
assert hasattr(response_auto, "body")
1464+
error_body = response_auto.body.decode()
1465+
assert "auto" in error_body
1466+
assert "--tool-call-parser" in error_body

vllm/entrypoints/openai/serving_chat.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,18 +204,37 @@ async def create_chat_completion(
204204
truncate_tool_call_ids(request)
205205
validate_request_params(request)
206206

207-
if (
208-
request.tool_choice == "auto"
209-
and not (self.enable_auto_tools and tool_parser is not None)
207+
# Check if tool parsing is unavailable
208+
tool_parsing_unavailable = (
209+
tool_parser is None
210210
and not isinstance(tokenizer, MistralTokenizer)
211211
and not self.use_harmony
212+
)
213+
214+
# Validate tool_choice when tool parsing is required but unavailable
215+
if tool_parsing_unavailable and request.tool_choice not in (
216+
None,
217+
"none",
212218
):
213-
# for hf tokenizers, "auto" tools requires
214-
# --enable-auto-tool-choice and --tool-call-parser
215-
return self.create_error_response(
216-
'"auto" tool choice requires '
217-
"--enable-auto-tool-choice and --tool-call-parser to be set"
218-
)
219+
if request.tool_choice == "auto":
220+
if not self.enable_auto_tools:
221+
# for hf tokenizers, "auto" tools requires
222+
# --enable-auto-tool-choice and --tool-call-parser
223+
return self.create_error_response(
224+
'"auto" tool choice requires '
225+
"--enable-auto-tool-choice and "
226+
"--tool-call-parser to be set"
227+
)
228+
# enable_auto_tools is set but tool_parser is None
229+
return self.create_error_response(
230+
'"auto" tool choice requires --tool-call-parser to be set'
231+
)
232+
else:
233+
# "required" or named tool requires tool parser
234+
return self.create_error_response(
235+
f'tool_choice="{request.tool_choice}" requires '
236+
"--tool-call-parser to be set"
237+
)
219238

220239
if request.tools is None or (
221240
request.tool_choice == "none"

0 commit comments

Comments
 (0)