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, 13:45 UTC
[CI] Return HTTP 400 for unsupported chat content part type (#41121)
Signed-off-by: haosdent <haosdent@gmail.com>
haosdent committed
Apr 29, 2026 at 10:23 UTC
4b95e9cec4e9a1a90d3f4b2afa62e88459b2b90e
2 files changed
+24
-56
tests/entrypoints/openai/test_openai_schema.py
+16
-55
@@ -1,18 +1,13 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import json
4
-from http import HTTPStatus
4
from typing import Final
5
6
import pytest
7
import schemathesis
9
-from httpx import URL
10
-from hypothesis import settings
8
+from hypothesis import HealthCheck, settings
9
from schemathesis import GenerationConfig
12
-from schemathesis.checks import not_a_server_error
13
-from schemathesis.internal.checks import CheckContext
10
from schemathesis.models import Case
15
-from schemathesis.transports.responses import GenericResponse
11
12
from vllm.platforms import current_platform
13
@@ -65,20 +60,10 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
60
61
def no_invalid_types(case: schemathesis.models.Case):
62
"""
68
- This filter skips test cases with invalid data that schemathesis
69
- incorrectly generates due to permissive schema configurations.
70
-
71
- 1. Skips `POST /tokenize` endpoint cases with `"type": "file"` in
72
- message content, which isn't implemented.
73
-
74
- 2. Skips tool_calls with `"type": "custom"` which schemathesis
75
- incorrectly generates instead of the valid `"type": "function"`.
76
-
77
- Example test cases that are skipped:
78
- curl -X POST -H 'Content-Type: application/json' \
79
- -d '{"messages": [{"content": [{"file": {}, "type": "file"}], "role": "user"}]}' \
80
- http://localhost:8000/tokenize
63
+ Skips tool_calls with `"type": "custom"` which schemathesis incorrectly
64
+ generates instead of the valid `"type": "function"`.
65
66
+ Example test case that is skipped:
67
curl -X POST -H 'Content-Type: application/json' \
68
-d '{"messages": [{"role": "assistant", "tool_calls": [{"custom": {"input": "", "name": ""}, "id": "", "type": "custom"}]}]}' \
69
http://localhost:8000/v1/chat/completions
@@ -93,20 +78,6 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
78
if not isinstance(message, dict):
79
continue
80
96
- # Check for invalid file type in tokenize endpoint
97
- if op.method.lower() == "post" and op.path == "/tokenize":
98
- content = message.get("content", [])
99
- if (
100
- isinstance(content, list)
101
- and len(content) > 0
102
- and any(
103
- isinstance(item, dict) and item.get("type") == "file"
104
- for item in content
105
- )
106
- ):
107
- return False
108
-
109
- # Check for invalid tool_calls with non-function types
81
tool_calls = message.get("tool_calls", [])
82
if isinstance(tool_calls, list):
83
for tool_call in tool_calls:
@@ -136,24 +107,19 @@ def before_generate_case(context: schemathesis.hooks.HookContext, strategy):
107
return strategy.filter(no_invalid_types)
108
109
139
-def customized_not_a_server_error(
140
- ctx: CheckContext, response: GenericResponse, case: Case
141
-) -> bool | None:
142
- try:
143
- return not_a_server_error(ctx, response, case)
144
- except Exception:
145
- if (
146
- URL(response.request.url).path
147
- in ["/v1/chat/completions/render", "/v1/chat/completions"]
148
- and response.status_code == HTTPStatus.NOT_IMPLEMENTED.value
149
- ):
150
- return True
151
- raise
152
-
153
-
110
@schema.parametrize()
111
@schema.override(headers={"Content-Type": "application/json"})
156
-@settings(deadline=LONG_TIMEOUT_SECONDS * 1000, max_examples=50)
112
+@settings(
113
+ deadline=LONG_TIMEOUT_SECONDS * 1000,
114
+ max_examples=50,
115
+ # Under CI's derandomized hypothesis seed, the schemathesis strategy
116
+ # for /v1/chat/completions/batch's nested-message body, combined with
117
+ # the no_invalid_types filter (notably the grammar=="" rule), exceeds
118
+ # the default filtered-vs-good ratio. The filter is intentional, so
119
+ # suppress the health check rather than drop the filter — dropping it
120
+ # exposes pre-existing server bugs out of scope here.
121
+ suppress_health_check=[HealthCheck.filter_too_much],
122
+)
123
def test_openapi_stateless(case: Case):
124
key = (
125
case.operation.method.upper(),
@@ -180,9 +146,4 @@ def test_openapi_stateless(case: Case):
146
}.get(key, DEFAULT_TIMEOUT_SECONDS)
147
148
# No need to verify SSL certificate for localhost
183
- case.call_and_validate(
184
- verify=False,
185
- timeout=timeout,
186
- additional_checks=(customized_not_a_server_error,),
187
- excluded_checks=(not_a_server_error,),
188
- )
149
+ case.call_and_validate(verify=False, timeout=timeout)
vllm/entrypoints/chat_utils.py
+8
-1
@@ -40,6 +40,7 @@ from typing_extensions import Required, TypedDict
40
41
from vllm import envs
42
from vllm.config import ModelConfig
43
+from vllm.exceptions import VLLMValidationError
44
from vllm.inputs import MultiModalDataDict, MultiModalUUIDDict
45
from vllm.logger import init_logger
46
from vllm.model_executor.models import SupportsMultiModal
@@ -1501,7 +1502,13 @@ def _parse_chat_message_content_part(
1502
mm_parser.parse_video(str_content, uuid)
1503
modality = "video"
1504
else:
1504
- raise NotImplementedError(f"Unknown part type: {part_type}")
1505
+ supported = sorted(MM_PARSER_MAP.keys() | set(PART_TYPES_TO_SKIP_NONE_CONTENT))
1506
+ raise VLLMValidationError(
1507
+ f"Unsupported chat content part type: {part_type!r}. "
1508
+ f"Supported types: {', '.join(supported)}.",
1509
+ parameter="type",
1510
+ value=part_type,
1511
+ )
1512
1513
if wrap_dicts:
1514
return {"type": modality}