Files
vllm-project--vllm/vllm/parser/deepseek_v4.py
T
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

238 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""DeepSeek V4 parser: ``<think>``/``</think>``
reasoning plus DSML tool calls in a single state machine.
DeepSeek V4 output format::
<think>
...reasoning...
</think>
<DSMLtool_calls>
<DSMLinvoke name="func_name">
<DSMLparameter name="location" string="true">杭州</DSMLparameter>
<DSMLparameter name="count" string="false">5</DSMLparameter>
</DSMLinvoke>
</DSMLtool_calls>
"""
from __future__ import annotations
import contextlib
import functools
import json
from typing import TYPE_CHECKING
import regex as re
from vllm.parser.engine.events import EventType
from vllm.parser.engine.parser_engine import ParserEngine
from vllm.parser.engine.parser_engine_config import (
ParserEngineConfig,
ParserState,
Transition,
)
from vllm.tool_parsers.utils import find_tool_properties
if TYPE_CHECKING:
from vllm.tokenizers import TokenizerLike
from vllm.tool_parsers.abstract_tool_parser import Tool
_DSML = "DSML"
DSML_THINK_START = "<think>"
DSML_THINK_END = "</think>"
DSML_TOOL_START = f"<{_DSML}tool_calls>"
DSML_TOOL_END = f"</{_DSML}tool_calls>"
DSML_INVOKE_PREFIX = f'<{_DSML}invoke name="'
DSML_INVOKE_NAME_END = '">'
DSML_INVOKE_END = f"</{_DSML}invoke>"
DSML_PARAM_CLOSE = f"</{_DSML}parameter>"
_ESCAPED_DSML = re.escape(_DSML)
_PARAM_RE = re.compile(
rf'<{_ESCAPED_DSML}parameter\s+name="([^"]+)"\s+string="(true|false)">'
rf"(.*?)</{_ESCAPED_DSML}parameter>",
re.DOTALL,
)
_PARTIAL_PARAM_RE = re.compile(
rf'<{_ESCAPED_DSML}parameter\s+name="([^"]+)"\s+string="(true|false)">'
rf"(.*)$",
re.DOTALL,
)
def _dsml_arg_converter(raw_args: str, partial: bool) -> str:
params: dict[str, object] = {}
last_end = 0
for m in _PARAM_RE.finditer(raw_args):
name, is_str, value = m.group(1), m.group(2), m.group(3)
if is_str == "true":
params[name] = value
else:
try:
params[name] = json.loads(value)
except (json.JSONDecodeError, ValueError):
params[name] = value
last_end = m.end()
if partial:
pm = _PARTIAL_PARAM_RE.search(raw_args, last_end)
if pm:
name, is_str, value = pm.group(1), pm.group(2), pm.group(3)
if is_str == "true":
params[name] = value
else:
with contextlib.suppress(json.JSONDecodeError, ValueError):
params[name] = json.loads(value)
return json.dumps(params, ensure_ascii=False)
def _unwrap_wrapper_args(
args_json: str,
tools: list[Tool] | None,
func_name: str | None,
) -> str:
if not tools or not func_name:
return args_json
try:
args = json.loads(args_json)
except (json.JSONDecodeError, ValueError):
return args_json
if not isinstance(args, dict):
return args_json
properties = find_tool_properties(tools, func_name)
if not properties:
return args_json
allowed = set(properties.keys())
for wrapper in ("arguments", "input"):
if set(args.keys()) != {wrapper} or wrapper in allowed:
continue
inner = args[wrapper]
if isinstance(inner, str):
try:
inner = json.loads(inner)
except json.JSONDecodeError:
return args_json
if isinstance(inner, dict) and set(inner.keys()).issubset(allowed):
return json.dumps(inner, ensure_ascii=False)
return args_json
@functools.cache
def deepseek_v4_config(thinking: bool = False) -> ParserEngineConfig:
return ParserEngineConfig(
name="deepseek_v4",
initial_state=ParserState.REASONING if thinking else ParserState.CONTENT,
terminals={
"THINK_START": DSML_THINK_START,
"THINK_END": DSML_THINK_END,
"TOOL_START": DSML_TOOL_START,
"TOOL_END": DSML_TOOL_END,
"INVOKE_PREFIX": DSML_INVOKE_PREFIX,
"INVOKE_NAME_END": DSML_INVOKE_NAME_END,
"INVOKE_END": DSML_INVOKE_END,
"PARAM_CLOSE": DSML_PARAM_CLOSE,
},
token_id_terminals={
"THINK_START": DSML_THINK_START,
"THINK_END": DSML_THINK_END,
"TOOL_START": DSML_TOOL_START,
"TOOL_END": DSML_TOOL_END,
},
transitions={
(ParserState.CONTENT, "THINK_START"): Transition(
ParserState.REASONING,
(EventType.REASONING_START,),
),
# Absorb a bare </think> with no prior <think>
(ParserState.CONTENT, "THINK_END"): Transition(
ParserState.CONTENT,
(),
),
# Absorb a duplicate <think> while already reasoning
(ParserState.REASONING, "THINK_START"): Transition(
ParserState.REASONING,
(),
),
(ParserState.REASONING, "THINK_END"): Transition(
ParserState.CONTENT,
(EventType.REASONING_END,),
),
# Tool call beginning while still inside <think>
(ParserState.REASONING, "TOOL_START"): Transition(
ParserState.TOOL_PREAMBLE,
(EventType.REASONING_END,),
),
(ParserState.CONTENT, "TOOL_START"): Transition(
ParserState.TOOL_PREAMBLE,
(),
),
(ParserState.TOOL_PREAMBLE, "INVOKE_PREFIX"): Transition(
ParserState.TOOL_NAME,
(EventType.TOOL_CALL_START,),
),
(ParserState.TOOL_NAME, "INVOKE_NAME_END"): Transition(
ParserState.TOOL_ARGS,
(),
),
(ParserState.TOOL_ARGS, "INVOKE_END"): Transition(
ParserState.TOOL_BETWEEN,
(EventType.TOOL_CALL_END,),
),
(ParserState.TOOL_ARGS, "TOOL_END"): Transition(
ParserState.CONTENT,
(EventType.TOOL_CALL_END,),
),
# Parallel tool calls
(ParserState.TOOL_BETWEEN, "INVOKE_PREFIX"): Transition(
ParserState.TOOL_NAME,
(EventType.TOOL_CALL_START,),
),
(ParserState.TOOL_BETWEEN, "TOOL_END"): Transition(
ParserState.CONTENT,
(),
),
},
content_events={
ParserState.CONTENT: EventType.TEXT_CHUNK,
ParserState.REASONING: EventType.REASONING_CHUNK,
ParserState.TOOL_NAME: EventType.TOOL_NAME,
ParserState.TOOL_ARGS: EventType.ARG_VALUE_CHUNK,
},
arg_converter=_dsml_arg_converter,
arg_structural_chars=frozenset(">"),
strip_content_whitespace_with_tools=False,
tool_args_json=False,
)
class DeepSeekV4Parser(ParserEngine):
def __init__(
self,
tokenizer: TokenizerLike,
tools: list[Tool] | None = None,
**kwargs,
) -> None:
chat_kwargs = kwargs.pop("chat_template_kwargs", None) or {}
thinking = (
bool(chat_kwargs.get("thinking") or chat_kwargs.get("enable_thinking"))
and chat_kwargs.get("reasoning_effort") != "none"
)
super().__init__(
tokenizer,
tools,
parser_engine_config=deepseek_v4_config(thinking=thinking),
**kwargs,
)
self._arg_converter = self._convert_args
def _convert_args(self, raw_args: str, partial: bool) -> str:
result = _dsml_arg_converter(raw_args, partial)
if not self._tools:
return result
func_name = next((s.name for s in self._tool_slots if s.args == raw_args), None)
return _unwrap_wrapper_args(result, self._tools, func_name)