@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 676f82ae81
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 8, 01:15 UTC

Add validation to reject non-text content in system messages (#34072)

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>

Varun Chawla committed Feb 19, 2026 at 21:30 UTC 676f82ae8140a512dae73bcae6c6d23907f55e0e
2 files changed +187 -1
tests/entrypoints/openai/test_chat_error.py
+138 -1
@@ -4,7 +4,7 @@
4 from dataclasses import dataclass, field
5 from http import HTTPStatus
6 from typing import Any
7 -from unittest.mock import AsyncMock, MagicMock
7 +from unittest.mock import AsyncMock, MagicMock, patch
8
9 import pytest
10
@@ -233,3 +233,140 @@ async def test_chat_error_stream():
233 f"Expected error message in chunks: {chunks}"
234 )
235 assert chunks[-1] == "data: [DONE]\n\n"
236 +
237 +
238 +@pytest.mark.parametrize(
239 + "image_content",
240 + [
241 + [{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}],
242 + [{"image_url": {"url": "https://example.com/image.jpg"}}],
243 + ],
244 +)
245 +def test_system_message_warns_on_image(image_content):
246 + """Test that system messages with image content trigger a warning."""
247 + with patch(
248 + "vllm.entrypoints.openai.chat_completion.protocol.logger"
249 + ) as mock_logger:
250 + ChatCompletionRequest(
251 + model=MODEL_NAME,
252 + messages=[
253 + {
254 + "role": "system",
255 + "content": image_content,
256 + }
257 + ],
258 + )
259 +
260 + mock_logger.warning_once.assert_called()
261 + call_args = str(mock_logger.warning_once.call_args)
262 + assert "System messages should only contain text" in call_args
263 + assert "image_url" in call_args
264 +
265 +
266 +def test_system_message_accepts_text():
267 + """Test that system messages can contain text content."""
268 + # Should not raise an exception
269 + request = ChatCompletionRequest(
270 + model=MODEL_NAME,
271 + messages=[
272 + {"role": "system", "content": "You are a helpful assistant."},
273 + ],
274 + )
275 + assert request.messages[0]["role"] == "system"
276 +
277 +
278 +def test_system_message_accepts_text_array():
279 + """Test that system messages can contain an array with text content."""
280 + # Should not raise an exception
281 + request = ChatCompletionRequest(
282 + model=MODEL_NAME,
283 + messages=[
284 + {
285 + "role": "system",
286 + "content": [{"type": "text", "text": "You are a helpful assistant."}],
287 + },
288 + ],
289 + )
290 + assert request.messages[0]["role"] == "system"
291 +
292 +
293 +def test_user_message_accepts_image():
294 + """Test that user messages can still contain image content."""
295 + # Should not raise an exception
296 + request = ChatCompletionRequest(
297 + model=MODEL_NAME,
298 + messages=[
299 + {
300 + "role": "user",
301 + "content": [
302 + {"type": "text", "text": "What's in this image?"},
303 + {
304 + "type": "image_url",
305 + "image_url": {"url": "https://example.com/image.jpg"},
306 + },
307 + ],
308 + },
309 + ],
310 + )
311 + assert request.messages[0]["role"] == "user"
312 +
313 +
314 +@pytest.mark.parametrize(
315 + "audio_content",
316 + [
317 + [
318 + {
319 + "type": "input_audio",
320 + "input_audio": {"data": "base64data", "format": "wav"},
321 + }
322 + ],
323 + [{"input_audio": {"data": "base64data", "format": "wav"}}],
324 + ],
325 +)
326 +def test_system_message_warns_on_audio(audio_content):
327 + """Test that system messages with audio content trigger a warning."""
328 + with patch(
329 + "vllm.entrypoints.openai.chat_completion.protocol.logger"
330 + ) as mock_logger:
331 + ChatCompletionRequest(
332 + model=MODEL_NAME,
333 + messages=[
334 + {
335 + "role": "system",
336 + "content": audio_content,
337 + }
338 + ],
339 + )
340 +
341 + mock_logger.warning_once.assert_called()
342 + call_args = str(mock_logger.warning_once.call_args)
343 + assert "System messages should only contain text" in call_args
344 + assert "input_audio" in call_args
345 +
346 +
347 +@pytest.mark.parametrize(
348 + "video_content",
349 + [
350 + [{"type": "video_url", "video_url": {"url": "https://example.com/video.mp4"}}],
351 + [{"video_url": {"url": "https://example.com/video.mp4"}}],
352 + ],
353 +)
354 +def test_system_message_warns_on_video(video_content):
355 + """Test that system messages with video content trigger a warning."""
356 + with patch(
357 + "vllm.entrypoints.openai.chat_completion.protocol.logger"
358 + ) as mock_logger:
359 + ChatCompletionRequest(
360 + model=MODEL_NAME,
361 + messages=[
362 + {
363 + "role": "system",
364 + "content": video_content,
365 + }
366 + ],
367 + )
368 +
369 + mock_logger.warning_once.assert_called()
370 + call_args = str(mock_logger.warning_once.call_args)
371 + assert "System messages should only contain text" in call_args
372 + assert "video_url" in call_args
vllm/entrypoints/openai/chat_completion/protocol.py
+49
@@ -674,3 +674,52 @@ class ChatCompletionRequest(OpenAIBaseModel):
674 "Parameter 'cache_salt' must be a non-empty string if provided."
675 )
676 return data
677 +
678 + @model_validator(mode="before")
679 + @classmethod
680 + def check_system_message_content_type(cls, data):
681 + """Warn if system messages contain non-text content.
682 +
683 + According to OpenAI API spec, system messages can only be of type
684 + 'text'. We log a warning instead of rejecting to avoid breaking
685 + users who intentionally send multimodal system messages.
686 + See: https://platform.openai.com/docs/api-reference/chat/create#chat_create-messages-system_message
687 + """
688 + if not isinstance(data, dict):
689 + return data
690 + messages = data.get("messages", [])
691 + for msg in messages:
692 + # Check if this is a system message
693 + if isinstance(msg, dict) and msg.get("role") == "system":
694 + content = msg.get("content")
695 +
696 + # If content is a list (multimodal format)
697 + if isinstance(content, list):
698 + for part in content:
699 + if isinstance(part, dict):
700 + part_type = part.get("type")
701 + # Infer type when 'type' field is not explicit
702 + if part_type is None:
703 + if "image_url" in part or "image_pil" in part:
704 + part_type = "image_url"
705 + elif "image_embeds" in part:
706 + part_type = "image_embeds"
707 + elif "audio_url" in part:
708 + part_type = "audio_url"
709 + elif "input_audio" in part:
710 + part_type = "input_audio"
711 + elif "audio_embeds" in part:
712 + part_type = "audio_embeds"
713 + elif "video_url" in part:
714 + part_type = "video_url"
715 +
716 + # Warn about non-text content in system messages
717 + if part_type and part_type != "text":
718 + logger.warning_once(
719 + "System messages should only contain text "
720 + "content according to the OpenAI API spec. "
721 + "Found content type: '%s'.",
722 + part_type,
723 + )
724 +
725 + return data