@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 18013df6ae
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 5, 02:15 UTC

[Bugfix] Reject empty tools array with HTTP 400 (#39780)

Signed-off-by: Jigang Zhou <zjg0907008@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>

jigangz committed Apr 15, 2026 at 21:08 UTC 18013df6ae27c3fb941307c46c975227126d641f
4 files changed +23 -23
tests/tool_parsers/test_ernie45_moe_tool_parser.py
+1 -1
@@ -328,7 +328,7 @@ def test_extract_tool_calls_streaming_incremental(
328 expected_content,
329 ):
330 """Verify the Ernie45 Parser streaming behavior by verifying each chunk is as expected.""" # noqa: E501
331 - request = ChatCompletionRequest(model=MODEL, messages=[], tools=[])
331 + request = ChatCompletionRequest(model=MODEL, messages=[])
332
333 tool_calls_dict = {}
334 for delta_message in stream_delta_message_generator(
tests/tool_parsers/test_xlam_tool_parser.py
+1 -1
@@ -484,7 +484,7 @@ def test_extract_tool_calls_streaming_incremental(
484 expected_content,
485 ):
486 """Verify the XLAM Parser streaming behavior by verifying each chunk is as expected.""" # noqa: E501
487 - request = ChatCompletionRequest(model=MODEL, messages=[], tools=[])
487 + request = ChatCompletionRequest(model=MODEL, messages=[])
488
489 chunks = []
490 for delta_message in stream_delta_message_generator(
tests/tool_use/test_chat_completion_request_validations.py
+9 -9
@@ -26,15 +26,15 @@ def test_chat_completion_request_with_no_tools():
26 )
27 assert request.tool_choice == "none"
28
29 - # tools key present but empty
30 - request = ChatCompletionRequest.model_validate(
31 - {
32 - "messages": [{"role": "user", "content": "Hello"}],
33 - "model": "facebook/opt-125m",
34 - "tools": [],
35 - }
36 - )
37 - assert request.tool_choice == "none"
29 + # tools key present but empty -- should be rejected
30 + with pytest.raises(ValueError, match="must not be an empty array"):
31 + ChatCompletionRequest.model_validate(
32 + {
33 + "messages": [{"role": "user", "content": "Hello"}],
34 + "model": "facebook/opt-125m",
35 + "tools": [],
36 + }
37 + )
38
39
40 @pytest.mark.parametrize("tool_choice", ["auto", "required"])
vllm/entrypoints/openai/chat_completion/protocol.py
+12 -12
@@ -681,6 +681,18 @@ class ChatCompletionRequest(OpenAIBaseModel):
681 @model_validator(mode="before")
682 @classmethod
683 def check_tool_usage(cls, data):
684 + if isinstance(data, ValueError):
685 + raise data
686 + if not isinstance(data, dict):
687 + return data
688 +
689 + # Reject empty tools array, matching OpenAI API behavior
690 + if data.get("tools") == []:
691 + raise ValueError(
692 + "`tools` must not be an empty array. "
693 + "Either provide at least one tool or omit the field entirely."
694 + )
695 +
696 # if "tool_choice" is not specified but tools are provided,
697 # default to "auto" tool_choice
698 if "tool_choice" not in data and data.get("tools"):
@@ -707,18 +719,6 @@ class ChatCompletionRequest(OpenAIBaseModel):
719 "are supported."
720 )
721
710 - # if tool_choice is "required" but the "tools" list is empty,
711 - # override the data to behave like "none" to align with
712 - # OpenAI’s behavior.
713 - if (
714 - data["tool_choice"] == "required"
715 - and isinstance(data["tools"], list)
716 - and len(data["tools"]) == 0
717 - ):
718 - data["tool_choice"] = "none"
719 - del data["tools"]
720 - return data
721 -
722 # ensure that if "tool_choice" is specified as an object,
723 # it matches a valid tool
724 correct_usage_message = (