@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 85a65e7f51
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] 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