[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>
This commit is contained in:
alexbi29
2026-06-16 17:48:01 +00:00
committed by GitHub
co-authored by Alex Bilichenko
parent f00e163f35
commit 7d567172fc
2 changed files with 40 additions and 0 deletions
@@ -15,7 +15,12 @@ import pytest
from tests.parser.engine.conftest import make_mock_tokenizer
from tests.parser.engine.streaming_helpers import simulate_reasoning_streaming
from vllm.parser.abstract_parser import DelegatingParser
from vllm.parser.engine.parser_engine_config import ParserState
from vllm.parser.engine.registered_adapters import (
Qwen3ParserReasoningAdapter,
Qwen3ParserToolAdapter,
)
from vllm.parser.qwen3 import Qwen3Parser, qwen3_config
_THINK_START_ID = 50
@@ -32,6 +37,11 @@ _QWEN3_VOCAB = {
}
class _Qwen3DelegatingParser(DelegatingParser):
reasoning_parser_cls = Qwen3ParserReasoningAdapter
tool_parser_cls = Qwen3ParserToolAdapter
@pytest.fixture
def mock_tokenizer():
return make_mock_tokenizer(_QWEN3_VOCAB)
@@ -162,6 +172,10 @@ class TestIsReasoningEnd:
"""Unpaired <tool_call> is implicit reasoning end."""
assert parser.is_reasoning_end([_THINK_START_ID, 1, _TOOL_CALL_ID])
def test_prompt_tool_example_before_generation_think_not_end(self, parser):
"""Tool examples before the generation <think> must not end reasoning."""
assert not parser.is_reasoning_end([_TOOL_CALL_ID, _TEXT_ID, _THINK_START_ID])
def test_paired_tool_call_not_end(self, parser):
"""Paired <tool_call>...</tool_call> (from template) is NOT end."""
assert not parser.is_reasoning_end(
@@ -178,6 +192,26 @@ class TestIsReasoningEnd:
assert not parser.is_reasoning_end([])
class TestDelegatingPromptDetection:
def test_prompt_tool_example_does_not_skip_streaming_reasoning(
self, mock_tokenizer, mock_request
):
parser = _Qwen3DelegatingParser(mock_tokenizer)
prompt_ids = [_TOOL_CALL_ID, _TEXT_ID, _THINK_START_ID]
delta = parser.parse_delta(
"thinking",
[_TEXT_ID],
mock_request,
prompt_token_ids=prompt_ids,
finished=False,
)
assert delta is not None
assert delta.reasoning == "thinking"
assert delta.content is None
class TestStreaming:
def test_basic_streaming(self, parser):
reasoning, content = simulate_reasoning_streaming(
+6
View File
@@ -206,8 +206,14 @@ class Qwen3Parser(ParserEngine):
return True
tool_call_id = self._tool_call_token_id
tool_call_end_id = self._tool_call_end_token_id
reasoning_start_id = self._reasoning_start_token_id
if tool_call_id is not None:
for i in range(len(input_ids) - 1, -1, -1):
if (
reasoning_start_id is not None
and input_ids[i] == reasoning_start_id
):
return False
if input_ids[i] == tool_call_id:
if tool_call_end_id is not None and any(
input_ids[j] == tool_call_end_id