From 65f3fca5683b4c7d48906ff8b2fd44903ec5dba6 Mon Sep 17 00:00:00 2001 From: Mikhail Podvitskii Date: Thu, 10 Sep 2026 01:29:30 +0200 Subject: [PATCH] [Bugfix][Parser] Seed-OSS turn-boundary tokens + boundary-fallback tests (#54264) Signed-off-by: Mikhail Podvitskii Co-authored-by: Flora Feng <4florafeng@gmail.com> --- tests/parser/engine/test_qwen3_reasoning.py | 13 +++++ tests/parser/engine/test_seed_oss.py | 56 +++++++++++++++++++-- vllm/parser/seed_oss.py | 7 ++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/tests/parser/engine/test_qwen3_reasoning.py b/tests/parser/engine/test_qwen3_reasoning.py index a9bec494bc..37e6b5d130 100644 --- a/tests/parser/engine/test_qwen3_reasoning.py +++ b/tests/parser/engine/test_qwen3_reasoning.py @@ -261,6 +261,19 @@ class TestIsReasoningEndTurnBoundaries: 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( diff --git a/tests/parser/engine/test_seed_oss.py b/tests/parser/engine/test_seed_oss.py index ce118faa44..e01f2fd9f6 100644 --- a/tests/parser/engine/test_seed_oss.py +++ b/tests/parser/engine/test_seed_oss.py @@ -33,15 +33,24 @@ TOOL_CALL_START = "" TOOL_CALL_END = "" THINK_START = "" THINK_END = "" +TURN_START = "" +TURN_END = "" +_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, } @@ -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 ``role`` / + ```` and replays prior-turn ``reasoning_content`` inside + ``...``, so a stale ```` 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): @@ -133,7 +179,7 @@ def test_streaming_think_end_and_tool_call_same_delta(parser): f"{THINK_END}{TOOL_CALL_START}", "", ], - [(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 diff --git a/vllm/parser/seed_oss.py b/vllm/parser/seed_oss.py index 2f709f0ad6..ceaa04f561 100644 --- a/vllm/parser/seed_oss.py +++ b/vllm/parser/seed_oss.py @@ -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:: -> -> -> -> + <|im_start|> -> + <|im_end|> -> ```` and ```` are byte-identical, so the entire transition table and ``_qwen3_arg_converter`` are inherited from @@ -26,3 +28,4 @@ class SeedOssParser(Qwen3Parser): THINK_END = "" TOOL_START = "" TOOL_END = "" + TURN_BOUNDARIES = frozenset(("", ""))