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 15, 13:30 UTC
[Bugfix] validate urls object for multimodal content parts (#16990)
Signed-off-by: Guillaume Calmettes <gcalmettes@scaleway.com>
Guillaume Calmettes committed
Apr 23, 2025 at 03:43 UTC
36fe78769ff4134c59bb675f2805364938092079
4 files changed
+94
-4
tests/entrypoints/openai/test_audio.py
+29
@@ -104,6 +104,35 @@ async def test_single_chat_session_audio(client: openai.AsyncOpenAI,
104
assert message.content is not None and len(message.content) >= 0
105
106
107
+@pytest.mark.asyncio
108
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
109
+@pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
110
+async def test_error_on_invalid_audio_url_type(client: openai.AsyncOpenAI,
111
+ model_name: str,
112
+ audio_url: str):
113
+ messages = [{
114
+ "role":
115
+ "user",
116
+ "content": [
117
+ {
118
+ "type": "audio_url",
119
+ "audio_url": audio_url
120
+ },
121
+ {
122
+ "type": "text",
123
+ "text": "What's happening in this audio?"
124
+ },
125
+ ],
126
+ }]
127
+
128
+ # audio_url should be a dict {"url": "some url"}, not directly a string
129
+ with pytest.raises(openai.BadRequestError):
130
+ _ = await client.chat.completions.create(model=model_name,
131
+ messages=messages,
132
+ max_completion_tokens=10,
133
+ temperature=0.0)
134
+
135
+
136
@pytest.mark.asyncio
137
@pytest.mark.parametrize("model_name", [MODEL_NAME])
138
@pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
tests/entrypoints/openai/test_video.py
+29
@@ -108,6 +108,35 @@ async def test_single_chat_session_video(client: openai.AsyncOpenAI,
108
assert message.content is not None and len(message.content) >= 0
109
110
111
+@pytest.mark.asyncio
112
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
113
+@pytest.mark.parametrize("video_url", TEST_VIDEO_URLS)
114
+async def test_error_on_invalid_video_url_type(client: openai.AsyncOpenAI,
115
+ model_name: str,
116
+ video_url: str):
117
+ messages = [{
118
+ "role":
119
+ "user",
120
+ "content": [
121
+ {
122
+ "type": "video_url",
123
+ "video_url": video_url
124
+ },
125
+ {
126
+ "type": "text",
127
+ "text": "What's in this video?"
128
+ },
129
+ ],
130
+ }]
131
+
132
+ # video_url should be a dict {"url": "some url"}, not directly a string
133
+ with pytest.raises(openai.BadRequestError):
134
+ _ = await client.chat.completions.create(model=model_name,
135
+ messages=messages,
136
+ max_completion_tokens=10,
137
+ temperature=0.0)
138
+
139
+
140
@pytest.mark.asyncio
141
@pytest.mark.parametrize("model_name", [MODEL_NAME])
142
@pytest.mark.parametrize("video_url", TEST_VIDEO_URLS)
tests/entrypoints/openai/test_vision.py
+30
@@ -137,6 +137,36 @@ async def test_single_chat_session_image(client: openai.AsyncOpenAI,
137
assert message.content is not None and len(message.content) >= 0
138
139
140
+@pytest.mark.asyncio
141
+@pytest.mark.parametrize("model_name", [MODEL_NAME])
142
+@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
143
+async def test_error_on_invalid_image_url_type(client: openai.AsyncOpenAI,
144
+ model_name: str,
145
+ image_url: str):
146
+ content_text = "What's in this image?"
147
+ messages = [{
148
+ "role":
149
+ "user",
150
+ "content": [
151
+ {
152
+ "type": "image_url",
153
+ "image_url": image_url
154
+ },
155
+ {
156
+ "type": "text",
157
+ "text": content_text
158
+ },
159
+ ],
160
+ }]
161
+
162
+ # image_url should be a dict {"url": "some url"}, not directly a string
163
+ with pytest.raises(openai.BadRequestError):
164
+ _ = await client.chat.completions.create(model=model_name,
165
+ messages=messages,
166
+ max_completion_tokens=10,
167
+ temperature=0.0)
168
+
169
+
170
@pytest.mark.asyncio
171
@pytest.mark.parametrize("model_name", [MODEL_NAME])
172
@pytest.mark.parametrize("image_url", TEST_IMAGE_URLS)
vllm/entrypoints/chat_utils.py
+6
-4
@@ -27,10 +27,11 @@ from openai.types.chat import (ChatCompletionMessageToolCallParam,
27
ChatCompletionToolMessageParam)
28
from openai.types.chat.chat_completion_content_part_input_audio_param import (
29
InputAudio)
30
+from pydantic import TypeAdapter
31
# yapf: enable
31
-# pydantic needs the TypedDict from typing_extensions
32
from transformers import (PreTrainedTokenizer, PreTrainedTokenizerFast,
33
ProcessorMixin)
34
+# pydantic needs the TypedDict from typing_extensions
35
from typing_extensions import Required, TypeAlias, TypedDict
36
37
from vllm.config import ModelConfig
@@ -879,12 +880,13 @@ def _get_full_multimodal_text_prompt(placeholder_counts: dict[str, int],
880
881
# No need to validate using Pydantic again
882
_TextParser = partial(cast, ChatCompletionContentPartTextParam)
882
-_ImageParser = partial(cast, ChatCompletionContentPartImageParam)
883
_ImageEmbedsParser = partial(cast, ChatCompletionContentPartImageEmbedsParam)
884
-_AudioParser = partial(cast, ChatCompletionContentPartAudioParam)
884
_InputAudioParser = partial(cast, ChatCompletionContentPartInputAudioParam)
885
_RefusalParser = partial(cast, ChatCompletionContentPartRefusalParam)
887
-_VideoParser = partial(cast, ChatCompletionContentPartVideoParam)
886
+# Need to validate url objects
887
+_ImageParser = TypeAdapter(ChatCompletionContentPartImageParam).validate_python
888
+_AudioParser = TypeAdapter(ChatCompletionContentPartAudioParam).validate_python
889
+_VideoParser = TypeAdapter(ChatCompletionContentPartVideoParam).validate_python
890
891
_ContentPart: TypeAlias = Union[str, dict[str, str], InputAudio]
892