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] Add DeepSeek-V3.1 reasoning parser (split from PR #24972) (#25589)
Signed-off-by: taohui <taohui3@gmail.com> Signed-off-by: Tao Hui <taohui3@gmail.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Chauncey <chaunceyjiang@gmail.com>
Tao Hui committed
Oct 15, 2025 at 11:09 UTC
85a65e7f51ad6901979ff43c95deb6ac727a9430
6 files changed
+215
-3
docs/features/reasoning_outputs.md
+3
-1
@@ -11,6 +11,7 @@ vLLM currently supports the following reasoning models:
11
| Model Series | Parser Name | Structured Output Support | Tool Calling |
12
|--------------|-------------|------------------|-------------|
13
| [DeepSeek R1 series](https://huggingface.co/collections/deepseek-ai/deepseek-r1-678e1e131c0169c0bc89728d) | `deepseek_r1` | `json`, `regex` | ❌ |
14
+| [DeepSeek-V3.1](https://huggingface.co/collections/deepseek-ai/deepseek-v31-68a491bed32bd77e7fca048f) | `deepseek_v3` | `json`, `regex` | ❌ |
15
| [ERNIE-4.5-VL series](https://huggingface.co/baidu/ERNIE-4.5-VL-28B-A3B-PT) | `ernie45` | `json`, `regex` | ❌ |
16
| [ERNIE-4.5-21B-A3B-Thinking](https://huggingface.co/baidu/ERNIE-4.5-21B-A3B-Thinking) | `ernie45` | `json`, `regex` | ✅ |
17
| [QwQ-32B](https://huggingface.co/Qwen/QwQ-32B) | `deepseek_r1` | `json`, `regex` | ✅ |
@@ -20,8 +21,9 @@ vLLM currently supports the following reasoning models:
21
| [GLM-4.5 series](https://huggingface.co/collections/zai-org/glm-45-687c621d34bda8c9e4bf503b) | `glm45` | `json`, `regex` | ✅ |
22
23
!!! note
23
- IBM Granite 3.2 reasoning is disabled by default; to enable it, you must also pass `thinking=True` in your `chat_template_kwargs`.
24
+ IBM Granite 3.2 and DeepSeek-V3.1 reasoning is disabled by default; to enable it, you must also pass `thinking=True` in your `chat_template_kwargs`.
25
The reasoning feature for the Qwen3 series is enabled by default. To disable it, you must pass `enable_thinking=False` in your `chat_template_kwargs`.
26
+ DeepSeek-V3.1 tool calling is supported in non-thinking mode.
27
28
## Quickstart
29
tests/reasoning/test_deepseekv3_reasoning_parser.py
new
+76
@@ -0,0 +1,76 @@
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 vllm.entrypoints.openai.protocol import ChatCompletionRequest, DeltaMessage
8
+from vllm.reasoning import (
9
+ DeepSeekR1ReasoningParser,
10
+ DeepSeekV3ReasoningParser,
11
+ IdentityReasoningParser,
12
+)
13
+
14
+REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-V3.1"
15
+
16
+
17
+@pytest.fixture(scope="module")
18
+def tokenizer():
19
+ return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)
20
+
21
+
22
+@pytest.mark.parametrize(
23
+ "thinking,expected_parser_type",
24
+ [
25
+ (True, DeepSeekR1ReasoningParser),
26
+ (False, IdentityReasoningParser),
27
+ ],
28
+)
29
+def test_parser_selection(tokenizer, thinking, expected_parser_type):
30
+ parser = DeepSeekV3ReasoningParser(
31
+ tokenizer, chat_template_kwargs={"thinking": thinking}
32
+ )
33
+
34
+ assert isinstance(parser._parser, expected_parser_type)
35
+
36
+
37
+def test_identity_reasoning_parser_basic(tokenizer):
38
+ parser = IdentityReasoningParser(tokenizer)
39
+
40
+ # Test is_reasoning_end always returns True
41
+ input_text = "This is some output"
42
+ input_tokens = tokenizer.tokenize(input_text)
43
+ input_ids = tokenizer.convert_tokens_to_ids(input_tokens)
44
+ assert parser.is_reasoning_end(input_ids) is True
45
+
46
+ # Test extract_content_ids returns all input_ids
47
+ assert parser.extract_content_ids(input_ids) == input_ids
48
+
49
+ # Test extract_reasoning_content returns (None, model_output)
50
+ request = ChatCompletionRequest(model="test-model", messages=[], temperature=1.0)
51
+ reasoning, content = parser.extract_reasoning_content(input_text, request)
52
+ assert reasoning is None
53
+ assert content == input_text
54
+
55
+ # Test extract_reasoning_content_streaming returns DeltaMessage or None
56
+ result = parser.extract_reasoning_content_streaming(
57
+ previous_text="",
58
+ current_text="Hello world",
59
+ delta_text="Hello world",
60
+ previous_token_ids=[],
61
+ current_token_ids=input_ids,
62
+ delta_token_ids=input_ids,
63
+ )
64
+ assert isinstance(result, DeltaMessage)
65
+ assert result.content == "Hello world"
66
+
67
+ # If delta_text is empty, should return None
68
+ result_none = parser.extract_reasoning_content_streaming(
69
+ previous_text="Hello world",
70
+ current_text="Hello world",
71
+ delta_text="",
72
+ previous_token_ids=input_ids,
73
+ current_token_ids=input_ids,
74
+ delta_token_ids=[],
75
+ )
76
+ assert result_none is None
vllm/entrypoints/openai/serving_chat.py
+8
-2
@@ -570,7 +570,10 @@ class OpenAIServingChat(OpenAIServing):
570
571
try:
572
if self.reasoning_parser:
573
- reasoning_parser = self.reasoning_parser(tokenizer)
573
+ reasoning_parser = self.reasoning_parser(
574
+ tokenizer,
575
+ chat_template_kwargs=request.chat_template_kwargs, # type: ignore
576
+ )
577
except RuntimeError as e:
578
logger.exception("Error in reasoning parser creation.")
579
data = self.create_streaming_error_response(str(e))
@@ -1335,7 +1338,10 @@ class OpenAIServingChat(OpenAIServing):
1338
1339
if self.reasoning_parser:
1340
try:
1338
- reasoning_parser = self.reasoning_parser(tokenizer)
1341
+ reasoning_parser = self.reasoning_parser(
1342
+ tokenizer,
1343
+ chat_template_kwargs=request.chat_template_kwargs, # type: ignore
1344
+ )
1345
except RuntimeError as e:
1346
logger.exception("Error in reasoning parser creation.")
1347
return self.create_error_response(str(e))
vllm/reasoning/__init__.py
+4
@@ -4,11 +4,13 @@
4
from .abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
5
from .basic_parsers import BaseThinkingReasoningParser
6
from .deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
7
+from .deepseek_v3_reasoning_parser import DeepSeekV3ReasoningParser
8
from .ernie45_reasoning_parser import Ernie45ReasoningParser
9
from .glm4_moe_reasoning_parser import Glm4MoeModelReasoningParser
10
from .gptoss_reasoning_parser import GptOssReasoningParser
11
from .granite_reasoning_parser import GraniteReasoningParser
12
from .hunyuan_a13b_reasoning_parser import HunyuanA13BReasoningParser
13
+from .identity_reasoning_parser import IdentityReasoningParser
14
from .mistral_reasoning_parser import MistralReasoningParser
15
from .olmo3_reasoning_parser import Olmo3ReasoningParser
16
from .qwen3_reasoning_parser import Qwen3ReasoningParser
@@ -20,6 +22,8 @@ __all__ = [
22
"BaseThinkingReasoningParser",
23
"ReasoningParserManager",
24
"DeepSeekR1ReasoningParser",
25
+ "IdentityReasoningParser",
26
+ "DeepSeekV3ReasoningParser",
27
"Ernie45ReasoningParser",
28
"GraniteReasoningParser",
29
"HunyuanA13BReasoningParser",
vllm/reasoning/deepseek_v3_reasoning_parser.py
new
+66
@@ -0,0 +1,66 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Sequence
5
+
6
+from transformers import PreTrainedTokenizerBase
7
+
8
+from vllm.entrypoints.openai.protocol import ChatCompletionRequest, DeltaMessage
9
+from vllm.logger import init_logger
10
+from vllm.reasoning import (
11
+ DeepSeekR1ReasoningParser,
12
+ ReasoningParser,
13
+ ReasoningParserManager,
14
+)
15
+
16
+from .identity_reasoning_parser import IdentityReasoningParser
17
+
18
+logger = init_logger(__name__)
19
+
20
+
21
+@ReasoningParserManager.register_module("deepseek_v3")
22
+class DeepSeekV3ReasoningParser(ReasoningParser):
23
+ """
24
+ V3 parser that delegates to either DeepSeekR1ReasoningParser or
25
+ IdentityReasoningParser based on `thinking` and `separate_reasoning`.
26
+ """
27
+
28
+ def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
29
+ super().__init__(tokenizer, *args, **kwargs)
30
+
31
+ chat_kwargs = kwargs.pop("chat_template_kwargs", {}) or {}
32
+ thinking = bool(chat_kwargs.pop("thinking", False))
33
+
34
+ if thinking:
35
+ self._parser = DeepSeekR1ReasoningParser(tokenizer, *args, **kwargs)
36
+ else:
37
+ self._parser = IdentityReasoningParser(tokenizer, *args, **kwargs)
38
+
39
+ def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
40
+ return self._parser.is_reasoning_end(input_ids)
41
+
42
+ def extract_content_ids(self, input_ids: list[int]) -> list[int]:
43
+ return self._parser.extract_content_ids(input_ids)
44
+
45
+ def extract_reasoning_content(
46
+ self, model_output: str, request: ChatCompletionRequest
47
+ ) -> tuple[str | None, str | None]:
48
+ return self._parser.extract_reasoning_content(model_output, request)
49
+
50
+ def extract_reasoning_content_streaming(
51
+ self,
52
+ previous_text: str,
53
+ current_text: str,
54
+ delta_text: str,
55
+ previous_token_ids: Sequence[int],
56
+ current_token_ids: Sequence[int],
57
+ delta_token_ids: Sequence[int],
58
+ ) -> DeltaMessage | None:
59
+ return self._parser.extract_reasoning_content_streaming(
60
+ previous_text,
61
+ current_text,
62
+ delta_text,
63
+ previous_token_ids,
64
+ current_token_ids,
65
+ delta_token_ids,
66
+ )
vllm/reasoning/identity_reasoning_parser.py
new
+58
@@ -0,0 +1,58 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Sequence
5
+
6
+from transformers import PreTrainedTokenizerBase
7
+
8
+from vllm.entrypoints.openai.protocol import ChatCompletionRequest, DeltaMessage
9
+from vllm.logger import init_logger
10
+from vllm.reasoning import ReasoningParser
11
+
12
+logger = init_logger(__name__)
13
+
14
+
15
+class IdentityReasoningParser(ReasoningParser):
16
+ """
17
+ Identity reasoning parser.
18
+
19
+ This parser does not attempt to parse or strip out reasoning tokens.
20
+ It treats the entire model output as content and ignores reasoning.
21
+ """
22
+
23
+ def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
24
+ super().__init__(tokenizer, *args, **kwargs)
25
+ if not self.model_tokenizer:
26
+ raise ValueError(
27
+ "The model tokenizer must be passed to the ReasoningParser "
28
+ "constructor during construction."
29
+ )
30
+
31
+ def is_reasoning_end(self, input_ids: list[int]) -> bool:
32
+ # Always return True, since we never treat reasoning specially
33
+ return True
34
+
35
+ def extract_content_ids(self, input_ids: list[int]) -> list[int]:
36
+ # Identity: return all tokens as content
37
+ return input_ids
38
+
39
+ def extract_reasoning_content_streaming(
40
+ self,
41
+ previous_text: str,
42
+ current_text: str,
43
+ delta_text: str,
44
+ previous_token_ids: Sequence[int],
45
+ current_token_ids: Sequence[int],
46
+ delta_token_ids: Sequence[int],
47
+ ) -> DeltaMessage | None:
48
+ # Just wrap delta_text as content, ignore reasoning
49
+ if delta_text:
50
+ return DeltaMessage(content=delta_text)
51
+ return None
52
+
53
+ def extract_reasoning_content(
54
+ self, model_output: str, request: ChatCompletionRequest
55
+ ) -> tuple[str | None, str | None]:
56
+ # No reasoning separation: return None for reasoning_content,
57
+ # and full model_output as content
58
+ return None, model_output