Skip to content

Commit 2c764c0

Browse files
Added real /v1/embeddings support for messages + chat_template_kw (#45173)
Signed-off-by: Taneem Ibrahim <taneem.ibrahim@gmail.com>
1 parent c621af1 commit 2c764c0

5 files changed

Lines changed: 400 additions & 10 deletions

File tree

tests/entrypoints/pooling/embed/test_io_processor.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,108 @@
33
"""Unit tests for EmbedIOProcessor."""
44

55
import pytest
6+
from pydantic import TypeAdapter
67

78
from vllm import PoolingParams
89
from vllm.entrypoints.pooling.embed.io_processor import EmbedIOProcessor
910
from vllm.entrypoints.pooling.embed.protocol import (
1011
CohereEmbedContent,
1112
CohereEmbedInput,
1213
CohereEmbedRequest,
14+
EmbeddingBatchChatInputRequest,
15+
EmbeddingBatchChatRequest,
16+
EmbeddingChatInputRequest,
17+
EmbeddingChatRequest,
18+
EmbeddingCompletionRequest,
19+
EmbeddingRequest,
1320
)
1421
from vllm.entrypoints.pooling.typing import PoolingServeContext
1522

1623

24+
class TestEmbeddingRequestParsing:
25+
"""Unit tests for OpenAI embedding request parsing."""
26+
27+
def test_input_messages_parses_as_chat_request(self):
28+
request = TypeAdapter(EmbeddingRequest).validate_python(
29+
{
30+
"model": "test",
31+
"input": [{"role": "user", "content": "hello"}],
32+
"chat_template_kwargs": {"instruction": "Represent the query: "},
33+
}
34+
)
35+
36+
assert isinstance(request, EmbeddingChatInputRequest)
37+
assert request.input == [{"role": "user", "content": "hello"}]
38+
assert request.messages == [{"role": "user", "content": "hello"}]
39+
assert request.chat_template_kwargs == {"instruction": "Represent the query: "}
40+
41+
def test_batched_input_messages_parses_as_batch_chat_input_request(self):
42+
request = TypeAdapter(EmbeddingRequest).validate_python(
43+
{
44+
"model": "test",
45+
"input": [
46+
[{"role": "user", "content": "hello"}],
47+
[{"role": "user", "content": "goodbye"}],
48+
],
49+
"chat_template_kwargs": {"instruction": "Represent the query: "},
50+
}
51+
)
52+
53+
assert isinstance(request, EmbeddingBatchChatInputRequest)
54+
assert request.input == [
55+
[{"role": "user", "content": "hello"}],
56+
[{"role": "user", "content": "goodbye"}],
57+
]
58+
assert request.messages == [
59+
[{"role": "user", "content": "hello"}],
60+
[{"role": "user", "content": "goodbye"}],
61+
]
62+
assert request.chat_template_kwargs == {"instruction": "Represent the query: "}
63+
64+
def test_token_ids_still_parse_as_completion_request(self):
65+
request = TypeAdapter(EmbeddingRequest).validate_python(
66+
{
67+
"model": "test",
68+
"input": [[1, 2, 3], [4, 5]],
69+
}
70+
)
71+
72+
assert isinstance(request, EmbeddingCompletionRequest)
73+
assert request.input == [[1, 2, 3], [4, 5]]
74+
75+
def test_messages_still_parses_as_chat_request(self):
76+
request = TypeAdapter(EmbeddingRequest).validate_python(
77+
{
78+
"model": "test",
79+
"messages": [{"role": "user", "content": "hello"}],
80+
"chat_template_kwargs": {"instruction": "Represent the query: "},
81+
}
82+
)
83+
84+
assert isinstance(request, EmbeddingChatRequest)
85+
assert request.messages == [{"role": "user", "content": "hello"}]
86+
assert request.chat_template_kwargs == {"instruction": "Represent the query: "}
87+
88+
def test_batched_messages_parses_as_batch_chat_request(self):
89+
request = TypeAdapter(EmbeddingRequest).validate_python(
90+
{
91+
"model": "test",
92+
"messages": [
93+
[{"role": "user", "content": "hello"}],
94+
[{"role": "user", "content": "goodbye"}],
95+
],
96+
"chat_template_kwargs": {"instruction": "Represent the query: "},
97+
}
98+
)
99+
100+
assert isinstance(request, EmbeddingBatchChatRequest)
101+
assert request.messages == [
102+
[{"role": "user", "content": "hello"}],
103+
[{"role": "user", "content": "goodbye"}],
104+
]
105+
assert request.chat_template_kwargs == {"instruction": "Represent the query: "}
106+
107+
17108
class TestResolveTruncation:
18109
"""Unit tests for EmbedIOProcessor._resolve_cohere_truncation."""
19110

@@ -324,3 +415,113 @@ def batch_render_chat(
324415
},
325416
)
326417
]
418+
419+
420+
class TestPreProcessOpenAIEmbeddingChatOnline:
421+
"""Unit tests for OpenAI embedding chat preprocessing."""
422+
423+
class _FakeModelConfig:
424+
max_model_len = 128
425+
encoder_config: dict[str, object] = {}
426+
pooler_config = None
427+
multimodal_config = None
428+
is_encoder_decoder = False
429+
430+
class _FakeRenderer:
431+
tokenizer = object()
432+
433+
def __init__(self):
434+
self.calls = []
435+
436+
def render_chat(
437+
self,
438+
all_messages,
439+
chat_params,
440+
tok_params,
441+
prompt_extras=None,
442+
):
443+
self.calls.append(
444+
{
445+
"all_messages": all_messages,
446+
"chat_params": chat_params,
447+
"tok_params": tok_params,
448+
"prompt_extras": prompt_extras,
449+
}
450+
)
451+
return all_messages, [
452+
{"prompt_token_ids": [index]} for index, _ in enumerate(all_messages)
453+
]
454+
455+
@classmethod
456+
def _make_handler(cls, renderer):
457+
handler = object.__new__(EmbedIOProcessor)
458+
handler.renderer = renderer
459+
handler.model_config = cls._FakeModelConfig()
460+
handler.chat_template = "template"
461+
handler.chat_template_content_format = "auto"
462+
handler.trust_request_chat_template = False
463+
handler.enable_chunked_processing = False
464+
return handler
465+
466+
@staticmethod
467+
def _make_context(
468+
request: (
469+
EmbeddingChatRequest
470+
| EmbeddingBatchChatRequest
471+
| EmbeddingChatInputRequest
472+
| EmbeddingBatchChatInputRequest
473+
),
474+
) -> PoolingServeContext[
475+
EmbeddingChatRequest
476+
| EmbeddingBatchChatRequest
477+
| EmbeddingChatInputRequest
478+
| EmbeddingBatchChatInputRequest
479+
]:
480+
return PoolingServeContext(
481+
request=request,
482+
pooling_params=PoolingParams(),
483+
model_name="test",
484+
request_id="embd-test",
485+
)
486+
487+
def test_chat_template_kwargs_forwarded_for_batched_input_messages(self):
488+
request = TypeAdapter(EmbeddingRequest).validate_python(
489+
{
490+
"model": "test",
491+
"input": [
492+
[{"role": "user", "content": "hello"}],
493+
[{"role": "user", "content": "goodbye"}],
494+
],
495+
"add_generation_prompt": True,
496+
"chat_template_kwargs": {"instruction": "Represent the query: "},
497+
"mm_processor_kwargs": {"max_pixels": 1},
498+
"cache_salt": "salt",
499+
}
500+
)
501+
assert isinstance(request, EmbeddingBatchChatInputRequest)
502+
503+
renderer = self._FakeRenderer()
504+
handler = self._make_handler(renderer)
505+
ctx = self._make_context(request)
506+
507+
handler.pre_process_online(ctx)
508+
509+
assert ctx.engine_inputs == [
510+
{"prompt_token_ids": [0]},
511+
{"prompt_token_ids": [1]},
512+
]
513+
assert len(renderer.calls) == 1
514+
515+
call = renderer.calls[0]
516+
assert call["all_messages"] == request.messages
517+
assert call["prompt_extras"] == {
518+
"mm_processor_kwargs": {"max_pixels": 1},
519+
"cache_salt": "salt",
520+
}
521+
522+
chat_template_kwargs = call["chat_params"].chat_template_kwargs
523+
assert chat_template_kwargs["instruction"] == "Represent the query: "
524+
assert chat_template_kwargs["add_generation_prompt"] is True
525+
assert chat_template_kwargs["continue_final_message"] is False
526+
assert "tools" not in chat_template_kwargs
527+
assert chat_template_kwargs["tokenize"] is False

