[DSV4] Cherry pick missing commits from deepseek_v4 branch and enhance tests (#24793)

Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
Co-authored-by: yueming-yuan <yym022502@gmail.com>
This commit is contained in:
Baizhou Zhang
2026-05-09 04:15:37 -07:00
committed by GitHub
co-authored by Xinyuan Tong yueming-yuan
parent 4b23f6bdc5
commit ef5e9f8aba
15 changed files with 481 additions and 87 deletions
@@ -81,8 +81,13 @@ class DeepSeekV32Detector(BaseFormatDetector):
self.function_calls_regex = (
r"<|DSML|function_calls>(.*?)</|DSML|function_calls>"
)
# Long-form `<|DSML|invoke name="x">...</|DSML|invoke>` and the
# self-closing `<|DSML|invoke name="x"/>` shape V4 emits for zero-arg
# tools. The `end` group is empty when the closer hasn't streamed in.
self.invoke_regex = (
r'<|DSML|invoke\s+name="([^"]+)"\s*>(.*?)(</|DSML|invoke>|$)'
r'<|DSML|invoke\s+name="(?P<name>[^"]+)"\s*'
r"(?:(?P<self_close>/>)"
r"|>(?P<body>.*?)(?P<end>(?:</|DSML|invoke>|$)))"
)
self.prefix_parameter_end_call = ["</", "|DSML|", "parameter"]
self.prefix_invoke_end_call = ["</", "|DSML|", "inv", "oke"]
@@ -92,6 +97,20 @@ class DeepSeekV32Detector(BaseFormatDetector):
"""Check if the text contains a deepseek v32 format tool call."""
return self.bot_token in text or "<|DSML|invoke" in text
@staticmethod
def _unpack_invoke_match(m: "re.Match[str]") -> tuple[str, str, bool]:
"""Returns (name, body, is_complete) for an invoke_regex match.
Self-closing invokes have empty body and are always complete.
Long-form bodies are always strings (possibly empty); they're
incomplete when matched against `$` because the closing tag
hasn't streamed in yet.
"""
name = m.group("name").strip()
if m.group("self_close"):
return name, "", True
return name, m.group("body"), bool(m.group("end"))
def _parse_parameters_from_xml(
self, invoke_content: str, allow_partial: bool = False
) -> str:
@@ -192,12 +211,10 @@ class DeepSeekV32Detector(BaseFormatDetector):
function_calls_content = function_calls_match.group(1)
# Find all invoke blocks
invoke_matches = re.findall(
for invoke_match in re.finditer(
self.invoke_regex, function_calls_content, re.DOTALL
)
for func_name, invoke_content, _ in invoke_matches:
# Parse parameters from XML format
):
func_name, invoke_content, _ = self._unpack_invoke_match(invoke_match)
func_args = self._parse_parameters_from_xml(invoke_content)
# construct match_result for parse_base_json
match_result = {"name": func_name, "parameters": json.loads(func_args)}
@@ -254,10 +271,9 @@ class DeepSeekV32Detector(BaseFormatDetector):
if not invoke_match:
break
func_name = invoke_match.group(1).strip()
invoke_content = invoke_match.group(2)
# group(3) is either "</|DSML|invoke>" (complete) or "" (incomplete, matched with $)
is_tool_end = bool(invoke_match.group(3))
func_name, invoke_content, is_tool_end = self._unpack_invoke_match(
invoke_match
)
# Initialize state if this is the first tool call
if self.current_tool_id == -1: