@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 7d567172fc
Fork 0

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