A high-throughput and memory-efficient inference and serving engine for LLMs
[Bugfix] Fix Qwen3 prompt tool-call reasoning false positive (#45763)
Signed-off-by: Alex Bilichenko <alexbi29@users.noreply.github.com> Co-authored-by: Alex Bilichenko <alexbi29@users.noreply.github.com>
alexbi29 committed
Jun 16, 2026 at 10:48 UTC
7d567172fcb7fe92bef7d129b1280919acf153dd
2 files changed
+40
tests/parser/engine/test_qwen3_reasoning.py
+34
@@ -15,7 +15,12 @@ import pytest
15
16
from tests.parser.engine.conftest import make_mock_tokenizer
17
from tests.parser.engine.streaming_helpers import simulate_reasoning_streaming
18
+from vllm.parser.abstract_parser import DelegatingParser
19
from vllm.parser.engine.parser_engine_config import ParserState
20
+from vllm.parser.engine.registered_adapters import (
21
+ Qwen3ParserReasoningAdapter,
22
+ Qwen3ParserToolAdapter,
23
+)
24
from vllm.parser.qwen3 import Qwen3Parser, qwen3_config
25
26
_THINK_START_ID = 50
@@ -32,6 +37,11 @@ _QWEN3_VOCAB = {
37
}
38
39
40
+class _Qwen3DelegatingParser(DelegatingParser):
41
+ reasoning_parser_cls = Qwen3ParserReasoningAdapter
42
+ tool_parser_cls = Qwen3ParserToolAdapter
43
+
44
+
45
@pytest.fixture
46
def mock_tokenizer():
47
return make_mock_tokenizer(_QWEN3_VOCAB)
@@ -162,6 +172,10 @@ class TestIsReasoningEnd:
172
"""Unpaired <tool_call> is implicit reasoning end."""
173
assert parser.is_reasoning_end([_THINK_START_ID, 1, _TOOL_CALL_ID])
174
175
+ def test_prompt_tool_example_before_generation_think_not_end(self, parser):
176
+ """Tool examples before the generation <think> must not end reasoning."""
177
+ assert not parser.is_reasoning_end([_TOOL_CALL_ID, _TEXT_ID, _THINK_START_ID])
178
+
179
def test_paired_tool_call_not_end(self, parser):
180
"""Paired <tool_call>...</tool_call> (from template) is NOT end."""
181
assert not parser.is_reasoning_end(
@@ -178,6 +192,26 @@ class TestIsReasoningEnd:
192
assert not parser.is_reasoning_end([])
193
194
195
+class TestDelegatingPromptDetection:
196
+ def test_prompt_tool_example_does_not_skip_streaming_reasoning(
197
+ self, mock_tokenizer, mock_request
198
+ ):
199
+ parser = _Qwen3DelegatingParser(mock_tokenizer)
200
+ prompt_ids = [_TOOL_CALL_ID, _TEXT_ID, _THINK_START_ID]
201
+
202
+ delta = parser.parse_delta(
203
+ "thinking",
204
+ [_TEXT_ID],
205
+ mock_request,
206
+ prompt_token_ids=prompt_ids,
207
+ finished=False,
208
+ )
209
+
210
+ assert delta is not None
211
+ assert delta.reasoning == "thinking"
212
+ assert delta.content is None
213
+
214
+
215
class TestStreaming:
216
def test_basic_streaming(self, parser):
217
reasoning, content = simulate_reasoning_streaming(
vllm/parser/qwen3.py
+6
@@ -206,8 +206,14 @@ class Qwen3Parser(ParserEngine):
206
return True
207
tool_call_id = self._tool_call_token_id
208
tool_call_end_id = self._tool_call_end_token_id
209
+ reasoning_start_id = self._reasoning_start_token_id
210
if tool_call_id is not None:
211
for i in range(len(input_ids) - 1, -1, -1):
212
+ if (
213
+ reasoning_start_id is not None
214
+ and input_ids[i] == reasoning_start_id
215
+ ):
216
+ return False
217
if input_ids[i] == tool_call_id:
218
if tool_call_end_id is not None and any(
219
input_ids[j] == tool_call_end_id