[Tool Call] Stream DeepSeek-V3.2 function call parameters in JSON format. (#16091)

Co-authored-by: Huixxi <uestc.hugo@gmail.com>
This commit is contained in:
Muqi Li
2026-03-03 17:46:29 +08:00
committed by GitHub
parent 4c95953b77
commit 666caaf9ce
2 changed files with 31 additions and 22 deletions

View File

@@ -85,6 +85,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
r'<DSMLinvoke\s+name="([^"]+)"\s*>(.*?)(</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:
@@ -93,9 +94,9 @@ class DeepSeekV32Detector(BaseFormatDetector):
def _parse_parameters_from_xml(
self, invoke_content: str, allow_partial: bool = False
) -> dict:
) -> str:
"""
Parse parameters from either XML-like format or JSON format to dict.
Parse parameters from either XML-like format or JSON format to str.
Supports two formats:
1. XML parameter tags: <DSMLparameter name="..." string="...">value</DSMLparameter>
@@ -103,17 +104,14 @@ class DeepSeekV32Detector(BaseFormatDetector):
"""
# First, try to parse as direct JSON (new format)
invoke_content_stripped = invoke_content.strip()
if invoke_content_stripped.startswith("{") and invoke_content_stripped.endswith(
"}"
):
try:
parameters = json.loads(invoke_content_stripped)
if isinstance(parameters, dict):
return parameters
except (json.JSONDecodeError, ValueError):
# If JSON parsing fails, fall through to XML parsing
pass
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 = {}
@@ -165,7 +163,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
except json.JSONDecodeError:
parameters[param_name] = param_value.strip()
return parameters
return json.dumps(parameters, ensure_ascii=False)
def detect_and_parse(self, text: str, tools: list[Tool]) -> StreamingParseResult:
"""
@@ -202,7 +200,7 @@ class DeepSeekV32Detector(BaseFormatDetector):
# Parse parameters from XML format
func_args = self._parse_parameters_from_xml(invoke_content)
# construct match_result for parse_base_json
match_result = {"name": func_name, "parameters": func_args}
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)
@@ -288,7 +286,6 @@ class DeepSeekV32Detector(BaseFormatDetector):
current_params = self._parse_parameters_from_xml(
invoke_content, allow_partial=not is_tool_end
)
current_args_json = json.dumps(current_params, ensure_ascii=False)
# 3. Calculate and send incremental arguments
sent_len = len(self.streamed_args_for_tool[self.current_tool_id])
@@ -300,12 +297,11 @@ class DeepSeekV32Detector(BaseFormatDetector):
if is_tool_end:
# If complete, send everything remaining
argument_diff = current_args_json[sent_len:]
argument_diff = current_params[sent_len:]
elif prev_params is not None:
# If partial, send stable prefix diff
prev_args_json = json.dumps(prev_params, ensure_ascii=False)
if current_args_json != prev_args_json:
prefix = _find_common_prefix(prev_args_json, current_args_json)
if current_params != prev_params:
prefix = _find_common_prefix(current_params, prev_params)
if len(prefix) > sent_len:
argument_diff = prefix[sent_len:]
@@ -353,5 +349,5 @@ class DeepSeekV32Detector(BaseFormatDetector):
return lambda name: StructureInfo(
begin=f'<DSMLinvoke name="{name}">',
end="</DSMLinvoke>",
trigger=f"<DSMLinvoke",
trigger="<DSMLinvoke",
)

View File

@@ -1391,9 +1391,11 @@ class TestDeepSeekV32Detector(unittest.TestCase):
tool_calls_by_index = {}
num_tool_call_chunks = 0
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, self.tools)
for call in result.calls:
num_tool_call_chunks += 1
if call.tool_index is not None:
if call.tool_index not in tool_calls_by_index:
tool_calls_by_index[call.tool_index] = {
@@ -1408,6 +1410,8 @@ class TestDeepSeekV32Detector(unittest.TestCase):
"parameters"
] += call.parameters
self.assertGreater(num_tool_call_chunks, 8)
self.assertEqual(len(tool_calls_by_index), 1)
self.assertEqual(tool_calls_by_index[0]["name"], "get_favorite_tourist_spot")
params = json.loads(tool_calls_by_index[0]["parameters"])
@@ -1422,7 +1426,13 @@ class TestDeepSeekV32Detector(unittest.TestCase):
text = """<DSMLfunction_calls>
<DSMLinvoke name="get_favorite_tourist_spot">
{
"city": "San Francisco"
"city": "San Francisco",
"another_city": "London",
"topn": 10,
"obj": {
"name": "John",
"age": 30
}
}
</DSMLinvoke>
</DSMLfunction_calls>"""
@@ -1436,9 +1446,11 @@ class TestDeepSeekV32Detector(unittest.TestCase):
tool_calls_by_index = {}
num_tool_call_chunks = 0
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, self.tools)
for call in result.calls:
num_tool_call_chunks += 1
if call.tool_index is not None:
if call.tool_index not in tool_calls_by_index:
tool_calls_by_index[call.tool_index] = {
@@ -1453,6 +1465,7 @@ class TestDeepSeekV32Detector(unittest.TestCase):
"parameters"
] += call.parameters
self.assertGreater(num_tool_call_chunks, 8)
self.assertEqual(len(tool_calls_by_index), 1)
self.assertEqual(tool_calls_by_index[0]["name"], "get_favorite_tourist_spot")