Skip to content

[BugFix][Frontend] Align DeepSeek V4 frontend behavior - #14632

Open
QwertyJack wants to merge 4 commits into
vllm-project:mainfrom
QwertyJack:fix/deepseek-v4-frontend-main
Open

QwertyJack wants to merge 4 commits into
vllm-project:mainfrom
QwertyJack:fix/deepseek-v4-frontend-main

Conversation

@QwertyJack

@QwertyJack QwertyJack commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does / why we need it?

Ports the remaining DeepSeek V4 frontend behavior from the v0.25 release branch to main as temporary compatibility patches:

The upstream status does not yet make these patches removable from the supported vLLM matrix:

The streaming fast path is installed only for the affected upstream contract, arg_structural_chars=frozenset('>'). It automatically skips installation after upstream removes that filter and fails fast on an unknown non-null contract.

Does this PR introduce any user-facing change?

Yes. DeepSeek V4 requests preserve their system prompt when tools are present, omitted thinking controls no longer leak reasoning/tool markup into content, and long string tool arguments stream incrementally rather than being buffered until parameter close.

How was this patch tested?

Tested against both vLLM sources supported by the current main branch:

PYTHONPATH=$VLLM_MAIN_SOURCE pytest -q \
  tests/ut/patch/platform/test_deepseek_v4_thinking.py \
  tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py
# 22 passed

VLLM_VERSION=0.27.1 PYTHONPATH=$VLLM_RELEASE_SOURCE pytest -q \
  tests/ut/patch/platform/test_deepseek_v4_thinking.py \
  tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py
# 22 passed

ruff check   tests/ut/patch/platform/test_deepseek_v4_thinking.py   tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py   vllm_ascend/patch/__init__.py   vllm_ascend/patch/platform/__init__.py   vllm_ascend/patch/platform/patch_deepseek_v4_frontend.py   vllm_ascend/patch/platform/patch_deepseek_v4_tool_streaming.py
# All checks passed!

ruff format --check   tests/ut/patch/platform/test_deepseek_v4_thinking.py   tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py   vllm_ascend/patch/__init__.py   vllm_ascend/patch/platform/__init__.py   vllm_ascend/patch/platform/patch_deepseek_v4_frontend.py   vllm_ascend/patch/platform/patch_deepseek_v4_tool_streaming.py
# 6 files already formatted

python -m py_compile   vllm_ascend/patch/platform/patch_deepseek_v4_frontend.py   vllm_ascend/patch/platform/patch_deepseek_v4_tool_streaming.py   tests/ut/patch/platform/test_deepseek_v4_thinking.py   tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py

git diff --check upstream/main...HEAD

The latest live vLLM main still has the affected tools-placement and structural-filter contracts. Its full vLLM-Ascend pytest import is currently blocked earlier by the unrelated removal of vllm.model_executor.layers.attention.pcp; the current pinned main and release-tag paths above are fully tested.

@github-actions

Copy link
Copy Markdown
Contributor

👋 Hi! Thank you for contributing to the vLLM Ascend project. The following points will speed up your PR merge:‌‌

  • A PR should do only one thing, smaller PRs enable faster reviews.
  • Every PR should include unit tests and end-to-end tests ‌to ensure it works and is not broken by other future PRs.
  • Write the commit message by fulfilling the PR description to help reviewer and future developers understand.

If CI fails, you can run linting and testing checks locally according Contributing and Testing.


Tip

💡 Consider Linking a Related Issue or RFC

Your PR title contains the [BugFix] tag, indicating a bug fix or new feature.

Linking a related issue or RFC in the PR description is strongly encouraged — it gives reviewers helpful context and speeds up the review. You can use any of these keywords:

  • Fixes #<issue_number>
  • Closes #<issue_number>
  • Resolves #<issue_number>
  • Refs #<rfc_or_issue_number> (for RFCs)

🙏 Thanks for helping us keep the project well-organized!

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request ports essential DeepSeek V4 frontend compatibility patches from the v0.25 release branch to the main branch. These changes address discrepancies in tool placement, thinking mode defaults, and tool argument streaming efficiency. The patches are designed to be temporary and include conditional logic to automatically disable themselves once the corresponding upstream vLLM improvements are integrated.

Highlights

  • Thinking Mode Defaulting: Updated the DeepSeek V4 parser to default to thinking mode when no explicit thinking controls are provided, ensuring consistent behavior with expected model outputs.
  • Tool Message Placement: Modified the tokenizer to attach request-level tools to an existing system message rather than inserting a redundant system message, preserving prompt structure.
  • Incremental Tool Streaming: Implemented a streaming fast path for long schema-typed string tool arguments, allowing them to stream incrementally instead of buffering until the parameter closing tag.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

