[Bugfix][Parser] Seed-OSS turn-boundary tokens + boundary-fallback tests (#54264)
Signed-off-by: Mikhail Podvitskii <podvitskiymichael@gmail.com> Co-authored-by: Flora Feng <4florafeng@gmail.com>
Mikhail Podvitskii committed
Sep 10, 2026 at 01:29 UTC
65f3fca5683b4c7d48906ff8b2fd44903ec5dba6
3 files changed
+69
-7
tests/parser/engine/test_qwen3_reasoning.py
+13
@@ -261,6 +261,19 @@ class TestIsReasoningEndTurnBoundaries:
261
parser = Qwen3Parser(make_mock_tokenizer(vocab))
262
assert parser.is_reasoning_end([_TEXT_ID, _THINK_END_ID, _TEXT_ID])
263
264
+ def test_boundary_with_thinking_disabled_is_end(self, mock_tokenizer):
265
+ """With thinking disabled (initial state CONTENT) a walk that stops at a turn
266
+ boundary must report reasoning as ended: the model never emits a marker."""
267
+ parser = Qwen3Parser(
268
+ mock_tokenizer, chat_template_kwargs={"enable_thinking": False}
269
+ )
270
+ assert parser.is_reasoning_end([_IM_START_ID, _TEXT_ID])
271
+
272
+ def test_boundary_with_thinking_enabled_not_end(self, parser):
273
+ """With thinking disabled (initial state REASONING) a walk that stops at a turn
274
+ boundary must keep reasoning open"""
275
+ assert not parser.is_reasoning_end([_IM_START_ID, _TEXT_ID])
276
+
277
278
class TestDelegatingPromptDetection:
279
def test_prompt_tool_example_does_not_skip_streaming_reasoning(
tests/parser/engine/test_seed_oss.py
+51
-5
@@ -33,15 +33,24 @@ TOOL_CALL_START = "<seed:tool_call>"
33
TOOL_CALL_END = "</seed:tool_call>"
34
THINK_START = "<seed:think>"
35
THINK_END = "</seed:think>"
36
+TURN_START = "<seed:bos>"
37
+TURN_END = "<seed:eos>"
38
39
+_THINK_START_ID = 50
40
_THINK_END_ID = 51
38
-_TOOL_CALL_ID = 60
41
+_TOOL_CALL_START_ID = 60
42
+_TOOL_CALL_END_ID = 61
43
+_SEED_BOS_ID = 70
44
+_SEED_EOS_ID = 71
45
+_TEXT_ID = 100
46
47
_SEED_OSS_VOCAB = {
41
- THINK_START: 50,
48
+ THINK_START: _THINK_START_ID,
49
THINK_END: _THINK_END_ID,
43
- TOOL_CALL_START: _TOOL_CALL_ID,
44
- TOOL_CALL_END: 61,
50
+ TOOL_CALL_START: _TOOL_CALL_START_ID,
51
+ TOOL_CALL_END: _TOOL_CALL_END_ID,
52
+ TURN_START: _SEED_BOS_ID,
53
+ TURN_END: _SEED_EOS_ID,
54
}
55
56
@@ -66,6 +75,43 @@ def test_token_overrides_wired(parser):
75
assert parser.parser_engine_config.name == "seed_oss"
76
assert parser.reasoning_start_str == THINK_START
77
assert parser.reasoning_end_str == THINK_END
78
+ assert parser.parser_engine_config.turn_boundary_tokens == frozenset(
79
+ (TURN_START, TURN_END)
80
+ )
81
+
82
+
83
+class TestSeedTurnBoundaries:
84
+ """The Seed-OSS chat template wraps every message in ``<seed:bos>role`` /
85
+ ``<seed:eos>`` and replays prior-turn ``reasoning_content`` inside
86
+ ``<seed:think>...</seed:think>``, so a stale ``</seed:think>`` in history
87
+ is the normal multi-turn case. The backward walk must stop at the Seed
88
+ boundary tokens; the inherited ChatML tokens are absent from the Seed
89
+ vocab and would disable the scoping entirely.
90
+ """
91
+
92
+ def test_replayed_reasoning_in_history_not_end(self, parser):
93
+ assert not parser.is_reasoning_end(
94
+ [
95
+ _SEED_BOS_ID,
96
+ _THINK_START_ID,
97
+ _TEXT_ID,
98
+ _THINK_END_ID,
99
+ _TEXT_ID,
100
+ _SEED_EOS_ID,
101
+ _SEED_BOS_ID,
102
+ ]
103
+ )
104
+
105
+ def test_think_end_in_current_turn_is_end(self, parser):
106
+ assert parser.is_reasoning_end(
107
+ [_SEED_BOS_ID, _TEXT_ID, _SEED_EOS_ID, _SEED_BOS_ID, _THINK_END_ID]
108
+ )
109
+
110
+ def test_boundary_with_thinking_disabled_is_end(self, mock_tokenizer):
111
+ parser = SeedOssParser(
112
+ mock_tokenizer, chat_template_kwargs={"enable_thinking": False}
113
+ )
114
+ assert parser.is_reasoning_end([_SEED_BOS_ID, _TEXT_ID])
115
116
117
def test_single_tool_call(tool_parser, mock_request):
@@ -133,7 +179,7 @@ def test_streaming_think_end_and_tool_call_same_delta(parser):
179
f"{THINK_END}{TOOL_CALL_START}",
180
"<function=read>",
181
],
136
- [(1,), (_THINK_END_ID, _TOOL_CALL_ID), (2,)],
182
+ [(1,), (_THINK_END_ID, _TOOL_CALL_START_ID), (2,)],
183
)
184
assert reasoning == "Let me list the directory."
185
assert THINK_END not in reasoning
vllm/parser/seed_oss.py
+5
-2
@@ -2,13 +2,15 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""seed_oss parser for tool calls and reasoning.
4
5
-seed_oss shares the Qwen3 XML grammar exactly; only the four wrapper
6
-token strings differ::
5
+seed_oss shares the Qwen3 XML grammar exactly; only the wrapper token
6
+strings and the turn-boundary tokens differ::
7
8
<think> -> <seed:think>
9
</think> -> </seed:think>
10
<tool_call> -> <seed:tool_call>
11
</tool_call> -> </seed:tool_call>
12
+ <|im_start|> -> <seed:bos>
13
+ <|im_end|> -> <seed:eos>
14
15
``<function=...>`` and ``<parameter=...>`` are byte-identical, so the
16
entire transition table and ``_qwen3_arg_converter`` are inherited from
@@ -26,3 +28,4 @@ class SeedOssParser(Qwen3Parser):
28
THINK_END = "</seed:think>"
29
TOOL_START = "<seed:tool_call>"
30
TOOL_END = "</seed:tool_call>"
31
+ TURN_BOUNDARIES = frozenset(("<seed:bos>", "<seed:eos>"))