@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 086b96339f
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 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"