From 6617db1bfba34fce90e56f8876db1490ca530bc5 Mon Sep 17 00:00:00 2001
From: Maxwill Lin <0312fs3@gmail.com>
Date: Tue, 23 Jun 2026 13:43:11 -0700
Subject: [PATCH] [Bugfix][Frontend] Emit non-ASCII tool-call arguments without
\uXXXX escapes (#46308)
Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com>
Co-authored-by: EazyReal <8047065+EazyReal@users.noreply.github.com>
---
.../test_hunyuan_a13b_tool_parser.py | 12 +++++++++
.../tool_parsers/test_seed_oss_tool_parser.py | 25 +++++++++++++++++++
tests/tool_parsers/test_xlam_tool_parser.py | 23 +++++++++++++++++
vllm/tool_parsers/hunyuan_a13b_tool_parser.py | 2 +-
vllm/tool_parsers/seed_oss_tool_parser.py | 16 +++++++-----
vllm/tool_parsers/xlam_tool_parser.py | 6 +++--
6 files changed, 75 insertions(+), 9 deletions(-)
diff --git a/tests/tool_parsers/test_hunyuan_a13b_tool_parser.py b/tests/tool_parsers/test_hunyuan_a13b_tool_parser.py
index 90f08bb82e..167ad66889 100644
--- a/tests/tool_parsers/test_hunyuan_a13b_tool_parser.py
+++ b/tests/tool_parsers/test_hunyuan_a13b_tool_parser.py
@@ -177,3 +177,15 @@ def test_hunyuan_a13b_tool_parser_streaming(model_deltas, expected_tool_calls):
reconstructor.tool_calls[idx].id = expected_tool_calls[idx].id
assert reconstructor.tool_calls == expected_tool_calls
+
+
+def test_hunyuan_a13b_tool_parser_non_ascii():
+ mock_tokenizer = MagicMock()
+ tool_parser: ToolParser = ToolParserManager.get_tool_parser("hunyuan_a13b")(
+ mock_tokenizer
+ )
+ model_output = '[{"name": "get_weather", "arguments": {"city": "北京"}}]'
+ _, tool_calls = run_tool_extraction(tool_parser, model_output, streaming=False)
+ args = tool_calls[0].function.arguments
+ assert "北京" in args
+ assert "\\u" not in args
diff --git a/tests/tool_parsers/test_seed_oss_tool_parser.py b/tests/tool_parsers/test_seed_oss_tool_parser.py
index 9dd13afe01..4ff96fb01b 100644
--- a/tests/tool_parsers/test_seed_oss_tool_parser.py
+++ b/tests/tool_parsers/test_seed_oss_tool_parser.py
@@ -495,3 +495,28 @@ def test_streaming_tool_calls(
actual_args = json.loads(arguments_str)
expected_args = json.loads(expected_tool.function.arguments)
assert actual_args == expected_args
+
+
+def test_streaming_tool_calls_non_ascii(
+ seed_oss_tool_parser, seed_oss_tokenizer, sample_tools
+):
+ request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
+ model_output = (
+ """\n\n\n"""
+ """The current thinking budget is 0, so I will directly start answering the question.\n\n"""
+ """\n\n"""
+ """北京\n\n"""
+ )
+
+ args = "".join(
+ tool_call.function.arguments
+ for delta_message in stream_delta_message_generator(
+ seed_oss_tool_parser, seed_oss_tokenizer, model_output, request
+ )
+ if delta_message.tool_calls
+ for tool_call in delta_message.tool_calls
+ if tool_call.function and tool_call.function.arguments is not None
+ )
+
+ assert "北京" in args
+ assert "\\u" not in args
diff --git a/tests/tool_parsers/test_xlam_tool_parser.py b/tests/tool_parsers/test_xlam_tool_parser.py
index 3853d2039a..5d39f0b575 100644
--- a/tests/tool_parsers/test_xlam_tool_parser.py
+++ b/tests/tool_parsers/test_xlam_tool_parser.py
@@ -532,3 +532,26 @@ def test_extract_tool_calls_streaming_incremental(
parsed_args = json.loads(full_args)
expected_args = json.loads(expected_first_tool.function.arguments)
assert parsed_args == expected_args
+
+
+@pytest.mark.parametrize("streaming", [False, True])
+def test_extract_tool_calls_non_ascii(xlam_tool_parser, xlam_tokenizer, streaming):
+ # Use parallel tool calls so the streaming path re-serializes arguments
+ # (the ensure_ascii fix only runs when tool_count > 1).
+ model_output = """[{"name": "get_current_weather", "arguments": {"city": "北京"}}, {"name": "get_current_weather", "arguments": {"city": "上海"}}]""" # noqa: E501
+
+ if streaming:
+ request = ChatCompletionRequest(model=MODEL, messages=[])
+ args = "".join(
+ delta.tool_calls[0].function.arguments
+ for delta in stream_delta_message_generator(
+ xlam_tool_parser, xlam_tokenizer, model_output, request
+ )
+ if delta.tool_calls and delta.tool_calls[0].function.arguments
+ )
+ else:
+ extracted = xlam_tool_parser.extract_tool_calls(model_output, request=None) # type: ignore[arg-type]
+ args = "".join(tc.function.arguments for tc in extracted.tool_calls)
+
+ assert "北京" in args
+ assert "\\u" not in args
diff --git a/vllm/tool_parsers/hunyuan_a13b_tool_parser.py b/vllm/tool_parsers/hunyuan_a13b_tool_parser.py
index 9723ef45d2..f5cd9f85a0 100644
--- a/vllm/tool_parsers/hunyuan_a13b_tool_parser.py
+++ b/vllm/tool_parsers/hunyuan_a13b_tool_parser.py
@@ -144,7 +144,7 @@ class HunyuanA13BToolParser(ToolParser):
function=FunctionCall(
name=call["name"],
arguments=(
- json.dumps(call["arguments"])
+ json.dumps(call["arguments"], ensure_ascii=False)
if isinstance(call["arguments"], dict)
else call["arguments"]
),
diff --git a/vllm/tool_parsers/seed_oss_tool_parser.py b/vllm/tool_parsers/seed_oss_tool_parser.py
index a90bdc76d9..42e4ba7769 100644
--- a/vllm/tool_parsers/seed_oss_tool_parser.py
+++ b/vllm/tool_parsers/seed_oss_tool_parser.py
@@ -528,7 +528,7 @@ class SeedOssToolParser(ToolParser):
'"'
+ self.current_param_name
+ '": "'
- + json.dumps(param_value)[1:-1]
+ + json.dumps(param_value, ensure_ascii=False)[1:-1]
+ '"'
)
else:
@@ -536,7 +536,7 @@ class SeedOssToolParser(ToolParser):
', "'
+ self.current_param_name
+ '": "'
- + json.dumps(param_value)[1:-1]
+ + json.dumps(param_value, ensure_ascii=False)[1:-1]
+ '"'
)
@@ -571,11 +571,11 @@ class SeedOssToolParser(ToolParser):
# Calculate incremental JSON
full_value = self.current_param_value + value_chunk
prev_escaped = (
- json.dumps(self.current_param_value)[1:-1]
+ json.dumps(self.current_param_value, ensure_ascii=False)[1:-1]
if self.current_param_value
else ""
)
- full_escaped = json.dumps(full_value)[1:-1]
+ full_escaped = json.dumps(full_value, ensure_ascii=False)[1:-1]
delta_escaped = full_escaped[len(prev_escaped) :]
self.in_param = False
@@ -606,12 +606,16 @@ class SeedOssToolParser(ToolParser):
if value_chunk:
# Stream the escaped delta
prev_escaped = (
- json.dumps(self.current_param_value)[1:-1]
+ json.dumps(self.current_param_value, ensure_ascii=False)[
+ 1:-1
+ ]
if self.current_param_value
else ""
)
self.current_param_value += value_chunk
- full_escaped = json.dumps(self.current_param_value)[1:-1]
+ full_escaped = json.dumps(
+ self.current_param_value, ensure_ascii=False
+ )[1:-1]
delta_escaped = full_escaped[len(prev_escaped) :]
if delta_escaped:
diff --git a/vllm/tool_parsers/xlam_tool_parser.py b/vllm/tool_parsers/xlam_tool_parser.py
index 61eaaf952b..d004e83352 100644
--- a/vllm/tool_parsers/xlam_tool_parser.py
+++ b/vllm/tool_parsers/xlam_tool_parser.py
@@ -165,7 +165,7 @@ class xLAMToolParser(ToolParser):
function=FunctionCall(
name=call["name"],
arguments=(
- json.dumps(call["arguments"])
+ json.dumps(call["arguments"], ensure_ascii=False)
if isinstance(call["arguments"], dict)
else call["arguments"]
),
@@ -473,7 +473,9 @@ class xLAMToolParser(ToolParser):
):
current_tool = parsed_tools[current_idx]
if isinstance(current_tool.get("arguments"), dict):
- args_text = json.dumps(current_tool["arguments"])
+ args_text = json.dumps(
+ current_tool["arguments"], ensure_ascii=False
+ )
else:
args_text = str(current_tool.get("arguments", "{}"))
except (json.JSONDecodeError, KeyError, IndexError):