[Bugfix][Frontend] Check EC requirements for each metadata item (#56070)

Signed-off-by: 子华 <huaxi.shx@alibaba-inc.com> Co-authored-by: Codex <noreply@openai.com>

shaohuaxi committed Sep 10, 2026 at 12:23 UTC ae71862c51f2e069998e0dd5dc30a03a3b475ab9
2 files changed +56 -6
tests/entrypoints/scale_out/token_in_token_out/test_mm_serde.py
+45
@@ -5,6 +5,7 @@ Roundtrip tests for multimodal serde used by the
5 token_in_token_out generate endpoint.
6 """
7
8 +import pytest
9 import torch
10 from pydantic import ValidationError
11
@@ -241,6 +242,50 @@ def test_metadata_only_generate_requires_ec_transfer_params():
242 assert set(item) == {"image_grid_thw"}
243
244
245 +@pytest.mark.parametrize("modality", ["image", "video"])
246 +@pytest.mark.parametrize(
247 + ("has_metadata", "has_ec"),
248 + [(True, False), (True, True), (False, False)],
249 + ids=["metadata-without-ec", "metadata-with-ec", "cache-hit-without-ec"],
250 +)
251 +def test_mixed_items_require_ec_only_for_metadata_only(
252 + modality: str, has_metadata: bool, has_ec: bool
253 +):
254 + """A full item must not bypass the EC requirement for another item."""
255 + engine_input, _, image_grid_thw = _image_engine_input()
256 + rendered = extract_mm_features(engine_input)
257 + assert rendered is not None
258 + features = rendered.model_dump()
259 + metadata = (
260 + encode_mm_kwargs_item(
261 + MultiModalKwargsItem({f"{modality}_grid_thw": image_grid_thw})
262 + )
263 + if has_metadata
264 + else None
265 + )
266 + second_item = {
267 + "mm_hashes": "second-item",
268 + "mm_placeholders": {"offset": 2, "length": 1},
269 + "kwargs_data": None,
270 + "mm_metadata": metadata,
271 + }
272 + for key, value in second_item.items():
273 + features[key].setdefault(modality, []).append(value)
274 + payload = {
275 + "token_ids": [1, 2, 3],
276 + "sampling_params": {"max_tokens": 1},
277 + "features": features,
278 + }
279 + if has_ec:
280 + payload["ec_transfer_params"] = {"second-item": {"peer_host": "10.0.0.1"}}
281 +
282 + if has_metadata and not has_ec:
283 + with pytest.raises(ValidationError, match="ec_transfer_params"):
284 + GenerateRequest.model_validate(payload)
285 + else:
286 + GenerateRequest.model_validate(payload)
287 +
288 +
289 def test_kwargs_and_metadata_generate_does_not_require_ec():
290 engine_input, pixel_values, image_grid_thw = _image_engine_input()
291 rendered = extract_mm_features(engine_input)
vllm/entrypoints/scale_out/token_in_token_out/protocol.py
+11 -6
@@ -192,14 +192,19 @@ class GenerateRequest(BaseModel):
192 return self
193 if not _has_serialized_mm_items(features.mm_metadata):
194 return self
195 - if _has_serialized_mm_items(features.kwargs_data):
196 - return self
195 if self.ec_transfer_params:
196 return self
199 - raise ValueError(
200 - "features.mm_metadata without features.kwargs_data requires "
201 - "ec_transfer_params so embeddings can be loaded by the EC connector"
202 - )
197 + kwargs_data = features.kwargs_data or {}
198 + for modality, metadata_items in (features.mm_metadata or {}).items():
199 + kwargs_items = kwargs_data.get(modality, [None] * len(metadata_items))
200 + if any(
201 + metadata is not None and kwargs is None
202 + for metadata, kwargs in zip(metadata_items, kwargs_items, strict=True)
203 + ):
204 + raise ValueError(
205 + "metadata-only multimodal items require ec_transfer_params"
206 + )
207 + return self
208
209 sampling_params: SamplingParams
210 """The sampling parameters for the model."""