Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
OpenDAS
dynamo
Commits
11d9cdfb
Unverified
Commit
11d9cdfb
authored
Jan 26, 2026
by
Ayush Agarwal
Committed by
GitHub
Jan 26, 2026
Browse files
feat: MiniMax tool parser (#5549)
Signed-off-by:
ayushag
<
ayushag@nvidia.com
>
parent
22fbc022
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
207 additions
and
2 deletions
+207
-2
lib/parsers/src/tool_calling/config.rs
lib/parsers/src/tool_calling/config.rs
+20
-0
lib/parsers/src/tool_calling/parsers.rs
lib/parsers/src/tool_calling/parsers.rs
+171
-0
lib/parsers/src/tool_calling/xml/parser.rs
lib/parsers/src/tool_calling/xml/parser.rs
+16
-2
No files found.
lib/parsers/src/tool_calling/config.rs
View file @
11d9cdfb
...
...
@@ -294,4 +294,24 @@ impl ToolCallConfig {
parser_config
:
ParserConfig
::
Dsml
(
DsmlParserConfig
::
default
()),
}
}
pub
fn
minimax_m2
()
->
Self
{
// MiniMax-M2.1 format:
// <minimax:tool_call>
// <invoke name="function_name">
// <parameter name="param_name">value</parameter>
// </invoke>
// </minimax:tool_call>
// Reference: https://huggingface.co/MiniMaxAI/MiniMax-M2.1/blob/main/docs/tool_calling_guide.md
Self
{
parser_config
:
ParserConfig
::
Xml
(
XmlParserConfig
{
tool_call_start_token
:
"<minimax:tool_call>"
.to_string
(),
tool_call_end_token
:
"</minimax:tool_call>"
.to_string
(),
function_start_token
:
"<invoke name="
.to_string
(),
function_end_token
:
"</invoke>"
.to_string
(),
parameter_start_token
:
"<parameter name="
.to_string
(),
parameter_end_token
:
"</parameter>"
.to_string
(),
}),
}
}
}
lib/parsers/src/tool_calling/parsers.rs
View file @
11d9cdfb
...
...
@@ -42,6 +42,7 @@ pub fn get_tool_parser_map() -> &'static HashMap<&'static str, ToolCallConfig> {
map
.insert
(
"deepseek_v3_2"
,
ToolCallConfig
::
deepseek_v3_2
());
map
.insert
(
"qwen3_coder"
,
ToolCallConfig
::
qwen3_coder
());
map
.insert
(
"jamba"
,
ToolCallConfig
::
jamba
());
map
.insert
(
"minimax_m2"
,
ToolCallConfig
::
minimax_m2
());
map
.insert
(
"default"
,
ToolCallConfig
::
default
());
map
.insert
(
"nemotron_nano"
,
ToolCallConfig
::
qwen3_coder
());
// nemotron nano follows qwen3_coder format
map
...
...
@@ -209,6 +210,7 @@ mod tests {
"qwen3_coder"
,
"jamba"
,
"nemotron_nano"
,
"minimax_m2"
,
];
for
parser
in
available_parsers
{
assert
!
(
parsers
.contains
(
&
parser
));
...
...
@@ -2972,4 +2974,173 @@ weather forecasting
assert
!
(
args
[
"items"
]
.is_array
());
assert_eq!
(
args
[
"items"
],
serde_json
::
json!
([
1
,
2
,
3
,
4
,
5
]));
}
// MiniMax-M2.1 parser tests
#[tokio::test]
async
fn
test_minimax_m2_simple_tool_call
()
{
let
input
=
r#"<minimax:tool_call>
<invoke name="get_weather">
<parameter name="location">San Francisco</parameter>
<parameter name="unit">celsius</parameter>
</invoke>
</minimax:tool_call>"#
;
let
(
result
,
content
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
None
)
.await
.unwrap
();
assert_eq!
(
content
,
Some
(
""
.to_string
()));
assert_eq!
(
result
.len
(),
1
);
let
(
name
,
args
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name
,
"get_weather"
);
assert_eq!
(
args
[
"location"
],
"San Francisco"
);
assert_eq!
(
args
[
"unit"
],
"celsius"
);
}
#[tokio::test]
async
fn
test_minimax_m2_multiple_tool_calls
()
{
let
input
=
r#"<minimax:tool_call>
<invoke name="search_web">
<parameter name="query_tag">["technology", "events"]</parameter>
<parameter name="query_list">["OpenAI", "latest", "release"]</parameter>
</invoke>
<invoke name="search_web">
<parameter name="query_tag">["technology", "events"]</parameter>
<parameter name="query_list">["Gemini", "latest", "release"]</parameter>
</invoke>
</minimax:tool_call>"#
;
let
tools
=
vec!
[
ToolDefinition
{
name
:
"search_web"
.to_string
(),
parameters
:
Some
(
serde_json
::
json!
({
"properties"
:
{
"query_tag"
:
{
"type"
:
"array"
},
"query_list"
:
{
"type"
:
"array"
}
}
})),
}];
let
(
result
,
_
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
Some
(
&
tools
))
.await
.unwrap
();
assert_eq!
(
result
.len
(),
2
);
// First call
let
(
name1
,
args1
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name1
,
"search_web"
);
assert
!
(
args1
[
"query_tag"
]
.is_array
());
assert_eq!
(
args1
[
"query_tag"
],
serde_json
::
json!
([
"technology"
,
"events"
])
);
assert
!
(
args1
[
"query_list"
]
.is_array
());
assert_eq!
(
args1
[
"query_list"
],
serde_json
::
json!
([
"OpenAI"
,
"latest"
,
"release"
])
);
// Second call
let
(
name2
,
args2
)
=
extract_name_and_args
(
result
[
1
]
.clone
());
assert_eq!
(
name2
,
"search_web"
);
assert
!
(
args2
[
"query_tag"
]
.is_array
());
assert_eq!
(
args2
[
"query_tag"
],
serde_json
::
json!
([
"technology"
,
"events"
])
);
assert
!
(
args2
[
"query_list"
]
.is_array
());
assert_eq!
(
args2
[
"query_list"
],
serde_json
::
json!
([
"Gemini"
,
"latest"
,
"release"
])
);
}
#[tokio::test]
async
fn
test_minimax_m2_with_normal_text
()
{
let
input
=
r#"I'll help you check the weather. <minimax:tool_call>
<invoke name="get_weather">
<parameter name="location">Tokyo</parameter>
<parameter name="unit">fahrenheit</parameter>
</invoke>
</minimax:tool_call> Let me get that information for you."#
;
let
(
result
,
content
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
None
)
.await
.unwrap
();
assert
!
(
content
.is_some
());
assert
!
(
content
.unwrap
()
.contains
(
"I'll help you check the weather."
)
);
assert_eq!
(
result
.len
(),
1
);
let
(
name
,
args
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name
,
"get_weather"
);
assert_eq!
(
args
[
"location"
],
"Tokyo"
);
assert_eq!
(
args
[
"unit"
],
"fahrenheit"
);
}
#[tokio::test]
async
fn
test_minimax_m2_empty_parameters
()
{
let
input
=
r#"<minimax:tool_call>
<invoke name="get_time">
</invoke>
</minimax:tool_call>"#
;
let
(
result
,
_
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
None
)
.await
.unwrap
();
assert_eq!
(
result
.len
(),
1
);
let
(
name
,
args
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name
,
"get_time"
);
assert_eq!
(
args
,
serde_json
::
json!
({}));
}
#[tokio::test]
async
fn
test_minimax_m2_with_type_conversion
()
{
let
input
=
r#"<minimax:tool_call>
<invoke name="process_data">
<parameter name="count">42</parameter>
<parameter name="temperature">98.6</parameter>
<parameter name="enabled">true</parameter>
</invoke>
</minimax:tool_call>"#
;
let
tools
=
vec!
[
ToolDefinition
{
name
:
"process_data"
.to_string
(),
parameters
:
Some
(
serde_json
::
json!
({
"properties"
:
{
"count"
:
{
"type"
:
"integer"
},
"temperature"
:
{
"type"
:
"number"
},
"enabled"
:
{
"type"
:
"boolean"
}
}
})),
}];
let
(
result
,
_
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
Some
(
&
tools
))
.await
.unwrap
();
assert_eq!
(
result
.len
(),
1
);
let
(
name
,
args
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name
,
"process_data"
);
assert_eq!
(
args
[
"count"
],
42
);
assert_eq!
(
args
[
"temperature"
],
98.6
);
assert_eq!
(
args
[
"enabled"
],
true
);
}
#[tokio::test]
async
fn
test_minimax_m2_array_parameter
()
{
let
input
=
r#"<minimax:tool_call>
<invoke name="batch_process">
<parameter name="items">[1, 2, 3, 4, 5]</parameter>
</invoke>
</minimax:tool_call>"#
;
let
tools
=
vec!
[
ToolDefinition
{
name
:
"batch_process"
.to_string
(),
parameters
:
Some
(
serde_json
::
json!
({
"properties"
:
{
"items"
:
{
"type"
:
"array"
}
}
})),
}];
let
(
result
,
_
)
=
detect_and_parse_tool_call
(
input
,
Some
(
"minimax_m2"
),
Some
(
&
tools
))
.await
.unwrap
();
assert_eq!
(
result
.len
(),
1
);
let
(
name
,
args
)
=
extract_name_and_args
(
result
[
0
]
.clone
());
assert_eq!
(
name
,
"batch_process"
);
assert
!
(
args
[
"items"
]
.is_array
());
assert_eq!
(
args
[
"items"
],
serde_json
::
json!
([
1
,
2
,
3
,
4
,
5
]));
}
}
lib/parsers/src/tool_calling/xml/parser.rs
View file @
11d9cdfb
...
...
@@ -14,6 +14,18 @@ use super::super::ToolDefinition;
use
super
::
super
::
config
::
XmlParserConfig
;
use
super
::
response
::{
CalledFunction
,
ToolCallResponse
,
ToolCallType
};
/// Strip surrounding quotes from a string if present
fn
strip_quotes
(
s
:
&
str
)
->
&
str
{
let
trimmed
=
s
.trim
();
if
(
trimmed
.starts_with
(
'"'
)
&&
trimmed
.ends_with
(
'"'
))
||
(
trimmed
.starts_with
(
'\''
)
&&
trimmed
.ends_with
(
'\''
))
{
&
trimmed
[
1
..
trimmed
.len
()
-
1
]
}
else
{
trimmed
}
}
/// Check if a chunk contains the start of a xml-style tool call.
/// Format: <tool_call><function=name><parameter=foo>...</parameter></function></tool_call>
pub
fn
detect_tool_call_start_xml
(
chunk
:
&
str
,
config
:
&
XmlParserConfig
)
->
bool
{
...
...
@@ -140,7 +152,8 @@ fn parse_tool_call_block(
// Find all function blocks.
for
func_cap
in
function_regex
.captures_iter
(
block
)
{
let
function_name
=
func_cap
.get
(
1
)
.map
(|
m
|
m
.as_str
()
.trim
())
.unwrap_or
(
""
);
let
function_name_raw
=
func_cap
.get
(
1
)
.map
(|
m
|
m
.as_str
()
.trim
())
.unwrap_or
(
""
);
let
function_name
=
strip_quotes
(
function_name_raw
);
let
function_body
=
func_cap
.get
(
2
)
.map
(|
m
|
m
.as_str
())
.unwrap_or
(
""
);
if
function_name
.is_empty
()
{
...
...
@@ -154,7 +167,8 @@ fn parse_tool_call_block(
let
mut
parameters
:
HashMap
<
String
,
serde_json
::
Value
>
=
HashMap
::
new
();
for
param_cap
in
parameter_regex
.captures_iter
(
function_body
)
{
let
param_name
=
param_cap
.get
(
1
)
.map
(|
m
|
m
.as_str
()
.trim
())
.unwrap_or
(
""
);
let
param_name_raw
=
param_cap
.get
(
1
)
.map
(|
m
|
m
.as_str
()
.trim
())
.unwrap_or
(
""
);
let
param_name
=
strip_quotes
(
param_name_raw
);
let
param_value
=
param_cap
.get
(
2
)
.map
(|
m
|
m
.as_str
())
.unwrap_or
(
""
);
if
!
param_name
.is_empty
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment