Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

373 lines
15 KiB
Python
Raw Permalink 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.
import json
import logging
import re
from partial_json_parser.core.options import Allow
from sglang.srt.entrypoints.openai.protocol import Tool
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
from sglang.srt.function_call.core_types import (
StreamingParseResult,
StructureInfo,
ToolCallItem,
_GetInfoFunc,
)
from sglang.srt.function_call.utils import _find_common_prefix, _partial_json_loads
logger = logging.getLogger(__name__)
class DeepSeekV32Detector(BaseFormatDetector):
"""
Detector for DeepSeek V3.2 model function call format.
The DeepSeek V3.2 format uses XML-like DSML tags to delimit function calls.
Supports two parameter formats:
Format 1 - XML Parameter Tags:
```
<DSMLfunction_calls>
<DSMLinvoke name="function_name">
<DSMLparameter name="param_name" string="true">value</DSMLparameter>
...
</DSMLinvoke>
</DSMLfunction_calls>
```
Format 2 - Direct JSON:
```
<DSMLfunction_calls>
<DSMLinvoke name="function_name">
{
"param_name": "value"
}
</DSMLinvoke>
</DSMLfunction_calls>
```
Examples:
```
<DSMLfunction_calls>
<DSMLinvoke name="get_favorite_tourist_spot">
<DSMLparameter name="city" string="true">San Francisco</DSMLparameter>
</DSMLinvoke>
</DSMLfunction_calls>
<DSMLfunction_calls>
<DSMLinvoke name="get_favorite_tourist_spot">
{ "city": "San Francisco" }
</DSMLinvoke>
</DSMLfunction_calls>
```
Key Components:
- Tool Calls Section: Wrapped between `<DSMLfunction_calls>` and `</DSMLfunction_calls>`
- Individual Tool Call: Wrapped between `<DSMLinvoke name="...">` and `</DSMLinvoke>`
- Parameters: Either XML tags or direct JSON format
- Supports multiple tool calls
Reference: DeepSeek V3.2 format specification
"""
def __init__(self):
super().__init__()
self.bot_token = "<DSMLfunction_calls>"
self.eot_token = "</DSMLfunction_calls>"
self.invoke_end_token = "</DSMLinvoke>"
self.parameter_regex = r'<DSMLparameter\s+name="([^"]+)"\s+string="([^"]+)"\s*>(.*?)</DSMLparameter>'
self.partial_parameter_regex = (
r'<DSMLparameter\s+name="([^"]+)"\s+string="([^"]+)"\s*>(.*)$'
)
self.function_calls_regex = (
r"<DSMLfunction_calls>(.*?)</DSMLfunction_calls>"
)
# Long-form `<DSMLinvoke name="x">...</DSMLinvoke>` and the
# self-closing `<DSMLinvoke 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'<DSMLinvoke\s+name="(?P<name>[^"]+)"\s*'
r"(?:(?P<self_close>/>)"
r"|>(?P<body>.*?)(?P<end>(?:</DSMLinvoke>|$)))"
)
self.prefix_parameter_end_call = ["</", "DSML", "parameter"]
self.prefix_invoke_end_call = ["</", "DSML", "inv", "oke"]
self.current_tool_id = -1
def has_tool_call(self, text: str) -> bool:
"""Check if the text contains a deepseek v32 format tool call."""
return self.bot_token in text or "<DSMLinvoke" 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:
"""
Parse parameters from either XML-like format or JSON format to str.
Supports two formats:
1. XML parameter tags: <DSMLparameter name="..." string="...">value</DSMLparameter>
2. Direct JSON: { "key": "value" }
"""
# First, try to parse as direct JSON (new format)
invoke_content_stripped = invoke_content.strip()
if invoke_content_stripped.startswith("{"):
if allow_partial:
# Remove incomplete invoke end call prefix in case they are captured by param
for token in reversed(self.prefix_invoke_end_call):
invoke_content_stripped = invoke_content_stripped.rstrip(token)
return invoke_content_stripped
elif invoke_content_stripped.endswith("}"):
return invoke_content_stripped
# Fall back to XML parameter tag parsing (original format)
parameters = {}
# Find all complete parameter matches
param_matches = list(
re.finditer(self.parameter_regex, invoke_content, re.DOTALL)
)
last_match_end = 0
for match in param_matches:
param_name = match.group(1)
param_type = match.group(2)
param_value = match.group(3)
last_match_end = match.end()
# Convert value based on type
if param_type == "true": # string type
parameters[param_name] = param_value.strip()
else:
# Try to parse as JSON for other types
try:
parameters[param_name] = json.loads(param_value.strip())
except (json.JSONDecodeError, ValueError):
parameters[param_name] = param_value.strip()
# If allowed, try to parse a partial parameter at the end
if allow_partial:
remaining_content = invoke_content[last_match_end:]
# Remove incomplete parameter_end_call prefix in case they are captured by param
for token in reversed(self.prefix_parameter_end_call):
remaining_content = remaining_content.rstrip(token)
# Match start of a parameter tag + value (potentially incomplete)
# Regex: <tag name="..." string="...">VALUE... (no end tag)
partial_match = re.search(
self.partial_parameter_regex, remaining_content, re.DOTALL
)
if partial_match and (param_value := partial_match.group(3)):
param_name = partial_match.group(1)
if partial_match.group(2) == "true":
parameters[param_name] = param_value.strip()
else:
try:
parameters[param_name] = _partial_json_loads(
param_value, Allow.ALL
)[0]
except json.JSONDecodeError:
parameters[param_name] = param_value.strip()
return json.dumps(parameters, ensure_ascii=False)
def detect_and_parse(self, text: str, tools: list[Tool]) -> StreamingParseResult:
"""
One-time parsing: Detects and parses tool calls in the provided text.
:param text: The complete text to parse.
:param tools: List of available tools.
:return: ParseResult indicating success or failure, consumed text, leftover text, and parsed calls.
"""
idx = text.find(self.bot_token)
normal_text = text[:idx].removesuffix("\n\n") if idx != -1 else text
if self.bot_token not in text:
return StreamingParseResult(normal_text=normal_text, calls=[])
calls = []
try:
# Extract content between function_calls tags
function_calls_match = re.search(
self.function_calls_regex,
text,
re.DOTALL,
)
if not function_calls_match:
return StreamingParseResult(normal_text=normal_text, calls=[])
function_calls_content = function_calls_match.group(1)
# Find all invoke blocks
for invoke_match in re.finditer(
self.invoke_regex, function_calls_content, re.DOTALL
):
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)}
calls.extend(self.parse_base_json(match_result, tools))
return StreamingParseResult(normal_text=normal_text, calls=calls)
except Exception as e:
logger.error(f"Error in detect_and_parse: {e}")
# return the normal text if parsing fails
return StreamingParseResult(normal_text=text)
def parse_streaming_increment(
self, new_text: str, tools: list[Tool]
) -> StreamingParseResult:
"""
Streaming incremental parsing tool calls for DeepSeekV32 format.
Supports multiple consecutive invoke blocks and argument streaming.
"""
self._buffer += new_text
current_text = self._buffer
# Check if buffer contains any DSML markers or ends with potential tag prefix
# This handles partial/streaming DSML content
dsml_markers = ["DSML", "<", "</"]
potentially_dsml = any(marker in current_text for marker in dsml_markers)
# Also check if text ends with start of a tag (to handle "<" arriving separately)
dsml_prefixes = ["<", "<", "</", "</"]
ends_with_prefix = any(
current_text.rstrip().endswith(prefix) for prefix in dsml_prefixes
)
if (
not self.has_tool_call(current_text)
and not potentially_dsml
and not ends_with_prefix
):
self._buffer = ""
for e_token in [self.eot_token, self.invoke_end_token]:
if e_token in current_text:
current_text = current_text.replace(e_token, "")
return StreamingParseResult(normal_text=current_text)
all_calls: list[ToolCallItem] = []
try:
# Loop to handle multiple consecutive invoke blocks
while True:
# Try to match an invoke block (may be partial)
invoke_match = re.search(
pattern=self.invoke_regex,
string=current_text,
flags=re.DOTALL,
)
if not invoke_match:
break
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:
self.current_tool_id = 0
self.prev_tool_call_arr = []
self.streamed_args_for_tool = [""]
# Ensure arrays are large enough for current tool
while len(self.prev_tool_call_arr) <= self.current_tool_id:
self.prev_tool_call_arr.append({})
while len(self.streamed_args_for_tool) <= self.current_tool_id:
self.streamed_args_for_tool.append("")
# 1. Send tool name if not sent yet
if not self.current_tool_name_sent:
all_calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
name=func_name,
parameters="",
)
)
self.current_tool_name_sent = True
# 2. Parse current parameters (partial or complete)
current_params = self._parse_parameters_from_xml(
invoke_content, allow_partial=not is_tool_end
)
# 3. Calculate and send incremental arguments
sent_len = len(self.streamed_args_for_tool[self.current_tool_id])
prev_params = self.prev_tool_call_arr[self.current_tool_id].get(
"arguments"
)
argument_diff = None
if is_tool_end:
# If complete, send everything remaining
argument_diff = current_params[sent_len:]
elif prev_params is not None:
# If partial, send stable prefix diff
if current_params != prev_params:
prefix = _find_common_prefix(current_params, prev_params)
if len(prefix) > sent_len:
argument_diff = prefix[sent_len:]
if argument_diff:
all_calls.append(
ToolCallItem(
tool_index=self.current_tool_id,
name=None,
parameters=argument_diff,
)
)
self.streamed_args_for_tool[self.current_tool_id] += argument_diff
# Update the stored arguments
self.prev_tool_call_arr[self.current_tool_id] = {
"name": func_name,
"arguments": current_params,
}
# Check if tool call is complete (has closing tag)
if is_tool_end:
# Remove the completed tool call from buffer
self._buffer = current_text[invoke_match.end() :]
current_text = self._buffer # Update for next iteration
# Move to next tool call
self.current_tool_id += 1
self.current_tool_name_sent = False
# Continue loop to check for more invoke blocks
continue
else:
# Tool call not complete yet, don't return anything
# Wait for more chunks until we see </DSMLinvoke>
break
# No more invoke blocks found
return StreamingParseResult(normal_text="", calls=all_calls)
except Exception as e:
logger.error(f"Error in parse_streaming_increment: {e}")
return StreamingParseResult(normal_text=current_text)
def structure_info(self) -> _GetInfoFunc:
return lambda name: StructureInfo(
begin=f'<DSMLinvoke name="{name}">',
end="</DSMLinvoke>",
trigger="<DSMLinvoke",
)
def get_structural_tag_name(self) -> str:
return "deepseek_v3_2"