[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>
Maxwill Lin committed
Jun 23, 2026 at 13:43 UTC
6617db1bfba34fce90e56f8876db1490ca530bc5
6 files changed
+75
-9
tests/tool_parsers/test_hunyuan_a13b_tool_parser.py
+12
@@ -177,3 +177,15 @@ def test_hunyuan_a13b_tool_parser_streaming(model_deltas, expected_tool_calls):
177
reconstructor.tool_calls[idx].id = expected_tool_calls[idx].id
178
179
assert reconstructor.tool_calls == expected_tool_calls
180
+
181
+
182
+def test_hunyuan_a13b_tool_parser_non_ascii():
183
+ mock_tokenizer = MagicMock()
184
+ tool_parser: ToolParser = ToolParserManager.get_tool_parser("hunyuan_a13b")(
185
+ mock_tokenizer
186
+ )
187
+ model_output = '<tool_calls>[{"name": "get_weather", "arguments": {"city": "北京"}}]</tool_calls>'
188
+ _, tool_calls = run_tool_extraction(tool_parser, model_output, streaming=False)
189
+ args = tool_calls[0].function.arguments
190
+ assert "北京" in args
191
+ assert "\\u" not in args
tests/tool_parsers/test_seed_oss_tool_parser.py
+25
@@ -495,3 +495,28 @@ def test_streaming_tool_calls(
495
actual_args = json.loads(arguments_str)
496
expected_args = json.loads(expected_tool.function.arguments)
497
assert actual_args == expected_args
498
+
499
+
500
+def test_streaming_tool_calls_non_ascii(
501
+ seed_oss_tool_parser, seed_oss_tokenizer, sample_tools
502
+):
503
+ request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
504
+ model_output = (
505
+ """<seed:think>\n</seed:cot_budget_reflect>\n</seed:cot_budget_reflect>\n"""
506
+ """The current thinking budget is 0, so I will directly start answering the question.\n</seed:think>\n"""
507
+ """<seed:tool_call>\n<function=get_weather>\n"""
508
+ """<parameter=location>北京</parameter>\n</function>\n</seed:tool_call>"""
509
+ )
510
+
511
+ args = "".join(
512
+ tool_call.function.arguments
513
+ for delta_message in stream_delta_message_generator(
514
+ seed_oss_tool_parser, seed_oss_tokenizer, model_output, request
515
+ )
516
+ if delta_message.tool_calls
517
+ for tool_call in delta_message.tool_calls
518
+ if tool_call.function and tool_call.function.arguments is not None
519
+ )
520
+
521
+ assert "北京" in args
522
+ assert "\\u" not in args
tests/tool_parsers/test_xlam_tool_parser.py
+23
@@ -532,3 +532,26 @@ def test_extract_tool_calls_streaming_incremental(
532
parsed_args = json.loads(full_args)
533
expected_args = json.loads(expected_first_tool.function.arguments)
534
assert parsed_args == expected_args
535
+
536
+
537
+@pytest.mark.parametrize("streaming", [False, True])
538
+def test_extract_tool_calls_non_ascii(xlam_tool_parser, xlam_tokenizer, streaming):
539
+ # Use parallel tool calls so the streaming path re-serializes arguments
540
+ # (the ensure_ascii fix only runs when tool_count > 1).
541
+ model_output = """[{"name": "get_current_weather", "arguments": {"city": "北京"}}, {"name": "get_current_weather", "arguments": {"city": "上海"}}]""" # noqa: E501
542
+
543
+ if streaming:
544
+ request = ChatCompletionRequest(model=MODEL, messages=[])
545
+ args = "".join(
546
+ delta.tool_calls[0].function.arguments
547
+ for delta in stream_delta_message_generator(
548
+ xlam_tool_parser, xlam_tokenizer, model_output, request
549
+ )
550
+ if delta.tool_calls and delta.tool_calls[0].function.arguments
551
+ )
552
+ else:
553
+ extracted = xlam_tool_parser.extract_tool_calls(model_output, request=None) # type: ignore[arg-type]
554
+ args = "".join(tc.function.arguments for tc in extracted.tool_calls)
555
+
556
+ assert "北京" in args
557
+ assert "\\u" not in args
vllm/tool_parsers/hunyuan_a13b_tool_parser.py
+1
-1
@@ -144,7 +144,7 @@ class HunyuanA13BToolParser(ToolParser):
144
function=FunctionCall(
145
name=call["name"],
146
arguments=(
147
- json.dumps(call["arguments"])
147
+ json.dumps(call["arguments"], ensure_ascii=False)
148
if isinstance(call["arguments"], dict)
149
else call["arguments"]
150
),
vllm/tool_parsers/seed_oss_tool_parser.py
+10
-6
@@ -528,7 +528,7 @@ class SeedOssToolParser(ToolParser):
528
'"'
529
+ self.current_param_name
530
+ '": "'
531
- + json.dumps(param_value)[1:-1]
531
+ + json.dumps(param_value, ensure_ascii=False)[1:-1]
532
+ '"'
533
)
534
else:
@@ -536,7 +536,7 @@ class SeedOssToolParser(ToolParser):
536
', "'
537
+ self.current_param_name
538
+ '": "'
539
- + json.dumps(param_value)[1:-1]
539
+ + json.dumps(param_value, ensure_ascii=False)[1:-1]
540
+ '"'
541
)
542
@@ -571,11 +571,11 @@ class SeedOssToolParser(ToolParser):
571
# Calculate incremental JSON
572
full_value = self.current_param_value + value_chunk
573
prev_escaped = (
574
- json.dumps(self.current_param_value)[1:-1]
574
+ json.dumps(self.current_param_value, ensure_ascii=False)[1:-1]
575
if self.current_param_value
576
else ""
577
)
578
- full_escaped = json.dumps(full_value)[1:-1]
578
+ full_escaped = json.dumps(full_value, ensure_ascii=False)[1:-1]
579
delta_escaped = full_escaped[len(prev_escaped) :]
580
581
self.in_param = False
@@ -606,12 +606,16 @@ class SeedOssToolParser(ToolParser):
606
if value_chunk:
607
# Stream the escaped delta
608
prev_escaped = (
609
- json.dumps(self.current_param_value)[1:-1]
609
+ json.dumps(self.current_param_value, ensure_ascii=False)[
610
+ 1:-1
611
+ ]
612
if self.current_param_value
613
else ""
614
)
615
self.current_param_value += value_chunk
614
- full_escaped = json.dumps(self.current_param_value)[1:-1]
616
+ full_escaped = json.dumps(
617
+ self.current_param_value, ensure_ascii=False
618
+ )[1:-1]
619
delta_escaped = full_escaped[len(prev_escaped) :]
620
621
if delta_escaped:
vllm/tool_parsers/xlam_tool_parser.py
+4
-2
@@ -165,7 +165,7 @@ class xLAMToolParser(ToolParser):
165
function=FunctionCall(
166
name=call["name"],
167
arguments=(
168
- json.dumps(call["arguments"])
168
+ json.dumps(call["arguments"], ensure_ascii=False)
169
if isinstance(call["arguments"], dict)
170
else call["arguments"]
171
),
@@ -473,7 +473,9 @@ class xLAMToolParser(ToolParser):
473
):
474
current_tool = parsed_tools[current_idx]
475
if isinstance(current_tool.get("arguments"), dict):
476
- args_text = json.dumps(current_tool["arguments"])
476
+ args_text = json.dumps(
477
+ current_tool["arguments"], ensure_ascii=False
478
+ )
479
else:
480
args_text = str(current_tool.get("arguments", "{}"))
481
except (json.JSONDecodeError, KeyError, IndexError):