Suggested PR Title:

[platform][Feature] Add patches for DeepSeek V4 frontend and tool streaming

Suggested PR Summary:

### What this PR does / why we need it?
This PR introduces patches for DeepSeek V4 in `vllm_ascend` to align behavior with the DeepSeek V4 tokenizer default and checkpoint renderer, and to enable incremental streaming of long string parameters during tool calls. Specifically:
1. **Frontend Patch (`patch_deepseek_v4_frontend.py`)**: Corrects tool attachment behavior by attaching tools to a shallow copy of the first system message (or inserting one if absent) and defaults the parser to thinking mode when thinking controls are omitted.
2. **Tool Streaming Patch (`patch_deepseek_v4_tool_streaming.py`)**: Allows direct streaming of plain body deltas for string parameters instead of buffering until `</parameter>` arrives.

Feedback on the implementation:
- In `patch_deepseek_v4_frontend.py`, wrapping `apply_chat_template` on every call to `get_deepseek_v4_tokenizer` can cause redundant wrapping and potential recursion errors. It should be guarded to run only once.
- The patch for `DeepSeekV4Parser.__init__` assumes `chat_template_kwargs` is always passed as a keyword argument, which can raise a `TypeError` if passed positionally.

### Does this PR introduce _any_ user-facing change?
Yes, it changes how tools are attached to system messages and defaults the parser to thinking mode for DeepSeek V4. It also improves streaming responsiveness for tool call arguments.

### How was this patch tested?
New unit tests have been added in `tests/ut/patch/platform/test_deepseek_v4_thinking.py` and `tests/ut/patch/platform/test_deepseek_v4_tool_streaming.py` to verify the patched tokenizer, parser thinking mode, and tool streaming behavior.

Comment on lines +14 to +43
def _patched_get_deepseek_v4_tokenizer(tokenizer: deepseek_v4.HfTokenizer):
dsv4_tokenizer = _original_get_deepseek_v4_tokenizer(tokenizer)
tokenizer_cls = type(dsv4_tokenizer)
original_apply_chat_template = tokenizer_cls.apply_chat_template

@wraps(original_apply_chat_template)
def apply_chat_template(
self,
messages,
tools=None,
**kwargs,
):
if tools:
conversation = kwargs.get("conversation", messages).copy()
system_index = next(
(index for index, message in enumerate(conversation) if message.get("role") == "system"),
None,
)
if system_index is None:
conversation.insert(0, {"role": "system", "tools": tools})
else:
system_message = conversation[system_index].copy()
system_message["tools"] = tools
conversation[system_index] = system_message
kwargs["conversation"] = conversation
tools = None
return original_apply_chat_template(self, messages, tools=tools, **kwargs)

tokenizer_cls.apply_chat_template = apply_chat_template
return dsv4_tokenizer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The current implementation of _patched_get_deepseek_v4_tokenizer wraps tokenizer_cls.apply_chat_template every time get_deepseek_v4_tokenizer is called. This leads to redundant wrapping, performance degradation, and potential RecursionError over time. Additionally, if conversation is passed as a keyword argument or if the first argument of apply_chat_template is named conversation instead of messages, calling original_apply_chat_template with both messages positionally and conversation in kwargs will raise a TypeError due to multiple values for the same argument.

We should check if the class has already been patched and safely handle both messages and conversation argument bindings.

def _patched_get_deepseek_v4_tokenizer(tokenizer: deepseek_v4.HfTokenizer):
    dsv4_tokenizer = _original_get_deepseek_v4_tokenizer(tokenizer)
    tokenizer_cls = type(dsv4_tokenizer)
    if not getattr(tokenizer_cls, "_patched_apply_chat_template", False):
        original_apply_chat_template = tokenizer_cls.apply_chat_template

        @wraps(original_apply_chat_template)
        def apply_chat_template(
            self,
            messages,
            tools=None,
            **kwargs,
        ):
            if tools:
                conversation = kwargs.get("conversation", messages).copy()
                system_index = next(
                    (index for index, message in enumerate(conversation) if message.get("role") == "system"),
                    None,
                )
                if system_index is None:
                    conversation.insert(0, {"role": "system", "tools": tools})
                else:
                    system_message = conversation[system_index].copy()
                    system_message["tools"] = tools
                    conversation[system_index] = system_message
                if "conversation" in kwargs:
                    kwargs["conversation"] = conversation
                else:
                    messages = conversation
                tools = None
            return original_apply_chat_template(self, messages, tools=tools, **kwargs)

        tokenizer_cls.apply_chat_template = apply_chat_template
        setattr(tokenizer_cls, "_patched_apply_chat_template", True)
    return dsv4_tokenizer

