main
py 187 lines 6.09 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5 import json
6 from pathlib import Path
7
8 import pytest
9
10 from tests.tokenizers_.test_deepseek_v4 import FakeHfTokenizer
11 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
12 from vllm.tokenizers.deepseek_v41 import get_deepseek_v41_tokenizer
13
14 FIXTURES = Path(__file__).parent / "fixtures"
15
16
17 def render(messages, **kwargs):
18 return get_deepseek_v41_tokenizer(FakeHfTokenizer()).apply_chat_template(
19 messages, tokenize=False, **kwargs
20 )
21
22
23 @pytest.mark.parametrize("case_id", [1, 2])
24 def test_reference_encoder_fixtures(case_id):
25 # Expected prompts come from the DeepSeek V4.1 reference encoder.
26 data = json.loads(
27 (FIXTURES / "deepseek_v4" / f"test_input_{case_id}.json").read_text()
28 )
29 messages = data["messages"] if isinstance(data, dict) else data
30 tools = data.get("tools") if isinstance(data, dict) else None
31 expected = (FIXTURES / "deepseek_v41" / f"test_output_{case_id}.txt").read_text()
32 assert render(messages, tools=tools) == expected
33
34
35 @pytest.mark.parametrize(
36 ("effort", "budget"),
37 [
38 (None, 75),
39 ("low", 50),
40 ("high", 75),
41 ("xhigh", 75),
42 ("max", 100),
43 (1, 1),
44 (42, 42),
45 (100, 100),
46 ],
47 )
48 def test_numeric_reasoning_effort(effort, budget):
49 assert render(
50 [{"role": "user", "content": "question"}], reasoning_effort=effort
51 ) == (
52 "<|begin▁of▁sentence|><|System|>"
53 f"Reasoning Effort: {budget} (range 1-100, the higher the value, "
54 "the more thorough the reasoning)\n\n"
55 "<|User|>question<|Assistant|><think>"
56 )
57
58
59 @pytest.mark.parametrize("effort", ["medium", "minimal", -1, 0, 101, True, 1.5, []])
60 def test_invalid_effort_is_a_request_error(effort):
61 with pytest.raises(ValueError, match="reasoning_effort"):
62 render([{"role": "user", "content": "question"}], reasoning_effort=effort)
63
64
65 @pytest.mark.parametrize(
66 "controls",
67 [
68 {"thinking": False},
69 {"enable_thinking": False},
70 {"thinking": True, "reasoning_effort": "none"},
71 {"enable_thinking": True, "reasoning_effort": "none"},
72 ],
73 )
74 def test_chat_mode_has_closed_thinking_prefix(controls):
75 assert render([{"role": "user", "content": "question"}], **controls) == (
76 "<|begin▁of▁sentence|><|User|>question<|Assistant|></think>"
77 )
78
79
80 def test_raw_text_parts_preserve_reference_separator():
81 messages = [
82 {
83 "role": "user",
84 "content": [
85 {"type": "text", "text": "first"},
86 {"type": "text", "text": "second"},
87 ],
88 }
89 ]
90 original = copy.deepcopy(messages)
91 assert (
92 render(
93 messages,
94 conversation=[{"role": "user", "content": "first\nsecond"}],
95 thinking=False,
96 )
97 == "<|begin▁of▁sentence|><|User|>first\n\nsecond<|Assistant|></think>"
98 )
99 assert messages == original
100
101
102 @pytest.mark.parametrize(
103 ("role", "responses_type"),
104 [("user", "input_text"), ("assistant", "output_text")],
105 )
106 def test_responses_text_parts_match_chat_text_parts(role, responses_type):
107 responses_message = {
108 "role": role,
109 "content": [{"type": responses_type, "text": "hello"}],
110 }
111 chat_message = {
112 "role": role,
113 "content": [{"type": "text", "text": "hello"}],
114 }
115 assert render([responses_message], thinking=False) == render(
116 [chat_message], thinking=False
117 )
118
119
120 def test_mid_system_gets_its_own_marker_and_generation_header():
121 assert render(
122 [
123 {"role": "user", "content": "question"},
124 {"role": "system", "content": "answer briefly"},
125 ],
126 thinking=False,
127 ) == (
128 "<|begin▁of▁sentence|><|User|>question"
129 "<|System|>answer briefly<|Assistant|></think>"
130 )
131
132
133 def test_top_level_effort_overrides_template_effort():
134 request = ChatCompletionRequest(
135 model="deepseek-ai/DeepSeek-V4.1-Flash",
136 messages=[{"role": "user", "content": "question"}],
137 reasoning_effort="low",
138 chat_template_kwargs={"reasoning_effort": 100},
139 )
140 kwargs = request.build_chat_params(None, "auto").get_apply_chat_template_kwargs()
141 assert "Reasoning Effort: 50 " in render(request.messages, **kwargs)
142
143
144 def test_encode_uses_one_bos_and_forwards_truncation():
145 tokenizer = get_deepseek_v41_tokenizer(FakeHfTokenizer())
146 tokenizer.apply_chat_template(
147 [{"role": "user", "content": "question"}],
148 thinking=False,
149 truncation=True,
150 max_length=16,
151 )
152 text, add_special_tokens, kwargs = tokenizer.last_encode
153 assert text.startswith("<|begin▁of▁sentence|>")
154 assert add_special_tokens is False
155 assert kwargs == {"truncation": True, "max_length": 16}
156
157
158 @pytest.mark.parametrize("image_type", ["image_url", "input_image", "image_pil"])
159 def test_images_preserve_content_order_and_reference_separator(image_type):
160 from PIL import Image
161
162 def image_part(color):
163 if image_type == "image_pil":
164 return {"type": image_type, "image_pil": Image.new("RGB", (2, 2), color)}
165 url = f"https://example.com/{color}.png"
166 return {
167 "type": image_type,
168 "image_url": {"url": url} if image_type == "image_url" else url,
169 }
170
171 parts = [
172 {"type": "text", "text": "first"},
173 image_part("red"),
174 {"type": "text", "text": "second"},
175 image_part("blue"),
176 {"type": "text", "text": "compare"},
177 ]
178 assert render([{"role": "user", "content": parts}], thinking=False) == (
179 "<|begin▁of▁sentence|><|User|>first\n\n<|deepseek_image|>\n\n"
180 "second\n\n<|deepseek_image|>\n\ncompare<|Assistant|></think>"
181 )
182 assert parts[1]["type"] == parts[3]["type"] == image_type
183
184
185 def test_unsupported_media_is_a_request_error():
186 with pytest.raises(ValueError, match="text and image content only"):
187 render([{"role": "user", "content": [{"type": "input_audio"}]}])