A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Model] Support SeedOss Reason Parser (#24263)
Signed-off-by: Yan Lu <luyan@nvidia.com> Co-authored-by: Michael Goin <mgoin64@gmail.com>
0xNullPath committed
Sep 24, 2025 at 08:15 UTC
be0bb568c92069a1ef3250307c818a46702255de
9 files changed
+887
-246
tests/reasoning/test_base_thinking_reasoning_parser.py
new
+392
@@ -0,0 +1,392 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import pytest
5
+from transformers import AutoTokenizer
6
+
7
+from tests.reasoning.utils import run_reasoning_extraction
8
+from vllm.entrypoints.openai.protocol import ChatCompletionRequest
9
+from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
10
+
11
+
12
+# Create a concrete test implementation of BaseThinkingReasoningParser
13
+class TestThinkingReasoningParser(BaseThinkingReasoningParser):
14
+ """Test implementation of BaseThinkingReasoningParser."""
15
+
16
+ @property
17
+ def start_token(self) -> str:
18
+ return "<test:think>"
19
+
20
+ @property
21
+ def end_token(self) -> str:
22
+ return "</test:think>"
23
+
24
+
25
+class TestThinkingReasoningParserAlt(BaseThinkingReasoningParser):
26
+ """Alternative test implementation with different tokens."""
27
+
28
+ @property
29
+ def start_token(self) -> str:
30
+ return "<alt:start>"
31
+
32
+ @property
33
+ def end_token(self) -> str:
34
+ return "<alt:end>"
35
+
36
+
37
+# Use a test model
38
+REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
39
+
40
+
41
+@pytest.fixture(scope="module")
42
+def test_tokenizer():
43
+ tokenizer = AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
44
+ # Add custom test tokens
45
+ test_tokens = ["<test:think>", "</test:think>", "<alt:start>", "<alt:end>"]
46
+ existing_tokens = set(tokenizer.get_vocab().keys())
47
+ new_tokens = [
48
+ token for token in test_tokens if token not in existing_tokens
49
+ ]
50
+ if new_tokens:
51
+ tokenizer.add_tokens(new_tokens)
52
+ return tokenizer
53
+
54
+
55
+class TestBaseThinkingReasoningParserInit:
56
+ """
57
+ Test initialization and basic properties of
58
+ BaseThinkingReasoningParser.
59
+ """
60
+
61
+ def test_successful_initialization(self, test_tokenizer):
62
+ """Test successful initialization with valid tokens."""
63
+ parser = TestThinkingReasoningParser(test_tokenizer)
64
+ assert parser.start_token == "<test:think>"
65
+ assert parser.end_token == "</test:think>"
66
+ assert parser.start_token_id is not None
67
+ assert parser.end_token_id is not None
68
+
69
+ def test_initialization_with_missing_tokenizer(self):
70
+ """Test that initialization fails without tokenizer."""
71
+ with pytest.raises(ValueError, match="model tokenizer must be passed"):
72
+ TestThinkingReasoningParser(None)
73
+
74
+ def test_initialization_with_missing_tokens(self, test_tokenizer):
75
+ """Test that initialization fails when tokens are not in vocabulary."""
76
+
77
+ # Create a parser with tokens not in vocabulary
78
+ class MissingTokenParser(BaseThinkingReasoningParser):
79
+
80
+ @property
81
+ def start_token(self) -> str:
82
+ return "<missing:start>"
83
+
84
+ @property
85
+ def end_token(self) -> str:
86
+ return "<missing:end>"
87
+
88
+ with pytest.raises(RuntimeError,
89
+ match="could not locate think start/end tokens"):
90
+ MissingTokenParser(test_tokenizer)
91
+
92
+ def test_initialization_with_empty_tokens(self, test_tokenizer):
93
+ """Test that initialization fails with empty token strings."""
94
+
95
+ class EmptyTokenParser(BaseThinkingReasoningParser):
96
+
97
+ @property
98
+ def start_token(self) -> str:
99
+ return ""
100
+
101
+ @property
102
+ def end_token(self) -> str:
103
+ return ""
104
+
105
+ with pytest.raises(ValueError,
106
+ match="start_token and end_token must be defined"):
107
+ EmptyTokenParser(test_tokenizer)
108
+
109
+
110
+class TestBaseThinkingReasoningParserMethods:
111
+ """Test the methods of BaseThinkingReasoningParser."""
112
+
113
+ def test_is_reasoning_end(self, test_tokenizer):
114
+ """Test the is_reasoning_end method."""
115
+ parser = TestThinkingReasoningParser(test_tokenizer)
116
+ end_token_id = parser.end_token_id
117
+
118
+ # Test with end token present
119
+ assert parser.is_reasoning_end([1, 2, end_token_id, 4]) is True
120
+
121
+ # Test without end token
122
+ assert parser.is_reasoning_end([1, 2, 3, 4]) is False
123
+
124
+ # Test with empty list
125
+ assert parser.is_reasoning_end([]) is False
126
+
127
+ def test_extract_content_ids(self, test_tokenizer):
128
+ """Test the extract_content_ids method."""
129
+ parser = TestThinkingReasoningParser(test_tokenizer)
130
+ end_token_id = parser.end_token_id
131
+
132
+ # Test with end token in the middle
133
+ input_ids = [1, 2, end_token_id, 4, 5]
134
+ content_ids = parser.extract_content_ids(input_ids)
135
+ assert content_ids == [4, 5]
136
+
137
+ # Test with end token at the end
138
+ input_ids = [1, 2, 3, end_token_id]
139
+ content_ids = parser.extract_content_ids(input_ids)
140
+ assert content_ids == []
141
+
142
+ # Test without end token
143
+ input_ids = [1, 2, 3, 4]
144
+ content_ids = parser.extract_content_ids(input_ids)
145
+ assert content_ids == []
146
+
147
+ # Test with end token as last element (should not extract)
148
+ input_ids = [1, 2, 3, end_token_id]
149
+ content_ids = parser.extract_content_ids(input_ids)
150
+ assert content_ids == []
151
+
152
+
153
+class TestBaseThinkingReasoningParserExtraction:
154
+ """Test reasoning content extraction methods."""
155
+
156
+ def test_extract_reasoning_content_with_both_tokens(self, test_tokenizer):
157
+ """Test extraction when both start and end tokens are present."""
158
+ parser = TestThinkingReasoningParser(test_tokenizer)
159
+ request = ChatCompletionRequest(messages=[], model="test-model")
160
+
161
+ model_output = ("<test:think>This is reasoning"
162
+ "</test:think>This is content")
163
+ reasoning, content = parser.extract_reasoning_content(
164
+ model_output, request)
165
+
166
+ assert reasoning == "This is reasoning"
167
+ assert content == "This is content"
168
+
169
+ def test_extract_reasoning_content_only_end_token(self, test_tokenizer):
170
+ """Test extraction when only end token is present."""
171
+ parser = TestThinkingReasoningParser(test_tokenizer)
172
+ request = ChatCompletionRequest(messages=[], model="test-model")
173
+
174
+ model_output = ("This is reasoning</test:think>This is content")
175
+ reasoning, content = parser.extract_reasoning_content(
176
+ model_output, request)
177
+
178
+ assert reasoning == "This is reasoning"
179
+ assert content == "This is content"
180
+
181
+ def test_extract_reasoning_content_no_end_token(self, test_tokenizer):
182
+ """Test extraction when no end token is present."""
183
+ parser = TestThinkingReasoningParser(test_tokenizer)
184
+ request = ChatCompletionRequest(messages=[], model="test-model")
185
+
186
+ model_output = "This is just content"
187
+ reasoning, content = parser.extract_reasoning_content(
188
+ model_output, request)
189
+
190
+ assert reasoning == "This is just content"
191
+ assert content is None
192
+
193
+ def test_extract_reasoning_content_empty_output(self, test_tokenizer):
194
+ """Test extraction with empty output."""
195
+ parser = TestThinkingReasoningParser(test_tokenizer)
196
+ request = ChatCompletionRequest(messages=[], model="test-model")
197
+
198
+ model_output = ""
199
+ reasoning, content = parser.extract_reasoning_content(
200
+ model_output, request)
201
+
202
+ assert reasoning == ""
203
+ assert content is None
204
+
205
+ def test_extract_reasoning_content_only_tokens(self, test_tokenizer):
206
+ """Test extraction with only tokens and no content."""
207
+ parser = TestThinkingReasoningParser(test_tokenizer)
208
+ request = ChatCompletionRequest(messages=[], model="test-model")
209
+
210
+ model_output = ("<test:think></test:think>")
211
+ reasoning, content = parser.extract_reasoning_content(
212
+ model_output, request)
213
+
214
+ assert reasoning == ""
215
+ assert content is None
216
+
217
+
218
+class TestBaseThinkingReasoningParserStreaming:
219
+ """Test streaming functionality of BaseThinkingReasoningParser."""
220
+
221
+ @pytest.mark.parametrize("streaming", [True, False])
222
+ def test_simple_reasoning_extraction(self, test_tokenizer, streaming):
223
+ """
224
+ Test basic reasoning extraction in both
225
+ streaming and non-streaming modes.
226
+ """
227
+ parser = TestThinkingReasoningParser(test_tokenizer)
228
+
229
+ model_output = [
230
+ "<test:think>", "Some ", "reasoning ", "content", "</test:think>",
231
+ "Final ", "answer"
232
+ ]
233
+
234
+ reasoning, content = run_reasoning_extraction(parser,
235
+ model_output,
236
+ streaming=streaming)
237
+
238
+ assert reasoning == "Some reasoning content"
239
+ assert content == "Final answer"
240
+
241
+ def test_streaming_with_incremental_deltas(self, test_tokenizer):
242
+ """Test streaming processing with small incremental deltas."""
243
+ parser = TestThinkingReasoningParser(test_tokenizer)
244
+
245
+ deltas = [
246
+ "<test:think>",
247
+ "Some ",
248
+ "reasoning ",
249
+ "content",
250
+ "</test:think>",
251
+ "Final ",
252
+ "answer",
253
+ ]
254
+
255
+ reasoning, content = run_reasoning_extraction(parser,
256
+ deltas,
257
+ streaming=True)
258
+
259
+ assert reasoning == "Some reasoning content"
260
+ assert content == "Final answer"
261
+
262
+ def test_streaming_with_start_token(self, test_tokenizer):
263
+ """Test streaming with start token included."""
264
+ parser = TestThinkingReasoningParser(test_tokenizer)
265
+
266
+ deltas = [
267
+ "<test:think>",
268
+ "Some ",
269
+ "reasoning",
270
+ "</test:think>",
271
+ "Answer",
272
+ ]
273
+
274
+ reasoning, content = run_reasoning_extraction(parser,
275
+ deltas,
276
+ streaming=True)
277
+
278
+ assert reasoning == "Some reasoning"
279
+ assert content == "Answer"
280
+
281
+ def test_streaming_no_end_token(self, test_tokenizer):
282
+ """Test streaming when no end token is encountered."""
283
+ parser = TestThinkingReasoningParser(test_tokenizer)
284
+
285
+ deltas = [
286
+ "<test:think>",
287
+ "Some ",
288
+ "reasoning ",
289
+ "without ",
290
+ "end",
291
+ ]
292
+
293
+ reasoning, content = run_reasoning_extraction(parser,
294
+ deltas,
295
+ streaming=True)
296
+
297
+ assert reasoning == "Some reasoning without end"
298
+ assert content is None
299
+
300
+ def test_streaming_only_end_token(self, test_tokenizer):
301
+ """Test streaming when only end token appears."""
302
+ parser = TestThinkingReasoningParser(test_tokenizer)
303
+
304
+ deltas = [
305
+ "<test:think>",
306
+ "Reasoning ",
307
+ "content",
308
+ "</test:think>",
309
+ "Final",
310
+ ]
311
+
312
+ reasoning, content = run_reasoning_extraction(parser,
313
+ deltas,
314
+ streaming=True)
315
+
316
+ assert reasoning == "Reasoning content"
317
+ assert content == "Final"
318
+
319
+
320
+class TestBaseThinkingReasoningParserMultipleImplementations:
321
+ """
322
+ Test that multiple implementations of
323
+ BaseThinkingReasoningParser work correctly.
324
+ """
325
+
326
+ def test_different_token_implementations(self, test_tokenizer):
327
+ """
328
+ Test that different implementations
329
+ with different tokens work independently.
330
+ """
331
+ parser1 = TestThinkingReasoningParser(test_tokenizer)
332
+ parser2 = TestThinkingReasoningParserAlt(test_tokenizer)
333
+
334
+ # Test parser1
335
+ model_output1 = ("Reasoning1</test:think>Content1")
336
+ reasoning1, content1 = run_reasoning_extraction(
337
+ parser1, [model_output1])
338
+ assert reasoning1 == "Reasoning1"
339
+ assert content1 == "Content1"
340
+
341
+ # Test parser2
342
+ model_output2 = "Reasoning2<alt:end>Content2"
343
+ reasoning2, content2 = run_reasoning_extraction(
344
+ parser2, [model_output2])
345
+ assert reasoning2 == "Reasoning2"
346
+ assert content2 == "Content2"
347
+
348
+ # Verify tokens are different
349
+ assert parser1.start_token != parser2.start_token
350
+ assert parser1.end_token != parser2.end_token
351
+ assert parser1.start_token_id != parser2.start_token_id
352
+ assert parser1.end_token_id != parser2.end_token_id
353
+
354
+
355
+class TestBaseThinkingReasoningParserEdgeCases:
356
+ """Test edge cases and error conditions."""
357
+
358
+ def test_multiple_end_tokens(self, test_tokenizer):
359
+ """Test behavior with multiple end tokens."""
360
+ parser = TestThinkingReasoningParser(test_tokenizer)
361
+
362
+ model_output = ("First</test:think>Middle</test:think>Last")
363
+ reasoning, content = run_reasoning_extraction(parser, [model_output])
364
+
365
+ # Should stop at first end token
366
+ assert reasoning == "First"
367
+ assert content == "Middle</test:think>Last"
368
+
369
+ def test_nested_tokens(self, test_tokenizer):
370
+ """Test behavior with nested-like token patterns."""
371
+ parser = TestThinkingReasoningParser(test_tokenizer)
372
+
373
+ model_output = ("<test:think>Outer"
374
+ "<test:think>Inner</test:think>Content")
375
+ reasoning, content = run_reasoning_extraction(parser, [model_output])
376
+
377
+ # Should process normally, start from first start token
378
+ assert reasoning == "Outer<test:think>Inner"
379
+ assert content == "Content"
380
+
381
+ def test_malformed_tokens(self, test_tokenizer):
382
+ """Test behavior with malformed token-like strings."""
383
+ parser = TestThinkingReasoningParser(test_tokenizer)
384
+
385
+ model_output = ("<test:thinking>Not a real token"
386
+ "</test:thinking>Content")
387
+ reasoning, content = run_reasoning_extraction(parser, [model_output])
388
+
389
+ # Should treat as regular content since tokens don't match exactly
390
+ assert reasoning == ("<test:thinking>Not a real token"
391
+ "</test:thinking>Content")
392
+ assert content is None
tests/reasoning/test_seedoss_reasoning_parser.py
new
+237
@@ -0,0 +1,237 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import Any, cast
5
+
6
+import pytest
7
+from transformers import AutoTokenizer
8
+
9
+from tests.reasoning.utils import run_reasoning_extraction
10
+from vllm.reasoning import ReasoningParser, ReasoningParserManager
11
+
12
+parser_name = "seed_oss"
13
+start_token = "<seed:think>"
14
+end_token = "</seed:think>"
15
+
16
+# Use a test model that contains our custom tokens
17
+REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
18
+
19
+
20
+@pytest.fixture(scope="module")
21
+def seedoss_tokenizer():
22
+ tokenizer = AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
23
+ # Add custom SeedOSS tokens if they don't exist
24
+ if start_token not in tokenizer.get_vocab():
25
+ tokenizer.add_tokens([start_token, end_token])
26
+ return tokenizer
27
+
28
+
29
+SIMPLE_REASONING: dict[str, Any] = {
30
+ "output": "This is a reasoning section</seed:think>This is the rest",
31
+ "reasoning_content": "This is a reasoning section",
32
+ "content": "This is the rest",
33
+ "is_reasoning_end": True,
34
+}
35
+COMPLETE_REASONING: dict[str, Any] = {
36
+ "output": "This is a reasoning section</seed:think>",
37
+ "reasoning_content": "This is a reasoning section",
38
+ "content": None,
39
+ "is_reasoning_end": True,
40
+}
41
+NO_CONTENT: dict[str, Any] = {
42
+ "output": "This is content",
43
+ "reasoning_content": "This is content",
44
+ "content": None,
45
+ "is_reasoning_end": False,
46
+}
47
+NO_REASONING_STREAMING: dict[str, Any] = {
48
+ "output": "This is a reasoning section",
49
+ "reasoning_content": "This is a reasoning section",
50
+ "content": None,
51
+ "is_reasoning_end": False,
52
+}
53
+MULTIPLE_LINES: dict[str, Any] = {
54
+ "output": "This\nThat</seed:think>This is the rest\nThat",
55
+ "reasoning_content": "This\nThat",
56
+ "content": "This is the rest\nThat",
57
+ "is_reasoning_end": True,
58
+}
59
+WITH_START_TOKEN: dict[str, Any] = {
60
+ "output": ("<seed:think>This is a reasoning section"
61
+ "</seed:think>This is the rest"),
62
+ "reasoning_content":
63
+ "This is a reasoning section",
64
+ "content":
65
+ "This is the rest",
66
+ "is_reasoning_end":
67
+ True,
68
+}
69
+ONLY_END_TOKEN: dict[str, Any] = {
70
+ "output": "Some reasoning</seed:think>This is the rest",
71
+ "reasoning_content": "Some reasoning",
72
+ "content": "This is the rest",
73
+ "is_reasoning_end": True,
74
+}
75
+NO_TOKENS: dict[str, Any] = {
76
+ "output": "This is just content without any reasoning tokens",
77
+ "reasoning_content": "This is just content without any reasoning tokens",
78
+ "content": None,
79
+ "is_reasoning_end": False,
80
+}
81
+
82
+
83
+def test_seedoss_reasoning_parser_creation(seedoss_tokenizer):
84
+ """Test that the SeedOSS reasoning parser can be created and registered."""
85
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
86
+ parser = parser_cls(seedoss_tokenizer)
87
+ assert isinstance(parser, ReasoningParser)
88
+ assert parser.start_token == start_token
89
+ assert parser.end_token == end_token
90
+
91
+
92
+@pytest.mark.parametrize("streaming", [True, False])
93
+def test_simple_reasoning(seedoss_tokenizer, streaming):
94
+ """Test basic reasoning extraction with both tokens."""
95
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
96
+ parser = parser_cls(seedoss_tokenizer)
97
+
98
+ reasoning, content = run_reasoning_extraction(
99
+ parser, [cast(str, SIMPLE_REASONING["output"])], streaming=streaming)
100
+
101
+ assert reasoning == SIMPLE_REASONING["reasoning_content"]
102
+ assert content == SIMPLE_REASONING["content"]
103
+
104
+
105
+@pytest.mark.parametrize("streaming", [True, False])
106
+def test_complete_reasoning(seedoss_tokenizer, streaming):
107
+ """Test reasoning extraction when there's no content after reasoning."""
108
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
109
+ parser = parser_cls(seedoss_tokenizer)
110
+
111
+ reasoning, content = run_reasoning_extraction(
112
+ parser, [cast(str, COMPLETE_REASONING["output"])], streaming=streaming)
113
+
114
+ assert reasoning == COMPLETE_REASONING["reasoning_content"]
115
+ assert content == COMPLETE_REASONING["content"]
116
+
117
+
118
+@pytest.mark.parametrize("streaming", [True, False])
119
+def test_no_content(seedoss_tokenizer, streaming):
120
+ """Test when there's no end token - everything is reasoning content."""
121
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
122
+ parser = parser_cls(seedoss_tokenizer)
123
+
124
+ reasoning, content = run_reasoning_extraction(
125
+ parser, [cast(str, NO_CONTENT["output"])], streaming=streaming)
126
+
127
+ assert reasoning == NO_CONTENT["reasoning_content"]
128
+ assert content == NO_CONTENT["content"]
129
+
130
+
131
+@pytest.mark.parametrize("streaming", [True, False])
132
+def test_multiple_lines(seedoss_tokenizer, streaming):
133
+ """Test reasoning extraction with multiline content."""
134
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
135
+ parser = parser_cls(seedoss_tokenizer)
136
+
137
+ reasoning, content = run_reasoning_extraction(
138
+ parser, [cast(str, MULTIPLE_LINES["output"])], streaming=streaming)
139
+
140
+ assert reasoning == MULTIPLE_LINES["reasoning_content"]
141
+ assert content == MULTIPLE_LINES["content"]
142
+
143
+
144
+@pytest.mark.parametrize("streaming", [True, False])
145
+def test_with_start_token(seedoss_tokenizer, streaming):
146
+ """Test reasoning extraction with both start and end tokens."""
147
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
148
+ parser = parser_cls(seedoss_tokenizer)
149
+
150
+ reasoning, content = run_reasoning_extraction(
151
+ parser, [cast(str, WITH_START_TOKEN["output"])], streaming=streaming)
152
+
153
+ assert reasoning == WITH_START_TOKEN["reasoning_content"]
154
+ assert content == WITH_START_TOKEN["content"]
155
+
156
+
157
+@pytest.mark.parametrize("streaming", [True, False])
158
+def test_only_end_token(seedoss_tokenizer, streaming):
159
+ """
160
+ Test reasoning extraction with only end token
161
+ (SeedOSS typical behavior).
162
+ """
163
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
164
+ parser = parser_cls(seedoss_tokenizer)
165
+
166
+ reasoning, content = run_reasoning_extraction(
167
+ parser, [cast(str, ONLY_END_TOKEN["output"])], streaming=streaming)
168
+
169
+ assert reasoning == ONLY_END_TOKEN["reasoning_content"]
170
+ assert content == ONLY_END_TOKEN["content"]
171
+
172
+
173
+@pytest.mark.parametrize("streaming", [True, False])
174
+def test_no_tokens(seedoss_tokenizer, streaming):
175
+ """Test when there are no reasoning tokens at all."""
176
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
177
+ parser = parser_cls(seedoss_tokenizer)
178
+
179
+ reasoning, content = run_reasoning_extraction(
180
+ parser, [cast(str, NO_TOKENS["output"])], streaming=streaming)
181
+
182
+ assert reasoning == NO_TOKENS["reasoning_content"]
183
+ assert content == NO_TOKENS["content"]
184
+
185
+
186
+def test_is_reasoning_end(seedoss_tokenizer):
187
+ """Test the is_reasoning_end method."""
188
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
189
+ parser = parser_cls(seedoss_tokenizer)
190
+
191
+ # Test with end token present
192
+ end_token_id = parser.end_token_id
193
+ assert parser.is_reasoning_end([1, 2, end_token_id, 4]) is True
194
+
195
+ # Test without end token
196
+ assert parser.is_reasoning_end([1, 2, 3, 4]) is False
197
+
198
+
199
+def test_extract_content_ids(seedoss_tokenizer):
200
+ """Test the extract_content_ids method."""
201
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
202
+ parser = parser_cls(seedoss_tokenizer)
203
+
204
+ end_token_id = parser.end_token_id
205
+
206
+ # Test with end token in the middle
207
+ input_ids = [1, 2, end_token_id, 4, 5]
208
+ content_ids = parser.extract_content_ids(input_ids)
209
+ assert content_ids == [4, 5]
210
+
211
+ # Test with end token at the end
212
+ input_ids = [1, 2, 3, end_token_id]
213
+ content_ids = parser.extract_content_ids(input_ids)
214
+ assert content_ids == []
215
+
216
+ # Test without end token
217
+ input_ids = [1, 2, 3, 4]
218
+ content_ids = parser.extract_content_ids(input_ids)
219
+ assert content_ids == []
220
+
221
+
222
+def test_streaming_delta_processing(seedoss_tokenizer):
223
+ """Test streaming processing with small deltas."""
224
+ parser_cls = ReasoningParserManager.get_reasoning_parser(parser_name)
225
+ parser = parser_cls(seedoss_tokenizer)
226
+
227
+ # Test streaming with incremental tokens
228
+ deltas = [
229
+ "Some ", "reasoning ", "content", "</seed:think>", "Final ", "answer"
230
+ ]
231
+
232
+ reasoning, content = run_reasoning_extraction(parser,
233
+ deltas,
234
+ streaming=True)
235
+
236
+ assert reasoning == "Some reasoning content"
237
+ assert content == "Final answer"
vllm/reasoning/__init__.py
+4
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from .abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
5
+from .basic_parsers import BaseThinkingReasoningParser
6
from .deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
7
from .glm4_moe_reasoning_parser import Glm4MoeModelReasoningParser
8
from .gptoss_reasoning_parser import GptOssReasoningParser
@@ -9,10 +10,12 @@ from .granite_reasoning_parser import GraniteReasoningParser
10
from .hunyuan_a13b_reasoning_parser import HunyuanA13BReasoningParser
11
from .mistral_reasoning_parser import MistralReasoningParser
12
from .qwen3_reasoning_parser import Qwen3ReasoningParser
13
+from .seedoss_reasoning_parser import SeedOSSReasoningParser
14
from .step3_reasoning_parser import Step3ReasoningParser
15
16
__all__ = [
17
"ReasoningParser",
18
+ "BaseThinkingReasoningParser",
19
"ReasoningParserManager",
20
"DeepSeekR1ReasoningParser",
21
"GraniteReasoningParser",
@@ -22,4 +25,5 @@ __all__ = [
25
"MistralReasoningParser",
26
"Step3ReasoningParser",
27
"GptOssReasoningParser",
28
+ "SeedOSSReasoningParser",
29
]
vllm/reasoning/abs_reasoning_parsers.py
+4
-4
@@ -7,7 +7,7 @@ import os
7
from abc import abstractmethod
8
from collections.abc import Sequence
9
from functools import cached_property
10
-from typing import TYPE_CHECKING, Any, Callable, Optional, Union
10
+from typing import TYPE_CHECKING, Any, Callable, Union
11
12
from vllm.logger import init_logger
13
from vllm.utils import import_from_path, is_list_of
@@ -77,7 +77,7 @@ class ReasoningParser:
77
self,
78
model_output: str,
79
request: Union[ChatCompletionRequest, ResponsesRequest],
80
- ) -> tuple[Optional[str], Optional[str]]:
80
+ ) -> tuple[str | None, str | None]:
81
"""
82
Extract reasoning content from a complete model-generated string.
83
@@ -135,7 +135,7 @@ class ReasoningParserManager:
135
def _register_module(
136
cls,
137
module: type,
138
- module_name: Optional[Union[str, list[str]]] = None,
138
+ module_name: Union[str, list[str]] | None = None,
139
force: bool = True,
140
) -> None:
141
if not issubclass(module, ReasoningParser):
@@ -155,7 +155,7 @@ class ReasoningParserManager:
155
@classmethod
156
def register_module(
157
cls,
158
- name: Optional[Union[str, list[str]]] = None,
158
+ name: Union[str, list[str]] | None = None,
159
force: bool = True,
160
module: Union[type, None] = None,
161
) -> Union[type, Callable]:
vllm/reasoning/basic_parsers.py
new
+156
@@ -0,0 +1,156 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from abc import abstractmethod
5
+from collections.abc import Sequence
6
+from typing import Optional, Union
7
+
8
+from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
9
+ DeltaMessage, ResponsesRequest)
10
+from vllm.reasoning.abs_reasoning_parsers import ReasoningParser
11
+from vllm.transformers_utils.tokenizer import AnyTokenizer
12
+
13
+
14
+class BaseThinkingReasoningParser(ReasoningParser):
15
+ """
16
+ Base class for reasoning parsers that use thinking tokens.
17
+
18
+ This class provides common functionality for parsers that use start and end
19
+ tokens to delimit reasoning content (
20
+ e.g., <think>...</think>, <seed:think>...</seed:think>).
21
+
22
+ Subclasses must implement the start and end tokens via abstract
23
+ properties.
24
+ """
25
+
26
+ @property
27
+ @abstractmethod
28
+ def start_token(self) -> str:
29
+ """The token that starts reasoning content."""
30
+ raise NotImplementedError
31
+
32
+ @property
33
+ @abstractmethod
34
+ def end_token(self) -> str:
35
+ """The token that ends reasoning content."""
36
+ raise NotImplementedError
37
+
38
+ def __init__(self, tokenizer: AnyTokenizer):
39
+ super().__init__(tokenizer)
40
+
41
+ if not self.model_tokenizer:
42
+ raise ValueError(
43
+ "The model tokenizer must be passed to the ReasoningParser "
44
+ "constructor during construction.")
45
+
46
+ if not self.start_token or not self.end_token:
47
+ raise ValueError(
48
+ "start_token and end_token must be defined in subclasses")
49
+
50
+ self.start_token_id = self.vocab.get(self.start_token)
51
+ self.end_token_id = self.vocab.get(self.end_token)
52
+ if self.start_token_id is None or self.end_token_id is None:
53
+ raise RuntimeError(
54
+ f"{self.__class__.__name__} reasoning parser could not locate "
55
+ "think start/end tokens in the tokenizer!")
56
+
57
+ def is_reasoning_end(self, input_ids: list[int]) -> bool:
58
+ return self.end_token_id in input_ids
59
+
60
+ def extract_content_ids(self, input_ids: list[int]) -> list[int]:
61
+ """
62
+ Extract the content after the end tokens
63
+ """
64
+ if self.end_token_id not in input_ids[:-1]:
65
+ return []
66
+ else:
67
+ return input_ids[input_ids.index(self.end_token_id) + 1:]
68
+
69
+ def extract_reasoning_content_streaming(
70
+ self,
71
+ previous_text: str,
72
+ current_text: str,
73
+ delta_text: str,
74
+ previous_token_ids: Sequence[int],
75
+ current_token_ids: Sequence[int],
76
+ delta_token_ids: Sequence[int],
77
+ ) -> Union[DeltaMessage, None]:
78
+ """
79
+ Extract reasoning content from a delta message.
80
+ Handles streaming output where previous + delta = current.
81
+ Uses token IDs for faster processing.
82
+ """
83
+ # Skip single special tokens
84
+ if len(delta_token_ids) == 1 and (delta_token_ids[0] in [
85
+ self.start_token_id, self.end_token_id
86
+ ]):
87
+ return None
88
+
89
+ # Check if start token is present in previous or delta.
90
+ # Keep compatibility with models that don't generate start tokens.
91
+ if self.start_token_id in previous_token_ids:
92
+ if self.end_token_id in delta_token_ids:
93
+ # start token in previous, end token in delta,
94
+ # extract reasoning content
95
+ end_index = delta_text.find(self.end_token)
96
+ reasoning_content = delta_text[:end_index]
97
+ content = delta_text[end_index + len(self.end_token):]
98
+ return DeltaMessage(
99
+ reasoning_content=reasoning_content,
100
+ content=content if content else None,
101
+ )
102
+ elif self.end_token_id in previous_token_ids:
103
+ # start token in previous, end token in previous,
104
+ # reasoning content continues
105
+ return DeltaMessage(content=delta_text)
106
+ else:
107
+ # start token in previous, no end token in previous or delta,
108
+ # reasoning content continues
109
+ return DeltaMessage(reasoning_content=delta_text)
110
+ elif self.start_token_id in delta_token_ids:
111
+ if self.end_token_id in delta_token_ids:
112
+ # start token in delta, end token in delta,
113
+ # extract reasoning content
114
+ start_index = delta_text.find(self.start_token)
115
+ end_index = delta_text.find(self.end_token)
116
+ reasoning_content = delta_text[start_index +
117
+ len(self.start_token):end_index]
118
+ content = delta_text[end_index + len(self.end_token):]
119
+ return DeltaMessage(
120
+ reasoning_content=reasoning_content,
121
+ content=content if content else None,
122
+ )
123
+ else:
124
+ # start token in delta, no end token in delta,
125
+ # reasoning content continues
126
+ return DeltaMessage(reasoning_content=delta_text)
127
+ else:
128
+ # not find thinking start token
129
+ return DeltaMessage(content=delta_text)
130
+
131
+ def extract_reasoning_content(
132
+ self, model_output: str, request: Union[ChatCompletionRequest,
133
+ ResponsesRequest]
134
+ ) -> tuple[Optional[str], Optional[str]]:
135
+ """
136
+ Extract reasoning content from the model output.
137
+
138
+ This is the base implementation that works for most models.
139
+ Subclasses can override this method for specific behavior.
140
+ """
141
+ # Check if the start token is present in the model output, remove it
142
+ # if it is present.
143
+ model_output_parts = model_output.partition(self.start_token)
144
+ model_output = model_output_parts[2] if model_output_parts[
145
+ 1] else model_output_parts[0]
146
+
147
+ # For models that may not generate start token,
148
+ # assume the reasoning content is always at the start.
149
+ if self.end_token not in model_output:
150
+ return model_output, None
151
+ else:
152
+ reasoning_content, _, content = model_output.partition(
153
+ self.end_token)
154
+ # If generation stops right after end-of-think, return null content
155
+ final_content = content or None
156
+ return reasoning_content, final_content
vllm/reasoning/deepseek_r1_reasoning_parser.py
+27
-133
@@ -2,20 +2,15 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from collections.abc import Sequence
5
-from typing import Optional, Union
5
+from typing import Union
6
7
-from transformers import PreTrainedTokenizerBase
8
-
9
-from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
10
- DeltaMessage)
11
-from vllm.logger import init_logger
12
-from vllm.reasoning import ReasoningParser, ReasoningParserManager
13
-
14
-logger = init_logger(__name__)
7
+from vllm.entrypoints.openai.protocol import DeltaMessage
8
+from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager
9
+from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
10
11
12
@ReasoningParserManager.register_module("deepseek_r1")
18
-class DeepSeekR1ReasoningParser(ReasoningParser):
13
+class DeepSeekR1ReasoningParser(BaseThinkingReasoningParser):
14
"""
15
Reasoning parser for DeepSeek R1 model.
16
@@ -23,38 +18,15 @@ class DeepSeekR1ReasoningParser(ReasoningParser):
18
text. This parser extracts the reasoning content from the model output.
19
"""
20
26
- start_token_id: int
27
- end_token_id: int
28
-
29
- start_token: str = "<think>"
30
- end_token: str = "</think>"
31
-
32
- def __init__(self, tokenizer: PreTrainedTokenizerBase):
33
- super().__init__(tokenizer)
34
-
35
- if not self.model_tokenizer:
36
- raise ValueError(
37
- "The model tokenizer must be passed to the ReasoningParser "
38
- "constructor during construction.")
21
+ @property
22
+ def start_token(self) -> str:
23
+ """The token that starts reasoning content."""
24
+ return "<think>"
25
40
- self.start_token_id = self.vocab.get(self.start_token)
41
- self.end_token_id = self.vocab.get(self.end_token)
42
- if self.start_token_id is None or self.end_token_id is None:
43
- raise RuntimeError(
44
- "DeepSeek R1 reasoning parser could not locate think start/end "
45
- "tokens in the tokenizer!")
46
-
47
- def is_reasoning_end(self, input_ids: list[int]) -> bool:
48
- return self.end_token_id in input_ids
49
-
50
- def extract_content_ids(self, input_ids: list[int]) -> list[int]:
51
- """
52
- Extract the content after the end tokens
53
- """
54
- if self.end_token_id not in input_ids[:-1]:
55
- return []
56
- else:
57
- return input_ids[input_ids.index(self.end_token_id) + 1:]
26
+ @property
27
+ def end_token(self) -> str:
28
+ """The token that ends reasoning content."""
29
+ return "</think>"
30
31
def extract_reasoning_content_streaming(
32
self,
@@ -65,63 +37,18 @@ class DeepSeekR1ReasoningParser(ReasoningParser):
37
current_token_ids: Sequence[int],
38
delta_token_ids: Sequence[int],
39
) -> Union[DeltaMessage, None]:
68
- """
69
- Extract reasoning content from a delta message.
70
- Handles streaming output where previous + delta = current.
71
- Uses token IDs for faster processing.
72
- For text <think>abc</think>xyz:
73
- - 'abc' goes to reasoning_content
74
- - 'xyz' goes to content
75
- """
76
- # Skip single special tokens
77
- if len(delta_token_ids) == 1 and (delta_token_ids[0] in [
78
- self.start_token_id, self.end_token_id
79
- ]):
80
- return None
81
-
82
- # Check if <think> is present in previous or delta.
83
- # Keep compatibility with models that don't generate <think> tokens.
84
- if self.start_token_id in previous_token_ids:
40
+ ret = super().extract_reasoning_content_streaming(
41
+ previous_text,
42
+ current_text,
43
+ delta_text,
44
+ previous_token_ids,
45
+ current_token_ids,
46
+ delta_token_ids,
47
+ )
48
+ if (ret is not None and self.start_token_id not in previous_token_ids
49
+ and self.start_token_id not in delta_token_ids):
50
if self.end_token_id in delta_token_ids:
86
- # <think> in previous, </think> in delta,
87
- # extract reasoning content
88
- end_index = delta_text.find(self.end_token)
89
- reasoning_content = delta_text[:end_index]
90
- content = delta_text[end_index + len(self.end_token):]
91
- return DeltaMessage(
92
- reasoning_content=reasoning_content,
93
- content=content if content else None,
94
- )
95
- elif self.end_token_id in previous_token_ids:
96
- # <think> in previous, </think> in previous,
97
- # reasoning content continues
98
- return DeltaMessage(content=delta_text)
99
- else:
100
- # <think> in previous, no </think> in previous or delta,
101
- # reasoning content continues
102
- return DeltaMessage(reasoning_content=delta_text)
103
- elif self.start_token_id in delta_token_ids:
104
- if self.end_token_id in delta_token_ids:
105
- # <think> in delta, </think> in delta, extract reasoning content
106
- start_index = delta_text.find(self.start_token)
107
- end_index = delta_text.find(self.end_token)
108
- reasoning_content = delta_text[start_index +
109
- len(self.start_token):end_index]
110
- content = delta_text[end_index + len(self.end_token):]
111
- return DeltaMessage(
112
- reasoning_content=reasoning_content,
113
- content=content if content else None,
114
- )
115
- else:
116
- # <think> in delta, no </think> in delta,
117
- # reasoning content continues
118
- return DeltaMessage(reasoning_content=delta_text)
119
- else:
120
- # No <think> in previous or delta, also need to check for </think>.
121
- # Because the model may have generated </think> without <think>
122
- # Ref https://huggingface.co/deepseek-ai/DeepSeek-R1/commit/8a58a132790c9935686eb97f042afa8013451c9f
123
- if self.end_token_id in delta_token_ids:
124
- # </think> in delta with more tokens,
51
+ # end token in delta with more tokens,
52
# extract reasoning content and content
53
end_index = delta_text.find(self.end_token)
54
reasoning_content = delta_text[:end_index]
@@ -131,43 +58,10 @@ class DeepSeekR1ReasoningParser(ReasoningParser):
58
content=content if content else None,
59
)
60
elif self.end_token_id in previous_token_ids:
134
- # </think> in previous, thinking content ends
61
+ # end token in previous, thinking content ends
62
return DeltaMessage(content=delta_text)
63
else:
137
- # no </think> in previous or delta, reasoning content continues
64
+ # no end token in previous or delta, reasoning content continues
65
return DeltaMessage(reasoning_content=delta_text)
66
140
- def extract_reasoning_content(
141
- self, model_output: str, request: ChatCompletionRequest
142
- ) -> tuple[Optional[str], Optional[str]]:
143
- """
144
- Extract reasoning content from the model output.
145
-
146
- For text <think>abc</think>xyz:
147
- - 'abc' goes to reasoning_content
148
- - 'xyz' goes to content
149
-
150
- Returns:
151
- tuple[Optional[str], Optional[str]]: reasoning content and content
152
- """
153
-
154
- # Check if the start token is present in the model output, remove it
155
- # if it is present.
156
- model_output_parts = model_output.partition(self.start_token)
157
- model_output = model_output_parts[2] if model_output_parts[
158
- 1] else model_output_parts[0]
159
-
160
- # DeepSeek R1 doesn't generate <think> now.
161
- # Thus we assume the reasoning content is always at the start.
162
- # Ref https://huggingface.co/deepseek-ai/DeepSeek-R1/commit/8a58a132790c9935686eb97f042afa8013451c9f
163
- if self.end_token not in model_output:
164
- return model_output, None
165
- else:
166
- reasoning_content, _, content = model_output.partition(
167
- self.end_token)
168
- # If the end token is not found, return the model output as is.
169
- # It should not happen since we already checked for the presence
170
- # of the end token.
171
- # If generation stops right after end-of-think, return null content
172
- final_content = content or None
173
- return reasoning_content, final_content
67
+ return ret
vllm/reasoning/mistral_reasoning_parser.py
+14
-5
@@ -1,6 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+from functools import cached_property
5
+
6
from vllm.logger import init_logger
7
from vllm.reasoning import ReasoningParser, ReasoningParserManager
8
from vllm.reasoning.deepseek_r1_reasoning_parser import (
@@ -31,11 +33,6 @@ class MistralReasoningParser(DeepSeekR1ReasoningParser):
33
"The model tokenizer must be passed to the ReasoningParser "
34
"constructor during construction.")
35
34
- from mistral_common.tokens.tokenizers.base import SpecialTokens
35
-
36
- self.start_token = SpecialTokens.begin_think
37
- self.end_token = SpecialTokens.end_think
38
-
36
self.start_token_id = tokenizer.tokenizer.get_control_token(
37
self.start_token)
38
self.end_token_id = tokenizer.tokenizer.get_control_token(
@@ -45,3 +42,15 @@ class MistralReasoningParser(DeepSeekR1ReasoningParser):
42
raise RuntimeError(
43
"Mistral reasoning parser could not locate think start/end "
44
"tokens in the tokenizer!")
45
+
46
+ @cached_property
47
+ def start_token(self) -> str:
48
+ """The token that starts reasoning content."""
49
+ from mistral_common.tokens.tokenizers.base import SpecialTokens
50
+ return SpecialTokens.begin_think
51
+
52
+ @cached_property
53
+ def end_token(self) -> str:
54
+ """The token that ends reasoning content."""
55
+ from mistral_common.tokens.tokenizers.base import SpecialTokens
56
+ return SpecialTokens.end_think
vllm/reasoning/qwen3_reasoning_parser.py
+25
-104
@@ -1,21 +1,16 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from collections.abc import Sequence
4
from typing import Optional, Union
5
7
-from transformers import PreTrainedTokenizerBase
8
-
6
from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
10
- DeltaMessage)
11
-from vllm.logger import init_logger
12
-from vllm.reasoning import ReasoningParser, ReasoningParserManager
13
-
14
-logger = init_logger(__name__)
7
+ ResponsesRequest)
8
+from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager
9
+from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
10
11
12
@ReasoningParserManager.register_module("qwen3")
18
-class Qwen3ReasoningParser(ReasoningParser):
13
+class Qwen3ReasoningParser(BaseThinkingReasoningParser):
14
"""
15
Reasoning parser for the Qwen3 model.
16
@@ -26,100 +21,25 @@ class Qwen3ReasoningParser(ReasoningParser):
21
output.
22
"""
23
29
- def __init__(self, tokenizer: PreTrainedTokenizerBase):
30
- super().__init__(tokenizer)
31
- self.think_start_token = "<think>"
32
- self.think_end_token = "</think>"
33
-
34
- if not self.model_tokenizer:
35
- raise ValueError(
36
- "The model tokenizer must be passed to the ReasoningParser "
37
- "constructor during construction.")
38
-
39
- self.think_start_token_id = self.vocab.get(self.think_start_token)
40
- self.think_end_token_id = self.vocab.get(self.think_end_token)
41
- if (self.think_start_token_id is None
42
- or self.think_end_token_id is None):
43
- raise RuntimeError(
44
- "Qwen3 reasoning parser could not locate think start/end "
45
- "tokens in the tokenizer!")
46
-
47
- def is_reasoning_end(self, input_ids: list[int]) -> bool:
48
- return self.think_end_token_id in input_ids
49
-
50
- def extract_content_ids(self, input_ids: list[int]) -> list[int]:
51
- """
52
- Extract the content after the end tokens
53
- """
54
- if self.think_end_token_id not in input_ids[:-1]:
55
- return []
56
- else:
57
- return input_ids[input_ids.index(self.think_end_token_id) + 1:]
58
-
59
- def extract_reasoning_content_streaming(
60
- self,
61
- previous_text: str,
62
- current_text: str,
63
- delta_text: str,
64
- previous_token_ids: Sequence[int],
65
- current_token_ids: Sequence[int],
66
- delta_token_ids: Sequence[int],
67
- ) -> Union[DeltaMessage, None]:
68
- """
69
- Extract reasoning content from a delta message.
70
- Handles streaming output where previous + delta = current.
71
- Uses token IDs for faster processing.
72
- For text <think>abc</think>xyz:
73
- - 'abc' goes to reasoning_content
74
- - 'xyz' goes to content
75
- """
76
- # Skip single special tokens
77
- if len(delta_token_ids) == 1 and (delta_token_ids[0] in [
78
- self.think_start_token_id, self.think_end_token_id
79
- ]):
80
- return None
24
+ @property
25
+ def start_token(self) -> str:
26
+ """The token that starts reasoning content."""
27
+ return "<think>"
28
82
- if self.think_start_token_id in previous_token_ids:
83
- if self.think_end_token_id in delta_token_ids:
84
- # <think> in previous, </think> in delta,
85
- # extract reasoning content
86
- end_index = delta_text.find(self.think_end_token)
87
- reasoning_content = delta_text[:end_index]
88
- content = delta_text[end_index + len(self.think_end_token):]
89
- return DeltaMessage(reasoning_content=reasoning_content,
90
- content=content if content else None)
91
- elif self.think_end_token_id in previous_token_ids:
92
- # <think> in previous, </think> in previous,
93
- # reasoning content continues
94
- return DeltaMessage(content=delta_text)
95
- else:
96
- # <think> in previous, no </think> in previous or delta,
97
- # reasoning content continues
98
- return DeltaMessage(reasoning_content=delta_text)
99
- elif self.think_start_token_id in delta_token_ids:
100
- if self.think_end_token_id in delta_token_ids:
101
- # <think> in delta, </think> in delta, extract reasoning content
102
- start_index = delta_text.find(self.think_start_token)
103
- end_index = delta_text.find(self.think_end_token)
104
- reasoning_content = delta_text[start_index +
105
- len(self.think_start_token
106
- ):end_index]
107
- content = delta_text[end_index + len(self.think_end_token):]
108
- return DeltaMessage(reasoning_content=reasoning_content,
109
- content=content if content else None)
110
- else:
111
- # <think> in delta, no </think> in delta,
112
- # reasoning content continues
113
- return DeltaMessage(reasoning_content=delta_text)
114
- else:
115
- # thinking is disabled, just content
116
- return DeltaMessage(content=delta_text)
29
+ @property
30
+ def end_token(self) -> str:
31
+ """The token that ends reasoning content."""
32
+ return "</think>"
33
34
def extract_reasoning_content(
119
- self, model_output: str, request: ChatCompletionRequest
35
+ self, model_output: str, request: Union[ChatCompletionRequest,
36
+ ResponsesRequest]
37
) -> tuple[Optional[str], Optional[str]]:
38
"""
39
Extract reasoning content from the model output.
40
+
41
+ Qwen3 has stricter requirements - it needs both start and end tokens
42
+ to be present, unlike other models that work with just the end token.
43
44
For text <think>abc</think>xyz:
45
- 'abc' goes to reasoning_content
@@ -129,23 +49,24 @@ class Qwen3ReasoningParser(ReasoningParser):
49
tuple[Optional[str], Optional[str]]: reasoning content and content
50
"""
51
132
- # Check if the model output contains the <think> and </think> tokens.
133
- if (self.think_start_token not in model_output
134
- or self.think_end_token not in model_output):
52
+ # Check if the model output contains both <think> and </think> tokens.
53
+ if (self.start_token not in model_output
54
+ or self.end_token not in model_output):
55
return None, model_output
56
+
57
# Check if the <think> is present in the model output, remove it
58
# if it is present.
138
- model_output_parts = model_output.partition(self.think_start_token)
59
+ model_output_parts = model_output.partition(self.start_token)
60
model_output = model_output_parts[2] if model_output_parts[
61
1] else model_output_parts[0]
62
+
63
# Check if the model output contains the </think> tokens.
64
# If the end token is not found, return the model output as is.
143
- if self.think_end_token not in model_output:
65
+ if self.end_token not in model_output:
66
return None, model_output
67
68
# Extract reasoning content from the model output.
147
- reasoning_content, _, content = model_output.partition(
148
- self.think_end_token)
69
+ reasoning_content, _, content = model_output.partition(self.end_token)
70
71
final_content = content or None
72
return reasoning_content, final_content
vllm/reasoning/seedoss_reasoning_parser.py
new
+28
@@ -0,0 +1,28 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from vllm.reasoning.abs_reasoning_parsers import ReasoningParserManager
5
+from vllm.reasoning.basic_parsers import BaseThinkingReasoningParser
6
+
7
+
8
+@ReasoningParserManager.register_module("seed_oss")
9
+class SeedOSSReasoningParser(BaseThinkingReasoningParser):
10
+ """
11
+ Reasoning parser for SeedOSS model.
12
+
13
+ The SeedOSS model uses <seed:think>...</seed:think> tokens to
14
+ denote reasoning content text. This parser extracts
15
+ the reasoning content from the model output.
16
+ Similar to DeepSeek R1, it supports cases
17
+ where the model doesn't generate the start token.
18
+ """
19
+
20
+ @property
21
+ def start_token(self) -> str:
22
+ """The token that starts reasoning content."""
23
+ return "<seed:think>"
24
+
25
+ @property
26
+ def end_token(self) -> str:
27
+ """The token that ends reasoning content."""
28
+ return "</seed:think>"