main
py 112 lines 4.31 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for the token_in_token_out request/response protocol.
4
5 These tests intentionally avoid spinning up a server — they exercise the
6 pydantic validators on ``GenerateRequest`` directly so they run fast and
7 fail loudly if the validator semantics ever drift.
8 """
9
10 import json
11
12 import pytest
13
14 from vllm.entrypoints.scale_out.token_in_token_out.protocol import (
15 GenerateRequest,
16 MultiModalFeatures,
17 PlaceholderRangeInfo,
18 )
19 from vllm.sampling_params import SamplingParams
20
21
22 def _base_payload() -> dict:
23 return {"token_ids": [1, 2, 3], "sampling_params": {}}
24
25
26 def test_omitted_max_tokens_is_not_provided():
27 """Body without ``max_tokens`` must surface as 'not provided' so the
28 server can apply its own default instead of the dataclass 16."""
29 req = GenerateRequest.model_validate(_base_payload())
30 # SamplingParams' dataclass default leaks through the parsed instance —
31 # this is exactly the bug the server-side defaulting works around.
32 assert req.sampling_params.max_tokens == 16
33 assert not req.is_sampling_param_provided("max_tokens")
34
35
36 def test_explicit_max_tokens_is_provided():
37 """Even when the client picks the same value as the dataclass default,
38 it must register as explicitly set so the server won't override it."""
39 payload = _base_payload()
40 payload["sampling_params"] = {"max_tokens": 16}
41 req = GenerateRequest.model_validate(payload)
42 assert req.sampling_params.max_tokens == 16
43 assert req.is_sampling_param_provided("max_tokens")
44
45 payload["sampling_params"] = {"max_tokens": 256}
46 req = GenerateRequest.model_validate(payload)
47 assert req.sampling_params.max_tokens == 256
48 assert req.is_sampling_param_provided("max_tokens")
49
50
51 def test_other_fields_tracked_independently():
52 payload = _base_payload()
53 payload["sampling_params"] = {"temperature": 0.0}
54 req = GenerateRequest.model_validate(payload)
55 assert not req.is_sampling_param_provided("max_tokens")
56 assert req.is_sampling_param_provided("temperature")
57
58
59 def test_json_roundtrip_preserves_provided_keys():
60 payload = _base_payload()
61 payload["sampling_params"] = {"temperature": 0.5}
62 req = GenerateRequest.model_validate_json(json.dumps(payload))
63 assert not req.is_sampling_param_provided("max_tokens")
64 assert req.is_sampling_param_provided("temperature")
65
66
67 def test_internal_instance_construction_treats_all_as_provided():
68 """When internal callers build ``GenerateRequest`` from a pre-resolved
69 ``SamplingParams`` instance, every field is considered explicitly set
70 so server-side defaulting can't clobber values resolved upstream."""
71 sp = SamplingParams(max_tokens=500, temperature=0.0)
72 req = GenerateRequest(token_ids=[1, 2, 3], sampling_params=sp)
73 assert req.is_sampling_param_provided("max_tokens")
74 assert req.is_sampling_param_provided("temperature")
75 # And keys we never touched should also count as provided in this path.
76 assert req.is_sampling_param_provided("top_p")
77
78
79 def test_multimodal_features_reject_mismatched_parallel_fields():
80 with pytest.raises(ValueError, match="same length"):
81 MultiModalFeatures(
82 mm_hashes={"image": ["a", "b"]},
83 mm_placeholders={"image": [PlaceholderRangeInfo(offset=0, length=1)]},
84 kwargs_data={"image": ["encoded"]},
85 )
86
87
88 def test_multimodal_features_reject_overlapping_placeholders():
89 with pytest.raises(ValueError, match="non-overlapping"):
90 MultiModalFeatures(
91 mm_hashes={"image": ["a", "b"]},
92 mm_placeholders={
93 "image": [
94 PlaceholderRangeInfo(offset=1, length=2),
95 PlaceholderRangeInfo(offset=2, length=2),
96 ]
97 },
98 kwargs_data={"image": ["a", "b"]},
99 )
100
101
102 def test_generate_request_rejects_placeholder_outside_prompt():
103 with pytest.raises(ValueError, match="within the token_ids sequence"):
104 GenerateRequest(
105 token_ids=[1, 2, 3],
106 sampling_params=SamplingParams(),
107 features=MultiModalFeatures(
108 mm_hashes={"image": ["a"]},
109 mm_placeholders={"image": [PlaceholderRangeInfo(offset=2, length=2)]},
110 kwargs_data={"image": ["encoded"]},
111 ),
112 )