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):