main
py 318 lines 9.61 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import copy
5
6 import pytest
7 from transformers import AutoTokenizer, PreTrainedTokenizerBase
8
9 from tests.reasoning.utils import run_reasoning_extraction
10 from vllm.parser.engine.adapters import ParserEngineReasoningAdapter
11 from vllm.parser.glm47_moe import Glm47MoeParser
12 from vllm.reasoning import ReasoningParser, ReasoningParserManager
13
14 parser_name = "glm45"
15 start_token = "<think>"
16 end_token = "</think>"
17
18 REASONING_MODEL_NAME = "zai-org/GLM-4.7"
19
20
21 @pytest.fixture(scope="module")
22 def glm45_tokenizer() -> PreTrainedTokenizerBase:
23 return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
24
25
26 WITH_THINK = {
27 "output": "<think>This is a reasoning section</think>This is the rest",
28 "reasoning": "This is a reasoning section",
29 "content": "This is the rest",
30 "is_reasoning_end": True,
31 }
32
33 WITH_THINK_STREAM = {
34 "output": "<think>This is a reasoning section</think>This is the rest",
35 "reasoning": "This is a reasoning section",
36 "content": "This is the rest",
37 "is_reasoning_end": True,
38 }
39
40 WITHOUT_THINK = {
41 "output": "This is the rest",
42 "reasoning": "This is the rest",
43 "content": None,
44 "is_reasoning_end": False,
45 }
46
47 WITHOUT_THINK_STREAM = {
48 "output": "This is the rest",
49 "reasoning": "This is the rest",
50 "content": None,
51 "is_reasoning_end": False,
52 }
53
54 WITHOUT_OPEN_THINK = {
55 "output": "This is a reasoning section</think>This is the rest",
56 "reasoning": "This is a reasoning section",
57 "content": "This is the rest",
58 "is_reasoning_end": True,
59 }
60
61 WITHOUT_OPEN_THINK_STREAM = {
62 "output": "This is a reasoning section</think>This is the rest",
63 "reasoning": "This is a reasoning section",
64 "content": "This is the rest",
65 "is_reasoning_end": True,
66 }
67
68 COMPLETE_REASONING = {
69 "output": "<think>This is a reasoning section</think>",
70 "reasoning": "This is a reasoning section",
71 "content": None,
72 "is_reasoning_end": True,
73 }
74 MULTILINE_REASONING = {
75 "output": "<think>This is a reasoning\nsection</think>This is the rest\nThat",
76 "reasoning": "This is a reasoning\nsection",
77 "content": "This is the rest\nThat",
78 "is_reasoning_end": True,
79 }
80 ONLY_OPEN_TAG = {
81 "output": "<think>This is a reasoning section",
82 "reasoning": "This is a reasoning section",
83 "content": None,
84 "is_reasoning_end": False,
85 }
86
87 ONLY_OPEN_TAG_STREAM = {
88 "output": "<think>This is a reasoning section",
89 "reasoning": "This is a reasoning section",
90 "content": None,
91 "is_reasoning_end": False,
92 }
93
94 TEST_CASES = [
95 pytest.param(
96 False,
97 WITH_THINK,
98 id="with_think",
99 ),
100 pytest.param(
101 True,
102 WITH_THINK_STREAM,
103 id="with_think_stream",
104 ),
105 pytest.param(
106 False,
107 WITHOUT_THINK,
108 id="without_think",
109 ),
110 pytest.param(
111 True,
112 WITHOUT_THINK_STREAM,
113 id="without_think_stream",
114 ),
115 pytest.param(
116 False,
117 WITHOUT_OPEN_THINK,
118 id="without_open_think",
119 ),
120 pytest.param(
121 True,
122 WITHOUT_OPEN_THINK_STREAM,
123 id="without_open_think_stream",
124 ),
125 pytest.param(
126 False,
127 COMPLETE_REASONING,
128 id="complete_reasoning",
129 ),
130 pytest.param(
131 True,
132 COMPLETE_REASONING,
133 id="complete_reasoning_stream",
134 ),
135 pytest.param(
136 False,
137 MULTILINE_REASONING,
138 id="multiline_reasoning",
139 ),
140 pytest.param(
141 True,
142 MULTILINE_REASONING,
143 id="multiline_reasoning_stream",
144 ),
145 pytest.param(
146 False,
147 ONLY_OPEN_TAG,
148 id="only_open_tag",
149 ),
150 pytest.param(
151 True,
152 ONLY_OPEN_TAG_STREAM,
153 id="only_open_tag_stream",
154 ),
155 ]
156
157 STILL_REASONING_PROMPT = """[gMASK]<sop><|system|>
158 You are a helpful assistant.<|user|>
159 What is the capital of France?<|assistant|>
160 <think>The user is asking for the capital of"""
161
162 DONE_REASONING_PROMPT = """[gMASK]<sop><|system|>
163 You are a helpful assistant.<|user|>
164 What is the capital of France?<|assistant|>
165 <think>The user is asking for the capital of France.</think>
166 The capital of France is Paris."""
167
168 MULTI_TURN_STILL_REASONING_PROMPT = """[gMASK]<sop><|system|>
169 You are a helpful assistant.<|user|>
170 What is the capital of France?<|assistant|>
171 <think></think>
172 The capital of France is Paris.<|user|>
173 What about Chile?<|assistant|>
174 <think>The user is asking for the capital of"""
175
176 MULTI_TURN_DONE_REASONING_PROMPT = """[gMASK]<sop><|system|>
177 You are a helpful assistant.<|user|>
178 What is the capital of France?<|assistant|>
179 <think></think>
180 The capital of France is Paris.<|user|>
181 What about Chile?<|assistant|>
182 <think>The user is asking for the capital of Chile.</think>
183 The capital of Chile is Santiago."""
184
185 REASONING_END_TEST_CASES = [
186 pytest.param(STILL_REASONING_PROMPT, False, id="still_reasoning"),
187 pytest.param(DONE_REASONING_PROMPT, True, id="done_reasoning"),
188 pytest.param(
189 MULTI_TURN_STILL_REASONING_PROMPT, False, id="multi_turn_still_reasoning"
190 ),
191 pytest.param(
192 MULTI_TURN_DONE_REASONING_PROMPT, True, id="multi_turn_done_reasoning"
193 ),
194 ]
195
196
197 @pytest.mark.parametrize("streaming, param_dict", TEST_CASES)
198 def test_reasoning(
199 streaming: bool,
200 param_dict: dict,
201 glm45_tokenizer,
202 ):
203 output = glm45_tokenizer.tokenize(param_dict["output"])
204 output_tokens: list[str] = [
205 glm45_tokenizer.convert_tokens_to_string([token]) for token in output
206 ]
207 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
208 glm45_tokenizer
209 )
210
211 reasoning, content = run_reasoning_extraction(
212 parser, output_tokens, streaming=streaming
213 )
214
215 assert reasoning == param_dict["reasoning"]
216 assert content == param_dict["content"]
217
218 output_ids = glm45_tokenizer.convert_tokens_to_ids(output)
219 is_reasoning_end = parser.is_reasoning_end(output_ids)
220 assert is_reasoning_end == param_dict["is_reasoning_end"]
221
222
223 @pytest.mark.parametrize("prompt, is_reasoning_end", REASONING_END_TEST_CASES)
224 def test_is_reasoning_end_full_prompt(
225 prompt: str, is_reasoning_end: bool, glm45_tokenizer
226 ):
227 parser: ReasoningParser = ReasoningParserManager.get_reasoning_parser(parser_name)(
228 glm45_tokenizer
229 )
230 tokens = glm45_tokenizer.tokenize(prompt)
231 token_ids = glm45_tokenizer.convert_tokens_to_ids(tokens)
232 check_is_reasoning_end = parser.is_reasoning_end(token_ids)
233 assert check_is_reasoning_end == is_reasoning_end
234
235
236 GLM53_TEMPLATE = (
237 "[gMASK]<sop>\n"
238 "{%- set effective_reasoning_effort = reasoning_effort if reasoning_effort is"
239 " defined and reasoning_effort in ['low', 'high'] else 'max' -%}\n"
240 "<|system|>Reasoning Effort: {{ effective_reasoning_effort | capitalize }}\n"
241 "{% for tc in m.tool_calls %}\n"
242 "{{- '<tool_call>' + tc.name -}}\n"
243 "{% set _args = tc.arguments %}"
244 "{% for k, v in _args.items() %}"
245 "<arg_key>{{ k }}</arg_key><arg_value>{{ v }}</arg_value>"
246 "{% endfor %}</tool_call>\n"
247 "{% endfor %}\n"
248 "<|assistant|>{{- '<think>' -}}"
249 )
250
251 GLM53_LEAK = {
252 "output": "Simple question.</think>2 + 2 = **4**",
253 "reasoning": "Simple question.",
254 "content": "2 + 2 = **4**",
255 "is_reasoning_end": True,
256 }
257
258
259 @pytest.fixture()
260 def glm53_style_tokenizer(glm45_tokenizer):
261 tokenizer = copy.copy(glm45_tokenizer)
262 tokenizer.chat_template = GLM53_TEMPLATE
263 return tokenizer
264
265
266 def _glm_engine(parser: ReasoningParser) -> Glm47MoeParser:
267 assert isinstance(parser, ParserEngineReasoningAdapter)
268 engine = parser._parser_engine
269 assert isinstance(engine, Glm47MoeParser)
270 return engine
271
272
273 @pytest.mark.parametrize(
274 "disable_kwargs", [{"enable_thinking": False}, {"thinking": False}]
275 )
276 def test_glm53_template_forces_reasoning(disable_kwargs: dict, glm53_style_tokenizer):
277 parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
278 parser = parser_cls(glm53_style_tokenizer, chat_template_kwargs=disable_kwargs)
279 assert _glm_engine(parser).thinking_enabled
280
281 output = glm53_style_tokenizer.tokenize(GLM53_LEAK["output"])
282 output_tokens: list[str] = [
283 glm53_style_tokenizer.convert_tokens_to_string([token]) for token in output
284 ]
285 reasoning, content = run_reasoning_extraction(parser, output_tokens)
286 assert reasoning == GLM53_LEAK["reasoning"]
287 assert content == GLM53_LEAK["content"]
288
289 output_ids = glm53_style_tokenizer.convert_tokens_to_ids(output)
290 assert parser.is_reasoning_end(output_ids) == GLM53_LEAK["is_reasoning_end"]
291
292
293 def test_glm53_template_forces_reasoning_streaming(glm53_style_tokenizer):
294 parser = ReasoningParserManager.get_reasoning_parser(parser_name)(
295 glm53_style_tokenizer, chat_template_kwargs={"enable_thinking": False}
296 )
297 output = glm53_style_tokenizer.tokenize(GLM53_LEAK["output"])
298 output_tokens: list[str] = [
299 glm53_style_tokenizer.convert_tokens_to_string([token]) for token in output
300 ]
301 reasoning, content = run_reasoning_extraction(parser, output_tokens, streaming=True)
302 assert reasoning == GLM53_LEAK["reasoning"]
303 assert content == GLM53_LEAK["content"]
304
305
306 def test_glm47_template_honors_thinking_disable(glm45_tokenizer):
307 parser = ReasoningParserManager.get_reasoning_parser(parser_name)(
308 glm45_tokenizer, chat_template_kwargs={"enable_thinking": False}
309 )
310 assert not _glm_engine(parser).thinking_enabled
311
312 output = glm45_tokenizer.tokenize(GLM53_LEAK["output"])
313 output_tokens: list[str] = [
314 glm45_tokenizer.convert_tokens_to_string([token]) for token in output
315 ]
316 reasoning, content = run_reasoning_extraction(parser, output_tokens)
317 assert reasoning is None
318 assert content == GLM53_LEAK["output"]