mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-08-02 20:53:38 +00:00
common/chat: add specialized minimax m3 parser (#26210)
This commit is contained in:
parent
d6b61ac0d3
commit
6ba5ef2470
6 changed files with 1100 additions and 0 deletions
|
|
@ -1056,3 +1056,141 @@ void common_chat_peg_gemma4_mapper::visit(const common_peg_ast_arena & arena, co
|
|||
visit(arena, child_id);
|
||||
}
|
||||
}
|
||||
|
||||
static void minimax_m3_collect(const common_peg_ast_arena & arena,
|
||||
const common_peg_ast_node & node,
|
||||
const std::string & tag,
|
||||
std::vector<common_peg_ast_id> & out) {
|
||||
for (auto child_id : node.children) {
|
||||
const auto & child = arena.get(child_id);
|
||||
if (child.tag == tag) {
|
||||
out.push_back(child_id);
|
||||
} else {
|
||||
minimax_m3_collect(arena, child, tag, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static common_peg_ast_id minimax_m3_value_of(const common_peg_ast_arena & arena, const common_peg_ast_node & node) {
|
||||
for (auto child_id : node.children) {
|
||||
const auto & tag = arena.get(child_id).tag;
|
||||
if (tag == common_chat_peg_builder::TOOL_ARG_VALUE ||
|
||||
tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE ||
|
||||
tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT ||
|
||||
tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY) {
|
||||
return child_id;
|
||||
}
|
||||
}
|
||||
return COMMON_PEG_INVALID_AST_ID;
|
||||
}
|
||||
|
||||
static std::string minimax_m3_value_to_json(const common_peg_ast_arena & arena, common_peg_ast_id id, bool closed);
|
||||
|
||||
static std::string minimax_m3_member_to_json(const common_peg_ast_arena & arena, const common_peg_ast_node & node) {
|
||||
auto name_id = arena.find_by_tag(node, common_chat_peg_builder::TOOL_ARG_NAME);
|
||||
if (name_id == COMMON_PEG_INVALID_AST_ID) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return ordered_json(arena.get(name_id).text).dump() + ":" +
|
||||
minimax_m3_value_to_json(arena, minimax_m3_value_of(arena, node), !node.is_partial);
|
||||
}
|
||||
|
||||
static std::string minimax_m3_container_to_json(const common_peg_ast_arena & arena,
|
||||
const common_peg_ast_node & node,
|
||||
bool is_object,
|
||||
bool closed) {
|
||||
const std::string tag = is_object ? common_chat_peg_builder::TOOL_ARG
|
||||
: common_chat_peg_minimax_m3_mapper::TOOL_ARG_ITEM;
|
||||
|
||||
std::vector<common_peg_ast_id> entries;
|
||||
minimax_m3_collect(arena, node, tag, entries);
|
||||
|
||||
std::string result = is_object ? "{" : "[";
|
||||
|
||||
bool add_comma = false;
|
||||
for (auto entry_id : entries) {
|
||||
const auto & entry = arena.get(entry_id);
|
||||
|
||||
std::string text;
|
||||
if (is_object) {
|
||||
text = minimax_m3_member_to_json(arena, entry);
|
||||
} else {
|
||||
text = minimax_m3_value_to_json(arena, minimax_m3_value_of(arena, entry), !entry.is_partial);
|
||||
}
|
||||
|
||||
if (text.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (add_comma) {
|
||||
result += ",";
|
||||
}
|
||||
add_comma = true;
|
||||
result += text;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += is_object ? "}" : "]";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string minimax_m3_value_to_json(const common_peg_ast_arena & arena, common_peg_ast_id id, bool closed) {
|
||||
if (id == COMMON_PEG_INVALID_AST_ID) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto & node = arena.get(id);
|
||||
|
||||
if (node.tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT) {
|
||||
return minimax_m3_container_to_json(arena, node, /* is_object = */ true, closed);
|
||||
}
|
||||
|
||||
if (node.tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY) {
|
||||
return minimax_m3_container_to_json(arena, node, /* is_object = */ false, closed);
|
||||
}
|
||||
|
||||
if (node.tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE) {
|
||||
return "\"" + escape_json_string_inner(std::string(node.text)) + (closed ? "\"" : "");
|
||||
}
|
||||
|
||||
// Numbers and booleans are written verbatim by the template
|
||||
return std::string(node.text);
|
||||
}
|
||||
|
||||
void common_chat_peg_minimax_m3_mapper::from_ast(const common_peg_ast_arena & arena,
|
||||
const common_peg_parse_result & result) {
|
||||
for (const auto & node : result.nodes) {
|
||||
visit(arena, node);
|
||||
}
|
||||
}
|
||||
|
||||
void common_chat_peg_minimax_m3_mapper::visit(const common_peg_ast_arena & arena, common_peg_ast_id id) {
|
||||
const auto & node = arena.get(id);
|
||||
|
||||
if (node.tag == common_chat_peg_builder::REASONING) {
|
||||
result.reasoning_content += std::string(node.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tag == common_chat_peg_builder::CONTENT) {
|
||||
result.content += std::string(node.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.tag == common_chat_peg_builder::TOOL) {
|
||||
auto name_id = arena.find_by_tag(node, common_chat_peg_builder::TOOL_NAME);
|
||||
if (name_id != COMMON_PEG_INVALID_AST_ID) {
|
||||
common_chat_tool_call call;
|
||||
call.name = std::string(arena.get(name_id).text);
|
||||
call.arguments = minimax_m3_container_to_json(arena, node, /* is_object = */ true, !node.is_partial);
|
||||
result.tool_calls.push_back(call);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto child_id : node.children) {
|
||||
visit(arena, child_id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ class common_chat_peg_gemma4_mapper : public common_chat_peg_mapper {
|
|||
void visit(const common_peg_ast_arena & arena, common_peg_ast_id id);
|
||||
};
|
||||
|
||||
class common_chat_peg_minimax_m3_mapper : public common_chat_peg_mapper {
|
||||
public:
|
||||
static constexpr const char * TOOL_ARG_OBJECT = "tool-arg-object";
|
||||
static constexpr const char * TOOL_ARG_ARRAY = "tool-arg-array";
|
||||
static constexpr const char * TOOL_ARG_ITEM = "tool-arg-item";
|
||||
|
||||
common_chat_peg_minimax_m3_mapper(common_chat_msg & msg) : common_chat_peg_mapper(msg) {}
|
||||
virtual void from_ast(const common_peg_ast_arena & arena, const common_peg_parse_result & result);
|
||||
private:
|
||||
void visit(const common_peg_ast_arena & arena, common_peg_ast_id id);
|
||||
};
|
||||
|
||||
struct content_structure;
|
||||
struct tool_call_structure;
|
||||
|
||||
|
|
|
|||
273
common/chat.cpp
273
common/chat.cpp
|
|
@ -816,6 +816,8 @@ const char * common_chat_format_name(common_chat_format format) {
|
|||
return "peg-native";
|
||||
case COMMON_CHAT_FORMAT_PEG_GEMMA4:
|
||||
return "peg-gemma4";
|
||||
case COMMON_CHAT_FORMAT_PEG_MINIMAX_M3:
|
||||
return "peg-minimax-m3";
|
||||
default:
|
||||
throw std::runtime_error("Unknown chat format");
|
||||
}
|
||||
|
|
@ -2270,6 +2272,264 @@ static common_chat_params common_chat_params_init_cohere2moe(const common_chat_t
|
|||
return data;
|
||||
}
|
||||
|
||||
static common_chat_params common_chat_params_init_minimax_m3(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.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
|
||||
data.format = COMMON_CHAT_FORMAT_PEG_MINIMAX_M3;
|
||||
data.supports_thinking = true;
|
||||
data.thinking_start_tag = "<mm:think>";
|
||||
data.thinking_end_tags = {"</mm:think>"};
|
||||
|
||||
// M3 prefixes every tool tag with the namespace token "]<]minimax[>[";
|
||||
// params use the parameter name as the tag (<file_path>...</file_path>).
|
||||
const std::string NS = "]<]minimax[>[";
|
||||
const std::string THINK_START = "<mm:think>";
|
||||
const std::string THINK_END = "</mm:think>";
|
||||
const std::string FC_START = NS + "<tool_call>";
|
||||
const std::string FC_END = NS + "</tool_call>";
|
||||
const std::string INVOKE_END = NS + "</invoke>";
|
||||
|
||||
data.preserved_tokens = {
|
||||
NS,
|
||||
"<tool_call>",
|
||||
"</tool_call>",
|
||||
THINK_START,
|
||||
THINK_END,
|
||||
};
|
||||
|
||||
data.message_delimiters = {
|
||||
{ COMMON_CHAT_ROLE_ASSISTANT, "]~b]ai" },
|
||||
{ COMMON_CHAT_ROLE_USER, "]~b]user" },
|
||||
{ COMMON_CHAT_ROLE_TOOL, "]~b]tool" },
|
||||
{ COMMON_CHAT_ROLE_SYSTEM, "]~b]developer" },
|
||||
{ COMMON_CHAT_ROLE_SYSTEM, "]~b]system" },
|
||||
};
|
||||
|
||||
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
||||
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
||||
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
||||
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
||||
|
||||
const std::string GEN_PROMPT = data.generation_prompt;
|
||||
|
||||
using mm3 = common_chat_peg_minimax_m3_mapper;
|
||||
|
||||
if (inputs.has_continuation()) {
|
||||
const auto & msg = inputs.continue_msg;
|
||||
|
||||
data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
|
||||
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
||||
data.generation_prompt += THINK_END + msg.render_content();
|
||||
}
|
||||
|
||||
data.prompt += data.generation_prompt;
|
||||
}
|
||||
|
||||
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
||||
auto generation_prompt = p.prefix(GEN_PROMPT, THINK_START);
|
||||
auto end = p.end();
|
||||
|
||||
auto reasoning = p.eps();
|
||||
if (extract_reasoning) {
|
||||
auto block = inputs.enable_thinking
|
||||
? p.literal(THINK_START) + p.space() +
|
||||
p.ac(p.reasoning(p.until(THINK_END)) + p.literal(THINK_END), THINK_END)
|
||||
: p.literal(THINK_START) + p.ac(p.until(THINK_END) + p.literal(THINK_END), THINK_END);
|
||||
|
||||
// A turn without reasoning is prefixed with a bare </mm:think>, written either by the
|
||||
// generation prompt (thinking_mode = "disabled") or by the model itself.
|
||||
reasoning = p.optional(p.choice({ block, p.literal(THINK_END) }));
|
||||
}
|
||||
|
||||
if (has_response_format) {
|
||||
auto response_format = p.rule("response-format",
|
||||
p.literal("```json") + p.space() +
|
||||
p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
|
||||
p.space() + p.literal("```"));
|
||||
return generation_prompt + reasoning + response_format + end;
|
||||
}
|
||||
|
||||
if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
|
||||
return generation_prompt + reasoning + p.content(p.rest()) + end;
|
||||
}
|
||||
|
||||
auto alternatives_of = [](const json & schema) -> std::optional<json> {
|
||||
for (const auto * keyword : { "oneOf", "anyOf" }) {
|
||||
if (schema.contains(keyword) && schema.at(keyword).is_array() && !schema.at(keyword).empty()) {
|
||||
return schema.at(keyword);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
|
||||
auto tool_choice = p.choice();
|
||||
foreach_function(inputs.tools, [&](const json & tool) {
|
||||
const auto & function = tool.at("function");
|
||||
std::string name = function.at("name");
|
||||
auto params = function.contains("parameters") ? function.at("parameters") : json::object();
|
||||
|
||||
auto schema_info = common_schema_info();
|
||||
schema_info.resolve_refs(params);
|
||||
|
||||
// The template expands argument values recursively in XML (see the to_xml() macro)
|
||||
std::function<common_peg_parser(const json &, const std::string &, const std::string &)> value_of;
|
||||
std::function<common_peg_parser(const json &, const std::string &)> members_of;
|
||||
|
||||
auto element_of = [&](const std::string & tag, const json & schema, const std::string & rule_name) {
|
||||
const std::string close = NS + "</" + tag + ">";
|
||||
return p.rule(rule_name,
|
||||
p.tool_arg(
|
||||
p.tool_arg_open(
|
||||
p.literal(NS + "<") +
|
||||
p.tool_arg_name(p.literal(tag)) +
|
||||
p.literal(">")) +
|
||||
value_of(schema, rule_name, close)));
|
||||
};
|
||||
|
||||
value_of = [&](const json & schema,
|
||||
const std::string & rule_name,
|
||||
const std::string & close) -> common_peg_parser {
|
||||
auto close_tag = p.tool_arg_close(p.literal(close));
|
||||
|
||||
// A string accepts anything, so a union with a string alternative is a string
|
||||
if (schema_info.resolves_to_string(schema)) {
|
||||
return p.ac(p.tool_arg_string_value(p.until(close)) + close_tag, close);
|
||||
}
|
||||
|
||||
if (auto alternatives = alternatives_of(schema)) {
|
||||
std::vector<common_peg_parser> choices;
|
||||
|
||||
size_t index = 0;
|
||||
for (const auto & alternative : *alternatives) {
|
||||
const std::string alt_name = rule_name + "-" + std::to_string(index++);
|
||||
|
||||
// There is a risk that this breaks streaming deltas, but that's a risk we
|
||||
// assume to provide tool arg streaming.
|
||||
choices.push_back(value_of(alternative, alt_name, close));
|
||||
}
|
||||
|
||||
return p.choice(choices);
|
||||
}
|
||||
|
||||
const std::string type = schema.contains("type") && schema.at("type").is_string()
|
||||
? schema.at("type").get<std::string>()
|
||||
: "";
|
||||
|
||||
if (type == "object" && schema.contains("properties")) {
|
||||
return p.tag(mm3::TOOL_ARG_OBJECT, members_of(schema, rule_name)) + p.space() + close_tag;
|
||||
}
|
||||
|
||||
if (type == "array" && schema.contains("items")) {
|
||||
const std::string item_close = NS + "</item>";
|
||||
auto item = p.rule(rule_name + "-item",
|
||||
p.tag(mm3::TOOL_ARG_ITEM,
|
||||
p.literal(NS + "<item>") +
|
||||
value_of(schema.at("items"), rule_name + "-item", item_close)));
|
||||
return p.tag(mm3::TOOL_ARG_ARRAY, p.repeat(p.space() + item, 0, -1)) + p.space() + close_tag;
|
||||
}
|
||||
|
||||
return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", schema, false)) + close_tag;
|
||||
};
|
||||
|
||||
// Required properties in schema order, then any number of optional ones in any order.
|
||||
members_of = [&](const json & schema, const std::string & rule_prefix) -> common_peg_parser {
|
||||
const auto & props = schema.at("properties");
|
||||
|
||||
std::set<std::string> required;
|
||||
if (schema.contains("required")) {
|
||||
schema.at("required").get_to(required);
|
||||
}
|
||||
|
||||
std::vector<common_peg_parser> required_elements;
|
||||
std::vector<common_peg_parser> optional_elements;
|
||||
for (const auto & [key, key_schema] : props.items()) {
|
||||
auto element = element_of(key, key_schema, rule_prefix + "-" + key);
|
||||
if (required.find(key) != required.end()) {
|
||||
required_elements.push_back(element);
|
||||
} else {
|
||||
optional_elements.push_back(element);
|
||||
}
|
||||
}
|
||||
|
||||
common_peg_parser members = p.eps();
|
||||
for (size_t i = 0; i < required_elements.size(); i++) {
|
||||
if (i > 0) {
|
||||
members = members + p.space();
|
||||
}
|
||||
members = members + required_elements[i];
|
||||
}
|
||||
|
||||
if (!optional_elements.empty()) {
|
||||
common_peg_parser any_optional = p.choice();
|
||||
for (const auto & element : optional_elements) {
|
||||
any_optional |= element;
|
||||
}
|
||||
members = members + p.repeat(p.space() + any_optional, 0, -1);
|
||||
}
|
||||
|
||||
return members;
|
||||
};
|
||||
|
||||
common_peg_parser invoke_body =
|
||||
params.contains("properties") ? members_of(params, "tool-" + name + "-arg") : p.eps();
|
||||
|
||||
auto func_parser = p.tool(
|
||||
p.tool_open(p.literal(NS + "<invoke name=\"") +
|
||||
p.tool_name(p.literal(name)) + p.literal("\">")) +
|
||||
p.space() + invoke_body + p.space() +
|
||||
p.tool_close(p.literal(INVOKE_END)));
|
||||
|
||||
tool_choice |= p.rule("tool-" + name, func_parser);
|
||||
});
|
||||
|
||||
auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
||||
|
||||
common_peg_parser tool_calls = p.eps();
|
||||
if (inputs.parallel_tool_calls) {
|
||||
tool_calls = p.trigger_rule("tool-call",
|
||||
p.literal(FC_START) + p.space() + tool_choice +
|
||||
p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
|
||||
} else {
|
||||
tool_calls = p.trigger_rule("tool-call",
|
||||
p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
|
||||
}
|
||||
|
||||
if (!require_tools) {
|
||||
tool_calls = p.optional(tool_calls);
|
||||
}
|
||||
|
||||
auto content_before_tools = p.content(p.until(FC_START));
|
||||
return generation_prompt + reasoning + content_before_tools + tool_calls + end;
|
||||
});
|
||||
|
||||
data.parser = parser.save();
|
||||
|
||||
if (include_grammar) {
|
||||
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
||||
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.contains("parameters") ? function.at("parameters") : json::object();
|
||||
builder.resolve_refs(schema);
|
||||
});
|
||||
if (has_response_format) {
|
||||
auto schema = inputs.json_schema;
|
||||
builder.resolve_refs(schema);
|
||||
}
|
||||
parser.build_grammar(builder, data.grammar_lazy);
|
||||
});
|
||||
|
||||
data.grammar_triggers = {
|
||||
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
namespace workaround {
|
||||
|
||||
static void map_developer_role_to_system(json & messages) {
|
||||
|
|
@ -2707,6 +2967,15 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
|
|||
return common_chat_params_init_gigachat_v3(tmpl, params);
|
||||
}
|
||||
|
||||
// MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's
|
||||
// markup delimiters, so detect the template and use a dedicated parser.
|
||||
if (src.find("]<]minimax[>[") != std::string::npos &&
|
||||
src.find("<tool_call>") != std::string::npos &&
|
||||
src.find("<invoke name=") != std::string::npos) {
|
||||
LOG_DBG("Using specialized template: MiniMax-M3\n");
|
||||
return common_chat_params_init_minimax_m3(tmpl, params);
|
||||
}
|
||||
|
||||
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
|
||||
// The template source contains the token as a variable assignment, not as a literal in markup.
|
||||
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
|
||||
|
|
@ -2998,6 +3267,8 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
|
|||
std::unique_ptr<common_chat_peg_mapper> mapper;
|
||||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||||
} else {
|
||||
mapper = std::make_unique<common_chat_peg_mapper>(msg);
|
||||
}
|
||||
|
|
@ -3020,6 +3291,8 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & src_pars
|
|||
std::unique_ptr<common_chat_peg_mapper> mapper;
|
||||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||||
} else {
|
||||
mapper = std::make_unique<common_chat_peg_mapper>(msg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ enum common_chat_format {
|
|||
COMMON_CHAT_FORMAT_PEG_SIMPLE,
|
||||
COMMON_CHAT_FORMAT_PEG_NATIVE,
|
||||
COMMON_CHAT_FORMAT_PEG_GEMMA4,
|
||||
COMMON_CHAT_FORMAT_PEG_MINIMAX_M3,
|
||||
|
||||
COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats
|
||||
};
|
||||
|
|
|
|||
247
models/templates/MiniMax-M3.jinja
Normal file
247
models/templates/MiniMax-M3.jinja
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
{# ---------- special token variables ---------- #}
|
||||
{%- set ns_token = ']<]minimax[>[' -%}
|
||||
{%- set bod_token = ']~!b[' -%}
|
||||
{%- set bos_token = ']~b]' -%}
|
||||
{%- set eos_token = '[e~[' -%}
|
||||
{%- set toolcall_begin_token = ns_token ~ '<tool_call>' -%}
|
||||
{%- set toolcall_end_token = ns_token ~ '</tool_call>' -%}
|
||||
{%- set think_begin_token = '<mm:think>' -%}
|
||||
{%- set think_end_token = '</mm:think>' -%}
|
||||
{%- set image_token = ']<]image[>[' -%}
|
||||
{%- set video_token = ']<]video[>[' -%}
|
||||
{#- Thinking mode: "enabled" / "disabled" / "adaptive" / not defined -#}
|
||||
{#- Recursive XML renderer for tool_call arguments ======================== -#}
|
||||
{#- None values are intentionally skipped in mapping iteration so that
|
||||
`<key>null</key>` (which would round-trip to the literal string "null")
|
||||
never appears in the rendered tool_call. The convention is: omit the
|
||||
field entirely. The top-level `_args` loop applies the same rule.
|
||||
The `val is none` branch below is a safety net only — upstream cleaning
|
||||
(drop_none_in_tool_arguments) should ensure no None ever reaches here. -#}
|
||||
{%- macro to_xml(val, ns) -%}
|
||||
{%- if val is mapping -%}
|
||||
{%- for k, v in val.items() if v is not none -%}
|
||||
{{ ns }}<{{ k }}>{{ to_xml(v, ns) }}{{ ns }}</{{ k }}>
|
||||
{%- endfor -%}
|
||||
{%- elif val is iterable and val is not string -%}
|
||||
{%- for item in val -%}
|
||||
{{ ns }}<item>{{ to_xml(item, ns) }}{{ ns }}</item>
|
||||
{%- endfor -%}
|
||||
{%- elif val is none -%}
|
||||
{#- Should be unreachable when upstream cleaning is applied. -#}
|
||||
{%- elif val is boolean -%}
|
||||
{{ val | tojson }}
|
||||
{%- else -%}
|
||||
{{ val }}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{#- Tool Rendering Functions ============================================== -#}
|
||||
{%- macro render_tool_namespace(namespace_name, tool_list) -%}
|
||||
{%- for tool in tool_list -%}
|
||||
<tool>{{ tool.function | tojson(ensure_ascii=False) }}</tool>
|
||||
{% endfor -%}
|
||||
{%- endmacro -%}
|
||||
{%- macro visible_text(content) -%}
|
||||
{%- if content is string -%}
|
||||
{{ content }}
|
||||
{%- elif content is iterable and content is not mapping -%}
|
||||
{%- for item in content -%}
|
||||
{%- if item is mapping and item.type == 'text' -%}
|
||||
{{- item.text }}
|
||||
{%- elif item is mapping and item.type == 'image' -%}
|
||||
{{- image_token }}
|
||||
{%- elif item is mapping and item.type == 'video' -%}
|
||||
{{- video_token}}
|
||||
{%- elif item is string -%}
|
||||
{{- item }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- elif content is none -%}
|
||||
{{- '' }}
|
||||
{%- else -%}
|
||||
{{- content }}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{#- System Message Construction ============================================ -#}
|
||||
{%- macro build_system_message(system_message) -%}
|
||||
{%- if system_message and system_message.content -%}
|
||||
{{- visible_text(system_message.content) }}
|
||||
{%- else -%}
|
||||
{{- 'Your model version is MiniMax-M3, developed by MiniMax. Knowledge cutoff: January 2026. Founded in early 2022, MiniMax is a global AI foundation model company committed to advancing the frontiers of AI towards AGI.' }}
|
||||
{%- endif -%}
|
||||
|
||||
{#- Thinking mode instructions -#}
|
||||
{{- '\n\n<thinking_instructions>\n' }}
|
||||
{{- 'You have a thinking capability that allows you to reason step by step before responding. When thinking is enabled, wrap your reasoning in ' ~ think_begin_token ~ think_end_token ~ ' tags before your response. When thinking is disabled, begin your response directly after the ' ~ think_end_token ~ ' prefix. When thinking is adaptive, decide on your own whether to think for the current turn.\n' }}
|
||||
{%- if thinking_mode is defined -%}
|
||||
{%- if thinking_mode == "enabled" -%}
|
||||
{{- 'Current thinking mode: enabled. You MUST think step by step before every response, including after receiving function/tool results.\n' }}
|
||||
{%- elif thinking_mode == "disabled" -%}
|
||||
{{- 'Current thinking mode: disabled. Do not output any thinking process.\n' }}
|
||||
{%- elif thinking_mode == "adaptive" -%}
|
||||
{{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\n' }}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{{- 'Current thinking mode: adaptive. You are encouraged to think for complex decision-making, multi-step reasoning, or when analyzing function/tool results.\n' }}
|
||||
{%- endif -%}
|
||||
{{- '</thinking_instructions>' }}
|
||||
{%- endmacro -%}
|
||||
{%- macro build_developer_message(developer_message) -%}
|
||||
{%- if developer_message and developer_message.content -%}
|
||||
{{- visible_text(developer_message.content) }}
|
||||
{%- else -%}
|
||||
{%- if model_identity is not defined -%}
|
||||
{%- set model_identity = "You are a helpful assistant." -%}
|
||||
{%- endif -%}
|
||||
{{- model_identity }}
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
{#- Main Template Logic ================================================= -#}
|
||||
{#- Role mapping: root -> system sp (high priority), system/developer -> developer sp (low priority) -#}
|
||||
{%- set system_message = none -%}
|
||||
{%- set developer_message = none -%}
|
||||
{%- set conversation_messages = messages -%}
|
||||
{%- if messages and messages[0].role == "root" -%}
|
||||
{%- set system_message = messages[0] -%}
|
||||
{%- set conversation_messages = messages[1:] -%}
|
||||
{%- if conversation_messages and conversation_messages[0].role in ["system", "developer"] -%}
|
||||
{%- set developer_message = conversation_messages[0] -%}
|
||||
{%- set conversation_messages = conversation_messages[1:] -%}
|
||||
{%- endif -%}
|
||||
{%- elif messages and messages[0].role in ["system", "developer"] -%}
|
||||
{%- set developer_message = messages[0] -%}
|
||||
{%- set conversation_messages = messages[1:] -%}
|
||||
{%- endif -%}
|
||||
{#- Render system sp (higher priority, root role only) -#}
|
||||
{{- bod_token ~ bos_token ~ 'system' ~ '\n' }}
|
||||
{{- build_system_message(system_message) }}
|
||||
{{- eos_token ~ '\n' }}
|
||||
|
||||
{#- Render developer sp (lower priority: system/developer role + tools) -#}
|
||||
{{- bos_token ~ 'developer' ~ '\n' }}
|
||||
{{- build_developer_message(developer_message) }}
|
||||
{%- if tools -%}
|
||||
{{- '\n\n' ~ '# Tools' ~ '\n' ~ 'You may call one or more tools to assist with the user query.\nHere are the tools available in JSONSchema format:' ~ '\n' }}
|
||||
{{- '\n' ~ '<tools>' ~ '\n' }}
|
||||
{{- render_tool_namespace("functions", tools) }}
|
||||
{{- '</tools>' ~ '\n\n' }}
|
||||
{{- 'To call tools, wrap all invocations in a single ' ~ toolcall_begin_token ~ toolcall_end_token ~ ' block. Parameter values containing nested objects or arrays are recursively expanded into XML elements. Example:\n' }}
|
||||
{{- '\n' ~ toolcall_begin_token ~ '\n' }}
|
||||
{{- ns_token + '<invoke name="tool-name-1">' }}
|
||||
{{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}
|
||||
{{- ns_token + '<param-2>' }}
|
||||
{{- ns_token + '<item>' }}
|
||||
{{- ns_token + '<key-a>val-a' + ns_token + '</key-a>' }}
|
||||
{{- ns_token + '<key-b>val-b' + ns_token + '</key-b>' }}
|
||||
{{- ns_token + '</item>' }}
|
||||
{{- ns_token + '</param-2>' }}
|
||||
{{- ns_token + '</invoke>\n' }}
|
||||
{{- ns_token + '<invoke name="tool-name-2">' }}
|
||||
{{- ns_token + '<param-1>value-1' + ns_token + '</param-1>' }}
|
||||
{{- ns_token + '</invoke>\n' }}
|
||||
{{- toolcall_end_token }}
|
||||
{%- endif -%}
|
||||
{{- eos_token ~ '\n' }}
|
||||
|
||||
{#- Render messages -#}
|
||||
{%- set last_tool_call = namespace(name=none) -%}
|
||||
{%- for message in conversation_messages -%}
|
||||
{%- if message.role == 'assistant' -%}
|
||||
{{- bos_token ~ 'ai' ~ '\n' }}
|
||||
|
||||
{%- set reasoning_content = '' %}
|
||||
{%- set content = visible_text(message.content) %}
|
||||
{%- if message.reasoning_content is string %}
|
||||
{%- set reasoning_content = message.reasoning_content %}
|
||||
{%- else %}
|
||||
{%- if think_end_token in content %}
|
||||
{%- set reasoning_content = content.split(think_end_token)[0].strip('\n').split(think_begin_token)[-1].strip('\n') %}
|
||||
{%- set content = content.split(think_end_token)[-1].strip('\n') %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if reasoning_content -%}
|
||||
{#- Render thinking for every assistant turn (all-turn visible) -#}
|
||||
{{- think_begin_token ~ reasoning_content ~ think_end_token }}
|
||||
{%- else -%}
|
||||
{#- No thinking rendered → prefix with think_end_token -#}
|
||||
{{- think_end_token }}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if content -%}
|
||||
{{- content }}
|
||||
{%- endif -%}
|
||||
{%- if message.tool_calls -%}
|
||||
{{- toolcall_begin_token ~ '\n' }}
|
||||
|
||||
{%- for tool_call in message.tool_calls -%}
|
||||
{%- if tool_call.function -%}
|
||||
{%- set tool_call = tool_call.function -%}
|
||||
{%- endif -%}
|
||||
{{- ns_token + '<invoke name="' + tool_call.name + '">' }}
|
||||
{%- set _args = tool_call.arguments -%}
|
||||
{%- for k, v in _args.items() if v is not none %}
|
||||
{{- ns_token + '<' + k + '>' -}}
|
||||
{{- to_xml(v, ns_token) -}}
|
||||
{{- ns_token + '</' + k + '>' }}
|
||||
{%- endfor -%}
|
||||
{{- ns_token + '</invoke>' ~ '\n' }}
|
||||
{%- endfor -%}
|
||||
|
||||
{{- toolcall_end_token }}
|
||||
{%- if message.tool_calls[-1].function -%}
|
||||
{%- set last_tool_call.name = message.tool_calls[-1].function.name -%}
|
||||
{%- else -%}
|
||||
{%- set last_tool_call.name = message.tool_calls[-1].name -%}
|
||||
{%- endif -%}
|
||||
{%- else -%}
|
||||
{%- set last_tool_call.name = none -%}
|
||||
{%- endif -%}
|
||||
{{- eos_token ~ '\n' }}
|
||||
|
||||
{%- elif message.role == 'tool' -%}
|
||||
{%- if last_tool_call.name is none -%}
|
||||
{{- raise_exception("Message has tool role, but there was no previous assistant message with a tool call!") }}
|
||||
{%- endif -%}
|
||||
{%- if loop.first or (conversation_messages[loop.index0 - 1].role != 'tool') -%}
|
||||
{{- bos_token ~ 'tool' }}
|
||||
{%- endif -%}
|
||||
{{- '\n<response>' }}
|
||||
{%- if message.content is string -%}
|
||||
{{- message.content }}
|
||||
{%- else -%}
|
||||
{%- for tr in message.content -%}
|
||||
{%- if tr is mapping and tr.type is defined and tr.type == 'image' -%}
|
||||
{{- image_token }}
|
||||
{%- elif tr is mapping and tr.type is defined and tr.type == 'video' -%}
|
||||
{{- video_token }}
|
||||
{%- else -%}
|
||||
{{- tr.output if tr.output is defined else (tr.text if tr.type == 'text' and tr.text is defined else tr) }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{{- '</response>' }}
|
||||
{%- if loop.last or (conversation_messages[loop.index0 + 1].role != 'tool') -%}
|
||||
{{- eos_token ~ '\n' -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- elif message.role == 'user' -%}
|
||||
{{- bos_token ~ 'user' ~ '\n' }}
|
||||
{{- visible_text(message.content) }}
|
||||
{{- eos_token ~ '\n' }}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{#- Generation prompt -#}
|
||||
{%- if add_generation_prompt -%}
|
||||
{{- bos_token ~ 'ai' ~ '\n' }}
|
||||
{%- if thinking_mode is defined and thinking_mode == "disabled" -%}
|
||||
{{- think_end_token }}
|
||||
{%- elif thinking_mode is defined and thinking_mode == "adaptive" -%}
|
||||
{#- adaptive: no prefix, let model decide -#}
|
||||
{%- elif thinking_mode is defined and thinking_mode == "enabled" -%}
|
||||
{#- enabled or not defined: default to think -#}
|
||||
{{- think_begin_token }}
|
||||
{%- else -%}
|
||||
{#- adaptive: no prefix, let model decide -#}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
|
@ -730,6 +730,71 @@ static common_chat_tool imaginary_number_tool{
|
|||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool nested_args_tool{
|
||||
/* .name = */ "nested_args",
|
||||
/* .description = */ "Tool with nested array arguments",
|
||||
/* .parameters = */ R"({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"label": { "type": "string" }
|
||||
},
|
||||
"required": ["id", "label"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tags", "entries"]
|
||||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool union_args_tool{
|
||||
/* .name = */ "union_args",
|
||||
/* .description = */ "Tool with union arguments",
|
||||
/* .parameters = */ R"({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [
|
||||
{ "type": "array", "items": { "type": "string" } },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"op": { "type": "string" }
|
||||
},
|
||||
"required": ["field", "op"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"label": {
|
||||
"oneOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "object", "properties": { "text": { "type": "string" } } }
|
||||
]
|
||||
},
|
||||
"limit": {
|
||||
"oneOf": [
|
||||
{ "type": "integer" },
|
||||
{
|
||||
"type": "object",
|
||||
"properties": { "max": { "type": "integer" } },
|
||||
"required": ["max"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})",
|
||||
};
|
||||
|
||||
static common_chat_tool nullable_string_tool{
|
||||
/* .name = */ "set_nullable_str",
|
||||
/* .description = */ "Set a nullable string value",
|
||||
|
|
@ -4850,6 +4915,370 @@ static void test_template_output_peg_parsers(bool detailed_debug) {
|
|||
.run();
|
||||
}
|
||||
|
||||
// MiniMax-M3 tests - namespaced XML invoke format, the parameter name is the tag
|
||||
// Format:
|
||||
// ]<]minimax[>[<tool_call>
|
||||
// ]<]minimax[>[<invoke name="get_time">]<]minimax[>[<city>Tokyo]<]minimax[>[</city>]<]minimax[>[</invoke>
|
||||
// ]<]minimax[>[</tool_call>
|
||||
// Reasoning uses <mm:think>...</mm:think>. The generation prompt is only "]~b]ai\n", so the model
|
||||
// opens the thinking block itself; a turn without reasoning is prefixed with a bare </mm:think>.
|
||||
{
|
||||
auto tst = peg_tester("models/templates/MiniMax-M3.jinja", detailed_debug);
|
||||
|
||||
// Content only (bare </mm:think> prefix)
|
||||
tst.test("</mm:think>Hello, world!\nWhat's up?")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.expect(message_assist)
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Thinking + content
|
||||
tst.test("<mm:think>I'm\nthinking</mm:think>Hello, world!\nWhat's up?")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.expect(message_assist_thoughts)
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Thinking + tool call (single, string param)
|
||||
tst.test(
|
||||
"<mm:think>Let me check the time</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"get_time\">"
|
||||
"]<]minimax[>[<city>Tokyo]<]minimax[>[</city>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ get_time_tool })
|
||||
.expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time"))
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Tool call without reasoning, integer param
|
||||
tst.test(
|
||||
"</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"special_function\">"
|
||||
"]<]minimax[>[<arg1>1]<]minimax[>[</arg1>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ special_function_tool })
|
||||
.expect(message_assist_call)
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Tool call with no parameters
|
||||
tst.test(
|
||||
"<mm:think>Let's call a tool:</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"empty_args\">"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ empty_args_tool })
|
||||
.expect(message_with_reasoning_and_tool_call("Let's call a tool:", "empty_args", "{}"))
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Multiple parallel tool calls in one block
|
||||
tst.test(
|
||||
"<mm:think>Calling both</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"get_time\">"
|
||||
"]<]minimax[>[<city>Paris]<]minimax[>[</city>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[<invoke name=\"get_weather\">"
|
||||
"]<]minimax[>[<city>Paris]<]minimax[>[</city>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.parallel_tool_calls(true)
|
||||
.tools({ get_time_tool, get_weather_tool })
|
||||
.expect(message_with_reasoning_content_and_multiple_tool_calls(
|
||||
"Calling both", "",
|
||||
{ { "get_time", R"({"city": "Paris"})" }, { "get_weather", R"({"city": "Paris"})" } }))
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Content before the tool call block
|
||||
tst.test(
|
||||
"<mm:think>Thinking about it</mm:think>"
|
||||
"Let me call the function."
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"special_function\">"
|
||||
"]<]minimax[>[<arg1>1]<]minimax[>[</arg1>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ special_function_tool })
|
||||
.expect_reasoning("Thinking about it")
|
||||
.expect_content("Let me call the function.")
|
||||
.expect_tool_calls({
|
||||
{ "special_function", R"({"arg1": 1})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Negative number
|
||||
tst.test(
|
||||
"<mm:think>Test negative</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"magic_int\">"
|
||||
"]<]minimax[>[<ref>-14]<]minimax[>[</ref>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ magic_int_tool })
|
||||
.expect_reasoning("Test negative")
|
||||
.expect_tool_calls({
|
||||
{ "magic_int", R"({"ref": -14})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Decimal number
|
||||
tst.test(
|
||||
"<mm:think>Test decimal</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"amount\">"
|
||||
"]<]minimax[>[<orig>3.14]<]minimax[>[</orig>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ amount_tool })
|
||||
.expect_reasoning("Test decimal")
|
||||
.expect_tool_calls({
|
||||
{ "amount", R"({"orig": 3.14})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Boolean
|
||||
tst.test(
|
||||
"<mm:think>Test boolean</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"toggle\">"
|
||||
"]<]minimax[>[<enabled>true]<]minimax[>[</enabled>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ toggle_tool })
|
||||
.expect_reasoning("Test boolean")
|
||||
.expect_tool_calls({
|
||||
{ "toggle", R"({"enabled": true})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Multiple params of mixed types (required int first, then optional string)
|
||||
tst.test(
|
||||
"<mm:think>Multi-arg call</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"magic_int\">"
|
||||
"]<]minimax[>[<ref>42]<]minimax[>[</ref>"
|
||||
"]<]minimax[>[<name>foo bar]<]minimax[>[</name>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ magic_int_tool })
|
||||
.expect_reasoning("Multi-arg call")
|
||||
.expect_tool_calls({
|
||||
{ "magic_int", R"({"ref": 42, "name": "foo bar"})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Nested object param, expanded into one element per key
|
||||
tst.test(
|
||||
"<mm:think>Nested object</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"imaginary_number\">"
|
||||
"]<]minimax[>[<number>"
|
||||
"]<]minimax[>[<real>1.5]<]minimax[>[</real>"
|
||||
"]<]minimax[>[<imaginary>-2.5]<]minimax[>[</imaginary>"
|
||||
"]<]minimax[>[</number>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ imaginary_number_tool })
|
||||
.expect_reasoning("Nested object")
|
||||
.expect_tool_calls({
|
||||
{ "imaginary_number", R"({"number": {"real": 1.5, "imaginary": -2.5}})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Array params, expanded into <item> elements (of scalars and of objects)
|
||||
tst.test(
|
||||
"<mm:think>Nested arrays</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"nested_args\">"
|
||||
"]<]minimax[>[<tags>"
|
||||
"]<]minimax[>[<item>alpha]<]minimax[>[</item>"
|
||||
"]<]minimax[>[<item>beta]<]minimax[>[</item>"
|
||||
"]<]minimax[>[</tags>"
|
||||
"]<]minimax[>[<entries>"
|
||||
"]<]minimax[>[<item>"
|
||||
"]<]minimax[>[<id>1]<]minimax[>[</id>"
|
||||
"]<]minimax[>[<label>one]<]minimax[>[</label>"
|
||||
"]<]minimax[>[</item>"
|
||||
"]<]minimax[>[</entries>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ nested_args_tool })
|
||||
.expect_reasoning("Nested arrays")
|
||||
.expect_tool_calls({
|
||||
{ "nested_args", R"({"tags": ["alpha", "beta"], "entries": [{"id": 1, "label": "one"}]})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Union params (anyOf/oneOf), expanded as a choice of the alternatives
|
||||
tst.test(
|
||||
"<mm:think>Union array</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"union_args\">"
|
||||
"]<]minimax[>[<filter>"
|
||||
"]<]minimax[>[<item>alpha]<]minimax[>[</item>"
|
||||
"]<]minimax[>[<item>beta]<]minimax[>[</item>"
|
||||
"]<]minimax[>[</filter>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ union_args_tool })
|
||||
.expect_reasoning("Union array")
|
||||
.expect_tool_calls({
|
||||
{ "union_args", R"({"filter": ["alpha", "beta"]})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// oneOf between a scalar and an object
|
||||
tst.test(
|
||||
"<mm:think>Union scalar</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"union_args\">"
|
||||
"]<]minimax[>[<limit>5]<]minimax[>[</limit>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ union_args_tool })
|
||||
.expect_reasoning("Union scalar")
|
||||
.expect_tool_calls({
|
||||
{ "union_args", R"({"limit": 5})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
tst.test(
|
||||
"<mm:think>Union nested</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"union_args\">"
|
||||
"]<]minimax[>[<limit>"
|
||||
"]<]minimax[>[<max>10]<]minimax[>[</max>"
|
||||
"]<]minimax[>[</limit>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ union_args_tool })
|
||||
.expect_reasoning("Union nested")
|
||||
.expect_tool_calls({
|
||||
{ "union_args", R"({"limit": {"max": 10}})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// A union with a string alternative is a string
|
||||
tst.test(
|
||||
"<mm:think>Union string</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"union_args\">"
|
||||
"]<]minimax[>[<label>hi]<]minimax[>[</label>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ union_args_tool })
|
||||
.expect_reasoning("Union string")
|
||||
.expect_tool_calls({
|
||||
{ "union_args", R"({"label": "hi"})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// ... even when the value looks structured
|
||||
tst.test(
|
||||
"<mm:think>Union string</mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"union_args\">"
|
||||
"]<]minimax[>[<label>"
|
||||
"]<]minimax[>[<text>hi]<]minimax[>[</text>"
|
||||
"]<]minimax[>[</label>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ union_args_tool })
|
||||
.expect_reasoning("Union string")
|
||||
.expect_tool_calls({
|
||||
{ "union_args", R"({"label": "]<]minimax[>[<text>hi]<]minimax[>[</text>"})", {} },
|
||||
})
|
||||
.expect_reconstruction()
|
||||
.run();
|
||||
|
||||
// Edge case: empty reasoning followed by a tool call
|
||||
tst.test(
|
||||
"<mm:think></mm:think>"
|
||||
"]<]minimax[>[<tool_call>\n"
|
||||
"]<]minimax[>[<invoke name=\"get_time\">"
|
||||
"]<]minimax[>[<city>XYZCITY]<]minimax[>[</city>"
|
||||
"]<]minimax[>[</invoke>\n"
|
||||
"]<]minimax[>[</tool_call>")
|
||||
.enable_thinking(true)
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.tools({ get_time_tool })
|
||||
.expect(message_with_tool_calls("get_time", R"({"city": "XYZCITY"})"))
|
||||
.run();
|
||||
|
||||
// Continuation tests
|
||||
tst.test("world!\nWhat's up?")
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.enable_thinking(true)
|
||||
.messages({ message_user, message_assist_prefill_content })
|
||||
.add_generation_prompt(false)
|
||||
.continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
|
||||
.expect_reasoning("I'm thinking")
|
||||
.expect_content("Hello, world!\nWhat's up?")
|
||||
.run();
|
||||
|
||||
tst.test(" thinking</mm:think>Hello, world!\nWhat's up?")
|
||||
.reasoning_format(COMMON_REASONING_FORMAT_AUTO)
|
||||
.enable_thinking(true)
|
||||
.messages({ message_user, message_assist_prefill_reasoning })
|
||||
.add_generation_prompt(false)
|
||||
.continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
|
||||
.expect_reasoning("I'm thinking")
|
||||
.expect_content("Hello, world!\nWhat's up?")
|
||||
.run();
|
||||
}
|
||||
|
||||
// NVIDIA-Nemotron-Nano-v2 tests - <TOOLCALL>...</TOOLCALL> format
|
||||
// Format: <TOOLCALL>[{"name": "func", "arguments": {...}}]</TOOLCALL>
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue