main
py 108 lines 3.84 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Reasoning-boundary tests for the GLM-4.7 parser.
4
5 GLM can transition straight from reasoning into a tool call without ever
6 emitting ``</think>``, so ``<tool_call>`` is an implicit reasoning
7 terminator. These cover that path plus the multi-turn prompts where an
8 earlier turn's markers must not be read as the current turn's state.
9 """
10
11 import pytest
12
13 from tests.parser.engine.conftest import make_mock_tokenizer
14 from vllm.parser.glm47_moe import (
15 THINK_END,
16 THINK_START,
17 TOOL_CALL_END,
18 TOOL_CALL_START,
19 Glm47MoeParser,
20 )
21
22 THINK_S, THINK_E = 1000, 1001
23 TOOL_S, TOOL_E = 1002, 1003
24 ASSISTANT, OBSERVATION, USER = 1004, 1005, 1006
25 TEXT = 42 # stand-in for an ordinary reasoning/content token
26
27 VOCAB = {
28 THINK_START: THINK_S,
29 THINK_END: THINK_E,
30 TOOL_CALL_START: TOOL_S,
31 TOOL_CALL_END: TOOL_E,
32 "<|assistant|>": ASSISTANT,
33 "<|observation|>": OBSERVATION,
34 "<|user|>": USER,
35 }
36
37
38 @pytest.fixture
39 def parser():
40 return Glm47MoeParser(make_mock_tokenizer(VOCAB))
41
42
43 @pytest.fixture
44 def no_thinking_parser():
45 return Glm47MoeParser(
46 make_mock_tokenizer(VOCAB),
47 chat_template_kwargs={"thinking": False},
48 )
49
50
51 class TestIsReasoningEnd:
52 def test_open_reasoning(self, parser):
53 assert not parser.is_reasoning_end([THINK_S, TEXT])
54
55 def test_think_end(self, parser):
56 assert parser.is_reasoning_end([THINK_S, TEXT, THINK_E, TEXT])
57
58 def test_tool_call_without_think_end(self, parser):
59 """Reasoning -> tool call with no ``</think>`` still ends reasoning."""
60 assert parser.is_reasoning_end([THINK_S, TEXT, TOOL_S, TEXT])
61
62 def test_previous_turn_tool_call_ignored(self, parser):
63 """A finished tool call from an earlier turn says nothing about the
64 turn currently being generated."""
65 prompt = [THINK_S, TEXT, TOOL_S, TEXT, TOOL_E, OBSERVATION, TEXT, ASSISTANT]
66 assert not parser.is_reasoning_end(prompt)
67
68 def test_previous_turn_think_end_ignored(self, parser):
69 prompt = [THINK_S, TEXT, THINK_E, TEXT, USER, TEXT, ASSISTANT]
70 assert not parser.is_reasoning_end(prompt)
71
72 def test_empty_input(self, parser):
73 assert not parser.is_reasoning_end([])
74
75 def test_thinking_disabled(self, no_thinking_parser):
76 assert no_thinking_parser.is_reasoning_end([THINK_S, TEXT])
77
78
79 class TestExtractContentIds:
80 def test_think_end_wins_over_later_tool_call(self, parser):
81 """Content between ``</think>`` and a tool call must survive."""
82 ids = [THINK_S, TEXT, THINK_E, 20, 21, TOOL_S, 30, TOOL_E]
83 assert parser.extract_content_ids(ids) == [20, 21, TOOL_S, 30, TOOL_E]
84
85 def test_every_tool_call_after_think_end_kept(self, parser):
86 ids = [THINK_E, 20, TOOL_S, 30, TOOL_E, 21, TOOL_S, 31, TOOL_E]
87 assert parser.extract_content_ids(ids) == ids[1:]
88
89 def test_falls_back_to_tool_call(self, parser):
90 """Without ``</think>``, content starts at the opener itself so the
91 tool parser still receives a well-formed call."""
92 ids = [THINK_S, TEXT, TOOL_S, 30, TOOL_E]
93 assert parser.extract_content_ids(ids) == [TOOL_S, 30, TOOL_E]
94
95 def test_falls_back_to_first_tool_call_of_turn(self, parser):
96 ids = [THINK_S, TEXT, TOOL_S, 30, TOOL_E, TOOL_S, 31, TOOL_E]
97 assert parser.extract_content_ids(ids) == ids[2:]
98
99 def test_previous_turn_tool_call_ignored(self, parser):
100 ids = [TOOL_S, 30, TOOL_E, OBSERVATION, TEXT, ASSISTANT, THINK_S, TEXT]
101 assert parser.extract_content_ids(ids) == ids
102
103 def test_no_markers_returns_input_ids(self, parser):
104 assert parser.extract_content_ids([20, 21]) == [20, 21]
105
106 def test_thinking_disabled(self, no_thinking_parser):
107 ids = [THINK_S, TEXT, TOOL_S]
108 assert no_thinking_parser.extract_content_ids(ids) == ids