Comment on lines +47 to +58
def _patched_deepseek_v4_parser_init(
self: DeepSeekV4Parser,
*args: Any,
**kwargs: Any,
) -> None:
chat_kwargs = kwargs.get("chat_template_kwargs") or {}
if "thinking" not in chat_kwargs and "enable_thinking" not in chat_kwargs:
chat_kwargs = dict(chat_kwargs)
chat_kwargs["enable_thinking"] = True
kwargs["chat_template_kwargs"] = chat_kwargs

_original_deepseek_v4_parser_init(self, *args, **kwargs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The _patched_deepseek_v4_parser_init function assumes chat_template_kwargs is always passed as a keyword argument. If it is passed positionally (e.g., DeepSeekV4Parser(tokenizer, chat_template_kwargs)), kwargs.get("chat_template_kwargs") will be None, and setting kwargs["chat_template_kwargs"] will result in a TypeError: __init__() got multiple values for keyword argument 'chat_template_kwargs' when calling the original __init__.

We should robustly handle both positional and keyword arguments for chat_template_kwargs.

def _patched_deepseek_v4_parser_init(
    self: DeepSeekV4Parser,
    *args: Any,
    **kwargs: Any,
) -> None:
    chat_kwargs = None
    passed_positionally = False
    if len(args) >= 2:
        chat_kwargs = args[1]
        passed_positionally = True
    else:
        chat_kwargs = kwargs.get("chat_template_kwargs")

    if chat_kwargs is None:
        chat_kwargs = {}

    if "thinking" not in chat_kwargs and "enable_thinking" not in chat_kwargs:
        chat_kwargs = dict(chat_kwargs)
        chat_kwargs["enable_thinking"] = True
        if passed_positionally:
            args_list = list(args)
            args_list[1] = chat_kwargs
            args = tuple(args_list)
        else:
            kwargs["chat_template_kwargs"] = chat_kwargs

    _original_deepseek_v4_parser_init(self, *args, **kwargs)

@QwertyJack
QwertyJack force-pushed the fix/deepseek-v4-frontend-main branch from 6b1774b to f5daf03 Compare September 1, 2026 08:12
kunpengW-code
kunpengW-code previously approved these changes Sep 1, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@QwertyJack

Copy link
Copy Markdown
Collaborator Author

/rerun

@QwertyJack

QwertyJack commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

/rerun
[Bot]: rerun completed.

Rerun (failed jobs only):

  • E2E

@QwertyJack
QwertyJack force-pushed the fix/deepseek-v4-frontend-main branch from 0dce027 to e633dfb Compare September 4, 2026 08:25
@QwertyJack

QwertyJack commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

/rerun
[Bot]: rerun completed.

Rerun (failed jobs only):

  • E2E

1 similar comment
@QwertyJack

QwertyJack commented Sep 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

/rerun
[Bot]: rerun completed.

Rerun (failed jobs only):

  • E2E

@QwertyJack
QwertyJack force-pushed the fix/deepseek-v4-frontend-main branch from 3a1060f to 332db9f Compare September 5, 2026 02:14
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

1 similar comment
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

linfeng-yuan pushed a commit that referenced this pull request Sep 9, 2026
)

## What this PR does / why we need it?

Backport PR #14632 to `releases/v0.27.1rc`. This aligns DeepSeek V4
frontend rendering, reasoning effort handling, compatibility behavior,
incremental tool argument streaming, and related unit tests.

## Does this PR introduce any user-facing change?

Yes. DeepSeek V4 chat template rendering and long tool argument
streaming are corrected on the v0.27 release line.

## How was this patch tested?

- Cherry-picked all four commits from PR #14632.
- Added and updated DeepSeek V4 frontend unit tests.
- Resolved release-branch patch registration conflicts.

- vLLM main:
vllm-project/vllm@ba07e4a

---------

Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Co-authored-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Port the release parser default, system/tools rendering, and incremental long string argument streaming behavior to main.

Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Keep checkpoint-specific reasoning effort behavior and isolate tokenizer wrappers per instance while preserving legacy frontend compatibility.

Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Remove the pickle round-trip test rejected by the forbidden-import check and keep the streaming test tokenizer compatible with the current scanner API.

Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants