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