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 14, 05:30 UTC
Run ruff format on a few files. (#24075)
Signed-off-by: Chenheli Hua <huachenheli@outlook.com>
Chenheli Hua committed
Sep 2, 2025 at 10:55 UTC
f399182e8c401303f7399b953417b926e4eb64f2
3 files changed
+947
-690
tests/entrypoints/test_chat_utils.py
+578
-452
@@ -46,23 +46,27 @@ MISTRAL_MODEL_ID = "mistralai/Mistral-Small-3.1-24B-Instruct-2503"
46
47
@pytest.fixture(scope="function")
48
def phi3v_model_config():
49
- return ModelConfig(PHI3V_MODEL_ID,
50
- runner="generate",
51
- trust_remote_code=True,
52
- limit_mm_per_prompt={
53
- "image": 2,
54
- })
49
+ return ModelConfig(
50
+ PHI3V_MODEL_ID,
51
+ runner="generate",
52
+ trust_remote_code=True,
53
+ limit_mm_per_prompt={
54
+ "image": 2,
55
+ },
56
+ )
57
58
59
@pytest.fixture(scope="function")
60
def phi3v_model_config_mm_interleaved():
59
- return ModelConfig(PHI3V_MODEL_ID,
60
- runner="generate",
61
- trust_remote_code=True,
62
- interleave_mm_strings=True,
63
- limit_mm_per_prompt={
64
- "image": 2,
65
- })
61
+ return ModelConfig(
62
+ PHI3V_MODEL_ID,
63
+ runner="generate",
64
+ trust_remote_code=True,
65
+ interleave_mm_strings=True,
66
+ limit_mm_per_prompt={
67
+ "image": 2,
68
+ },
69
+ )
70
71
72
@pytest.fixture(scope="module")
@@ -77,14 +81,16 @@ def phi3v_tokenizer():
81
82
@pytest.fixture(scope="function")
83
def qwen25omni_model_config_mm_interleaved():
80
- return ModelConfig(QWEN25OMNI_MODEL_ID,
81
- runner="generate",
82
- interleave_mm_strings=True,
83
- limit_mm_per_prompt={
84
- "image": 2,
85
- "audio": 1,
86
- "video": 1,
87
- })
84
+ return ModelConfig(
85
+ QWEN25OMNI_MODEL_ID,
86
+ runner="generate",
87
+ interleave_mm_strings=True,
88
+ limit_mm_per_prompt={
89
+ "image": 2,
90
+ "audio": 1,
91
+ "video": 1,
92
+ },
93
+ )
94
95
96
@pytest.fixture(scope="module")
@@ -99,11 +105,13 @@ def qwen25omni_tokenizer():
105
106
@pytest.fixture(scope="module")
107
def mllama_model_config():
102
- return ModelConfig(MLLAMA_MODEL_ID,
103
- runner="generate",
104
- limit_mm_per_prompt={
105
- "image": 2,
106
- })
108
+ return ModelConfig(
109
+ MLLAMA_MODEL_ID,
110
+ runner="generate",
111
+ limit_mm_per_prompt={
112
+ "image": 2,
113
+ },
114
+ )
115
116
117
@pytest.fixture(scope="module")
@@ -118,11 +126,13 @@ def mllama_tokenizer():
126
127
@pytest.fixture(scope="function")
128
def mistral_model_config():
121
- return ModelConfig(MISTRAL_MODEL_ID,
122
- runner="generate",
123
- limit_mm_per_prompt={
124
- "image": 2,
125
- })
129
+ return ModelConfig(
130
+ MISTRAL_MODEL_ID,
131
+ runner="generate",
132
+ limit_mm_per_prompt={
133
+ "image": 2,
134
+ },
135
+ )
136
137
138
@pytest.fixture(scope="module")
@@ -137,21 +147,21 @@ def mistral_tokenizer():
147
148
@pytest.fixture(scope="module")
149
def image_url():
140
- image = ImageAsset('cherry_blossom')
150
+ image = ImageAsset("cherry_blossom")
151
base64 = encode_image_base64(image.pil_image)
152
return f"data:image/jpeg;base64,{base64}"
153
154
155
@pytest.fixture(scope="module")
156
def video_url():
147
- video = VideoAsset('baby_reading', 1)
157
+ video = VideoAsset("baby_reading", 1)
158
base64 = encode_video_base64(video.np_ndarrays)
159
return f"data:video/jpeg;base64,{base64}"
160
161
162
@pytest.fixture(scope="module")
163
def audio_url():
154
- audio = AudioAsset('mary_had_lamb')
164
+ audio = AudioAsset("mary_had_lamb")
165
base64 = encode_audio_base64(*audio.audio_and_sample_rate)
166
return f"data:audio/ogg;base64,{base64}"
167
@@ -195,15 +205,18 @@ def test_parse_chat_messages_single_image(
205
[{
206
"role":
207
"user",
198
- "content": [{
199
- "type": "image_url",
200
- "image_url": {
201
- "url": image_url
202
- }
203
- }, {
204
- "type": "text",
205
- "text": "What's in the image?"
206
- }]
208
+ "content": [
209
+ {
210
+ "type": "image_url",
211
+ "image_url": {
212
+ "url": image_url
213
+ }
214
+ },
215
+ {
216
+ "type": "text",
217
+ "text": "What's in the image?"
218
+ },
219
+ ],
220
}],
221
phi3v_model_config,
222
phi3v_tokenizer,
@@ -223,58 +236,69 @@ def test_parse_chat_messages_empty_system(
236
):
237
# Test string format
238
conversation, _ = parse_chat_messages(
226
- [{
227
- "role": "system",
228
- "content": ""
229
- }, {
230
- "role": "user",
231
- "content": [{
232
- "type": "text",
233
- "text": "Who are you?"
234
- }]
235
- }],
239
+ [
240
+ {
241
+ "role": "system",
242
+ "content": ""
243
+ },
244
+ {
245
+ "role": "user",
246
+ "content": [{
247
+ "type": "text",
248
+ "text": "Who are you?"
249
+ }],
250
+ },
251
+ ],
252
mistral_model_config,
253
mistral_tokenizer,
254
content_format="string",
255
)
240
- assert conversation == [{
241
- "role": "system",
242
- "content": ""
243
- }, {
244
- "role": "user",
245
- "content": "Who are you?"
246
- }]
256
+ assert conversation == [
257
+ {
258
+ "role": "system",
259
+ "content": ""
260
+ },
261
+ {
262
+ "role": "user",
263
+ "content": "Who are you?"
264
+ },
265
+ ]
266
267
# Test openai format
268
conversation, _ = parse_chat_messages(
250
- [{
269
+ [
270
+ {
271
+ "role": "system",
272
+ "content": ""
273
+ },
274
+ {
275
+ "role": "user",
276
+ "content": [{
277
+ "type": "text",
278
+ "text": "Who are you?"
279
+ }],
280
+ },
281
+ ],
282
+ mistral_model_config,
283
+ mistral_tokenizer,
284
+ content_format="openai",
285
+ )
286
+ assert conversation == [
287
+ {
288
"role": "system",
252
- "content": ""
253
- }, {
289
+ "content": [{
290
+ "type": "text",
291
+ "text": ""
292
+ }]
293
+ },
294
+ {
295
"role": "user",
296
"content": [{
297
"type": "text",
298
"text": "Who are you?"
299
}]
259
- }],
260
- mistral_model_config,
261
- mistral_tokenizer,
262
- content_format="openai",
263
- )
264
- assert conversation == [{
265
- "role": "system",
266
- "content": [{
267
- "type": "text",
268
- "text": ""
269
- }]
270
- }, {
271
- "role":
272
- "user",
273
- "content": [{
274
- "type": "text",
275
- "text": "Who are you?"
276
- }]
277
- }]
300
+ },
301
+ ]
302
303
304
@pytest.mark.asyncio
@@ -287,15 +311,18 @@ async def test_parse_chat_messages_single_image_async(
311
[{
312
"role":
313
"user",
290
- "content": [{
291
- "type": "image_url",
292
- "image_url": {
293
- "url": image_url
294
- }
295
- }, {
296
- "type": "text",
297
- "text": "What's in the image?"
298
- }]
314
+ "content": [
315
+ {
316
+ "type": "image_url",
317
+ "image_url": {
318
+ "url": image_url
319
+ }
320
+ },
321
+ {
322
+ "type": "text",
323
+ "text": "What's in the image?"
324
+ },
325
+ ],
326
}],
327
phi3v_model_config,
328
phi3v_tokenizer,
@@ -318,18 +345,22 @@ def test_parse_chat_messages_multiple_images(
345
[{
346
"role":
347
"user",
321
- "content": [{
322
- "type": "image_url",
323
- "image_url": {
324
- "url": image_url
325
- }
326
- }, {
327
- "type": "image_pil",
328
- "image_pil": ImageAsset('cherry_blossom').pil_image
329
- }, {
330
- "type": "text",
331
- "text": "What's in these images?"
332
- }]
348
+ "content": [
349
+ {
350
+ "type": "image_url",
351
+ "image_url": {
352
+ "url": image_url
353
+ }
354
+ },
355
+ {
356
+ "type": "image_pil",
357
+ "image_pil": ImageAsset("cherry_blossom").pil_image,
358
+ },
359
+ {
360
+ "type": "text",
361
+ "text": "What's in these images?"
362
+ },
363
+ ],
364
}],
365
phi3v_model_config,
366
phi3v_tokenizer,
@@ -340,7 +371,7 @@ def test_parse_chat_messages_multiple_images(
371
"role":
372
"user",
373
"content":
343
- "<|image_1|>\n<|image_2|>\nWhat's in these images?"
374
+ "<|image_1|>\n<|image_2|>\nWhat's in these images?",
375
}]
376
_assert_mm_data_is_image_input(mm_data, 2)
377
@@ -355,18 +386,22 @@ async def test_parse_chat_messages_multiple_images_async(
386
[{
387
"role":
388
"user",
358
- "content": [{
359
- "type": "image_url",
360
- "image_url": {
361
- "url": image_url
362
- }
363
- }, {
364
- "type": "image_pil",
365
- "image_pil": ImageAsset('cherry_blossom').pil_image
366
- }, {
367
- "type": "text",
368
- "text": "What's in these images?"
369
- }]
389
+ "content": [
390
+ {
391
+ "type": "image_url",
392
+ "image_url": {
393
+ "url": image_url
394
+ }
395
+ },
396
+ {
397
+ "type": "image_pil",
398
+ "image_pil": ImageAsset("cherry_blossom").pil_image,
399
+ },
400
+ {
401
+ "type": "text",
402
+ "text": "What's in these images?"
403
+ },
404
+ ],
405
}],
406
phi3v_model_config,
407
phi3v_tokenizer,
@@ -377,7 +412,7 @@ async def test_parse_chat_messages_multiple_images_async(
412
"role":
413
"user",
414
"content":
380
- "<|image_1|>\n<|image_2|>\nWhat's in these images?"
415
+ "<|image_1|>\n<|image_2|>\nWhat's in these images?",
416
}]
417
_assert_mm_data_is_image_input(await mm_future, 2)
418
@@ -391,22 +426,26 @@ def test_parse_chat_messages_placeholder_already_in_prompt(
426
[{
427
"role":
428
"user",
394
- "content": [{
395
- "type": "image_url",
396
- "image_url": {
397
- "url": image_url
398
- }
399
- }, {
400
- "type": "image_url",
401
- "image_url": {
402
- "url": image_url
403
- }
404
- }, {
405
- "type":
406
- "text",
407
- "text":
408
- "What's in <|image_1|> and how does it compare to <|image_2|>?"
409
- }]
429
+ "content": [
430
+ {
431
+ "type": "image_url",
432
+ "image_url": {
433
+ "url": image_url
434
+ }
435
+ },
436
+ {
437
+ "type": "image_url",
438
+ "image_url": {
439
+ "url": image_url
440
+ }
441
+ },
442
+ {
443
+ "type":
444
+ "text",
445
+ "text":
446
+ "What's in <|image_1|> and how does it compare to <|image_2|>?", # noqa: E501
447
+ },
448
+ ],
449
}],
450
phi3v_model_config,
451
phi3v_tokenizer,
@@ -416,7 +455,7 @@ def test_parse_chat_messages_placeholder_already_in_prompt(
455
"role":
456
"user",
457
"content":
419
- "What's in <|image_1|> and how does it compare to <|image_2|>?"
458
+ "What's in <|image_1|> and how does it compare to <|image_2|>?",
459
}]
460
_assert_mm_data_is_image_input(mm_data, 2)
461
@@ -447,9 +486,9 @@ def test_parse_chat_messages_placeholder_one_already_in_prompt(
486
"type":
487
"text",
488
"text":
450
- "What's in <|image_1|> and how does it compare to the other one?" # noqa: E501
451
- }
452
- ]
489
+ "What's in <|image_1|> and how does it compare to the other one?", # noqa: E501
490
+ },
491
+ ],
492
}],
493
phi3v_model_config,
494
phi3v_tokenizer,
@@ -461,7 +500,7 @@ def test_parse_chat_messages_placeholder_one_already_in_prompt(
500
"user",
501
"content":
502
"<|image_2|>\nWhat's in <|image_1|> and how does it compare to the "
464
- "other one?"
503
+ "other one?",
504
}]
505
_assert_mm_data_is_image_input(mm_data, 2)
506
@@ -472,34 +511,44 @@ def test_parse_chat_messages_multiple_images_across_messages(
511
image_url,
512
):
513
conversation, mm_data = parse_chat_messages(
475
- [{
476
- "role":
477
- "user",
478
- "content": [{
479
- "type": "image_url",
480
- "image_url": {
481
- "url": image_url
482
- }
483
- }, {
484
- "type": "text",
485
- "text": "What's in this image?"
486
- }]
487
- }, {
488
- "role": "assistant",
489
- "content": "Some stuff."
490
- }, {
491
- "role":
492
- "user",
493
- "content": [{
494
- "type": "image_url",
495
- "image_url": {
496
- "url": image_url
497
- }
498
- }, {
499
- "type": "text",
500
- "text": "What about this one?"
501
- }]
502
- }],
514
+ [
515
+ {
516
+ "role":
517
+ "user",
518
+ "content": [
519
+ {
520
+ "type": "image_url",
521
+ "image_url": {
522
+ "url": image_url
523
+ }
524
+ },
525
+ {
526
+ "type": "text",
527
+ "text": "What's in this image?"
528
+ },
529
+ ],
530
+ },
531
+ {
532
+ "role": "assistant",
533
+ "content": "Some stuff."
534
+ },
535
+ {
536
+ "role":
537
+ "user",
538
+ "content": [
539
+ {
540
+ "type": "image_url",
541
+ "image_url": {
542
+ "url": image_url
543
+ }
544
+ },
545
+ {
546
+ "type": "text",
547
+ "text": "What about this one?"
548
+ },
549
+ ],
550
+ },
551
+ ],
552
phi3v_model_config,
553
phi3v_tokenizer,
554
content_format="string",
@@ -527,19 +576,23 @@ def test_parse_chat_messages_context_text_format(
576
phi3v_tokenizer,
577
):
578
conversation, mm_data = parse_chat_messages(
530
- [{
531
- "role": "user",
532
- "content": [{
533
- "type": "text",
534
- "text": "What's in this text?"
535
- }]
536
- }, {
537
- "role": "assistant",
538
- "content": "Some stuff."
539
- }, {
540
- "role": "user",
541
- "content": "What about this one?"
542
- }],
579
+ [
580
+ {
581
+ "role": "user",
582
+ "content": [{
583
+ "type": "text",
584
+ "text": "What's in this text?"
585
+ }],
586
+ },
587
+ {
588
+ "role": "assistant",
589
+ "content": "Some stuff."
590
+ },
591
+ {
592
+ "role": "user",
593
+ "content": "What about this one?"
594
+ },
595
+ ],
596
phi3v_model_config,
597
phi3v_tokenizer,
598
content_format="openai",
@@ -551,21 +604,21 @@ def test_parse_chat_messages_context_text_format(
604
"content": [{
605
"type": "text",
606
"text": "What's in this text?"
554
- }]
607
+ }],
608
},
609
{
610
"role": "assistant",
611
"content": [{
612
"type": "text",
613
"text": "Some stuff."
561
- }]
614
+ }],
615
},
616
{
617
"role": "user",
618
"content": [{
619
"type": "text",
620
"text": "What about this one?"
568
- }]
621
+ }],
622
},
623
]
624
@@ -578,31 +631,37 @@ def test_parse_chat_messages_rejects_too_many_images_in_one_message(
631
with warnings.catch_warnings():
632
warnings.filterwarnings(
633
"ignore",
581
- message="coroutine 'async_get_and_parse_image' was never awaited")
634
+ message="coroutine 'async_get_and_parse_image' was never awaited",
635
+ )
636
with pytest.raises(ValueError, match="At most"):
637
parse_chat_messages(
638
[{
639
"role":
640
"user",
587
- "content": [{
588
- "type": "image_url",
589
- "image_url": {
590
- "url": image_url
591
- }
592
- }, {
593
- "type": "image_url",
594
- "image_url": {
595
- "url": image_url
596
- }
597
- }, {
598
- "type": "image_url",
599
- "image_url": {
600
- "url": image_url
601
- }
602
- }, {
603
- "type": "text",
604
- "text": "What's in these images?"
605
- }]
641
+ "content": [
642
+ {
643
+ "type": "image_url",
644
+ "image_url": {
645
+ "url": image_url
646
+ },
647
+ },
648
+ {
649
+ "type": "image_url",
650
+ "image_url": {
651
+ "url": image_url
652
+ },
653
+ },
654
+ {
655
+ "type": "image_url",
656
+ "image_url": {
657
+ "url": image_url
658
+ },
659
+ },
660
+ {
661
+ "type": "text",
662
+ "text": "What's in these images?"
663
+ },
664
+ ],
665
}],
666
phi3v_model_config,
667
phi3v_tokenizer,
@@ -618,42 +677,54 @@ def test_parse_chat_messages_rejects_too_many_images_across_messages(
677
with warnings.catch_warnings():
678
warnings.filterwarnings(
679
"ignore",
621
- message="coroutine 'async_get_and_parse_image' was never awaited")
680
+ message="coroutine 'async_get_and_parse_image' was never awaited",
681
+ )
682
with pytest.raises(ValueError, match="At most"):
683
parse_chat_messages(
624
- [{
625
- "role":
626
- "user",
627
- "content": [{
628
- "type": "image_url",
629
- "image_url": {
630
- "url": image_url
631
- }
632
- }, {
633
- "type": "text",
634
- "text": "What's in this image?"
635
- }]
636
- }, {
637
- "role": "assistant",
638
- "content": "Some stuff."
639
- }, {
640
- "role":
641
- "user",
642
- "content": [{
643
- "type": "image_url",
644
- "image_url": {
645
- "url": image_url
646
- }
647
- }, {
648
- "type": "image_url",
649
- "image_url": {
650
- "url": image_url
651
- }
652
- }, {
653
- "type": "text",
654
- "text": "What about these two?"
655
- }]
656
- }],
684
+ [
685
+ {
686
+ "role":
687
+ "user",
688
+ "content": [
689
+ {
690
+ "type": "image_url",
691
+ "image_url": {
692
+ "url": image_url
693
+ },
694
+ },
695
+ {
696
+ "type": "text",
697
+ "text": "What's in this image?"
698
+ },
699
+ ],
700
+ },
701
+ {
702
+ "role": "assistant",
703
+ "content": "Some stuff."
704
+ },
705
+ {
706
+ "role":
707
+ "user",
708
+ "content": [
709
+ {
710
+ "type": "image_url",
711
+ "image_url": {
712
+ "url": image_url
713
+ },
714
+ },
715
+ {
716
+ "type": "image_url",
717
+ "image_url": {
718
+ "url": image_url
719
+ },
720
+ },
721
+ {
722
+ "type": "text",
723
+ "text": "What about these two?"
724
+ },
725
+ ],
726
+ },
727
+ ],
728
phi3v_model_config,
729
phi3v_tokenizer,
730
content_format="string",
@@ -670,12 +741,14 @@ def test_parse_chat_messages_multiple_images_uncommon_input(
741
"role":
742
"user",
743
"content": [
673
- "What's in these images?", {
744
+ "What's in these images?",
745
+ {
746
"image_url": image_url
675
- }, {
747
+ },
748
+ {
749
"image_url": image_url
677
- }
678
- ]
750
+ },
751
+ ],
752
}],
753
phi3v_model_config,
754
phi3v_tokenizer,
@@ -686,7 +759,7 @@ def test_parse_chat_messages_multiple_images_uncommon_input(
759
"role":
760
"user",
761
"content":
689
- "<|image_1|>\n<|image_2|>\nWhat's in these images?"
762
+ "<|image_1|>\n<|image_2|>\nWhat's in these images?",
763
}]
764
_assert_mm_data_is_image_input(mm_data, 2)
765
@@ -700,26 +773,32 @@ def test_parse_chat_messages_multiple_images_interleave(
773
[{
774
"role":
775
"user",
703
- "content": [{
704
- "type": "text",
705
- "text": "I need you to compare this image"
706
- }, {
707
- "type": "image_url",
708
- "image_url": {
709
- "url": image_url
710
- }
711
- }, {
712
- "type": "text",
713
- "text": "and this one"
714
- }, {
715
- "type": "image_url",
716
- "image_url": {
717
- "url": image_url
718
- }
719
- }, {
720
- "type": "text",
721
- "text": "Do they have differences?"
722
- }]
776
+ "content": [
777
+ {
778
+ "type": "text",
779
+ "text": "I need you to compare this image",
780
+ },
781
+ {
782
+ "type": "image_url",
783
+ "image_url": {
784
+ "url": image_url
785
+ }
786
+ },
787
+ {
788
+ "type": "text",
789
+ "text": "and this one"
790
+ },
791
+ {
792
+ "type": "image_url",
793
+ "image_url": {
794
+ "url": image_url
795
+ }
796
+ },
797
+ {
798
+ "type": "text",
799
+ "text": "Do they have differences?"
800
+ },
801
+ ],
802
}],
803
phi3v_model_config_mm_interleaved,
804
phi3v_tokenizer,
@@ -731,7 +810,7 @@ def test_parse_chat_messages_multiple_images_interleave(
810
"user",
811
"content":
812
"I need you to compare this image\n<|image_1|>\nand this one\n<|image_2|>\n" # noqa: E501
734
- "Do they have differences?"
813
+ "Do they have differences?",
814
}]
815
_assert_mm_data_is_image_input(mm_data, 2)
816
@@ -746,26 +825,32 @@ async def test_parse_chat_messages_multiple_images_interleave_async(
825
[{
826
"role":
827
"user",
749
- "content": [{
750
- "type": "text",
751
- "text": "I need you to compare this image"
752
- }, {
753
- "type": "image_url",
754
- "image_url": {
755
- "url": image_url
756
- }
757
- }, {
758
- "type": "text",
759
- "text": "and this one"
760
- }, {
761
- "type": "image_url",
762
- "image_url": {
763
- "url": image_url
764
- }
765
- }, {
766
- "type": "text",
767
- "text": "Do they have differences?"
768
- }]
828
+ "content": [
829
+ {
830
+ "type": "text",
831
+ "text": "I need you to compare this image",
832
+ },
833
+ {
834
+ "type": "image_url",
835
+ "image_url": {
836
+ "url": image_url
837
+ }
838
+ },
839
+ {
840
+ "type": "text",
841
+ "text": "and this one"
842
+ },
843
+ {
844
+ "type": "image_url",
845
+ "image_url": {
846
+ "url": image_url
847
+ }
848
+ },
849
+ {
850
+ "type": "text",
851
+ "text": "Do they have differences?"
852
+ },
853
+ ],
854
}],
855
phi3v_model_config_mm_interleaved,
856
phi3v_tokenizer,
@@ -777,7 +862,7 @@ async def test_parse_chat_messages_multiple_images_interleave_async(
862
"user",
863
"content":
864
"I need you to compare this image\n<|image_1|>\nand this one\n<|image_2|>\n" # noqa: E501
780
- "Do they have differences?"
865
+ "Do they have differences?",
866
}]
867
_assert_mm_data_is_image_input(await mm_data, 2)
868
@@ -788,135 +873,161 @@ def test_parse_chat_messages_multiple_images_multiple_messages_interleave(
873
image_url,
874
):
875
conversation, mm_data = parse_chat_messages(
791
- [{
792
- "role":
793
- "user",
794
- "content": [
795
- {
796
- "type": "text",
797
- "text": "What's on this image?"
798
- },
799
- {
800
- "type": "image_url",
801
- "image_url": {
802
- "url": image_url
803
- }
804
- },
805
- {
806
- "type": "text",
807
- "text": "Be accurate."
808
- },
809
- ]
810
- }, {
811
- "role": "assistant",
812
- "content": "Some stuff."
813
- }, {
814
- "role":
815
- "user",
816
- "content": [{
817
- "type": "text",
818
- "text": "What's on this image?"
819
- }, {
820
- "type": "image_url",
821
- "image_url": {
822
- "url": image_url
823
- }
824
- }]
825
- }],
876
+ [
877
+ {
878
+ "role":
879
+ "user",
880
+ "content": [
881
+ {
882
+ "type": "text",
883
+ "text": "What's on this image?"
884
+ },
885
+ {
886
+ "type": "image_url",
887
+ "image_url": {
888
+ "url": image_url
889
+ }
890
+ },
891
+ {
892
+ "type": "text",
893
+ "text": "Be accurate."
894
+ },
895
+ ],
896
+ },
897
+ {
898
+ "role": "assistant",
899
+ "content": "Some stuff."
900
+ },
901
+ {
902
+ "role":
903
+ "user",
904
+ "content": [
905
+ {
906
+ "type": "text",
907
+ "text": "What's on this image?"
908
+ },
909
+ {
910
+ "type": "image_url",
911
+ "image_url": {
912
+ "url": image_url
913
+ }
914
+ },
915
+ ],
916
+ },
917
+ ],
918
phi3v_model_config_mm_interleaved,
919
phi3v_tokenizer,
920
content_format="string",
921
)
922
831
- assert conversation == [{
832
- "role":
833
- "user",
834
- "content":
835
- "What's on this image?\n<|image_1|>\nBe accurate."
836
- }, {
837
- "role": "assistant",
838
- "content": "Some stuff."
839
- }, {
840
- "role": "user",
841
- "content": "What's on this image?\n<|image_2|>"
842
- }]
923
+ assert conversation == [
924
+ {
925
+ "role": "user",
926
+ "content": "What's on this image?\n<|image_1|>\nBe accurate.",
927
+ },
928
+ {
929
+ "role": "assistant",
930
+ "content": "Some stuff."
931
+ },
932
+ {
933
+ "role": "user",
934
+ "content": "What's on this image?\n<|image_2|>"
935
+ },
936
+ ]
937
_assert_mm_data_is_image_input(mm_data, 2)
938
939
940
def test_parse_chat_messages_multiple_modals_multiple_messages_interleave(
847
- qwen25omni_model_config_mm_interleaved, qwen25omni_tokenizer,
848
- image_url, video_url, audio_url):
941
+ qwen25omni_model_config_mm_interleaved,
942
+ qwen25omni_tokenizer,
943
+ image_url,
944
+ video_url,
945
+ audio_url,
946
+):
947
conversation, mm_data = parse_chat_messages(
850
- [{
948
+ [
949
+ {
950
+ "role":
951
+ "user",
952
+ "content": [
953
+ {
954
+ "type": "text",
955
+ "text": "What's on this image?"
956
+ },
957
+ {
958
+ "type": "image_url",
959
+ "image_url": {
960
+ "url": image_url
961
+ }
962
+ },
963
+ {
964
+ "type": "text",
965
+ "text": "Now listen to this audio"
966
+ },
967
+ {
968
+ "type": "audio_url",
969
+ "audio_url": {
970
+ "url": audio_url
971
+ }
972
+ },
973
+ ],
974
+ },
975
+ {
976
+ "role": "assistant",
977
+ "content": "Some stuff."
978
+ },
979
+ {
980
+ "role":
981
+ "user",
982
+ "content": [
983
+ {
984
+ "type": "text",
985
+ "text": "What's on this image?"
986
+ },
987
+ {
988
+ "type": "image_url",
989
+ "image_url": {
990
+ "url": image_url
991
+ }
992
+ },
993
+ {
994
+ "type": "text",
995
+ "text": "And what's in the video?"
996
+ },
997
+ {
998
+ "type": "video_url",
999
+ "video_url": {
1000
+ "url": video_url
1001
+ }
1002
+ },
1003
+ ],
1004
+ },
1005
+ ],
1006
+ qwen25omni_model_config_mm_interleaved,
1007
+ qwen25omni_tokenizer,
1008
+ content_format="string",
1009
+ )
1010
+
1011
+ assert conversation == [
1012
+ {
1013
"role":
1014
"user",
853
- "content": [
854
- {
855
- "type": "text",
856
- "text": "What's on this image?"
857
- },
858
- {
859
- "type": "image_url",
860
- "image_url": {
861
- "url": image_url
862
- }
863
- },
864
- {
865
- "type": "text",
866
- "text": "Now listen to this audio"
867
- },
868
- {
869
- "type": "audio_url",
870
- "audio_url": {
871
- "url": audio_url
872
- }
873
- },
874
- ]
875
- }, {
1015
+ "content":
1016
+ "What's on this image?\n<|vision_start|><|IMAGE|><|vision_end|>\n"
1017
+ "Now listen to this audio\nAudio 1: <|audio_bos|><|AUDIO|><|audio_eos|>", # noqa: E501
1018
+ },
1019
+ {
1020
"role": "assistant",
1021
"content": "Some stuff."
878
- }, {
1022
+ },
1023
+ {
1024
"role":
1025
"user",
881
- "content": [{
882
- "type": "text",
883
- "text": "What's on this image?"
884
- }, {
885
- "type": "image_url",
886
- "image_url": {
887
- "url": image_url
888
- }
889
- }, {
890
- "type": "text",
891
- "text": "And what's in the video?"
892
- }, {
893
- "type": "video_url",
894
- "video_url": {
895
- "url": video_url
896
- }
897
- }]
898
- }],
899
- qwen25omni_model_config_mm_interleaved,
900
- qwen25omni_tokenizer,
901
- content_format="string",
902
- )
903
-
904
- assert conversation == [{
905
- "role":
906
- "user",
907
- "content":
908
- "What's on this image?\n<|vision_start|><|IMAGE|><|vision_end|>\n"
909
- "Now listen to this audio\nAudio 1: <|audio_bos|><|AUDIO|><|audio_eos|>"
910
- }, {
911
- "role": "assistant",
912
- "content": "Some stuff."
913
- }, {
914
- "role":
915
- "user",
916
- "content":
917
- "What's on this image?\n<|vision_start|><|IMAGE|><|vision_end|>\n"
918
- "And what's in the video?\n<|vision_start|><|VIDEO|><|vision_end|>"
919
- }]
1026
+ "content":
1027
+ "What's on this image?\n<|vision_start|><|IMAGE|><|vision_end|>\n"
1028
+ "And what's in the video?\n<|vision_start|><|VIDEO|><|vision_end|>",
1029
+ },
1030
+ ]
1031
1032
_assert_mm_data_inputs(mm_data, {"image": 2, "video": 1, "audio": 1})
1033
@@ -929,7 +1040,8 @@ def test_parse_chat_messages_multiple_images_interleave_with_placeholders(
1040
with pytest.raises(
1041
ValueError,
1042
match=r"Found more '<|image_1|>' placeholders in input prompt "
932
- "than actual multimodal data items."):
1043
+ "than actual multimodal data items.",
1044
+ ):
1045
parse_chat_messages(
1046
[{
1047
"role":
@@ -952,9 +1064,9 @@ def test_parse_chat_messages_multiple_images_interleave_with_placeholders(
1064
"text",
1065
"text":
1066
"I need you to compare this image\n<|image_1|>\nand this one\n<|image_2|>\n" # noqa: E501
955
- "Do they have differences?"
1067
+ "Do they have differences?",
1068
},
957
- ]
1069
+ ],
1070
}],
1071
phi3v_model_config_mm_interleaved,
1072
phi3v_tokenizer,
@@ -973,12 +1085,15 @@ def test_mllama_single_image(
1085
[{
1086
"role":
1087
"user",
976
- "content": [{
977
- 'type': 'text',
978
- 'text': 'The content of this image is:'
979
- }, {
980
- "image_url": image_url
981
- }]
1088
+ "content": [
1089
+ {
1090
+ "type": "text",
1091
+ "text": "The content of this image is:"
1092
+ },
1093
+ {
1094
+ "image_url": image_url
1095
+ },
1096
+ ],
1097
}],
1098
mllama_model_config,
1099
mllama_tokenizer,
@@ -986,14 +1101,17 @@ def test_mllama_single_image(
1101
)
1102
_assert_mm_data_is_image_input(mm_data, 1)
1103
assert conversation == [{
989
- 'role':
990
- 'user',
991
- 'content': [{
992
- 'type': 'text',
993
- 'text': 'The content of this image is:'
994
- }, {
995
- 'type': 'image'
996
- }]
1104
+ "role":
1105
+ "user",
1106
+ "content": [
1107
+ {
1108
+ "type": "text",
1109
+ "text": "The content of this image is:"
1110
+ },
1111
+ {
1112
+ "type": "image"
1113
+ },
1114
+ ],
1115
}]
1116
1117
@@ -1009,20 +1127,20 @@ def test_mllama_interleaved_images(
1127
"user",
1128
"content": [
1129
{
1012
- 'type': 'text',
1013
- 'text': 'The content of the first image is:'
1130
+ "type": "text",
1131
+ "text": "The content of the first image is:",
1132
},
1133
{
1134
"image_url": image_url
1135
},
1136
{
1019
- 'type': 'text',
1020
- 'text': 'The content of the second image is:'
1137
+ "type": "text",
1138
+ "text": "The content of the second image is:",
1139
},
1140
{
1141
"image_url": image_url
1142
},
1025
- ]
1143
+ ],
1144
}],
1145
mllama_model_config,
1146
mllama_tokenizer,
@@ -1030,19 +1148,24 @@ def test_mllama_interleaved_images(
1148
)
1149
_assert_mm_data_is_image_input(mm_data, 2)
1150
assert conversation == [{
1033
- 'role':
1034
- 'user',
1035
- 'content': [{
1036
- 'type': 'text',
1037
- 'text': 'The content of the first image is:'
1038
- }, {
1039
- 'type': 'image'
1040
- }, {
1041
- 'type': 'text',
1042
- 'text': 'The content of the second image is:'
1043
- }, {
1044
- 'type': 'image'
1045
- }]
1151
+ "role":
1152
+ "user",
1153
+ "content": [
1154
+ {
1155
+ "type": "text",
1156
+ "text": "The content of the first image is:"
1157
+ },
1158
+ {
1159
+ "type": "image"
1160
+ },
1161
+ {
1162
+ "type": "text",
1163
+ "text": "The content of the second image is:"
1164
+ },
1165
+ {
1166
+ "type": "image"
1167
+ },
1168
+ ],
1169
}]
1170
1171
@@ -1053,34 +1176,36 @@ def test_multimodal_image_parsing_matches_hf(model, image_url):
1176
def get_conversation(is_hf: bool):
1177
img_part = {"type": "image_url", "image_url": {"url": image_url}}
1178
if is_hf:
1056
- img_part = {'type': 'image'}
1179
+ img_part = {"type": "image"}
1180
return [{
1058
- 'role':
1059
- 'user',
1060
- 'content': [
1181
+ "role":
1182
+ "user",
1183
+ "content": [
1184
{
1062
- 'type': 'text',
1063
- 'text': 'The content of the first image is:'
1185
+ "type": "text",
1186
+ "text": "The content of the first image is:",
1187
},
1188
img_part,
1189
{
1067
- 'type': 'text',
1068
- 'text': 'The content of the second image is:'
1190
+ "type": "text",
1191
+ "text": "The content of the second image is:",
1192
},
1193
img_part,
1194
{
1072
- 'type': 'text',
1073
- 'text': 'What animal is in the first image?'
1195
+ "type": "text",
1196
+ "text": "What animal is in the first image?",
1197
},
1075
- ]
1198
+ ],
1199
}]
1200
1201
# Build a config for the model
1079
- model_config = ModelConfig(model,
1080
- runner="generate",
1081
- limit_mm_per_prompt={
1082
- "image": 2,
1083
- })
1202
+ model_config = ModelConfig(
1203
+ model,
1204
+ runner="generate",
1205
+ limit_mm_per_prompt={
1206
+ "image": 2,
1207
+ },
1208
+ )
1209
1210
# Build the tokenizer group and grab the underlying tokenizer
1211
tokenizer_group = TokenizerGroup(
@@ -1126,7 +1251,8 @@ def test_multimodal_image_parsing_matches_hf(model, image_url):
1251
[
1252
QWEN2VL_MODEL_ID, # tokenizer.chat_template is of type str
1253
HERMES_MODEL_ID, # tokenizer.chat_template is of type dict
1129
- ])
1254
+ ],
1255
+)
1256
@pytest.mark.parametrize("use_tools", [True, False])
1257
def test_resolve_hf_chat_template(sample_json_schema, model, use_tools):
1258
"""checks that chat_template is a dict type for HF models."""
@@ -1152,14 +1278,14 @@ def test_resolve_hf_chat_template(sample_json_schema, model, use_tools):
1278
)
1279
tokenizer = tokenizer_group.tokenizer
1280
1155
- tools = [{
1281
+ tools = ([{
1282
"type": "function",
1283
"function": {
1284
"name": "dummy_function_name",
1285
"description": "This is a dummy function",
1160
- "parameters": sample_json_schema
1161
- }
1162
- }] if use_tools else None
1286
+ "parameters": sample_json_schema,
1287
+ },
1288
+ }] if use_tools else None)
1289
1290
# Test detecting the tokenizer's chat_template
1291
chat_template = resolve_hf_chat_template(
vllm/entrypoints/chat_utils.py
+228
-150
@@ -103,6 +103,7 @@ class PILImage(BaseModel):
103
"""
104
A PIL.Image.Image object.
105
"""
106
+
107
image_pil: Image.Image
108
model_config = ConfigDict(arbitrary_types_allowed=True)
109
@@ -115,6 +116,7 @@ class CustomChatCompletionContentPILImageParam(TypedDict, total=False):
116
"image_pil": ImageAsset('cherry_blossom').pil_image
117
}
118
"""
119
+
120
image_pil: Required[PILImage]
121
122
@@ -127,6 +129,7 @@ class CustomChatCompletionContentSimpleImageParam(TypedDict, total=False):
129
"image_url": "https://example.com/image.jpg"
130
}
131
"""
132
+
133
image_url: Required[str]
134
135
@@ -138,6 +141,7 @@ class CustomChatCompletionContentSimpleAudioParam(TypedDict, total=False):
141
"audio_url": "https://example.com/audio.mp3"
142
}
143
"""
144
+
145
audio_url: Required[str]
146
147
@@ -149,6 +153,7 @@ class CustomChatCompletionContentSimpleVideoParam(TypedDict, total=False):
153
"video_url": "https://example.com/video.mp4"
154
}
155
"""
156
+
157
video_url: Required[str]
158
159
@@ -174,19 +179,24 @@ class CustomThinkCompletionContentParam(TypedDict, total=False):
179
180
181
ChatCompletionContentPartParam: TypeAlias = Union[
177
- OpenAIChatCompletionContentPartParam, ChatCompletionContentPartAudioParam,
182
+ OpenAIChatCompletionContentPartParam,
183
+ ChatCompletionContentPartAudioParam,
184
ChatCompletionContentPartInputAudioParam,
179
- ChatCompletionContentPartVideoParam, ChatCompletionContentPartRefusalParam,
185
+ ChatCompletionContentPartVideoParam,
186
+ ChatCompletionContentPartRefusalParam,
187
CustomChatCompletionContentPILImageParam,
188
CustomChatCompletionContentSimpleImageParam,
189
ChatCompletionContentPartImageEmbedsParam,
190
CustomChatCompletionContentSimpleAudioParam,
184
- CustomChatCompletionContentSimpleVideoParam, str,
185
- CustomThinkCompletionContentParam]
191
+ CustomChatCompletionContentSimpleVideoParam,
192
+ str,
193
+ CustomThinkCompletionContentParam,
194
+]
195
196
197
class CustomChatCompletionMessageParam(TypedDict, total=False):
198
"""Enables custom roles in the Chat Completion API."""
199
+
200
role: Required[str]
201
"""The role of the message's author."""
202
@@ -207,9 +217,11 @@ class CustomChatCompletionMessageParam(TypedDict, total=False):
217
"""The tool calls generated by the model, such as function calls."""
218
219
210
-ChatCompletionMessageParam = Union[OpenAIChatCompletionMessageParam,
211
- CustomChatCompletionMessageParam,
212
- OpenAIHarmonyMessage]
220
+ChatCompletionMessageParam = Union[
221
+ OpenAIChatCompletionMessageParam,
222
+ CustomChatCompletionMessageParam,
223
+ OpenAIHarmonyMessage,
224
+]
225
226
227
# TODO: Make fields ReadOnly once mypy supports it
@@ -262,13 +274,13 @@ def _is_var_or_elems_access(
274
key: Optional[str] = None,
275
) -> bool:
276
if isinstance(node, jinja2.nodes.Filter):
265
- return (node.node is not None
266
- and _is_var_or_elems_access(node.node, varname, key))
277
+ return node.node is not None and _is_var_or_elems_access(
278
+ node.node, varname, key)
279
if isinstance(node, jinja2.nodes.Test):
280
return _is_var_or_elems_access(node.node, varname, key)
281
270
- if (isinstance(node, jinja2.nodes.Getitem)
271
- and isinstance(node.arg, jinja2.nodes.Slice)):
282
+ if isinstance(node, jinja2.nodes.Getitem) and isinstance(
283
+ node.arg, jinja2.nodes.Slice):
284
return _is_var_or_elems_access(node.node, varname, key)
285
286
# yapf: disable
@@ -373,15 +385,18 @@ def resolve_mistral_chat_template(
385
) -> Optional[str]:
386
if chat_template is not None:
387
logger.warning_once(
376
- "'chat_template' cannot be overridden for mistral tokenizer.")
388
+ "'chat_template' cannot be overridden for mistral tokenizer."
389
+ )
390
if "add_generation_prompt" in kwargs:
391
logger.warning_once(
392
"'add_generation_prompt' is not supported for mistral tokenizer, "
380
- "so it will be ignored.")
393
+ "so it will be ignored."
394
+ )
395
if "continue_final_message" in kwargs:
396
logger.warning_once(
397
"'continue_final_message' is not supported for mistral tokenizer, "
384
- "so it will be ignored.")
398
+ "so it will be ignored."
399
+ )
400
return None
401
402
@@ -401,23 +416,35 @@ def resolve_hf_chat_template(
416
try:
417
processor = cached_get_processor(
418
tokenizer.name_or_path,
404
- processor_cls=(PreTrainedTokenizer, PreTrainedTokenizerFast,
405
- ProcessorMixin),
419
+ processor_cls=(
420
+ PreTrainedTokenizer,
421
+ PreTrainedTokenizerFast,
422
+ ProcessorMixin,
423
+ ),
424
trust_remote_code=model_config.trust_remote_code,
425
)
408
- if isinstance(processor, ProcessorMixin) and \
409
- hasattr(processor, 'chat_template') and \
410
- processor.chat_template is not None:
426
+ if (
427
+ isinstance(processor, ProcessorMixin)
428
+ and hasattr(processor, "chat_template")
429
+ and processor.chat_template is not None
430
+ ):
431
return processor.chat_template
432
except Exception:
413
- logger.debug("Failed to load AutoProcessor chat template for %s", tokenizer.name_or_path, exc_info=True) # noqa: E501
433
+ logger.debug(
434
+ "Failed to load AutoProcessor chat template for %s",
435
+ tokenizer.name_or_path,
436
+ exc_info=True,
437
+ ) # noqa: E501
438
439
# 3rd priority: AutoTokenizer chat template
440
try:
441
return tokenizer.get_chat_template(chat_template, tools=tools)
442
except Exception:
419
- logger.debug("Failed to load AutoTokenizer chat template for %s",
420
- tokenizer.name_or_path, exc_info=True)
443
+ logger.debug(
444
+ "Failed to load AutoTokenizer chat template for %s",
445
+ tokenizer.name_or_path,
446
+ exc_info=True,
447
+ )
448
449
# 4th priority: Predefined fallbacks
450
path = get_chat_template_fallback_path(
@@ -425,12 +452,16 @@ def resolve_hf_chat_template(
452
tokenizer_name_or_path=model_config.tokenizer,
453
)
454
if path is not None:
428
- logger.info("Loading chat template fallback for %s as there isn't one "
429
- "defined on HF Hub.", tokenizer.name_or_path)
455
+ logger.info(
456
+ "Loading chat template fallback for %s as there isn't one "
457
+ "defined on HF Hub.",
458
+ tokenizer.name_or_path,
459
+ )
460
chat_template = load_chat_template(path)
461
else:
432
- logger.debug("There is no chat template fallback for %s",
433
- tokenizer.name_or_path)
462
+ logger.debug(
463
+ "There is no chat template fallback for %s", tokenizer.name_or_path
464
+ )
465
466
return chat_template
467
@@ -452,11 +483,17 @@ def _resolve_chat_template_content_format(
483
else:
484
hf_chat_template = None
485
455
- jinja_text = (hf_chat_template if isinstance(hf_chat_template, str)
456
- else load_chat_template(chat_template, is_literal=True))
486
+ jinja_text = (
487
+ hf_chat_template
488
+ if isinstance(hf_chat_template, str)
489
+ else load_chat_template(chat_template, is_literal=True)
490
+ )
491
458
- detected_format = ("string" if jinja_text is None else
459
- _detect_content_format(jinja_text, default="string"))
492
+ detected_format = (
493
+ "string"
494
+ if jinja_text is None
495
+ else _detect_content_format(jinja_text, default="string")
496
+ )
497
498
return detected_format
499
@@ -512,7 +549,6 @@ def resolve_chat_template_content_format(
549
return detected_format
550
551
515
-
552
ModalityStr = Literal["image", "audio", "video", "image_embeds"]
553
_T = TypeVar("_T")
554
@@ -539,6 +575,7 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
575
@cached_property
576
def model_cls(self) -> type[SupportsMultiModal]:
577
from vllm.model_executor.model_loader import get_model_cls
578
+
579
model_cls = get_model_cls(self.model_config)
580
return cast(type[SupportsMultiModal], model_cls)
581
@@ -574,28 +611,29 @@ class BaseMultiModalItemTracker(ABC, Generic[_T]):
611
612
613
class MultiModalItemTracker(BaseMultiModalItemTracker[object]):
577
-
614
def all_mm_data(self) -> Optional[MultiModalDataDict]:
615
if not self._items_by_modality:
616
return None
617
mm_inputs = {}
618
items_by_modality = dict(self._items_by_modality)
619
if "image" in items_by_modality and "image_embeds" in items_by_modality:
584
- raise ValueError(\
585
- "Mixing raw image and embedding inputs is not allowed")
620
+ raise ValueError(
621
+ "Mixing raw image and embedding inputs is not allowed"
622
+ )
623
624
if "image_embeds" in items_by_modality:
625
image_embeds_lst = items_by_modality["image_embeds"]
626
if len(image_embeds_lst) > 1:
590
- raise ValueError(\
591
- "Only one message can have {'type': 'image_embeds'}")
627
+ raise ValueError(
628
+ "Only one message can have {'type': 'image_embeds'}"
629
+ )
630
mm_inputs["image"] = image_embeds_lst[0]
631
if "image" in items_by_modality:
594
- mm_inputs["image"] = items_by_modality["image"] # A list of images
632
+ mm_inputs["image"] = items_by_modality["image"] # A list of images
633
if "audio" in items_by_modality:
596
- mm_inputs["audio"] = items_by_modality["audio"] # A list of audios
634
+ mm_inputs["audio"] = items_by_modality["audio"] # A list of audios
635
if "video" in items_by_modality:
598
- mm_inputs["video"] = items_by_modality["video"] # A list of videos
636
+ mm_inputs["video"] = items_by_modality["video"] # A list of videos
637
return mm_inputs
638
639
def create_parser(self) -> "BaseMultiModalContentParser":
@@ -603,32 +641,33 @@ class MultiModalItemTracker(BaseMultiModalItemTracker[object]):
641
642
643
class AsyncMultiModalItemTracker(BaseMultiModalItemTracker[Awaitable[object]]):
606
-
644
async def all_mm_data(self) -> Optional[MultiModalDataDict]:
645
if not self._items_by_modality:
646
return None
647
mm_inputs = {}
648
items_by_modality = {
612
- modality: await asyncio.gather(*items)
613
- for modality, items in self._items_by_modality.items()
614
- }
649
+ modality: await asyncio.gather(*items)
650
+ for modality, items in self._items_by_modality.items()
651
+ }
652
653
if "image" in items_by_modality and "image_embeds" in items_by_modality:
654
raise ValueError(
618
- "Mixing raw image and embedding inputs is not allowed")
655
+ "Mixing raw image and embedding inputs is not allowed"
656
+ )
657
658
if "image_embeds" in items_by_modality:
659
image_embeds_lst = items_by_modality["image_embeds"]
660
if len(image_embeds_lst) > 1:
661
raise ValueError(
624
- "Only one message can have {'type': 'image_embeds'}")
662
+ "Only one message can have {'type': 'image_embeds'}"
663
+ )
664
mm_inputs["image"] = image_embeds_lst[0]
665
if "image" in items_by_modality:
627
- mm_inputs["image"] = items_by_modality["image"] # A list of images
666
+ mm_inputs["image"] = items_by_modality["image"] # A list of images
667
if "audio" in items_by_modality:
629
- mm_inputs["audio"] = items_by_modality["audio"] # A list of audios
668
+ mm_inputs["audio"] = items_by_modality["audio"] # A list of audios
669
if "video" in items_by_modality:
631
- mm_inputs["video"] = items_by_modality["video"] # A list of videos
670
+ mm_inputs["video"] = items_by_modality["video"] # A list of videos
671
return mm_inputs
672
673
def create_parser(self) -> "BaseMultiModalContentParser":
@@ -636,7 +675,6 @@ class AsyncMultiModalItemTracker(BaseMultiModalItemTracker[Awaitable[object]]):
675
676
677
class BaseMultiModalContentParser(ABC):
639
-
678
def __init__(self) -> None:
679
super().__init__()
680
@@ -648,8 +686,9 @@ class BaseMultiModalContentParser(ABC):
686
# }
687
self._placeholder_storage: dict[str, list] = defaultdict(list)
688
651
- def _add_placeholder(self, modality: ModalityStr,
652
- placeholder: Optional[str]):
689
+ def _add_placeholder(
690
+ self, modality: ModalityStr, placeholder: Optional[str]
691
+ ):
692
mod_placeholder = MODALITY_PLACEHOLDERS_MAP[modality]
693
if placeholder:
694
self._placeholder_storage[mod_placeholder].append(placeholder)
@@ -662,8 +701,9 @@ class BaseMultiModalContentParser(ABC):
701
raise NotImplementedError
702
703
@abstractmethod
665
- def parse_image_embeds(self,
666
- image_embeds: Union[str, dict[str, str]]) -> None:
704
+ def parse_image_embeds(
705
+ self, image_embeds: Union[str, dict[str, str]]
706
+ ) -> None:
707
raise NotImplementedError
708
709
@abstractmethod
@@ -684,7 +724,6 @@ class BaseMultiModalContentParser(ABC):
724
725
726
class MultiModalContentParser(BaseMultiModalContentParser):
687
-
727
def __init__(self, tracker: MultiModalItemTracker) -> None:
728
super().__init__()
729
@@ -701,8 +740,9 @@ class MultiModalContentParser(BaseMultiModalContentParser):
740
placeholder = self._tracker.add("image", image)
741
self._add_placeholder("image", placeholder)
742
704
- def parse_image_embeds(self,
705
- image_embeds: Union[str, dict[str, str]]) -> None:
743
+ def parse_image_embeds(
744
+ self, image_embeds: Union[str, dict[str, str]]
745
+ ) -> None:
746
if isinstance(image_embeds, dict):
747
embeds = {
748
k: self._connector.fetch_image_embedding(v)
@@ -741,14 +781,13 @@ class MultiModalContentParser(BaseMultiModalContentParser):
781
782
783
class AsyncMultiModalContentParser(BaseMultiModalContentParser):
744
-
784
def __init__(self, tracker: AsyncMultiModalItemTracker) -> None:
785
super().__init__()
786
787
self._tracker = tracker
788
self._connector = MediaConnector(
789
media_io_kwargs=self._tracker._model_config.media_io_kwargs,
751
- allowed_local_media_path=tracker.allowed_local_media_path
790
+ allowed_local_media_path=tracker.allowed_local_media_path,
791
)
792
793
def parse_image(self, image_url: str) -> None:
@@ -757,8 +796,9 @@ class AsyncMultiModalContentParser(BaseMultiModalContentParser):
796
placeholder = self._tracker.add("image", image_coro)
797
self._add_placeholder("image", placeholder)
798
760
- def parse_image_embeds(self,
761
- image_embeds: Union[str, dict[str, str]]) -> None:
799
+ def parse_image_embeds(
800
+ self, image_embeds: Union[str, dict[str, str]]
801
+ ) -> None:
802
future: asyncio.Future[Union[str, dict[str, str]]] = asyncio.Future()
803
804
if isinstance(image_embeds, dict):
@@ -769,8 +809,7 @@ class AsyncMultiModalContentParser(BaseMultiModalContentParser):
809
future.set_result(embeds)
810
811
if isinstance(image_embeds, str):
772
- embedding = self._connector.\
773
- fetch_image_embedding(image_embeds)
812
+ embedding = self._connector.fetch_image_embedding(image_embeds)
813
future.set_result(embedding)
814
815
placeholder = self._tracker.add("image_embeds", future)
@@ -809,20 +848,23 @@ def validate_chat_template(chat_template: Optional[Union[Path, str]]):
848
return
849
850
elif isinstance(chat_template, Path) and not chat_template.exists():
812
- raise FileNotFoundError(
813
- "the supplied chat template path doesn't exist")
851
+ raise FileNotFoundError("the supplied chat template path doesn't exist")
852
853
elif isinstance(chat_template, str):
854
JINJA_CHARS = "{}\n"
817
- if not any(c in chat_template
818
- for c in JINJA_CHARS) and not Path(chat_template).exists():
855
+ if (
856
+ not any(c in chat_template for c in JINJA_CHARS)
857
+ and not Path(chat_template).exists()
858
+ ):
859
raise ValueError(
860
f"The supplied chat template string ({chat_template}) "
821
- f"appears path-like, but doesn't exist!")
861
+ f"appears path-like, but doesn't exist!"
862
+ )
863
864
else:
865
raise TypeError(
825
- f"{type(chat_template)} is not a valid chat template type")
866
+ f"{type(chat_template)} is not a valid chat template type"
867
+ )
868
869
870
def _load_chat_template(
@@ -835,8 +877,9 @@ def _load_chat_template(
877
878
if is_literal:
879
if isinstance(chat_template, Path):
838
- raise TypeError("chat_template is expected to be read directly "
839
- "from its value")
880
+ raise TypeError(
881
+ "chat_template is expected to be read directly from its value"
882
+ )
883
884
return chat_template
885
@@ -849,9 +892,11 @@ def _load_chat_template(
892
893
JINJA_CHARS = "{}\n"
894
if not any(c in chat_template for c in JINJA_CHARS):
852
- msg = (f"The supplied chat template ({chat_template}) "
853
- f"looks like a file path, but it failed to be "
854
- f"opened. Reason: {e}")
895
+ msg = (
896
+ f"The supplied chat template ({chat_template}) "
897
+ f"looks like a file path, but it failed to be "
898
+ f"opened. Reason: {e}"
899
+ )
900
raise ValueError(msg) from e
901
902
# If opening a file fails, set chat template to be args to
@@ -870,8 +915,9 @@ def load_chat_template(
915
return _cached_load_chat_template(chat_template, is_literal=is_literal)
916
917
873
-def _get_interleaved_text_prompt(placeholder_storage: dict[str, list],
874
- texts: list[str]) -> str:
918
+def _get_interleaved_text_prompt(
919
+ placeholder_storage: dict[str, list], texts: list[str]
920
+) -> str:
921
for idx, elem in enumerate(texts):
922
if elem in placeholder_storage:
923
texts[idx] = placeholder_storage[elem].pop(0)
@@ -881,10 +927,11 @@ def _get_interleaved_text_prompt(placeholder_storage: dict[str, list],
927
928
# TODO: Let user specify how to insert multimodal tokens into prompt
929
# (similar to chat template)
884
-def _get_full_multimodal_text_prompt(placeholder_storage: dict[str, list],
885
- texts: list[str],
886
- interleave_strings: bool
887
- ) -> str:
930
+def _get_full_multimodal_text_prompt(
931
+ placeholder_storage: dict[str, list],
932
+ texts: list[str],
933
+ interleave_strings: bool,
934
+) -> str:
935
"""Combine multimodal prompts for a multimodal language model."""
936
937
# flatten storage to make it looks like
@@ -907,7 +954,6 @@ def _get_full_multimodal_text_prompt(placeholder_storage: dict[str, list],
954
# Look through the text prompt to check for missing placeholders
955
missing_placeholders: list[str] = []
956
for placeholder in placeholder_counts:
910
-
957
# For any existing placeholder in the text prompt, we leave it as is
958
placeholder_counts[placeholder] -= text_prompt.count(placeholder)
959
@@ -916,15 +962,18 @@ def _get_full_multimodal_text_prompt(placeholder_storage: dict[str, list],
962
"Placeholder count is negative! "
963
"Ensure that the 'interleave_strings' flag is disabled "
964
"(current value: %s) "
919
- "when manually placing image placeholders.", interleave_strings
965
+ "when manually placing image placeholders.",
966
+ interleave_strings,
967
)
968
logger.debug("Input prompt: %s", text_prompt)
969
raise ValueError(
970
f"Found more '{placeholder}' placeholders in input prompt than "
924
- "actual multimodal data items.")
971
+ "actual multimodal data items."
972
+ )
973
926
- missing_placeholders.extend([placeholder] *
927
- placeholder_counts[placeholder])
974
+ missing_placeholders.extend(
975
+ [placeholder] * placeholder_counts[placeholder]
976
+ )
977
978
# NOTE: Default behaviour: we always add missing placeholders
979
# at the front of the prompt, if interleave_strings=False
@@ -944,7 +993,8 @@ _AudioParser = TypeAdapter(ChatCompletionContentPartAudioParam).validate_python
993
_VideoParser = TypeAdapter(ChatCompletionContentPartVideoParam).validate_python
994
995
_ResponsesInputImageParser = TypeAdapter(
947
- ResponseInputImageParam).validate_python
996
+ ResponseInputImageParam
997
+).validate_python
998
_ContentPart: TypeAlias = Union[str, dict[str, str], InputAudio, PILImage]
999
1000
# Define a mapping from part types to their corresponding parsing functions.
@@ -952,32 +1002,35 @@ MM_PARSER_MAP: dict[
1002
str,
1003
Callable[[ChatCompletionContentPartParam], _ContentPart],
1004
] = {
955
- "text":
956
- lambda part: _TextParser(part).get("text", None),
957
- "thinking":
958
- lambda part: _ThinkParser(part).get("thinking", None),
959
- "input_text":
960
- lambda part: _TextParser(part).get("text", None),
961
- "input_image":
962
- lambda part: _ResponsesInputImageParser(part).get("image_url", None),
963
- "image_url":
964
- lambda part: _ImageParser(part).get("image_url", {}).get("url", None),
965
- "image_embeds":
966
- lambda part: _ImageEmbedsParser(part).get("image_embeds", None),
1005
+ "text": lambda part: _TextParser(part).get("text", None),
1006
+ "thinking": lambda part: _ThinkParser(part).get("thinking", None),
1007
+ "input_text": lambda part: _TextParser(part).get("text", None),
1008
+ "input_image": lambda part: _ResponsesInputImageParser(part).get(
1009
+ "image_url", None
1010
+ ),
1011
+ "image_url": lambda part: _ImageParser(part)
1012
+ .get("image_url", {})
1013
+ .get("url", None),
1014
+ "image_embeds": lambda part: _ImageEmbedsParser(part).get(
1015
+ "image_embeds", None
1016
+ ),
1017
"image_pil": lambda part: _PILImageParser(part).get("image_pil", None),
968
- "audio_url":
969
- lambda part: _AudioParser(part).get("audio_url", {}).get("url", None),
970
- "input_audio":
971
- lambda part: _InputAudioParser(part).get("input_audio", None),
972
- "refusal":
973
- lambda part: _RefusalParser(part).get("refusal", None),
974
- "video_url":
975
- lambda part: _VideoParser(part).get("video_url", {}).get("url", None),
1018
+ "audio_url": lambda part: _AudioParser(part)
1019
+ .get("audio_url", {})
1020
+ .get("url", None),
1021
+ "input_audio": lambda part: _InputAudioParser(part).get(
1022
+ "input_audio", None
1023
+ ),
1024
+ "refusal": lambda part: _RefusalParser(part).get("refusal", None),
1025
+ "video_url": lambda part: _VideoParser(part)
1026
+ .get("video_url", {})
1027
+ .get("url", None),
1028
}
1029
1030
1031
def _parse_chat_message_content_mm_part(
980
- part: ChatCompletionContentPartParam) -> tuple[str, _ContentPart]:
1032
+ part: ChatCompletionContentPartParam,
1033
+) -> tuple[str, _ContentPart]:
1034
"""
1035
Parses a given multi-modal content part based on its type.
1036
@@ -993,7 +1046,8 @@ def _parse_chat_message_content_mm_part(
1046
ValueError: If the 'type' field is missing and no direct URL is found.
1047
"""
1048
assert isinstance(
996
- part, dict) # This is needed to avoid mypy errors: part.get() from str
1049
+ part, dict
1050
+ ) # This is needed to avoid mypy errors: part.get() from str
1051
part_type = part.get("type", None)
1052
1053
if isinstance(part_type, str) and part_type in MM_PARSER_MAP:
@@ -1002,8 +1056,10 @@ def _parse_chat_message_content_mm_part(
1056
# Special case for 'image_url.detail'
1057
# We only support 'auto', which is the default
1058
if part_type == "image_url" and part.get("detail", "auto") != "auto":
1005
- logger.warning("'image_url.detail' is currently not supported "
1006
- "and will be ignored.")
1059
+ logger.warning(
1060
+ "'image_url.detail' is currently not supported "
1061
+ "and will be ignored."
1062
+ )
1063
1064
return part_type, content
1065
@@ -1011,19 +1067,22 @@ def _parse_chat_message_content_mm_part(
1067
# 'type' is required field by pydantic
1068
if part_type is None:
1069
if part.get("image_url") is not None:
1014
- image_params = cast(CustomChatCompletionContentSimpleImageParam,
1015
- part)
1070
+ image_params = cast(
1071
+ CustomChatCompletionContentSimpleImageParam, part
1072
+ )
1073
return "image_url", image_params.get("image_url", "")
1074
if part.get("audio_url") is not None:
1018
- audio_params = cast(CustomChatCompletionContentSimpleAudioParam,
1019
- part)
1075
+ audio_params = cast(
1076
+ CustomChatCompletionContentSimpleAudioParam, part
1077
+ )
1078
return "audio_url", audio_params.get("audio_url", "")
1079
if part.get("input_audio") is not None:
1080
input_audio_params = cast(dict[str, str], part)
1081
return "input_audio", input_audio_params
1082
if part.get("video_url") is not None:
1025
- video_params = cast(CustomChatCompletionContentSimpleVideoParam,
1026
- part)
1083
+ video_params = cast(
1084
+ CustomChatCompletionContentSimpleVideoParam, part
1085
+ )
1086
return "video_url", video_params.get("video_url", "")
1087
# Raise an error if no 'type' or direct URL is found.
1088
raise ValueError("Missing 'type' field in multimodal part.")
@@ -1033,9 +1092,16 @@ def _parse_chat_message_content_mm_part(
1092
return part_type, "unknown part_type content"
1093
1094
1036
-VALID_MESSAGE_CONTENT_MM_PART_TYPES = ("text", "refusal", "image_url",
1037
- "image_embeds", "image_pil",
1038
- "audio_url", "input_audio", "video_url")
1095
+VALID_MESSAGE_CONTENT_MM_PART_TYPES = (
1096
+ "text",
1097
+ "refusal",
1098
+ "image_url",
1099
+ "image_embeds",
1100
+ "image_pil",
1101
+ "audio_url",
1102
+ "input_audio",
1103
+ "video_url",
1104
+)
1105
1106
1107
def _parse_chat_message_content_parts(
@@ -1055,21 +1121,20 @@ def _parse_chat_message_content_parts(
1121
part,
1122
mm_parser,
1123
wrap_dicts=wrap_dicts,
1058
- interleave_strings=interleave_strings
1124
+ interleave_strings=interleave_strings,
1125
)
1126
if parse_res:
1127
content.append(parse_res)
1128
1129
if wrap_dicts:
1130
# Parsing wraps images and texts as interleaved dictionaries
1065
- return [ConversationMessage(role=role,
1066
- content=content)] # type: ignore
1131
+ return [ConversationMessage(role=role, content=content)] # type: ignore
1132
texts = cast(list[str], content)
1133
mm_placeholder_storage = mm_parser.mm_placeholder_storage()
1134
if mm_placeholder_storage:
1070
- text_prompt = _get_full_multimodal_text_prompt(mm_placeholder_storage,
1071
- texts,
1072
- interleave_strings)
1135
+ text_prompt = _get_full_multimodal_text_prompt(
1136
+ mm_placeholder_storage, texts, interleave_strings
1137
+ )
1138
else:
1139
text_prompt = "\n".join(texts)
1140
@@ -1099,13 +1164,16 @@ def _parse_chat_message_content_part(
1164
if part_type in VALID_MESSAGE_CONTENT_MM_PART_TYPES and content is None:
1165
logger.warning(
1166
"Skipping multimodal part '%s' (type: '%s') "
1102
- "with empty / unparsable content.", part, part_type)
1167
+ "with empty / unparsable content.",
1168
+ part,
1169
+ part_type,
1170
+ )
1171
return None
1172
1173
if part_type in ("text", "input_text", "refusal", "thinking"):
1174
str_content = cast(str, content)
1175
if wrap_dicts:
1108
- return {'type': 'text', 'text': str_content}
1176
+ return {"type": "text", "text": str_content}
1177
else:
1178
return str_content
1179
@@ -1137,8 +1205,12 @@ def _parse_chat_message_content_part(
1205
else:
1206
raise NotImplementedError(f"Unknown part type: {part_type}")
1207
1140
- return {'type': modality} if wrap_dicts else (
1141
- MODALITY_PLACEHOLDERS_MAP[modality] if interleave_strings else None
1208
+ return (
1209
+ {"type": modality}
1210
+ if wrap_dicts
1211
+ else (
1212
+ MODALITY_PLACEHOLDERS_MAP[modality] if interleave_strings else None
1213
+ )
1214
)
1215
1216
@@ -1171,14 +1243,16 @@ def _parse_chat_message_content(
1243
)
1244
1245
for result_msg in result:
1174
- if role == 'assistant':
1246
+ if role == "assistant":
1247
parsed_msg = _AssistantParser(message)
1248
1249
# The 'tool_calls' is not None check ensures compatibility.
1250
# It's needed only if downstream code doesn't strictly
1251
# follow the OpenAI spec.
1180
- if ("tool_calls" in parsed_msg
1181
- and parsed_msg["tool_calls"] is not None):
1252
+ if (
1253
+ "tool_calls" in parsed_msg
1254
+ and parsed_msg["tool_calls"] is not None
1255
+ ):
1256
result_msg["tool_calls"] = list(parsed_msg["tool_calls"])
1257
elif role == "tool":
1258
parsed_msg = _ToolParser(message)
@@ -1198,12 +1272,15 @@ def _postprocess_messages(messages: list[ConversationMessage]) -> None:
1272
# so, for messages that have tool_calls, parse the string (which we get
1273
# from openAI format) to dict
1274
for message in messages:
1201
- if (message["role"] == "assistant" and "tool_calls" in message
1202
- and isinstance(message["tool_calls"], list)):
1203
-
1275
+ if (
1276
+ message["role"] == "assistant"
1277
+ and "tool_calls" in message
1278
+ and isinstance(message["tool_calls"], list)
1279
+ ):
1280
for item in message["tool_calls"]:
1281
item["function"]["arguments"] = json.loads(
1206
- item["function"]["arguments"])
1282
+ item["function"]["arguments"]
1283
+ )
1284
1285
1286
def parse_chat_messages(
@@ -1224,7 +1301,7 @@ def parse_chat_messages(
1301
content_format == "string"
1302
and model_config.multimodal_config is not None
1303
and model_config.multimodal_config.interleave_mm_strings
1227
- )
1304
+ ),
1305
)
1306
1307
conversation.extend(sub_messages)
@@ -1252,7 +1329,7 @@ def parse_chat_messages_futures(
1329
content_format == "string"
1330
and model_config.multimodal_config is not None
1331
and model_config.multimodal_config.interleave_mm_strings
1255
- )
1332
+ ),
1333
)
1334
1335
conversation.extend(sub_messages)
@@ -1283,10 +1360,10 @@ def apply_hf_chat_template(
1360
raise ValueError(
1361
"As of transformers v4.44, default chat template is no longer "
1362
"allowed, so you must provide a chat template if the tokenizer "
1286
- "does not define one.")
1363
+ "does not define one."
1364
+ )
1365
1366
try:
1289
-
1367
return tokenizer.apply_chat_template(
1368
conversation=conversation, # type: ignore[arg-type]
1369
tools=tools, # type: ignore[arg-type]
@@ -1298,13 +1375,14 @@ def apply_hf_chat_template(
1375
# External library exceptions can sometimes occur despite the framework's
1376
# internal exception management capabilities.
1377
except Exception as e:
1301
-
1378
# Log and report any library-related exceptions for further
1379
# investigation.
1380
logger.exception(
1305
- "An error occurred in `transformers` while applying chat template")
1381
+ "An error occurred in `transformers` while applying chat template"
1382
+ )
1383
raise ValueError(str(e)) from e
1384
1385
+
1386
def apply_mistral_chat_template(
1387
tokenizer: MistralTokenizer,
1388
messages: list[ChatCompletionMessageParam],
@@ -1337,26 +1415,26 @@ def apply_mistral_chat_template(
1415
# External library exceptions can sometimes occur despite the framework's
1416
# internal exception management capabilities.
1417
except Exception as e:
1340
-
1418
# Log and report any library-related exceptions for further
1419
# investigation.
1420
logger.exception(
1344
- "An error occurred in `mistral_common` while applying chat "
1345
- "template")
1421
+ "An error occurred in `mistral_common` while applying chat template"
1422
+ )
1423
raise ValueError(str(e)) from e
1424
1425
+
1426
def get_history_tool_calls_cnt(conversation: list[ConversationMessage]):
1427
idx = 0
1428
for msg in conversation:
1351
- if msg['role'] == 'assistant':
1352
- tool_calls = msg.get('tool_calls')
1353
- idx += len(list(tool_calls)) if tool_calls is not None else 0 # noqa
1429
+ if msg["role"] == "assistant":
1430
+ tool_calls = msg.get("tool_calls")
1431
+ idx += len(list(tool_calls)) if tool_calls is not None else 0 # noqa
1432
return idx
1433
1356
-def make_tool_call_id(id_type:str='random', func_name=None, idx=None):
1434
1358
- if id_type=='kimi_k2':
1359
- return f'functions.{func_name}:{idx}'
1435
+def make_tool_call_id(id_type: str = "random", func_name=None, idx=None):
1436
+ if id_type == "kimi_k2":
1437
+ return f"functions.{func_name}:{idx}"
1438
else:
1439
# by default return random
1440
return f"chatcmpl-tool-{random_uuid()}"
vllm/entrypoints/openai/serving_engine.py
+141
-88
@@ -82,16 +82,26 @@ from vllm.utils import (AsyncMicrobatchTokenizer, is_list_of,
82
83
logger = init_logger(__name__)
84
85
-CompletionLikeRequest = Union[CompletionRequest, DetokenizeRequest,
86
- EmbeddingCompletionRequest, RerankRequest,
87
- ClassificationRequest, ScoreRequest,
88
- TokenizeCompletionRequest]
85
+CompletionLikeRequest = Union[
86
+ CompletionRequest,
87
+ DetokenizeRequest,
88
+ EmbeddingCompletionRequest,
89
+ RerankRequest,
90
+ ClassificationRequest,
91
+ ScoreRequest,
92
+ TokenizeCompletionRequest,
93
+]
94
95
ChatLikeRequest = Union[ChatCompletionRequest, EmbeddingChatRequest,
96
TokenizeChatRequest]
97
SpeechToTextRequest = Union[TranscriptionRequest, TranslationRequest]
93
-AnyRequest = Union[CompletionLikeRequest, ChatLikeRequest, SpeechToTextRequest,
94
- ResponsesRequest, IOProcessorRequest]
98
+AnyRequest = Union[
99
+ CompletionLikeRequest,
100
+ ChatLikeRequest,
101
+ SpeechToTextRequest,
102
+ ResponsesRequest,
103
+ IOProcessorRequest,
104
+]
105
106
AnyResponse = Union[
107
CompletionResponse,
@@ -135,6 +145,7 @@ class RequestProcessingMixin(BaseModel):
145
Mixin for request processing,
146
handling prompt preparation and engine input.
147
"""
148
+
149
request_prompts: Optional[Sequence[RequestPrompt]] = []
150
engine_prompts: Optional[Union[list[EngineTokensPrompt],
151
list[EngineEmbedsPrompt]]] = []
@@ -147,6 +158,7 @@ class ResponseGenerationMixin(BaseModel):
158
Mixin for response generation,
159
managing result generators and final batch results.
160
"""
161
+
162
result_generator: Optional[AsyncGenerator[tuple[int, Union[
163
RequestOutput, PoolingRequestOutput]], None]] = None
164
final_res_batch: list[Union[RequestOutput, PoolingRequestOutput]] = Field(
@@ -155,8 +167,12 @@ class ResponseGenerationMixin(BaseModel):
167
model_config = ConfigDict(arbitrary_types_allowed=True)
168
169
158
-class ServeContext(RequestProcessingMixin, ResponseGenerationMixin, BaseModel,
159
- Generic[RequestT]):
170
+class ServeContext(
171
+ RequestProcessingMixin,
172
+ ResponseGenerationMixin,
173
+ BaseModel,
174
+ Generic[RequestT],
175
+):
176
# Shared across all requests
177
request: RequestT
178
raw_request: Optional[Request] = None
@@ -298,8 +314,8 @@ class OpenAIServing:
314
truncate_prompt_tokens = getattr(ctx.request, "truncate_prompt_tokens",
315
None)
316
301
- if truncate_prompt_tokens is not None and \
302
- truncate_prompt_tokens > self.max_model_len:
317
+ if (truncate_prompt_tokens is not None
318
+ and truncate_prompt_tokens > self.max_model_len):
319
return self.create_error_response(
320
"truncate_prompt_tokens value is "
321
"greater than max_model_len."
@@ -344,10 +360,12 @@ class OpenAIServing:
360
return self.create_error_response(
361
"Request prompts not available")
362
347
- self._log_inputs(request_id_item,
348
- ctx.request_prompts[i],
349
- params=pooling_params,
350
- lora_request=ctx.lora_request)
363
+ self._log_inputs(
364
+ request_id_item,
365
+ ctx.request_prompts[i],
366
+ params=pooling_params,
367
+ lora_request=ctx.lora_request,
368
+ )
369
370
# Mypy has an existing bug related to inferring the variance of
371
# TypedDicts with `builtins.enumerate`:
@@ -410,10 +428,11 @@ class OpenAIServing:
428
return self.create_error_response(str(e))
429
430
def create_error_response(
413
- self,
414
- message: str,
415
- err_type: str = "BadRequestError",
416
- status_code: HTTPStatus = HTTPStatus.BAD_REQUEST) -> ErrorResponse:
431
+ self,
432
+ message: str,
433
+ err_type: str = "BadRequestError",
434
+ status_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
435
+ ) -> ErrorResponse:
436
if self.log_error_stack:
437
exc_type, _, _ = sys.exc_info()
438
if exc_type is not None:
@@ -424,10 +443,11 @@ class OpenAIServing:
443
message=message, type=err_type, code=status_code.value))
444
445
def create_streaming_error_response(
427
- self,
428
- message: str,
429
- err_type: str = "BadRequestError",
430
- status_code: HTTPStatus = HTTPStatus.BAD_REQUEST) -> str:
446
+ self,
447
+ message: str,
448
+ err_type: str = "BadRequestError",
449
+ status_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
450
+ ) -> str:
451
json_str = json.dumps(
452
self.create_error_response(message=message,
453
err_type=err_type,
@@ -438,25 +458,25 @@ class OpenAIServing:
458
self,
459
request: AnyRequest,
460
) -> Optional[ErrorResponse]:
441
-
461
error_response = None
462
463
if self._is_model_supported(request.model):
464
return None
465
if request.model in self.models.lora_requests:
466
return None
448
- if envs.VLLM_ALLOW_RUNTIME_LORA_UPDATING and request.model and (
449
- load_result := await self.models.resolve_lora(request.model)):
467
+ if (envs.VLLM_ALLOW_RUNTIME_LORA_UPDATING and request.model and
468
+ (load_result := await self.models.resolve_lora(request.model))):
469
if isinstance(load_result, LoRARequest):
470
return None
452
- if isinstance(load_result, ErrorResponse) and \
453
- load_result.error.code == HTTPStatus.BAD_REQUEST.value:
471
+ if (isinstance(load_result, ErrorResponse) and
472
+ load_result.error.code == HTTPStatus.BAD_REQUEST.value):
473
error_response = load_result
474
475
return error_response or self.create_error_response(
476
message=f"The model `{request.model}` does not exist.",
477
err_type="NotFoundError",
459
- status_code=HTTPStatus.NOT_FOUND)
478
+ status_code=HTTPStatus.NOT_FOUND,
479
+ )
480
481
def _get_active_default_mm_loras(
482
self, request: AnyRequest) -> Optional[LoRARequest]:
@@ -487,7 +507,6 @@ class OpenAIServing:
507
request: AnyRequest,
508
supports_default_mm_loras: bool = False,
509
) -> Optional[LoRARequest]:
490
-
510
if request.model in self.models.lora_requests:
511
return self.models.lora_requests[request.model]
512
@@ -548,13 +567,15 @@ class OpenAIServing:
567
prompt,
568
add_special_tokens=add_special_tokens,
569
truncation=True,
551
- max_length=self.max_model_len)
570
+ max_length=self.max_model_len,
571
+ )
572
else:
573
encoded = await async_tokenizer(
574
prompt,
575
add_special_tokens=add_special_tokens,
576
truncation=True,
557
- max_length=truncate_prompt_tokens)
577
+ max_length=truncate_prompt_tokens,
578
+ )
579
580
input_ids = encoded.input_ids
581
input_text = prompt
@@ -595,16 +616,22 @@ class OpenAIServing:
616
617
# Note: EmbeddingRequest, ClassificationRequest,
618
# and ScoreRequest doesn't have max_tokens
598
- if isinstance(request,
599
- (EmbeddingChatRequest, EmbeddingCompletionRequest,
600
- ScoreRequest, RerankRequest, ClassificationRequest)):
601
-
619
+ if isinstance(
620
+ request,
621
+ (
622
+ EmbeddingChatRequest,
623
+ EmbeddingCompletionRequest,
624
+ ScoreRequest,
625
+ RerankRequest,
626
+ ClassificationRequest,
627
+ ),
628
+ ):
629
# Note: input length can be up to the entire model context length
630
# since these requests don't generate tokens.
631
if token_num > self.max_model_len:
632
operations: dict[type[AnyRequest], str] = {
633
ScoreRequest: "score",
607
- ClassificationRequest: "classification"
634
+ ClassificationRequest: "classification",
635
}
636
operation = operations.get(type(request),
637
"embedding generation")
@@ -618,8 +645,11 @@ class OpenAIServing:
645
646
# Note: TokenizeRequest and DetokenizeRequest doesn't have max_tokens
647
# and does not require model context length validation
621
- if isinstance(request, (TokenizeCompletionRequest, TokenizeChatRequest,
622
- DetokenizeRequest)):
648
+ if isinstance(
649
+ request,
650
+ (TokenizeCompletionRequest, TokenizeChatRequest,
651
+ DetokenizeRequest),
652
+ ):
653
return TextTokensPrompt(prompt=input_text,
654
prompt_token_ids=input_ids)
655
@@ -639,8 +669,8 @@ class OpenAIServing:
669
f"{token_num} input tokens. Please reduce the length of "
670
"the input messages.")
671
642
- if max_tokens is not None and \
643
- token_num + max_tokens > self.max_model_len:
672
+ if (max_tokens is not None
673
+ and token_num + max_tokens > self.max_model_len):
674
raise ValueError(
675
"'max_tokens' or 'max_completion_tokens' is too large: "
676
f"{max_tokens}. This model's maximum context length is "
@@ -745,13 +775,14 @@ class OpenAIServing:
775
tasks = []
776
for prompt_input in batch_inputs:
777
if prompt_input["is_tokens"] is False:
748
- assert tokenizer is not None, \
749
- "Tokenizer is required for text prompts"
778
+ assert tokenizer is not None, (
779
+ "Tokenizer is required for text prompts")
780
task = self._normalize_prompt_text_to_input(
781
request,
782
prompt_input["content"],
783
tokenizer=tokenizer,
754
- add_special_tokens=add_special_tokens)
784
+ add_special_tokens=add_special_tokens,
785
+ )
786
else:
787
task = self._normalize_prompt_tokens_to_input(
788
request, prompt_input["content"], tokenizer=tokenizer)
@@ -766,9 +797,14 @@ class OpenAIServing:
797
@overload
798
async def _preprocess_completion(
799
self,
769
- request: Union[DetokenizeRequest, EmbeddingCompletionRequest,
770
- RerankRequest, ClassificationRequest, ScoreRequest,
771
- TokenizeCompletionRequest],
800
+ request: Union[
801
+ DetokenizeRequest,
802
+ EmbeddingCompletionRequest,
803
+ RerankRequest,
804
+ ClassificationRequest,
805
+ ScoreRequest,
806
+ TokenizeCompletionRequest,
807
+ ],
808
tokenizer: Optional[AnyTokenizer],
809
input_or_inputs: Union[str, list[str], list[int], list[list[int]]],
810
add_special_tokens: bool = ...,
@@ -783,8 +819,10 @@ class OpenAIServing:
819
input_or_inputs: Optional[Union[str, list[str], list[int],
820
list[list[int]]]],
821
add_special_tokens: bool = ...,
786
- ) -> tuple[list[Union[TextTokensPrompt, EmbedsPrompt]], list[Union[
787
- EngineTokensPrompt, EngineEmbedsPrompt]]]:
822
+ ) -> tuple[
823
+ list[Union[TextTokensPrompt, EmbedsPrompt]],
824
+ list[Union[EngineTokensPrompt, EngineEmbedsPrompt]],
825
+ ]:
826
...
827
828
async def _preprocess_completion(
@@ -794,32 +832,38 @@ class OpenAIServing:
832
input_or_inputs: Optional[Union[str, list[str], list[int],
833
list[list[int]]]],
834
add_special_tokens: bool = True,
797
- ) -> tuple[Union[list[TextTokensPrompt], list[Union[
798
- TextTokensPrompt, EmbedsPrompt]]], Union[
799
- list[EngineTokensPrompt], list[Union[EngineTokensPrompt,
800
- EngineEmbedsPrompt]]]]:
801
- if not isinstance(request,
802
- CompletionRequest) and input_or_inputs is None:
835
+ ) -> tuple[
836
+ Union[list[TextTokensPrompt], list[Union[TextTokensPrompt,
837
+ EmbedsPrompt]]],
838
+ Union[
839
+ list[EngineTokensPrompt],
840
+ list[Union[EngineTokensPrompt, EngineEmbedsPrompt]],
841
+ ],
842
+ ]:
843
+ if (not isinstance(request, CompletionRequest)
844
+ and input_or_inputs is None):
845
raise ValueError(
846
"Prompt embeds with non-completion requests is not"
847
" currently supported.")
848
807
- (request_prompts_text, request_prompts_embeds
808
- ) = await self._tokenize_prompt_input_or_inputs_async(
809
- request,
810
- tokenizer,
811
- input_or_inputs,
812
- add_special_tokens=add_special_tokens,
813
- )
849
+ (
850
+ request_prompts_text,
851
+ request_prompts_embeds,
852
+ ) = await self._tokenize_prompt_input_or_inputs_async(
853
+ request,
854
+ tokenizer,
855
+ input_or_inputs,
856
+ add_special_tokens=add_special_tokens,
857
+ )
858
859
engine_prompts_text = [
860
EngineTokensPrompt(
861
prompt_token_ids=request_prompt_text["prompt_token_ids"])
862
for request_prompt_text in request_prompts_text
863
]
820
- cache_salt = request.cache_salt if (
821
- hasattr(request, "cache_salt")
822
- and request.cache_salt is not None) else None
864
+ cache_salt = (request.cache_salt if
865
+ (hasattr(request, "cache_salt")
866
+ and request.cache_salt is not None) else None)
867
if cache_salt:
868
for prompt_text in engine_prompts_text:
869
prompt_text["cache_salt"] = cache_salt
@@ -831,8 +875,8 @@ class OpenAIServing:
875
# non-completion requests and if we don't add the overload here,
876
# everywhere this function is used outside of serving_completion will
877
# need logic asserting that only text prompts are in the request.
834
- if not isinstance(request,
835
- CompletionRequest) and input_or_inputs is not None:
878
+ if (not isinstance(request, CompletionRequest)
879
+ and input_or_inputs is not None):
880
return request_prompts_text, engine_prompts_text
881
882
engine_prompts_embeds = [
@@ -862,8 +906,11 @@ class OpenAIServing:
906
chat_template_kwargs: Optional[dict[str, Any]] = None,
907
tool_parser: Optional[Callable[[AnyTokenizer], ToolParser]] = None,
908
add_special_tokens: bool = False,
865
- ) -> tuple[list[ConversationMessage], Sequence[RequestPrompt],
866
- list[EngineTokensPrompt]]:
909
+ ) -> tuple[
910
+ list[ConversationMessage],
911
+ Sequence[RequestPrompt],
912
+ list[EngineTokensPrompt],
913
+ ]:
914
model_config = self.model_config
915
916
resolved_content_format = resolve_chat_template_content_format(
@@ -925,8 +972,8 @@ class OpenAIServing:
972
973
if tokenizer is None:
974
assert isinstance(request_prompt, str), (
928
- "Prompt has to be a string", \
929
- "when the tokenizer is not initialised"
975
+ "Prompt has to be a string",
976
+ "when the tokenizer is not initialised",
977
)
978
prompt_inputs = TextTokensPrompt(prompt=request_prompt,
979
prompt_token_ids=[1])
@@ -943,7 +990,8 @@ class OpenAIServing:
990
"Prompt has to be either a string or a list of token ids")
991
prompt_inputs = TextTokensPrompt(
992
prompt=tokenizer.decode(request_prompt),
946
- prompt_token_ids=request_prompt)
993
+ prompt_token_ids=request_prompt,
994
+ )
995
996
engine_prompt = EngineTokensPrompt(
997
prompt_token_ids=prompt_inputs["prompt_token_ids"])
@@ -1007,22 +1055,23 @@ class OpenAIServing:
1055
prompt_token_ids=prompt_token_ids)
1056
request_prompt = prompt_token_ids
1057
# Update the sampling params.
1010
- sampling_params.max_tokens = (self.max_model_len -
1011
- len(prompt_token_ids))
1058
+ sampling_params.max_tokens = self.max_model_len - len(
1059
+ prompt_token_ids)
1060
# OPTIMIZATION
1061
priority = orig_priority - 1
1062
1063
@staticmethod
1064
def _load_prompt_embeds(
1065
prompt_embeds: Optional[Union[bytes, list[bytes]]],
1018
- truncate_prompt_tokens: Optional[Annotated[int, Field(ge=1)]] = None
1066
+ truncate_prompt_tokens: Optional[Annotated[int, Field(ge=1)]] = None,
1067
) -> list[EmbedsPrompt]:
1068
1069
def _load_and_validate_embed(embed: bytes) -> EmbedsPrompt:
1022
- tensor = torch.load(io.BytesIO(
1023
- pybase64.b64decode(embed, validate=True)),
1024
- weights_only=True,
1025
- map_location=torch.device("cpu"))
1070
+ tensor = torch.load(
1071
+ io.BytesIO(pybase64.b64decode(embed, validate=True)),
1072
+ weights_only=True,
1073
+ map_location=torch.device("cpu"),
1074
+ )
1075
assert isinstance(tensor, torch.Tensor) and tensor.dtype in (
1076
torch.float32,
1077
torch.bfloat16,
@@ -1061,7 +1110,7 @@ class OpenAIServing:
1110
prompt = inputs
1111
elif isinstance(inputs, list):
1112
prompt_token_ids = inputs
1064
- elif 'prompt_embeds' in inputs:
1113
+ elif "prompt_embeds" in inputs:
1114
prompt_embeds = inputs.get("prompt_embeds")
1115
else:
1116
prompt = inputs["prompt"]
@@ -1101,10 +1150,12 @@ class OpenAIServing:
1150
return raw_request.headers.get("X-Request-Id", default)
1151
1152
@staticmethod
1104
- def _get_decoded_token(logprob: Logprob,
1105
- token_id: int,
1106
- tokenizer: AnyTokenizer,
1107
- return_as_token_id: bool = False) -> str:
1153
+ def _get_decoded_token(
1154
+ logprob: Logprob,
1155
+ token_id: int,
1156
+ tokenizer: AnyTokenizer,
1157
+ return_as_token_id: bool = False,
1158
+ ) -> str:
1159
if return_as_token_id:
1160
return f"token_id:{token_id}"
1161
@@ -1117,9 +1168,11 @@ class OpenAIServing:
1168
return True
1169
return self.models.is_base_model(model_name)
1170
1120
- def _get_model_name(self,
1121
- model_name: Optional[str] = None,
1122
- lora_request: Optional[LoRARequest] = None) -> str:
1171
+ def _get_model_name(
1172
+ self,
1173
+ model_name: Optional[str] = None,
1174
+ lora_request: Optional[LoRARequest] = None,
1175
+ ) -> str:
1176
if lora_request:
1177
return lora_request.lora_name
1178
if not model_name:
@@ -1129,7 +1182,7 @@ class OpenAIServing:
1182
1183
def clamp_prompt_logprobs(
1184
prompt_logprobs: Union[PromptLogprobs,
1132
- None]) -> Union[PromptLogprobs, None]:
1185
+ None], ) -> Union[PromptLogprobs, None]:
1186
if prompt_logprobs is None:
1187
return prompt_logprobs
1188
@@ -1137,6 +1190,6 @@ def clamp_prompt_logprobs(
1190
if logprob_dict is None:
1191
continue
1192
for logprob_values in logprob_dict.values():
1140
- if logprob_values.logprob == float('-inf'):
1193
+ if logprob_values.logprob == float("-inf"):
1194
logprob_values.logprob = -9999.0
1195
return prompt_logprobs