Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions tests/parser/engine/test_qwen3_reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@ def test_boundary_tokens_missing_from_vocab_keep_global_walk(self):
parser = Qwen3Parser(make_mock_tokenizer(vocab))
assert parser.is_reasoning_end([_TEXT_ID, _THINK_END_ID, _TEXT_ID])

def test_boundary_with_thinking_disabled_is_end(self, mock_tokenizer):
"""With thinking disabled (initial state CONTENT) a walk that stops at a turn
boundary must report reasoning as ended: the model never emits a marker."""
parser = Qwen3Parser(
mock_tokenizer, chat_template_kwargs={"enable_thinking": False}
)
assert parser.is_reasoning_end([_IM_START_ID, _TEXT_ID])

def test_boundary_with_thinking_enabled_not_end(self, parser):
"""With thinking disabled (initial state REASONING) a walk that stops at a turn
boundary must keep reasoning open"""
assert not parser.is_reasoning_end([_IM_START_ID, _TEXT_ID])


class TestDelegatingPromptDetection:
def test_prompt_tool_example_does_not_skip_streaming_reasoning(
Expand Down
56 changes: 51 additions & 5 deletions tests/parser/engine/test_seed_oss.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,24 @@
TOOL_CALL_END = "</seed:tool_call>"
THINK_START = "<seed:think>"
THINK_END = "</seed:think>"
TURN_START = "<seed:bos>"
TURN_END = "<seed:eos>"

_THINK_START_ID = 50
_THINK_END_ID = 51
_TOOL_CALL_ID = 60
_TOOL_CALL_START_ID = 60
_TOOL_CALL_END_ID = 61
_SEED_BOS_ID = 70
_SEED_EOS_ID = 71
_TEXT_ID = 100

_SEED_OSS_VOCAB = {
THINK_START: 50,
THINK_START: _THINK_START_ID,
THINK_END: _THINK_END_ID,
TOOL_CALL_START: _TOOL_CALL_ID,
TOOL_CALL_END: 61,
TOOL_CALL_START: _TOOL_CALL_START_ID,
TOOL_CALL_END: _TOOL_CALL_END_ID,
TURN_START: _SEED_BOS_ID,
TURN_END: _SEED_EOS_ID,
}


Expand All @@ -66,6 +75,43 @@ def test_token_overrides_wired(parser):
assert parser.parser_engine_config.name == "seed_oss"
assert parser.reasoning_start_str == THINK_START
assert parser.reasoning_end_str == THINK_END
assert parser.parser_engine_config.turn_boundary_tokens == frozenset(
(TURN_START, TURN_END)
)


class TestSeedTurnBoundaries:
"""The Seed-OSS chat template wraps every message in ``<seed:bos>role`` /
``<seed:eos>`` and replays prior-turn ``reasoning_content`` inside
``<seed:think>...</seed:think>``, so a stale ``</seed:think>`` in history
is the normal multi-turn case. The backward walk must stop at the Seed
boundary tokens; the inherited ChatML tokens are absent from the Seed
vocab and would disable the scoping entirely.
"""

def test_replayed_reasoning_in_history_not_end(self, parser):
assert not parser.is_reasoning_end(
[
_SEED_BOS_ID,
_THINK_START_ID,
_TEXT_ID,
_THINK_END_ID,
_TEXT_ID,
_SEED_EOS_ID,
_SEED_BOS_ID,
]
)

def test_think_end_in_current_turn_is_end(self, parser):
assert parser.is_reasoning_end(
[_SEED_BOS_ID, _TEXT_ID, _SEED_EOS_ID, _SEED_BOS_ID, _THINK_END_ID]
)

def test_boundary_with_thinking_disabled_is_end(self, mock_tokenizer):
parser = SeedOssParser(
mock_tokenizer, chat_template_kwargs={"enable_thinking": False}
)
assert parser.is_reasoning_end([_SEED_BOS_ID, _TEXT_ID])


def test_single_tool_call(tool_parser, mock_request):
Expand Down Expand Up @@ -133,7 +179,7 @@ def test_streaming_think_end_and_tool_call_same_delta(parser):
f"{THINK_END}{TOOL_CALL_START}",
"<function=read>",
],
[(1,), (_THINK_END_ID, _TOOL_CALL_ID), (2,)],
[(1,), (_THINK_END_ID, _TOOL_CALL_START_ID), (2,)],
)
assert reasoning == "Let me list the directory."
assert THINK_END not in reasoning
Expand Down
7 changes: 5 additions & 2 deletions vllm/parser/seed_oss.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""seed_oss parser for tool calls and reasoning.

seed_oss shares the Qwen3 XML grammar exactly; only the four wrapper
token strings differ::
seed_oss shares the Qwen3 XML grammar exactly; only the wrapper token
strings and the turn-boundary tokens differ::

<think> -> <seed:think>
</think> -> </seed:think>
<tool_call> -> <seed:tool_call>
</tool_call> -> </seed:tool_call>
<|im_start|> -> <seed:bos>
<|im_end|> -> <seed:eos>

``<function=...>`` and ``<parameter=...>`` are byte-identical, so the
entire transition table and ``_qwen3_arg_converter`` are inherited from
Expand All @@ -26,3 +28,4 @@ class SeedOssParser(Qwen3Parser):
THINK_END = "</seed:think>"
TOOL_START = "<seed:tool_call>"
TOOL_END = "</seed:tool_call>"
TURN_BOUNDARIES = frozenset(("<seed:bos>", "<seed:eos>"))
Loading