@@ -643,6 +643,7 @@ const char * common_chat_format_name(common_chat_format format) {
643643 case COMMON_CHAT_FORMAT_NEMOTRON_V2: return " Nemotron V2" ;
644644 case COMMON_CHAT_FORMAT_APERTUS: return " Apertus" ;
645645 case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS: return " LFM2 with JSON tools" ;
646+ case COMMON_CHAT_FORMAT_MINIMAX_M2: return " MiniMax M2" ;
646647 default :
647648 throw std::runtime_error (" Unknown chat format" );
648649 }
@@ -2791,6 +2792,151 @@ static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
27912792 }
27922793}
27932794
2795+ static common_chat_params common_chat_params_init_minimax_m2 (
2796+ const common_chat_template & tmpl,
2797+ templates_params & params,
2798+ const common_chat_templates_inputs & inputs)
2799+ {
2800+ common_chat_params data;
2801+ data.prompt = apply (tmpl, params);
2802+ data.format = COMMON_CHAT_FORMAT_MINIMAX_M2;
2803+ if (string_ends_with (data.prompt , " <think>" )) {
2804+ if (!inputs.enable_thinking ) {
2805+ data.prompt += " </think>" ;
2806+ } else {
2807+ data.thinking_forced_open = true ;
2808+ }
2809+ }
2810+
2811+ if (params.tools .is_array () && !params.tools .empty ()) {
2812+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
2813+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
2814+ std::vector<std::string> tool_rules;
2815+ foreach_function (params.tools , [&](const json & tool) {
2816+ const auto & function = tool.at (" function" );
2817+ std::string name = function.at (" name" );
2818+ auto parameters = function.at (" parameters" );
2819+ builder.resolve_refs (parameters);
2820+
2821+ // Create rule for Seed-OSS function call format
2822+ std::string param_rules;
2823+ if (parameters.contains (" properties" )) {
2824+ for (const auto & [key, value] : parameters.at (" properties" ).items ()) {
2825+ param_rules += " \" <parameter name=\\\" " + key + " \\\" >\" " + builder.add_schema (name + " -arg-" + key, value) + " \" </parameter>\" space " ;
2826+ }
2827+ }
2828+ tool_rules.push_back (builder.add_rule (name + " -call" ,
2829+ " \" <minimax:tool_call>\" space \" <invoke name=\\\" " + name + " \\\" >\" space " +
2830+ param_rules +
2831+ " \" </invoke>\" space \" </minimax:tool_call>\" " ));
2832+ });
2833+
2834+ data.grammar_triggers .push_back ({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, " <minimax:tool_call>" });
2835+
2836+ data.preserved_tokens = {
2837+ " <minimax:tool_call>" , " </minimax:tool_call>" , " <think>" , " </think>" ,
2838+ " <function" , " </function>" , " <parameter" , " </parameter>" ,
2839+ };
2840+
2841+ builder.add_rule (" root" , string_join (tool_rules, " | " ));
2842+ });
2843+ }
2844+ return data;
2845+ }
2846+
2847+ static void common_chat_parse_minimax_m2 (common_chat_msg_parser & builder) {
2848+ // Parse thinking tags first - this handles the main reasoning content
2849+ // Chat template doesn't seem to handle interleaving thinking, so we don't worry about it either
2850+ builder.try_parse_reasoning (" <think>" , " </think>" );
2851+
2852+ if (!builder.syntax ().parse_tool_calls ) {
2853+ builder.add_content (builder.consume_rest ());
2854+ return ;
2855+ }
2856+
2857+ static const std::string tool_call_tag = " minimax:tool_call" ;
2858+ static const std::string function_tag = " invoke" ;
2859+ static const std::string parameter_tag = " parameter" ;
2860+
2861+ // Parse tool calls - similar to Seed OSS (pseudo-XML), but different syntax
2862+ static const common_regex tool_call_begin_regex (" <" + tool_call_tag + " >" );
2863+ static const common_regex tool_call_end_regex (" </" + tool_call_tag + " >" );
2864+ static const common_regex function_regex (" <" + function_tag + " name=\" ([^\" ]+)\" >" );
2865+ static const common_regex param_regex (" <" + parameter_tag + " name=\" ([^\" ]+)\" >" );
2866+
2867+ while (auto tool_res = builder.try_find_regex (tool_call_begin_regex)) {
2868+ builder.consume_spaces (); // Consume whitespace after <seed:tool_call>
2869+
2870+ // Look for function call inside tool call, ignore any content before it
2871+ if (auto func_res = builder.try_find_regex (function_regex, std::string::npos, false )) {
2872+ auto function_name = builder.str (func_res->groups [1 ]);
2873+
2874+ // Parse XML parameters <parameter name=\"name\">value</parameter>
2875+ json args = json::object ();
2876+ // Parse all parameters
2877+ while (auto param_res = builder.try_find_regex (param_regex, std::string::npos, false )) {
2878+ // again, ignore noise around parameters
2879+ auto param_name = builder.str (param_res->groups [1 ]);
2880+ builder.move_to (param_res->groups [0 ].end );
2881+ builder.consume_spaces (); // Consume whitespace after parameter
2882+ auto savedPos = builder.pos ();
2883+ if (auto param_parse = builder.try_find_literal (" </" + parameter_tag + " >" )) {
2884+ auto param = param_parse->prelude ;
2885+ builder.move_to (savedPos);
2886+ try {
2887+ if (auto param_res = builder.try_consume_json ()) {
2888+ args[param_name] = param_res->json ;
2889+ } else {
2890+ args[param_name] = param;
2891+ }
2892+ } catch (json::exception &) {
2893+ args[param_name] = param;
2894+ }
2895+ } else {
2896+ throw common_chat_msg_partial_exception (" Incomplete tool parameter" );
2897+ }
2898+ }
2899+ // Look for closing function tag
2900+ auto end_func = builder.try_find_literal (" </" + function_tag + " >" );
2901+ if (end_func) {
2902+ builder.move_to (end_func->groups [0 ].end );
2903+ builder.consume_spaces (); // Consume whitespace after </function>
2904+
2905+ // Add the tool call with parsed arguments, but only if we REALLY got the literal
2906+ auto eaten_fragment = builder.input ().substr (end_func->groups [0 ].begin , end_func->groups [0 ].end );
2907+ auto funlen = std::string (" </" + function_tag + " >" ).length ();
2908+ if (eaten_fragment.length () >= funlen && eaten_fragment.substr (0 , funlen) == std::string (" </" + function_tag + " >" )) {
2909+ if (!builder.add_tool_call (function_name, " " , args.dump ())) {
2910+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2911+ }
2912+ } else {
2913+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2914+ }
2915+ } else {
2916+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2917+ }
2918+ // Look for closing tool call tag
2919+ if (auto end_tool = builder.try_find_regex (tool_call_end_regex, std::string::npos, false )) {
2920+ builder.move_to (end_tool->groups [0 ].end );
2921+ builder.consume_spaces (); // Consume trailing whitespace after tool call
2922+ } else {
2923+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2924+ }
2925+ } else {
2926+ // No function found - don't consume content here, let it be handled at the end
2927+ break ;
2928+ }
2929+ }
2930+
2931+ // Consume any remaining whitespace after all tool call processing
2932+ builder.consume_spaces ();
2933+ auto remaining = builder.consume_rest ();
2934+ // If there's any non-whitespace content remaining, add it as content
2935+ if (!string_strip (remaining).empty ()) {
2936+ builder.add_content (remaining);
2937+ }
2938+ }
2939+
27942940static common_chat_params common_chat_params_init_without_tools (const common_chat_template & tmpl, const struct templates_params & inputs) {
27952941 common_chat_params data;
27962942 data.prompt = apply (tmpl, inputs);
@@ -2942,6 +3088,11 @@ static common_chat_params common_chat_templates_apply_jinja(
29423088 return common_chat_params_init_seed_oss (tmpl, params, inputs);
29433089 }
29443090
3091+ // MiniMax M2
3092+ if (src.find (" <minimax:tool_call>" ) != std::string::npos) {
3093+ return common_chat_params_init_minimax_m2 (tmpl, params, inputs);
3094+ }
3095+
29453096 // Nemotron v2
29463097 if (src.find (" <SPECIAL_10>" ) != std::string::npos) {
29473098 return common_chat_params_init_nemotron_v2 (tmpl, params);
@@ -3139,6 +3290,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
31393290 case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS:
31403291 common_chat_parse_lfm2 (builder);
31413292 break ;
3293+ case COMMON_CHAT_FORMAT_MINIMAX_M2:
3294+ common_chat_parse_minimax_m2 (builder);
3295+ break ;
31423296 default :
31433297 throw std::runtime_error (std::string (" Unsupported format: " ) + common_chat_format_name (builder.syntax ().format ));
31443298 }
0 commit comments