main
py 400 lines 12.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import json
5
6 import openai
7 import pytest
8 import pytest_asyncio
9
10 from tests.utils import RemoteOpenAIServer
11 from vllm.assets.audio import AudioAsset
12 from vllm.multimodal.utils import encode_audio_base64, encode_audio_url, fetch_audio
13
14 MODEL_NAME = "fixie-ai/ultravox-v0_5-llama-3_2-1b"
15 TEST_AUDIO_URLS = [
16 AudioAsset("winning_call").url,
17 AudioAsset("mary_had_lamb").url,
18 ]
19 MAXIMUM_AUDIOS = 2
20
21
22 @pytest.fixture(scope="module")
23 def server():
24 args = [
25 "--dtype",
26 "float32",
27 "--max-model-len",
28 "2048",
29 "--max-num-seqs",
30 "5",
31 "--enforce-eager",
32 "--trust-remote-code",
33 "--limit-mm-per-prompt",
34 json.dumps({"audio": MAXIMUM_AUDIOS}),
35 "--no-enable-prefix-caching",
36 ]
37
38 with RemoteOpenAIServer(MODEL_NAME, args) as remote_server:
39 yield remote_server
40
41
42 @pytest_asyncio.fixture
43 async def client(server):
44 async with server.get_async_client() as async_client:
45 yield async_client
46
47
48 @pytest.fixture(scope="session")
49 def base64_encoded_audio() -> dict[str, str]:
50 encoded = {}
51 for audio_url in TEST_AUDIO_URLS:
52 audio, sample_rate = fetch_audio(audio_url)
53 encoded[audio_url] = encode_audio_base64(audio, int(sample_rate))
54 return encoded
55
56
57 @pytest.fixture(scope="session")
58 def url_encoded_audio() -> dict[str, str]:
59 encoded = {}
60 for audio_url in TEST_AUDIO_URLS:
61 audio, sample_rate = fetch_audio(audio_url)
62 encoded[audio_url] = encode_audio_url(audio, int(sample_rate))
63 return encoded
64
65
66 def dummy_messages_from_audio_url(
67 audio_urls: str | list[str],
68 content_text: str = "What's happening in this audio?",
69 ):
70 if isinstance(audio_urls, str):
71 audio_urls = [audio_urls]
72
73 return [
74 {
75 "role": "user",
76 "content": [
77 *(
78 {"type": "audio_url", "audio_url": {"url": audio_url}}
79 for audio_url in audio_urls
80 ),
81 {"type": "text", "text": content_text},
82 ],
83 }
84 ]
85
86
87 @pytest.mark.asyncio
88 @pytest.mark.parametrize("model_name", [MODEL_NAME])
89 @pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
90 async def test_single_chat_session_audio(
91 client: openai.AsyncOpenAI, model_name: str, audio_url: str
92 ):
93 messages = dummy_messages_from_audio_url(audio_url)
94
95 # test single completion
96 chat_completion = await client.chat.completions.create(
97 model=model_name,
98 messages=messages,
99 max_completion_tokens=10,
100 logprobs=True,
101 temperature=0.0,
102 top_logprobs=5,
103 )
104 assert len(chat_completion.choices) == 1
105
106 choice = chat_completion.choices[0]
107 assert choice.finish_reason == "length"
108 assert chat_completion.usage == openai.types.CompletionUsage(
109 completion_tokens=10, prompt_tokens=202, total_tokens=212
110 )
111
112 message = choice.message
113 message = chat_completion.choices[0].message
114 assert message.content is not None and len(message.content) >= 10
115 assert message.role == "assistant"
116 messages.append({"role": "assistant", "content": message.content})
117
118 # test multi-turn dialogue
119 messages.append({"role": "user", "content": "express your result in json"})
120 chat_completion = await client.chat.completions.create(
121 model=model_name,
122 messages=messages,
123 max_completion_tokens=10,
124 )
125 message = chat_completion.choices[0].message
126 assert message.content is not None and len(message.content) >= 0
127
128
129 @pytest.mark.asyncio
130 @pytest.mark.parametrize("model_name", [MODEL_NAME])
131 @pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
132 async def test_error_on_invalid_audio_url_type(
133 client: openai.AsyncOpenAI, model_name: str, audio_url: str
134 ):
135 messages = [
136 {
137 "role": "user",
138 "content": [
139 {"type": "audio_url", "audio_url": audio_url},
140 {"type": "text", "text": "What's happening in this audio?"},
141 ],
142 }
143 ]
144
145 # audio_url should be a dict {"url": "some url"}, not directly a string
146 with pytest.raises(openai.BadRequestError):
147 _ = await client.chat.completions.create(
148 model=model_name,
149 messages=messages,
150 max_completion_tokens=10,
151 temperature=0.0,
152 )
153
154
155 @pytest.mark.asyncio
156 @pytest.mark.parametrize("model_name", [MODEL_NAME])
157 @pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
158 async def test_single_chat_session_audio_base64encoded(
159 client: openai.AsyncOpenAI,
160 model_name: str,
161 audio_url: str,
162 url_encoded_audio: dict[str, str],
163 ):
164 messages = dummy_messages_from_audio_url(url_encoded_audio[audio_url])
165
166 # test single completion
167 chat_completion = await client.chat.completions.create(
168 model=model_name,
169 messages=messages,
170 max_completion_tokens=10,
171 logprobs=True,
172 temperature=0.0,
173 top_logprobs=5,
174 )
175 assert len(chat_completion.choices) == 1
176
177 choice = chat_completion.choices[0]
178 assert choice.finish_reason == "length"
179 assert chat_completion.usage == openai.types.CompletionUsage(
180 completion_tokens=10, prompt_tokens=202, total_tokens=212
181 )
182
183 message = choice.message
184 message = chat_completion.choices[0].message
185 assert message.content is not None and len(message.content) >= 10
186 assert message.role == "assistant"
187 messages.append({"role": "assistant", "content": message.content})
188
189 # test multi-turn dialogue
190 messages.append({"role": "user", "content": "express your result in json"})
191 chat_completion = await client.chat.completions.create(
192 model=model_name,
193 messages=messages,
194 max_completion_tokens=10,
195 temperature=0.0,
196 )
197 message = chat_completion.choices[0].message
198 assert message.content is not None and len(message.content) >= 0
199
200
201 @pytest.mark.asyncio
202 @pytest.mark.parametrize("model_name", [MODEL_NAME])
203 @pytest.mark.parametrize("audio_url", [TEST_AUDIO_URLS[0]])
204 async def test_single_chat_session_input_audio(
205 client: openai.AsyncOpenAI,
206 model_name: str,
207 audio_url: str,
208 base64_encoded_audio: dict[str, str],
209 ):
210 messages = [
211 {
212 "role": "user",
213 "content": [
214 {
215 "type": "input_audio",
216 "input_audio": {
217 "data": base64_encoded_audio[audio_url],
218 "format": "wav",
219 },
220 },
221 {"type": "text", "text": "What's happening in this audio?"},
222 ],
223 }
224 ]
225
226 # test single completion
227 chat_completion = await client.chat.completions.create(
228 model=model_name,
229 messages=messages,
230 max_completion_tokens=10,
231 logprobs=True,
232 top_logprobs=5,
233 )
234 assert len(chat_completion.choices) == 1
235
236 choice = chat_completion.choices[0]
237 assert choice.finish_reason == "length"
238 assert chat_completion.usage == openai.types.CompletionUsage(
239 completion_tokens=10, prompt_tokens=202, total_tokens=212
240 )
241
242 message = choice.message
243 message = chat_completion.choices[0].message
244 assert message.content is not None and len(message.content) >= 10
245 assert message.role == "assistant"
246 messages.append({"role": "assistant", "content": message.content})
247
248 # test multi-turn dialogue
249 messages.append({"role": "user", "content": "express your result in json"})
250 chat_completion = await client.chat.completions.create(
251 model=model_name,
252 messages=messages,
253 max_completion_tokens=10,
254 )
255 message = chat_completion.choices[0].message
256 assert message.content is not None and len(message.content) >= 0
257
258
259 @pytest.mark.asyncio
260 @pytest.mark.parametrize("model_name", [MODEL_NAME])
261 @pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
262 async def test_chat_streaming_audio(
263 client: openai.AsyncOpenAI, model_name: str, audio_url: str
264 ):
265 messages = dummy_messages_from_audio_url(
266 audio_url, "What's a short title for this audio?"
267 )
268
269 # test single completion
270 chat_completion = await client.chat.completions.create(
271 model=model_name,
272 messages=messages,
273 max_completion_tokens=8,
274 temperature=0.0,
275 )
276 output = chat_completion.choices[0].message.content
277 stop_reason = chat_completion.choices[0].finish_reason
278
279 # test streaming
280 stream = await client.chat.completions.create(
281 model=model_name,
282 messages=messages,
283 max_completion_tokens=8,
284 temperature=0.0,
285 stream=True,
286 )
287 chunks: list[str] = []
288 finish_reason_count = 0
289 async for chunk in stream:
290 delta = chunk.choices[0].delta
291 if delta.role:
292 assert delta.role == "assistant"
293 if delta.content:
294 chunks.append(delta.content)
295 if chunk.choices[0].finish_reason is not None:
296 finish_reason_count += 1
297 # finish reason should only return in last block
298 assert finish_reason_count == 1
299 assert chunk.choices[0].finish_reason == stop_reason
300 assert delta.content
301 assert "".join(chunks) == output
302
303
304 @pytest.mark.asyncio
305 @pytest.mark.parametrize("model_name", [MODEL_NAME])
306 @pytest.mark.parametrize("audio_url", TEST_AUDIO_URLS)
307 async def test_chat_streaming_input_audio(
308 client: openai.AsyncOpenAI,
309 model_name: str,
310 audio_url: str,
311 base64_encoded_audio: dict[str, str],
312 ):
313 messages = [
314 {
315 "role": "user",
316 "content": [
317 {
318 "type": "input_audio",
319 "input_audio": {
320 "data": base64_encoded_audio[audio_url],
321 "format": "wav",
322 },
323 },
324 {"type": "text", "text": "What's a short title for this audio?"},
325 ],
326 }
327 ]
328
329 # test single completion
330 chat_completion = await client.chat.completions.create(
331 model=model_name,
332 messages=messages,
333 max_completion_tokens=8,
334 temperature=0.0,
335 )
336 output = chat_completion.choices[0].message.content
337 stop_reason = chat_completion.choices[0].finish_reason
338
339 # test streaming
340 stream = await client.chat.completions.create(
341 model=model_name,
342 messages=messages,
343 max_completion_tokens=8,
344 temperature=0.0,
345 stream=True,
346 )
347 chunks: list[str] = []
348 finish_reason_count = 0
349 async for chunk in stream:
350 delta = chunk.choices[0].delta
351 if delta.role:
352 assert delta.role == "assistant"
353 if delta.content:
354 chunks.append(delta.content)
355 if chunk.choices[0].finish_reason is not None:
356 finish_reason_count += 1
357 # finish reason should only return in last block
358 assert finish_reason_count == 1
359 assert chunk.choices[0].finish_reason == stop_reason
360 assert delta.content
361 assert "".join(chunks) == output
362
363
364 @pytest.mark.asyncio
365 @pytest.mark.parametrize("model_name", [MODEL_NAME])
366 @pytest.mark.parametrize(
367 "audio_urls", [TEST_AUDIO_URLS, TEST_AUDIO_URLS + [TEST_AUDIO_URLS[0]]]
368 )
369 async def test_multi_audio_input(
370 client: openai.AsyncOpenAI, model_name: str, audio_urls: list[str]
371 ):
372 messages = dummy_messages_from_audio_url(audio_urls)
373
374 if len(audio_urls) > MAXIMUM_AUDIOS:
375 with pytest.raises(openai.BadRequestError): # test multi-audio input
376 await client.chat.completions.create(
377 model=model_name,
378 messages=messages,
379 max_completion_tokens=10,
380 temperature=0.0,
381 )
382
383 # the server should still work afterwards
384 completion = await client.completions.create(
385 model=model_name,
386 prompt=[0, 0, 0, 0, 0],
387 max_tokens=5,
388 temperature=0.0,
389 )
390 completion = completion.choices[0].text
391 assert completion is not None and len(completion) >= 0
392 else:
393 chat_completion = await client.chat.completions.create(
394 model=model_name,
395 messages=messages,
396 max_completion_tokens=10,
397 temperature=0.0,
398 )
399 message = chat_completion.choices[0].message
400 assert message.content is not None and len(message.content) >= 0