diff --git a/python/sglang/srt/function_call/deepseekv32_detector.py b/python/sglang/srt/function_call/deepseekv32_detector.py index 736e02582..d9228743b 100644 --- a/python/sglang/srt/function_call/deepseekv32_detector.py +++ b/python/sglang/srt/function_call/deepseekv32_detector.py @@ -85,6 +85,7 @@ class DeepSeekV32Detector(BaseFormatDetector): r'<|DSML|invoke\s+name="([^"]+)"\s*>(.*?)(|$)' ) self.prefix_parameter_end_call = [" 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: <|DSML|parameter name="..." string="...">value @@ -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'<|DSML|invoke name="{name}">', end="", - trigger=f"<|DSML|invoke", + trigger="<|DSML|invoke", ) diff --git a/test/registered/function_call/test_function_call_parser.py b/test/registered/function_call/test_function_call_parser.py index 126d68e5d..5e8c1928b 100644 --- a/test/registered/function_call/test_function_call_parser.py +++ b/test/registered/function_call/test_function_call_parser.py @@ -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 = """<|DSML|function_calls> <|DSML|invoke name="get_favorite_tourist_spot"> { - "city": "San Francisco" + "city": "San Francisco", + "another_city": "London", + "topn": 10, + "obj": { + "name": "John", + "age": 30 + } } """ @@ -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")