Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/features/reasoning_outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ vLLM currently supports the following reasoning models:
| Model Series | Parser Name | Structured Output Support | Tool Calling |
|--------------|-------------|------------------|-------------|
| [DeepSeek R1 series](https://huggingface.co/collections/deepseek-ai/deepseek-r1-678e1e131c0169c0bc89728d) | `deepseek_r1` | `guided_json`, `guided_regex` | ❌ |
| [DeepSeek-V3.1](https://huggingface.co/deepseek-ai/DeepSeek-V3.1) | `deepseek_v3` | ❌ | ❌ |
| [QwQ-32B](https://huggingface.co/Qwen/QwQ-32B) | `deepseek_r1` | `guided_json`, `guided_regex` | ✅ |
| [IBM Granite 3.2 language models](https://huggingface.co/collections/ibm-granite/granite-32-language-models-67b3bc8c13508f6d064cff9a) | `granite` | ❌ | ❌ |
| [Qwen3 series](https://huggingface.co/collections/Qwen/qwen3-67dd247413f0e2e4f653967f) | `qwen3` | `guided_json`, `guided_regex` | ✅ |
Expand Down
75 changes: 75 additions & 0 deletions tests/reasoning/test_deepseekv3_reasoning_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

import pytest
from transformers import AutoTokenizer

from vllm.reasoning import (
DeepSeekV3ReasoningParser,
DeepSeekR1ReasoningParser,
IdentityReasoningParser,
)
from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
DeltaMessage)

REASONING_MODEL_NAME = "deepseek-ai/DeepSeek-V3.1"


@pytest.fixture(scope="module")
def tokenizer():
return AutoTokenizer.from_pretrained(REASONING_MODEL_NAME)


@pytest.mark.parametrize(
"thinking,expected_parser_type",
[
(True, DeepSeekR1ReasoningParser),
(False, IdentityReasoningParser),
],
)
def test_parser_selection(tokenizer, thinking, expected_parser_type):
parser = DeepSeekV3ReasoningParser(
tokenizer, thinking=thinking
)
assert isinstance(parser._parser, expected_parser_type)

def test_identity_reasoning_parser_basic(tokenizer):
parser = IdentityReasoningParser(tokenizer)

# Test is_reasoning_end always returns False
input_text = "This is some output"
input_tokens = tokenizer.tokenize(input_text)
input_ids = tokenizer.convert_tokens_to_ids(input_tokens)
assert parser.is_reasoning_end(input_ids) is False

# Test extract_content_ids returns all input_ids
assert parser.extract_content_ids(input_ids) == input_ids

# Test extract_reasoning_content returns (None, model_output)
request = ChatCompletionRequest(model="test-model", messages=[], temperature=1.0)
reasoning, content = parser.extract_reasoning_content(input_text, request)
assert reasoning is None
assert content == input_text

# Test extract_reasoning_content_streaming returns DeltaMessage or None
result = parser.extract_reasoning_content_streaming(
previous_text="",
current_text="Hello world",
delta_text="Hello world",
previous_token_ids=[],
current_token_ids=input_ids,
delta_token_ids=input_ids,
)
assert isinstance(result, DeltaMessage)
assert result.content == "Hello world"

# If delta_text is empty, should return None
result_none = parser.extract_reasoning_content_streaming(
previous_text="Hello world",
current_text="Hello world",
delta_text="",
previous_token_ids=input_ids,
current_token_ids=input_ids,
delta_token_ids=[],
)
assert result_none is None
4 changes: 2 additions & 2 deletions vllm/entrypoints/openai/serving_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ async def chat_completion_stream_generator(

try:
if self.reasoning_parser:
reasoning_parser = self.reasoning_parser(tokenizer)
reasoning_parser = self.reasoning_parser(tokenizer, **(request.chat_template_kwargs or {}))
except RuntimeError as e:
logger.exception("Error in reasoning parser creation.")
data = self.create_streaming_error_response(str(e))
Expand Down Expand Up @@ -1230,7 +1230,7 @@ async def chat_completion_full_generator(

if self.reasoning_parser:
try:
reasoning_parser = self.reasoning_parser(tokenizer)
reasoning_parser = self.reasoning_parser(tokenizer, **(request.chat_template_kwargs or {}))
except RuntimeError as e:
logger.exception("Error in reasoning parser creation.")
return self.create_error_response(str(e))
Expand Down
4 changes: 4 additions & 0 deletions vllm/reasoning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from .abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
from .deepseek_r1_reasoning_parser import DeepSeekR1ReasoningParser
from .identity_reasoning_parser import IdentityReasoningParser
from .deepseek_v3_reasoning_parser import DeepSeekV3ReasoningParser
from .glm4_moe_reasoning_parser import Glm4MoeModelReasoningParser
from .gptoss_reasoning_parser import GptOssReasoningParser
from .granite_reasoning_parser import GraniteReasoningParser
Expand All @@ -15,6 +17,8 @@
"ReasoningParser",
"ReasoningParserManager",
"DeepSeekR1ReasoningParser",
"IdentityReasoningParser",
"DeepSeekV3ReasoningParser",
"GraniteReasoningParser",
"HunyuanA13BReasoningParser",
"Qwen3ReasoningParser",
Expand Down
2 changes: 1 addition & 1 deletion vllm/reasoning/abs_reasoning_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ReasoningParser:
It is used to extract reasoning content from the model output.
"""

def __init__(self, tokenizer: AnyTokenizer):
def __init__(self, tokenizer: AnyTokenizer, *args, **kwargs):
self.model_tokenizer = tokenizer

@cached_property
Expand Down
62 changes: 62 additions & 0 deletions vllm/reasoning/deepseek_v3_reasoning_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

from collections.abc import Sequence
from typing import Optional, Union

from transformers import PreTrainedTokenizerBase

from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
DeltaMessage)
from vllm.logger import init_logger
from vllm.reasoning import ReasoningParser, ReasoningParserManager, IdentityReasoningParser, DeepSeekR1ReasoningParser

Check failure on line 12 in vllm/reasoning/deepseek_v3_reasoning_parser.py

View workflow job for this annotation

GitHub Actions / pre-commit

Ruff (E501)

vllm/reasoning/deepseek_v3_reasoning_parser.py:12:81: E501 Line too long (118 > 80)

logger = init_logger(__name__)


@ReasoningParserManager.register_module("deepseek_v3")
class DeepSeekV3ReasoningParser(ReasoningParser):

"""
V3 parser that delegates to either DeepSeekR1ReasoningParser or
IdentityReasoningParser based on `thinking` and `separate_reasoning`.
"""
def __init__(self, tokenizer: PreTrainedTokenizerBase, *args, **kwargs):
super().__init__(tokenizer)

thinking = bool(kwargs.pop("thinking", False))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, Qwen3 also supports parameters like

{
    "chat_template_kwargs": {
        "enable_thinking": enable_thinking
    }
}

to enable or disable reasoning, and unlike this PR, it does not require two separate reasoning_parsers.
Is there anything special about DeepSeek-V3 that necessitates this different approach?

https://github.com/vllm-project/vllm/blob/main/tests/entrypoints/openai/test_completion_with_function_calling.py#L177-L178

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for reply. DeepSeek-V3.1 cannot directly use the qwen3 reasoning parser, because DeepSeek-V3.1 only has the end tag but no start tag . If we use the qwen3 reasoning parser, all content would be placed into the reasoning_content field. It also cannot directly use the deepseek_r1 reasoning parser, because that parser cannot decide whether to enable reasoning based on the request. For example, if I specify --reasoning-parser deepseek_r1 but the request is in non-thinking mode, it would still put all content into the reasoning_content field. The key point here is that we need a way to decide whether to use the reasoning parser based on the request. Therefore, I made a modification: applying a Strategy Pattern to select different strategies based on the request, while keeping the interface unchanged.


if thinking:
self._parser = DeepSeekR1ReasoningParser(tokenizer)
else:
self._parser = IdentityReasoningParser(tokenizer)

def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
return self._parser.is_reasoning_end(input_ids)

def extract_content_ids(self, input_ids: list[int]) -> list[int]:
return self._parser.extract_content_ids(input_ids)

def extract_reasoning_content(
self, model_output: str, request: ChatCompletionRequest
) -> tuple[Optional[str], Optional[str]]:
return self._parser.extract_reasoning_content(model_output, request)

def extract_reasoning_content_streaming(
self,
previous_text: str,
current_text: str,
delta_text: str,
previous_token_ids: Sequence[int],
current_token_ids: Sequence[int],
delta_token_ids: Sequence[int],
) -> Union[DeltaMessage, None]:
return self._parser.extract_reasoning_content_streaming(
previous_text,
current_text,
delta_text,
previous_token_ids,
current_token_ids,
delta_token_ids,
)

60 changes: 60 additions & 0 deletions vllm/reasoning/identity_reasoning_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

from collections.abc import Sequence
from typing import Optional, Union

from transformers import PreTrainedTokenizerBase

from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
DeltaMessage)
from vllm.logger import init_logger
from vllm.reasoning import ReasoningParser, ReasoningParserManager

logger = init_logger(__name__)


@ReasoningParserManager.register_module("identity")
class IdentityReasoningParser(ReasoningParser):
"""
Identity reasoning parser.

This parser does not attempt to parse or strip out reasoning tokens.
It treats the entire model output as content and ignores reasoning.
"""

def __init__(self, tokenizer: PreTrainedTokenizerBase):
super().__init__(tokenizer)
if not self.model_tokenizer:
raise ValueError(
"The model tokenizer must be passed to the ReasoningParser "
"constructor during construction.")
Comment thread
taohui marked this conversation as resolved.
Outdated

def is_reasoning_end(self, input_ids: list[int]) -> bool:
# Always return False, since we never treat reasoning specially
return False

def extract_content_ids(self, input_ids: list[int]) -> list[int]:
# Identity: return all tokens as content
return input_ids

def extract_reasoning_content_streaming(
self,
previous_text: str,
current_text: str,
delta_text: str,
previous_token_ids: Sequence[int],
current_token_ids: Sequence[int],
delta_token_ids: Sequence[int],
) -> Union[DeltaMessage, None]:
# Just wrap delta_text as content, ignore reasoning
if delta_text:
return DeltaMessage(content=delta_text)
return None

def extract_reasoning_content(
self, model_output: str, request: ChatCompletionRequest
) -> tuple[Optional[str], Optional[str]]:
# No reasoning separation: return None for reasoning_content,
# and full model_output as content
return None, model_output
Loading