vllm/entrypoints/pooling/base/protocol.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,7 @@ class CompletionRequestMixin(OpenAIBaseModel):
168168
# --8<-- [end:completion-extra-params]
169169

170170

171-
class ChatRequestMixin(OpenAIBaseModel):
172-
# --8<-- [start:chat-params]
173-
messages: list[ChatCompletionMessageParam]
174-
# --8<-- [end:chat-params]
175-
171+
class ChatRequestOptionsMixin(OpenAIBaseModel):
176172
# --8<-- [start:chat-extra-params]
177173
add_generation_prompt: bool = Field(
178174
default=False,
@@ -256,6 +252,12 @@ def build_chat_params(
256252
)
257253

258254

255+
class ChatRequestMixin(ChatRequestOptionsMixin):
256+
# --8<-- [start:chat-params]
257+
messages: list[ChatCompletionMessageParam]
258+
# --8<-- [end:chat-params]
259+
260+
259261
class EncodingRequestMixin(OpenAIBaseModel):
260262
# --8<-- [start:encoding-params]
261263
encoding_format: EncodingFormat = "float"

vllm/entrypoints/pooling/embed/io_processor.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
CohereEmbedContent,
3737
CohereEmbedInput,
3838
CohereEmbedRequest,
39+
EmbeddingBatchChatInputRequest,
40+
EmbeddingBatchChatRequest,
41+
EmbeddingChatInputRequest,
3942
EmbeddingChatRequest,
4043
EmbeddingCompletionRequest,
4144
)
@@ -66,6 +69,16 @@ def __init__(self, *args, **kwargs):
6669
def pre_process_online(self, ctx: PoolingServeContext):
6770
if isinstance(ctx.request, CohereEmbedRequest):
6871
self._pre_process_cohere_online(ctx)
72+
elif isinstance(
73+
ctx.request,
74+
(
75+
EmbeddingChatRequest,
76+
EmbeddingBatchChatRequest,
77+
EmbeddingChatInputRequest,
78+
EmbeddingBatchChatInputRequest,
79+
),
80+
):
81+
self._pre_process_openai_chat_online(ctx)
6982
else:
7083
super().pre_process_online(ctx)
7184

