[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:
co-authored by
Alex Bilichenko
parent
f00e163f35
commit
7d567172fc
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user