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 11, 05:00 UTC
[Bugfix] Add validation for tool requests when tool_parser is unavailable (#30613)
Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
lif committed
Dec 19, 2025 at 18:23 UTC
086b96339ffe057f92cd0a20c8be820b17e24dbf
2 files changed
+89
-9
tests/entrypoints/openai/test_serving_chat.py
+67
@@ -15,6 +15,7 @@ from vllm.entrypoints.openai.parser.harmony_utils import get_encoding
15
from vllm.entrypoints.openai.protocol import (
16
ChatCompletionRequest,
17
ChatCompletionResponse,
18
+ ErrorResponse,
19
RequestResponseMetadata,
20
)
21
from vllm.entrypoints.openai.serving_chat import OpenAIServingChat
@@ -1444,3 +1445,69 @@ class TestServingChatWithHarmony:
1445
},
1446
],
1447
)
1448
+
1449
+
1450
+@pytest.mark.asyncio
1451
+async def test_tool_choice_validation_without_parser():
1452
+ """Test that tool_choice='required' or named tool without tool_parser
1453
+ returns an appropriate error message."""
1454
+ mock_engine = MagicMock(spec=AsyncLLM)
1455
+ mock_engine.get_tokenizer.return_value = get_tokenizer(MODEL_NAME)
1456
+ mock_engine.errored = False
1457
+ mock_engine.model_config = MockModelConfig()
1458
+ mock_engine.input_processor = MagicMock()
1459
+ mock_engine.io_processor = MagicMock()
1460
+
1461
+ models = OpenAIServingModels(
1462
+ engine_client=mock_engine,
1463
+ base_model_paths=BASE_MODEL_PATHS,
1464
+ )
1465
+ # Create serving_chat without tool_parser (enable_auto_tools=False)
1466
+ serving_chat = OpenAIServingChat(
1467
+ mock_engine,
1468
+ models,
1469
+ response_role="assistant",
1470
+ chat_template=CHAT_TEMPLATE,
1471
+ chat_template_content_format="auto",
1472
+ request_logger=None,
1473
+ enable_auto_tools=False, # No tool parser
1474
+ )
1475
+
1476
+ tools = [
1477
+ {
1478
+ "type": "function",
1479
+ "function": {
1480
+ "name": "get_weather",
1481
+ "description": "Get the weather in a given location",
1482
+ "parameters": {
1483
+ "type": "object",
1484
+ "properties": {"location": {"type": "string"}},
1485
+ "required": ["location"],
1486
+ },
1487
+ },
1488
+ }
1489
+ ]
1490
+
1491
+ # Test tool_choice="required" without tool_parser
1492
+ req_required = ChatCompletionRequest(
1493
+ model=MODEL_NAME,
1494
+ messages=[{"role": "user", "content": "What's the weather?"}],
1495
+ tools=tools,
1496
+ tool_choice="required",
1497
+ )
1498
+ response_required = await serving_chat.create_chat_completion(req_required)
1499
+ assert isinstance(response_required, ErrorResponse)
1500
+ assert "tool_choice" in response_required.error.message
1501
+ assert "--tool-call-parser" in response_required.error.message
1502
+
1503
+ # Test named tool_choice without tool_parser
1504
+ req_named = ChatCompletionRequest(
1505
+ model=MODEL_NAME,
1506
+ messages=[{"role": "user", "content": "What's the weather?"}],
1507
+ tools=tools,
1508
+ tool_choice={"type": "function", "function": {"name": "get_weather"}},
1509
+ )
1510
+ response_named = await serving_chat.create_chat_completion(req_named)
1511
+ assert isinstance(response_named, ErrorResponse)
1512
+ assert "tool_choice" in response_named.error.message
1513
+ assert "--tool-call-parser" in response_named.error.message
vllm/entrypoints/openai/serving_chat.py
+22
-9
@@ -253,18 +253,31 @@ class OpenAIServingChat(OpenAIServing):
253
truncate_tool_call_ids(request)
254
validate_request_params(request)
255
256
- if (
257
- request.tool_choice == "auto"
258
- and not (self.enable_auto_tools and tool_parser is not None)
256
+ # Check if tool parsing is unavailable (common condition)
257
+ tool_parsing_unavailable = (
258
+ tool_parser is None
259
and not isinstance(tokenizer, MistralTokenizer)
260
and not self.use_harmony
261
+ )
262
+
263
+ # Validate tool_choice when tool parsing is required but unavailable
264
+ if tool_parsing_unavailable and request.tool_choice not in (
265
+ None,
266
+ "none",
267
):
262
- # for hf tokenizers, "auto" tools requires
263
- # --enable-auto-tool-choice and --tool-call-parser
264
- return self.create_error_response(
265
- '"auto" tool choice requires '
266
- "--enable-auto-tool-choice and --tool-call-parser to be set"
267
- )
268
+ if request.tool_choice == "auto" and not self.enable_auto_tools:
269
+ # for hf tokenizers, "auto" tools requires
270
+ # --enable-auto-tool-choice and --tool-call-parser
271
+ return self.create_error_response(
272
+ '"auto" tool choice requires '
273
+ "--enable-auto-tool-choice and --tool-call-parser to be set"
274
+ )
275
+ elif request.tool_choice != "auto":
276
+ # "required" or named tool requires tool parser
277
+ return self.create_error_response(
278
+ f'tool_choice="{request.tool_choice}" requires '
279
+ "--tool-call-parser to be set"
280
+ )
281
282
if request.tools is None or (
283
request.tool_choice == "none"