[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>
This commit is contained in:
Maxwill Lin
2026-06-23 20:43:11 +00:00
committed by GitHub
co-authored by EazyReal
parent 899d72a58c
commit 6617db1bfb
6 changed files with 75 additions and 9 deletions
@@ -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 = '<tool_calls>[{"name": "get_weather", "arguments": {"city": "北京"}}]</tool_calls>'
_, 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
@@ -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 = (
"""<seed:think>\n</seed:cot_budget_reflect>\n</seed:cot_budget_reflect>\n"""
"""The current thinking budget is 0, so I will directly start answering the question.\n</seed:think>\n"""
"""<seed:tool_call>\n<function=get_weather>\n"""
"""<parameter=location>北京</parameter>\n</function>\n</seed:tool_call>"""
)
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
@@ -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
@@ -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"]
),
+10 -6
View File
@@ -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:
+4 -2
View File
@@ -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):