@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 4b95e9cec4
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 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}