Skip to content

Commit b012e1a

Browse files
authored
Add LongCat Flash tool parser (ml-explore#810)
* Add LongCat Flash tool parser * Add unit tests for both xml and json formats
1 parent f53a9b0 commit b012e1a

3 files changed

Lines changed: 89 additions & 2 deletions

File tree

mlx_lm/tokenizer_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ def __init__(
291291
self._tool_call_end = tool_call_end
292292

293293
vocab = tokenizer.get_vocab()
294-
THINK_TOKENS = [("<think>", "</think>")]
294+
THINK_TOKENS = [
295+
("<think>", "</think>"),
296+
("<longcat_think>", "</longcat_think>"),
297+
]
295298
for think_start, think_end in THINK_TOKENS:
296299
if think_start in vocab and think_end in vocab:
297300
self._think_start = think_start
@@ -472,6 +475,8 @@ def _infer_tool_parser(chat_template):
472475
return "minimax_m2"
473476
elif "<start_function_call>" in chat_template:
474477
return "function_gemma"
478+
elif "<longcat_tool_call>" in chat_template:
479+
return "longcat"
475480
elif "<arg_key>" in chat_template:
476481
return "glm47"
477482
elif "<tool_call>\n<function=" in chat_template:

mlx_lm/tool_parsers/longcat.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright © 2026 Apple Inc.
2+
3+
import ast
4+
import json
5+
from typing import Any
6+
7+
import regex as re
8+
9+
_func_name_regex = re.compile(r"^(.*?)<longcat_arg_key>", re.DOTALL)
10+
_func_arg_regex = re.compile(
11+
r"<longcat_arg_key>(.*?)</longcat_arg_key>(?:\\n|\s)*<longcat_arg_value>(.*?)</longcat_arg_value>",
12+
re.DOTALL,
13+
)
14+
15+
tool_call_start = "<longcat_tool_call>"
16+
tool_call_end = "</longcat_tool_call>"
17+
18+
19+
def _is_string_type(
20+
tool_name: str,
21+
arg_name: str,
22+
tools: list[Any] | None,
23+
) -> bool:
24+
if tools is None:
25+
return False
26+
for tool in tools:
27+
func = tool["function"]
28+
if func["name"] == tool_name:
29+
params = func["parameters"]
30+
if params is None:
31+
return False
32+
arg_type = params.get("properties", {}).get(arg_name, {}).get("type", None)
33+
return arg_type == "string"
34+
return False
35+
36+
37+
def _deserialize(value: str) -> Any:
38+
try:
39+
return json.loads(value)
40+
except Exception:
41+
pass
42+
43+
try:
44+
return ast.literal_eval(value)
45+
except Exception:
46+
pass
47+
return value
48+
49+
50+
def parse_tool_call(text: str, tools: list[Any] | None = None):
51+
text = text.strip()
52+
53+
if text.startswith("{"):
54+
try:
55+
return json.loads(text)
56+
except json.JSONDecodeError:
57+
pass
58+
59+
func_name = _func_name_regex.search(text).group(1).strip()
60+
pairs = _func_arg_regex.findall(text)
61+
arg_dct = {}
62+
for key, value in pairs:
63+
arg_key = key.strip()
64+
arg_val = value.strip()
65+
if not _is_string_type(func_name, arg_key, tools):
66+
arg_val = _deserialize(arg_val)
67+
arg_dct[arg_key] = arg_val
68+
return dict(name=func_name, arguments=arg_dct)

tests/test_tool_parsing.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
glm47,
77
json_tools,
88
kimi_k2,
9+
longcat,
910
minimax_m2,
1011
qwen3_coder,
1112
)
@@ -14,12 +15,23 @@
1415
class TestToolParsing(unittest.TestCase):
1516

1617
def test_parsers(self):
17-
parsers = [function_gemma, glm47, json_tools, kimi_k2, minimax_m2, qwen3_coder]
18+
parsers = [
19+
function_gemma,
20+
glm47,
21+
json_tools,
22+
longcat,
23+
longcat,
24+
kimi_k2,
25+
minimax_m2,
26+
qwen3_coder,
27+
]
1828

1929
test_cases = [
2030
"call:multiply{a:12234585,b:48838483920}",
2131
"multiply<arg_key>a</arg_key><arg_value>12234585</arg_value><arg_key>b</arg_key><arg_value>48838483920</arg_value>",
2232
'{"name": "multiply", "arguments": {"a": 12234585, "b": 48838483920}}',
33+
"multiply<longcat_arg_key>a</longcat_arg_key>\n<longcat_arg_value>12234585</longcat_arg_value>\n<longcat_arg_key>b</longcat_arg_key>\n<longcat_arg_value>48838483920</longcat_arg_value>",
34+
'{"name": "multiply", "arguments": {"a": 12234585, "b": 48838483920}}',
2335
'<|tool_call_begin|>functions.multiply:0<|tool_call_argument_begin|>{"a": 12234585, "b": 48838483920}<|tool_call_end|>',
2436
'<invoke name="multiply">\n<parameter name="a">12234585</parameter>\n<parameter name="b">48838483920</parameter>\n</invoke>',
2537
"<function=multiply>\n<parameter=a>\n12234585\n</parameter>\n<parameter=b>\n48838483920\n</parameter>\n</function>",
@@ -55,6 +67,8 @@ def test_parsers(self):
5567
"call:get_current_temperature{location:<escape>London<escape>}",
5668
'get_current_temperature<arg_key>location</arg_key><arg_value>"London"</arg_value>',
5769
'{"name": "get_current_temperature", "arguments": {"location": "London"}}',
70+
"get_current_temperature<longcat_arg_key>location</longcat_arg_key>\n<longcat_arg_value>London</longcat_arg_value>",
71+
'{"name": "get_current_temperature", "arguments": {"location": "London"}}',
5872
'<|tool_call_begin|>functions.get_current_temperature:0<|tool_call_argument_begin|>{"location": "London"}<|tool_call_end|>',
5973
'<invoke name="get_current_temperature">\n<parameter name="location">London</parameter>\n</invoke>',
6074
"<function=get_current_temperature>\n<parameter=location>\nLondon\n</parameter>\n</function>",

0 commit comments

Comments
 (0)