@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / be0bb568c9
Fork 0

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