From 2724b1100be6bde989c183a9fc19804d9ffb3ec8 Mon Sep 17 00:00:00 2001
From: lw9527 <952799980@qq.com>
Date: Tue, 6 Jan 2026 15:47:26 +0800
Subject: [PATCH] fix double Unicode escape issue in streaming tool_calls
parameters (#13518)
Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
---
.../srt/function_call/base_format_detector.py | 4 +-
.../test_function_call_parser.py | 134 ++++++++++++++++++
2 files changed, 136 insertions(+), 2 deletions(-)
diff --git a/python/sglang/srt/function_call/base_format_detector.py b/python/sglang/srt/function_call/base_format_detector.py
index 13a4f2df9..2b83893f5 100644
--- a/python/sglang/srt/function_call/base_format_detector.py
+++ b/python/sglang/srt/function_call/base_format_detector.py
@@ -249,7 +249,7 @@ class BaseFormatDetector(ABC):
if cur_arguments:
# Calculate how much of the arguments we've already streamed
sent = len(self.streamed_args_for_tool[self.current_tool_id])
- cur_args_json = json.dumps(cur_arguments)
+ cur_args_json = json.dumps(cur_arguments, ensure_ascii=False)
prev_arguments = None
if self.current_tool_id < len(self.prev_tool_call_arr):
prev_arguments = self.prev_tool_call_arr[
@@ -270,7 +270,7 @@ class BaseFormatDetector(ABC):
# If the tool is still being parsed, send incremental changes
elif prev_arguments:
- prev_args_json = json.dumps(prev_arguments)
+ prev_args_json = json.dumps(prev_arguments, ensure_ascii=False)
if cur_args_json != prev_args_json:
prefix = _find_common_prefix(prev_args_json, cur_args_json)
argument_diff = prefix[sent:]
diff --git a/test/registered/function_call/test_function_call_parser.py b/test/registered/function_call/test_function_call_parser.py
index d23658646..e2bb53f8b 100644
--- a/test/registered/function_call/test_function_call_parser.py
+++ b/test/registered/function_call/test_function_call_parser.py
@@ -719,6 +719,140 @@ class TestBaseFormatDetector(unittest.TestCase):
self.detector._buffer, "", "Buffer should be cleared for invalid tool"
)
+ def test_chinese_characters_not_double_escaped(self):
+ """Test that Chinese characters in tool call parameters are not double-escaped."""
+ # Test with Chinese city name "杭州" (Hangzhou)
+ chunks = [
+ "",
+ '{"name": "get_weather", ',
+ '"arguments": {"city": "杭州"}}',
+ "",
+ ]
+
+ accumulated_parameters = {}
+ for chunk in chunks:
+ result = self.detector.parse_streaming_increment(chunk, self.tools)
+ if result.calls:
+ for call in result.calls:
+ if call.parameters:
+ tool_idx = call.tool_index if call.tool_index is not None else 0
+ if tool_idx not in accumulated_parameters:
+ accumulated_parameters[tool_idx] = ""
+ accumulated_parameters[tool_idx] += call.parameters
+
+ # Verify that Chinese characters are preserved (not escaped as \uXXXX)
+ self.assertGreater(
+ len(accumulated_parameters), 0, "Should have parsed parameters"
+ )
+ final_params_str = accumulated_parameters[0]
+
+ # The parameters string should contain the actual Chinese characters, not escaped Unicode
+ self.assertIn(
+ "杭州", final_params_str, "Should contain actual Chinese characters"
+ )
+ self.assertNotIn(
+ "\\u676d", final_params_str, "Should not contain escaped Unicode sequences"
+ )
+ self.assertNotIn(
+ "\\u5dde", final_params_str, "Should not contain escaped Unicode sequences"
+ )
+
+ # Verify the JSON can be parsed and contains the correct value
+ params = json.loads(final_params_str)
+ self.assertEqual(
+ params["city"], "杭州", "Should correctly parse Chinese city name"
+ )
+
+ def test_chinese_characters_incremental_streaming(self):
+ """Test that Chinese characters work correctly with incremental streaming."""
+ # Test incremental streaming with Chinese characters
+ chunks = [
+ "",
+ '{"name": "get_weather", ',
+ '"arguments": {"city": "',
+ "杭州",
+ '"}}',
+ "",
+ ]
+
+ accumulated_parameters = {}
+ for chunk in chunks:
+ result = self.detector.parse_streaming_increment(chunk, self.tools)
+ if result.calls:
+ for call in result.calls:
+ if call.parameters:
+ tool_idx = call.tool_index if call.tool_index is not None else 0
+ if tool_idx not in accumulated_parameters:
+ accumulated_parameters[tool_idx] = ""
+ accumulated_parameters[tool_idx] += call.parameters
+
+ # Verify Chinese characters are preserved throughout streaming
+ self.assertGreater(
+ len(accumulated_parameters), 0, "Should have parsed parameters"
+ )
+ final_params_str = accumulated_parameters[0]
+
+ # Should contain actual Chinese characters, not escaped
+ self.assertIn(
+ "杭州", final_params_str, "Should contain actual Chinese characters"
+ )
+
+ # Parse and verify
+ params = json.loads(final_params_str)
+ self.assertEqual(
+ params["city"], "杭州", "Should correctly parse Chinese city name"
+ )
+
+ def test_multiple_chinese_parameters(self):
+ """Test multiple tool calls with Chinese parameters."""
+ # Test with multiple tool calls containing Chinese characters
+ chunks = [
+ "",
+ '{"name": "get_weather", "arguments": {"city": "北京"}}, ',
+ '{"name": "get_tourist_attractions", "arguments": {"city": "上海"}}',
+ "",
+ ]
+
+ accumulated_parameters = {}
+ for chunk in chunks:
+ result = self.detector.parse_streaming_increment(chunk, self.tools)
+ if result.calls:
+ for call in result.calls:
+ if call.parameters:
+ tool_idx = call.tool_index if call.tool_index is not None else 0
+ if tool_idx not in accumulated_parameters:
+ accumulated_parameters[tool_idx] = ""
+ accumulated_parameters[tool_idx] += call.parameters
+
+ # Verify both tool calls have correct Chinese characters
+ self.assertGreaterEqual(
+ len(accumulated_parameters), 1, "Should have parsed parameters"
+ )
+
+ # Check first tool call (北京 - Beijing)
+ if 0 in accumulated_parameters:
+ params0 = json.loads(accumulated_parameters[0])
+ self.assertIn(
+ "北京",
+ accumulated_parameters[0],
+ "Should contain actual Chinese characters",
+ )
+ self.assertEqual(
+ params0["city"], "北京", "Should correctly parse first Chinese city"
+ )
+
+ # Check second tool call (上海 - Shanghai) if present
+ if 1 in accumulated_parameters:
+ params1 = json.loads(accumulated_parameters[1])
+ self.assertIn(
+ "上海",
+ accumulated_parameters[1],
+ "Should contain actual Chinese characters",
+ )
+ self.assertEqual(
+ params1["city"], "上海", "Should correctly parse second Chinese city"
+ )
+
class TestLlama32Detector(unittest.TestCase):
def setUp(self):