Fix parallel tool call parsing bug when tool parameters contain arrays (#16345)

This commit is contained in:
Leoyzen
2026-01-11 11:31:38 +08:00
committed by GitHub
parent 7c25687c9b
commit cf14feba4d
2 changed files with 175 additions and 5 deletions

View File

@@ -166,15 +166,22 @@ class BaseFormatDetector(ABC):
try:
try:
tool_call_pos = current_text.find(self.bot_token)
if tool_call_pos != -1:
start_idx = tool_call_pos + len(self.bot_token)
elif self.current_tool_id > 0 and current_text.startswith(
# Priority check: if we're processing a subsequent tool (current_tool_id > 0),
# first check if text starts with the tool separator. This is critical for
# parallel tool calls because the bot_token (e.g., '[') can also
# appear inside array parameters of the current tool, and we must not
# mistakenly identify that as the start of a new tool.
if self.current_tool_id > 0 and current_text.startswith(
self.tool_call_separator
):
start_idx = len(self.tool_call_separator)
else:
start_idx = 0
# Only search for bot_token if not processing subsequent tool
tool_call_pos = current_text.find(self.bot_token)
if tool_call_pos != -1:
start_idx = tool_call_pos + len(self.bot_token)
else:
start_idx = 0
if start_idx >= len(current_text):
return StreamingParseResult()

View File

@@ -0,0 +1,163 @@
import json
"""
Test case for parallel tool call parsing.
This test verifies that the parser correctly handles parallel tool calls
with array parameters in JSON array format.
Scenario:
- Model outputs two parallel tool calls in JSON array format
- Both tools have array parameters (e.g., "title": ["7.8.9 H-9 ..."])
- First tool completes with closing braces
- Second tool starts with opening brace
- The parser must correctly handle the '[' characters in array parameters
without confusing them with the JSON array start
Expected behavior: Both tools should be parsed correctly.
"""
import unittest
from sglang.srt.entrypoints.openai.protocol import Function, Tool
from sglang.srt.function_call.json_array_parser import JsonArrayParser
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(1.0, "default")
class TestParallelToolCalls(unittest.TestCase):
"""Test case for parallel tool call parsing with array parameters."""
def setUp(self):
"""Set up test tools and detector."""
self.tools = [
Tool(
type="function",
function=Function(
name="search_docs",
description="Search documents",
parameters={
"type": "object",
"properties": {
"title": {
"type": "array",
"items": {"type": "string"},
"description": "Document title",
}
},
"required": ["title"],
},
),
),
]
self.detector = JsonArrayParser()
def _accumulate_tool_calls(self, tool_calls, result):
"""Helper method to accumulate tool call results from parsing output."""
if not result.calls:
return
for call in result.calls:
if call.tool_index is None:
continue
while len(tool_calls) <= call.tool_index:
tool_calls.append({"name": "", "parameters": ""})
if call.name:
tool_calls[call.tool_index]["name"] = call.name
if call.parameters:
tool_calls[call.tool_index]["parameters"] += call.parameters
def test_parallel_tool_calls_with_array_parameters(self):
"""
Test parsing two parallel tool calls where both have array parameters.
This test reproduces the specific scenario:
- Two tool calls separated by comma
- Both tools have array parameters containing '[' character
- First tool completes with '}},'
- Second tool starts with '{"name": ..., "parameters": {"title": ["'
Expected: Both tools should be parsed correctly without errors.
"""
# Simulate more realistic streaming chunks where
# the key issue is the comma separator followed by second tool with array param
chunks = [
"[\n",
' {"name": "search_docs", "parameters": {"title": ["7.8.9"',
'], "filename": "doc1"}},\n',
' {"name": "search_docs", "parameters": {"title": ',
'["4.8"], "filename": "doc2"}}',
"]",
]
tool_calls = []
errors = []
for i, chunk in enumerate(chunks):
try:
result = self.detector.parse_streaming_increment(chunk, self.tools)
# Collect tool calls
self._accumulate_tool_calls(tool_calls, result)
except Exception as e:
errors.append(f"Chunk {i} ({repr(chunk)}): {type(e).__name__}: {e}")
# Verify no errors occurred
if errors:
self.fail("Errors occurred during parsing:\n" + "\n".join(errors))
# Verify both tool calls were parsed
self.assertEqual(len(tool_calls), 2, "Should have parsed exactly 2 tool calls")
# Verify first tool call
self.assertEqual(
tool_calls[0]["name"],
"search_docs",
"First tool name should be search_docs",
)
params1 = json.loads(tool_calls[0]["parameters"])
self.assertEqual(params1["title"], ["7.8.9"], "First tool title should match")
self.assertEqual(
params1["filename"], "doc1", "First tool filename should be doc1"
)
# Verify second tool call
self.assertEqual(
tool_calls[1]["name"],
"search_docs",
"Second tool name should be search_docs",
)
params2 = json.loads(tool_calls[1]["parameters"])
self.assertEqual(params2["title"], ["4.8"], "Second tool title should match")
self.assertEqual(
params2["filename"], "doc2", "Second tool filename should be doc2"
)
def test_simple_parallel_tool_calls(self):
"""
Test a simpler case of two parallel tool calls with array parameters.
This is a minimal test case that still tests the core functionality.
"""
chunks = [
"[\n",
' {"name": "search_docs", "parameters": {"title": ["a"]}},',
"\n",
' {"name": "search_docs", "parameters": {"title": ["b"]}}',
"]",
]
tool_calls = []
for chunk in chunks:
result = self.detector.parse_streaming_increment(chunk, self.tools)
self._accumulate_tool_calls(tool_calls, result)
# Should parse both tools successfully
self.assertEqual(len(tool_calls), 2, "Should parse 2 tool calls")
self.assertEqual(tool_calls[0]["name"], "search_docs")
self.assertEqual(tool_calls[1]["name"], "search_docs")
if __name__ == "__main__":
unittest.main()