[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>"))