main
py 274 lines 8.04 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import pytest
4
5 from tests.reasoning.utils import run_reasoning_extraction
6 from vllm.reasoning import ReasoningParser, ReasoningParserManager
7 from vllm.reasoning.hy_v3_reasoning_parser import HYV3ReasoningParser
8 from vllm.tokenizers import get_tokenizer
9
10 parser_name = "hy_v3"
11 MODEL = "tencent/Hy3-preview"
12
13
14 @pytest.fixture(scope="module")
15 def hy_v3_tokenizer():
16 return get_tokenizer(tokenizer_name=MODEL)
17
18
19 WITH_THINK = {
20 "output": "This is a reasoning section</think>This is the rest",
21 "reasoning": "This is a reasoning section",
22 "content": "This is the rest",
23 "is_reasoning_end": True,
24 "reasoning_effort": "high",
25 }
26
27 WITH_THINK_STREAM = {
28 "output": "This is a reasoning section</think>This is the rest",
29 "reasoning": "This is a reasoning section",
30 "content": "This is the rest",
31 "is_reasoning_end": True,
32 "reasoning_effort": "high",
33 }
34
35 WITHOUT_THINK = {
36 "output": "This is the rest",
37 "reasoning": None,
38 "content": "This is the rest",
39 "is_reasoning_end": True,
40 "reasoning_effort": "no_think",
41 }
42
43 WITHOUT_THINK_STREAM = {
44 "output": "This is the rest",
45 "reasoning": None,
46 "content": "This is the rest",
47 "is_reasoning_end": True,
48 "reasoning_effort": "no_think",
49 }
50
51 WITH_REASONING_EFFORT_NONE = {
52 "output": "This is the rest",
53 "reasoning": None,
54 "content": "This is the rest",
55 "is_reasoning_end": True,
56 }
57
58 WITH_REASONING_EFFORT_NONE_STREAM = {
59 "output": "This is the rest",
60 "reasoning": None,
61 "content": "This is the rest",
62 "is_reasoning_end": True,
63 }
64
65 COMPLETE_REASONING = {
66 "output": "This is a reasoning section</think>",
67 "reasoning": "This is a reasoning section",
68 "content": None,
69 "is_reasoning_end": True,
70 "reasoning_effort": "high",
71 }
72 MULTILINE_REASONING = {
73 "output": "This is a reasoning\nsection</think>This is the rest\nThat",
74 "reasoning": "This is a reasoning\nsection",
75 "content": "This is the rest\nThat",
76 "is_reasoning_end": True,
77 "reasoning_effort": "high",
78 }
79 ONLY_OPEN_TAG = {
80 "output": "This is a reasoning section",
81 "reasoning": "This is a reasoning section",
82 "content": None,
83 "is_reasoning_end": False,
84 "reasoning_effort": "high",
85 }
86
87 ONLY_OPEN_TAG_STREAM = {
88 "output": "This is a reasoning section",
89 "reasoning": "This is a reasoning section",
90 "content": None,
91 "is_reasoning_end": False,
92 "reasoning_effort": "high",
93 }
94
95 TEST_CASES = [
96 pytest.param(
97 False,
98 WITH_THINK,
99 id="with_think",
100 ),
101 pytest.param(
102 True,
103 WITH_THINK_STREAM,
104 id="with_think_stream",
105 ),
106 pytest.param(
107 False,
108 WITHOUT_THINK,
109 id="without_think",
110 ),
111 pytest.param(
112 True,
113 WITHOUT_THINK_STREAM,
114 id="without_think_stream",
115 ),
116 pytest.param(
117 False,
118 WITH_REASONING_EFFORT_NONE,
119 id="with_reasoning_effort_none",
120 ),
121 pytest.param(
122 True,
123 WITH_REASONING_EFFORT_NONE_STREAM,
124 id="with_reasoning_effort_none_stream",
125 ),
126 pytest.param(
127 False,
128 COMPLETE_REASONING,
129 id="complete_reasoning",
130 ),
131 pytest.param(
132 True,
133 COMPLETE_REASONING,
134 id="complete_reasoning_stream",
135 ),
136 pytest.param(
137 False,
138 MULTILINE_REASONING,
139 id="multiline_reasoning",
140 ),
141 pytest.param(
142 True,
143 MULTILINE_REASONING,
144 id="multiline_reasoning_stream",
145 ),
146 pytest.param(
147 False,
148 ONLY_OPEN_TAG,
149 id="only_open_tag",
150 ),
151 pytest.param(
152 True,
153 ONLY_OPEN_TAG_STREAM,
154 id="only_open_tag_stream",
155 ),
156 ]
157
158 STILL_REASONING_PROMPT = """<|hy_begin▁of▁sentence|>
159 You are a helpful assistant.
160 <|reasoning_mode|>reasoning_effort:high<|hy_User|>
161 What is the capital of France?<|hy_Assistant|>
162 <think>The user is asking for the capital of"""
163
164 DONE_REASONING_PROMPT = """<|hy_begin▁of▁sentence|>
165 You are a helpful assistant.
166 <|reasoning_mode|>reasoning_effort:high<|hy_User|>
167 What is the capital of France?<|hy_Assistant|>
168 <think>The user is asking for the capital of France.</think>
169 The capital of France is Paris."""
170
171 MULTI_TURN_STILL_REASONING_PROMPT = """<|hy_begin▁of▁sentence|>
172 You are a helpful assistant.
173 <|reasoning_mode|>reasoning_effort:high<|hy_User|>
174 What is the capital of France?<|hy_Assistant|
175 ><think></think>The capital of France is Paris.<eos:6124c78e>
176 <|hy_User|>What about Chile?<|hy_Assistant|>
177 <think>The user is asking for the capital of"""
178
179 MULTI_TURN_DONE_REASONING_PROMPT = """<|hy_begin▁of▁sentence|>
180 You are a helpful assistant.
181 <|reasoning_mode|>reasoning_effort:high<|hy_User|>
182 What is the capital of France?<|hy_Assistant|
183 ><think></think>The capital of France is Paris.<eos:6124c78e>
184 <|hy_User|>What about Chile?<|hy_Assistant|>
185 <think>The user is asking for the capital of Chile.</think>
186 The capital of Chile is Santiago."""
187
188 REASONING_END_TEST_CASES = [
189 pytest.param(STILL_REASONING_PROMPT, False, id="still_reasoning"),
190 pytest.param(DONE_REASONING_PROMPT, True, id="done_reasoning"),
191 pytest.param(
192 MULTI_TURN_STILL_REASONING_PROMPT, False, id="multi_turn_still_reasoning"
193 ),
194 pytest.param(
195 MULTI_TURN_DONE_REASONING_PROMPT, True, id="multi_turn_done_reasoning"
196 ),
197 ]
198
199
200 @pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
201 def test_reasoning(
202 streaming: bool,
203 param_dict: dict,
204 hy_v3_tokenizer,
205 ):
206 output = hy_v3_tokenizer.tokenize(param_dict["output"])
207 output_tokens: list[str] = [
208 hy_v3_tokenizer.convert_tokens_to_string([token]) for token in output
209 ]
210
211 parser_kwargs = {}
212 if "reasoning_effort" in param_dict:
213 parser_kwargs["chat_template_kwargs"] = {
214 "reasoning_effort": param_dict["reasoning_effort"]
215 }
216 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
217 hy_v3_tokenizer,
218 **parser_kwargs,
219 )
220
221 reasoning, content = run_reasoning_extraction(
222 parser, output_tokens, streaming=streaming
223 )
224
225 assert reasoning == param_dict["reasoning"]
226 assert content == param_dict["content"]
227
228 output_ids = hy_v3_tokenizer.convert_tokens_to_ids(output)
229 is_reasoning_end = parser.is_reasoning_end(output_ids)
230 assert is_reasoning_end == param_dict["is_reasoning_end"]
231
232
233 @pytest.mark.parametrize("prompt, is_reasoning_end", REASONING_END_TEST_CASES)
234 def test_is_reasoning_end_full_prompt(
235 prompt: str, is_reasoning_end: bool, hy_v3_tokenizer
236 ):
237 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
238 hy_v3_tokenizer,
239 chat_template_kwargs={"reasoning_effort": "high"},
240 )
241 tokens = hy_v3_tokenizer.tokenize(prompt)
242 token_ids = hy_v3_tokenizer.convert_tokens_to_ids(tokens)
243 check_is_reasoning_end = parser.is_reasoning_end(token_ids)
244 assert check_is_reasoning_end == is_reasoning_end
245
246
247 def test_constructor_does_not_mutate_shared_chat_template_kwargs(hy_v3_tokenizer):
248 parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
249 chat_template_kwargs = {"reasoning_effort": "low"}
250
251 first_parser: ReasoningParser = parser_cls(
252 hy_v3_tokenizer,
253 chat_template_kwargs=chat_template_kwargs,
254 )
255 second_parser: ReasoningParser = parser_cls(
256 hy_v3_tokenizer,
257 chat_template_kwargs=chat_template_kwargs,
258 )
259
260 assert chat_template_kwargs == {"reasoning_effort": "low"}
261 assert isinstance(first_parser, HYV3ReasoningParser)
262 assert isinstance(second_parser, HYV3ReasoningParser)
263 assert first_parser._identity_parser is None
264 assert second_parser._identity_parser is None
265
266
267 def test_constructor_falls_back_to_outer_reasoning_effort(hy_v3_tokenizer):
268 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
269 hy_v3_tokenizer,
270 reasoning_effort="low",
271 )
272
273 assert isinstance(parser, HYV3ReasoningParser)
274 assert parser._identity_parser is None