main
py 148 lines 5.08 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import json
5
6 import pytest
7
8 from tests.parser.engine.replay_harness import (
9 DUMMY_TOOLS,
10 MockTokenizer,
11 _test_request,
12 collect_output,
13 replay_streaming,
14 )
15 from vllm.parser.deepseek_v41 import deepseek_v41_config
16 from vllm.parser.parser_manager import ParserManager
17
18 CALLS = (
19 '\n\n<|DSML| calls>\n<|DSML| invoke name="get_weather">\n'
20 '<|DSML| parameter name="city" string="true">杭州</|DSML| parameter>\n'
21 '<|DSML| parameter name="count" string="false">42</|DSML| parameter>\n'
22 '</|DSML| invoke>\n<|DSML| invoke name="add">\n'
23 '<|DSML| parameter name="x" string="false">1.5</|DSML| parameter>\n'
24 '<|DSML| parameter name="y" string="false">2.25</|DSML| parameter>\n'
25 "</|DSML| invoke>\n</|DSML| calls>"
26 )
27
28
29 def tokenizer_for(text, special_calls):
30 vocab = {"<think>": 50, "</think>": 51}
31 if special_calls:
32 vocab |= {"<|DSML| calls>": 52, "</|DSML| calls>": 53}
33 tokens: list[tuple[int, str]] = []
34 while text:
35 special = next((marker for marker in vocab if text.startswith(marker)), None)
36 piece = special or text[0]
37 tokens.append((vocab[piece] if special else 100 + len(tokens), piece))
38 text = text[len(piece) :]
39 return MockTokenizer(vocab, tokens), tokens
40
41
42 def parser_for(tokenizer, controls):
43 cls = ParserManager.get_parser(
44 tool_parser_name="deepseek_v41",
45 reasoning_parser_name="deepseek_v41",
46 enable_auto_tools=True,
47 )
48 return cls(tokenizer, chat_template_kwargs=controls)
49
50
51 @pytest.mark.parametrize("thinking", [False, True])
52 @pytest.mark.parametrize("special_calls", [False, True])
53 @pytest.mark.parametrize("chunk_size", [1, 7, 10000])
54 def test_registered_adapters_parse_parallel_calls_across_chunks(
55 thinking,
56 special_calls,
57 chunk_size,
58 ):
59 text = ("Plan.</think>" if thinking else "") + "Checking." + CALLS
60 tokenizer, tokens = tokenizer_for(text, special_calls)
61 parser = parser_for(tokenizer, {"thinking": thinking})
62 output = collect_output(
63 replay_streaming(
64 parser,
65 tokens,
66 chunk_size=chunk_size,
67 finished_on_last=True,
68 tools=DUMMY_TOOLS,
69 prompt_token_ids=[50 if thinking else 51],
70 )
71 )
72 assert output.reasoning == ("Plan." if thinking else "")
73 assert output.content.strip() == "Checking."
74 assert [call["name"] for call in output.tool_calls] == ["get_weather", "add"]
75 assert [json.loads(call["arguments"]) for call in output.tool_calls] == [
76 {"city": "杭州", "count": 42},
77 {"x": 1.5, "y": 2.25},
78 ]
79
80
81 @pytest.mark.parametrize("thinking", [False, True])
82 def test_registered_adapters_parse_complete_output(thinking):
83 text = ("Plan.</think>" if thinking else "") + CALLS
84 tokenizer, tokens = tokenizer_for(text, False)
85 parser = parser_for(tokenizer, {"thinking": thinking})
86 reasoning, content, calls = parser.parse(
87 text,
88 _test_request(tools=DUMMY_TOOLS),
89 enable_auto_tools=True,
90 model_output_token_ids=[tid for tid, _ in tokens],
91 )
92 assert (reasoning or "") == ("Plan." if thinking else "")
93 assert not (content or "").strip()
94 assert [call.name for call in calls] == ["get_weather", "add"]
95 assert json.loads(calls[0].arguments) == {"city": "杭州", "count": 42}
96
97
98 @pytest.mark.parametrize(
99 ("controls", "text", "expected_reasoning", "reasoning_tokens"),
100 [
101 ({"thinking": False}, "12", "", 0),
102 ({"enable_thinking": False}, "12", "", 0),
103 ({"thinking": True, "reasoning_effort": "none"}, "12", "", 0),
104 ({}, "</think>12", "", 0),
105 ({}, "Plan.</think>12", "Plan.", 5),
106 ],
107 )
108 def test_reasoning_adapter_controls_and_usage(
109 controls,
110 text,
111 expected_reasoning,
112 reasoning_tokens,
113 ):
114 tokenizer, tokens = tokenizer_for(text, False)
115 parser = parser_for(tokenizer, controls)
116 output = collect_output(
117 replay_streaming(
118 parser,
119 tokens,
120 chunk_size=1,
121 finished_on_last=True,
122 )
123 )
124 assert output.reasoning == expected_reasoning
125 assert output.content == "12"
126 assert (
127 parser.reasoning_parser.count_reasoning_tokens([tid for tid, _ in tokens])
128 == reasoning_tokens
129 )
130 assert parser.is_reasoning_end([50, 100, 51])
131 assert not parser.is_reasoning_end([51, 100, 50])
132
133
134 def test_python_argument_conversion_and_partial_values():
135 converter = deepseek_v41_config().arg_converter
136 raw = (
137 '<|DSML| parameter name="object" string="false">'
138 '{"a": [true, null]}</|DSML| parameter>'
139 '<|DSML| parameter name="bad" string="false">[broken</|DSML| parameter>'
140 '<|DSML| parameter name="bare">7</|DSML| parameter>'
141 '<|DSML| parameter name="text" string="true"> a<b'
142 )
143 assert json.loads(converter(raw, True)) == {
144 "object": {"a": [True, None]},
145 "bad": "[broken",
146 "bare": 7,
147 "text": " a<b",
148 }