@@ -367,6 +380,70 @@ def create_pooling_params(self, request):
367380
)
368381
return super().create_pooling_params(request)
369382

383+
def _pre_process_openai_chat_online(
384+
self,
385+
ctx: PoolingServeContext[
386+
EmbeddingChatRequest
387+
| EmbeddingBatchChatRequest
388+
| EmbeddingChatInputRequest
389+
| EmbeddingBatchChatInputRequest
390+
],
391+
) -> None:
392+
request = ctx.request
393+
self._validate_chat_template(
394+
request_chat_template=request.chat_template,
395+
chat_template_kwargs=request.chat_template_kwargs,
396+
trust_request_chat_template=self.trust_request_chat_template,
397+
)
398+
399+
if isinstance(
400+
request, (EmbeddingBatchChatRequest, EmbeddingBatchChatInputRequest)
401+
):
402+
all_messages = request.messages
403+
else:
404+
all_messages = [request.messages]
405+
ctx.engine_inputs = self._batch_render_openai_chat(request, all_messages)
406+
407+
def _batch_render_openai_chat(
408+
self,
409+
request: (
410+
EmbeddingChatRequest
411+
| EmbeddingBatchChatRequest
412+
| EmbeddingChatInputRequest
413+
| EmbeddingBatchChatInputRequest
414+
),
415+
all_messages: Sequence[list[ChatCompletionMessageParam]],
416+
) -> list[EngineInput]:
417+
renderer = self.renderer
418+
mm_config = self.model_config.multimodal_config
419+
420+
tok_params = request.build_tok_params(self.model_config)
421+
chat_params = request.build_chat_params(
422+
self.chat_template,
423+
self.chat_template_content_format,
424+
).with_defaults(
425+
merge_kwargs(
426+
None,
427+
dict(
428+
tools=None,
429+
tokenize=is_mistral_tokenizer(renderer.tokenizer),
430+
),
431+
),
432+
default_media_io_kwargs=(mm_config.media_io_kwargs if mm_config else None),
433+
)
434+
435+
_, engine_inputs = renderer.render_chat(
436+
all_messages,
437+
chat_params,
438+
tok_params,
439+
prompt_extras={
440+
k: v
441+
for k in ("mm_processor_kwargs", "cache_salt")
442+
if (v := getattr(request, k, None)) is not None
443+
},
444+
)
445+
return engine_inputs
446+
370447
def _pre_process_cohere_online(self, ctx: PoolingServeContext) -> None:
371448
"""Convert a ``CohereEmbedRequest`` into engine prompts.
372449

0 commit comments

Comments
 (0)