main
py 122 lines 3.93 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for the engine-based Step-3.5 parser.
4
5 Step-3.5 reuses the Qwen3 grammar, which ``test_qwen3.py`` covers. These tests
6 cover what differs: thinking is always on, the newline the model emits before
7 ``</think>`` is dropped, prior-turn reasoning does not end the current turn,
8 and the ``step3p5`` names resolve to the engine adapters.
9 """
10
11 import json
12
13 import pytest
14
15 from tests.parser.engine.conftest import make_mock_tokenizer
16 from tests.parser.engine.streaming_helpers import simulate_reasoning_streaming
17 from vllm.parser.engine.registered_adapters import (
18 Step3p5ParserReasoningAdapter,
19 Step3p5ParserToolAdapter,
20 )
21 from vllm.parser.step3p5 import Step3p5Parser
22 from vllm.reasoning import ReasoningParserManager
23 from vllm.tool_parsers import ToolParserManager
24
25 _THINK_START_ID = 50
26 _THINK_END_ID = 51
27 _IM_START_ID = 70
28 _IM_END_ID = 71
29 _TEXT_ID = 100
30
31 _STEP3P5_VOCAB = {
32 "<think>": _THINK_START_ID,
33 "</think>": _THINK_END_ID,
34 "<tool_call>": 60,
35 "</tool_call>": 61,
36 "<|im_start|>": _IM_START_ID,
37 "<|im_end|>": _IM_END_ID,
38 }
39
40
41 @pytest.fixture
42 def mock_tokenizer():
43 return make_mock_tokenizer(_STEP3P5_VOCAB)
44
45
46 @pytest.fixture
47 def parser(mock_tokenizer):
48 return Step3p5Parser(mock_tokenizer)
49
50
51 def test_parser_names_resolve_to_engine_adapters():
52 tool_parser_cls = ToolParserManager.get_tool_parser("step3p5")
53 assert issubclass(tool_parser_cls, Step3p5ParserToolAdapter)
54 assert tool_parser_cls.structural_tag_model == "qwen_3_coder"
55 reasoning_parser_cls = ReasoningParserManager.get_reasoning_parser("step3p5")
56 assert reasoning_parser_cls is Step3p5ParserReasoningAdapter
57
58
59 def test_enable_thinking_false_is_ignored(mock_tokenizer):
60 # The chat template always prefills "<think>\n" and has no thinking switch.
61 parser = Step3p5Parser(
62 mock_tokenizer, chat_template_kwargs={"enable_thinking": False}
63 )
64 assert parser.extract_reasoning("plan</think>answer", None) == ("plan", "answer")
65 assert not parser.is_reasoning_end([_IM_START_ID, _TEXT_ID, _THINK_START_ID])
66
67
68 def test_newline_before_think_end_dropped(parser):
69 text = "Line one.\nLine two.\n</think>\nThe answer."
70 assert parser.extract_reasoning(text, None) == (
71 "Line one.\nLine two.",
72 "\nThe answer.",
73 )
74
75
76 def test_newline_before_think_end_dropped_streaming(parser):
77 reasoning, content = simulate_reasoning_streaming(
78 parser,
79 ["Line one.\n", "Line two.\n", "</think>", "\nThe answer."],
80 [(_TEXT_ID,), (_TEXT_ID,), (_THINK_END_ID,), (_TEXT_ID,)],
81 )
82 assert reasoning == "Line one.\nLine two."
83 assert content == "\nThe answer."
84
85
86 class TestPriorTurnReasoning:
87 """The template replays earlier assistant turns as
88 ``<think>\\n...\\n</think>\\n``, so a ``</think>`` from history must not end
89 reasoning for the new turn (#34211)."""
90
91 _HISTORY = [
92 _IM_START_ID,
93 _TEXT_ID,
94 _THINK_START_ID,
95 _TEXT_ID,
96 _THINK_END_ID,
97 _TEXT_ID,
98 _IM_END_ID,
99 ]
100
101 def test_prior_turn_think_end_not_end(self, parser):
102 assert not parser.is_reasoning_end([*self._HISTORY, _IM_START_ID, _TEXT_ID])
103
104 def test_think_end_in_current_turn_is_end(self, parser):
105 assert parser.is_reasoning_end(
106 [*self._HISTORY, _IM_START_ID, _THINK_START_ID, _TEXT_ID, _THINK_END_ID]
107 )
108
109
110 def test_reasoning_then_tool_call(parser, mock_request):
111 text = (
112 "Need the weather.\n</think>\n<tool_call>\n<function=get_weather>\n"
113 "<parameter=city>\nTokyo\n</parameter>\n</function>\n</tool_call>"
114 )
115 reasoning, content, tool_calls = parser.parse(
116 text, mock_request, enable_auto_tools=True
117 )
118 assert reasoning == "Need the weather."
119 assert content is None
120 assert tool_calls is not None
121 assert tool_calls[0].name == "get_weather"
122 assert json.loads(tool_calls[0].arguments) == {"city": "Tokyo"}