Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions common/chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2041,6 +2041,130 @@ static void func_args_not_string(json & messages) {

}

// MiniMax M2 format: uses <minimax:tool_call>...<invoke name="tool_name"><parameter name="key">value</parameter>...</invoke>...</minimax:tool_call>
// - Reasoning: <think>{reasoning}</think> (optional)
static common_chat_params common_chat_params_init_minimax(const common_chat_template & tmpl,
const autoparser::generation_params & inputs) {
common_chat_params data;

data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
data.supports_thinking = true;
data.thinking_start_tag = "<think>";
data.thinking_end_tag = "</think>";
data.preserved_tokens = {
"<minimax:tool_call>",
"</minimax:tool_call>",
"<think>",
"</think>",
};

auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;

const std::string TC_START = "<minimax:tool_call>";
const std::string TC_END = "</minimax:tool_call>";
const std::string THINK_START = "<think>";
const std::string THINK_END = "</think>";

auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
auto end = p.end();

// Reasoning extraction (same pattern as Kimi K2)
auto reasoning = extract_reasoning ? p.optional(THINK_START + p.reasoning(
p.until_one_of({ THINK_END, TC_START })) +
p.optional(p.literal(THINK_END))) : p.eps();
auto generation_prompt = p.prefix(inputs.generation_prompt, THINK_START);

// Content only parser (no tools)
if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
return generation_prompt + reasoning + p.content(p.rest()) + end;
}

// Build tool call parsers for each available function
// MiniMax format: <invoke name="tool_name"><parameter name="key">value</parameter>...</invoke>
auto tool_choice = p.choice();
foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");
std::string name = function.at("name");
const auto & params = function.at("parameters");


// Build parameter parsers
auto arg_choice = p.choice();
if (params.contains("properties") && !params["properties"].empty()) {
for (const auto & el : params["properties"].items()) {
const std::string & prop_name = el.key();
const auto & prop_def = el.value();
bool is_string_type = (prop_def.contains("type") && prop_def["type"] == "string");

// <parameter name="prop_name">value</parameter>
auto arg_rule = p.tool_arg(
p.tool_arg_open(p.literal("<parameter name=\"")) +
p.tool_arg_name(p.literal(prop_name)) +
p.literal("\">") +
(is_string_type
? p.tool_arg_string_value(p.until("</parameter>"))
: p.tool_arg_value(p.until("</parameter>"))) +
p.tool_arg_close(p.literal("</parameter>"))
);
arg_choice |= arg_rule;
}
}
auto args = p.zero_or_more(p.space() + arg_choice);

// <invoke name="tool_name">...params...</invoke>
auto tool_parser = p.tool(
p.tool_open(
p.literal("<invoke name=\"") +
p.tool_name(p.literal(name)) +
p.literal("\">")
) +
p.tool_args(args) +
p.space() +
p.tool_close(p.literal("</invoke>"))
);

tool_choice |= p.rule("tool-" + name, tool_parser);
});

// Tool calls section: <minimax:tool_call>\n...tool calls...\n</minimax:tool_call>
auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0;
auto max_calls = inputs.parallel_tool_calls ? -1 : 1;
auto tool_calls = p.rule("tool-calls",
p.trigger_rule("tool-call",
p.literal(TC_START) + p.space() +
p.repeat(tool_choice + p.space(), min_calls, max_calls) +
p.optional(p.literal(TC_END)))
);

auto content_before_tools = p.content(p.until_one_of({ TC_START }));

return generation_prompt + reasoning + content_before_tools + tool_calls + end;
});

data.parser = parser.save();

if (include_grammar) {
data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");
auto schema = function.at("parameters");
builder.resolve_refs(schema);
});
parser.build_grammar(builder, data.grammar_lazy);
});

data.grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "<minimax:tool_call>" }
};
}

return data;
}

static json common_chat_extra_context() {
json ctx = json::object();
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
Expand Down Expand Up @@ -2116,6 +2240,14 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
return common_chat_params_init_deepseek_v3_2(tmpl, params);
}

// MiniMax M2 format detection
if (src.find("<minimax:tool_call>") != std::string::npos &&
src.find("<invoke name=") != std::string::npos &&
src.find("<parameter name=") != std::string::npos) {
LOG_DBG("Using specialized template: MiniMax M2\n");
return common_chat_params_init_minimax(tmpl, params);
}

// Gemma4 format detection
if (src.find("'<|tool_call>call:'") != std::string::npos) {
if (src.find("{#- OpenAI Chat Completions:") == std::string::npos) {
Expand Down
68 changes: 68 additions & 0 deletions tests/test-chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3448,6 +3448,74 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
.expect(message_assist_call)
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.run();

// Parallel tool calls (two different tools) inside one wrapper
tst.test(
"</think><minimax:tool_call>\n"
"<invoke name=\"special_function\">\n<parameter name=\"arg1\">1</parameter>\n</invoke>\n"
"<invoke name=\"python\">\n<parameter name=\"code\">print('hey')</parameter>\n</invoke>\n"
"</minimax:tool_call>")
.tools({ special_function_tool, python_tool })
.parallel_tool_calls(true)
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_with_reasoning_content_and_multiple_tool_calls(
"", "",
{
{ "special_function", R"x({"arg1": 1})x" },
{ "python", R"x({"code": "print('hey')"})x" },
}))
.run();

// String parameter with embedded XML-ish content
tst.test(
"</think><minimax:tool_call>\n<invoke name=\"html\">\n"
"<parameter name=\"markup\"><div><script>alert('x')</script></div></parameter>\n"
"</invoke>\n</minimax:tool_call>")
.tools({ html_tool })
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_with_tool_calls(
"html", R"x({"markup": "<div><script>alert('x')</script></div>"})x"))
.run();

// Multi-line string parameter value
tst.test(
"</think><minimax:tool_call>\n<invoke name=\"python\">\n"
"<parameter name=\"code\">import os\nfor k in os.environ:\n print(k)</parameter>\n"
"</invoke>\n</minimax:tool_call>")
.tools({ python_tool })
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_with_tool_calls(
"python", R"x({"code": "import os\nfor k in os.environ:\n print(k)"})x"))
.run();

// Tool with two integer parameters
tst.test(
"</think><minimax:tool_call>\n<invoke name=\"special_function_with_opt\">\n"
"<parameter name=\"arg1\">1</parameter>\n"
"<parameter name=\"arg2\">42</parameter>\n"
"</invoke>\n</minimax:tool_call>")
.tools({ special_function_tool_with_optional_param })
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_with_tool_calls(
"special_function_with_opt", R"x({"arg1": 1, "arg2": 42})x"))
.run();

// Parallel calls to the same tool inside one wrapper
tst.test(
"</think><minimax:tool_call>\n"
"<invoke name=\"python\">\n<parameter name=\"code\">print('a')</parameter>\n</invoke>\n"
"<invoke name=\"python\">\n<parameter name=\"code\">print('b')</parameter>\n</invoke>\n"
"</minimax:tool_call>")
.tools({ python_tool })
.parallel_tool_calls(true)
.reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
.expect(message_with_reasoning_content_and_multiple_tool_calls(
"", "",
{
{ "python", R"x({"code": "print('a')"})x" },
{ "python", R"x({"code": "print('b')"})x" },
}))
.run();
}

// NVIDIA-Nemotron-Nano-v2 tests - <TOOLCALL>...</TOOLCALL> format
Expand Down