@@ -612,6 +612,7 @@ const char * common_chat_format_name(common_chat_format format) {
612612 case COMMON_CHAT_FORMAT_CONTENT_ONLY: return " Content-only" ;
613613 case COMMON_CHAT_FORMAT_GENERIC: return " Generic" ;
614614 case COMMON_CHAT_FORMAT_MISTRAL_NEMO: return " Mistral Nemo" ;
615+ case COMMON_CHAT_FORMAT_MAGISTRAL: return " Magistral" ;
615616 case COMMON_CHAT_FORMAT_LLAMA_3_X: return " Llama 3.x" ;
616617 case COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS: return " Llama 3.x with builtin tools" ;
617618 case COMMON_CHAT_FORMAT_DEEPSEEK_R1: return " DeepSeek R1" ;
@@ -625,6 +626,7 @@ const char * common_chat_format_name(common_chat_format format) {
625626 case COMMON_CHAT_FORMAT_GPT_OSS: return " GPT-OSS" ;
626627 case COMMON_CHAT_FORMAT_SEED_OSS: return " Seed-OSS" ;
627628 case COMMON_CHAT_FORMAT_NEMOTRON_V2: return " Nemotron V2" ;
629+ case COMMON_CHAT_FORMAT_APERTUS: return " Apertus" ;
628630 default :
629631 throw std::runtime_error (" Unknown chat format" );
630632 }
@@ -788,6 +790,7 @@ static std::string apply(
788790 }
789791 tmpl_inputs.add_generation_prompt = inputs.add_generation_prompt ;
790792 tmpl_inputs.extra_context = inputs.extra_context ;
793+ tmpl_inputs.extra_context [" enable_thinking" ] = inputs.enable_thinking ;
791794 if (additional_context) {
792795 tmpl_inputs.extra_context .merge_patch (*additional_context);
793796 }
@@ -968,6 +971,65 @@ static common_chat_params common_chat_params_init_mistral_nemo(const common_chat
968971 data.format = COMMON_CHAT_FORMAT_MISTRAL_NEMO;
969972 return data;
970973}
974+
975+ static common_chat_params common_chat_params_init_magistral (const common_chat_template & tmpl, const struct templates_params & inputs) {
976+ common_chat_params data;
977+ data.prompt = apply (tmpl, inputs);
978+ data.format = COMMON_CHAT_FORMAT_MAGISTRAL;
979+ data.preserved_tokens = {
980+ " [THINK]" ,
981+ " [/THINK]" ,
982+ };
983+
984+ if (inputs.tools .is_array () && !inputs.tools .empty ()) {
985+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
986+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
987+ auto schemas = json::array ();
988+ foreach_function (inputs.tools , [&](const json & tool) {
989+ const auto & function = tool.at (" function" );
990+ schemas.push_back ({
991+ {" type" , " object" },
992+ {" properties" , {
993+ {" name" , {
994+ {" type" , " string" },
995+ {" const" , function.at (" name" )},
996+ }},
997+ {" arguments" , function.at (" parameters" )},
998+ {" id" , {
999+ {" type" , " string" },
1000+ {" pattern" , " ^[a-zA-Z0-9]{9}$" },
1001+ }},
1002+ }},
1003+ {" required" , json::array ({" name" , " arguments" , " id" })},
1004+ });
1005+ });
1006+ auto schema = json {
1007+ {" type" , " array" },
1008+ {" items" , schemas.size () == 1 ? schemas[0 ] : json {{" anyOf" , schemas}}},
1009+ {" minItems" , 1 },
1010+ };
1011+ if (!inputs.parallel_tool_calls ) {
1012+ schema[" maxItems" ] = 1 ;
1013+ }
1014+ builder.add_rule (" root" , " \" [TOOL_CALLS]\" " + builder.add_schema (" tool_calls" , schema));
1015+ });
1016+ data.grammar_triggers .push_back ({COMMON_GRAMMAR_TRIGGER_TYPE_WORD, " [TOOL_CALLS]" });
1017+ data.preserved_tokens .push_back (" [TOOL_CALLS]" );
1018+ } else {
1019+ data.grammar_lazy = false ;
1020+ if (!inputs.json_schema .is_null ()) {
1021+ if (!inputs.grammar .empty ()) {
1022+ throw std::runtime_error (" Either \" json_schema\" or \" grammar\" can be specified, but not both" );
1023+ }
1024+ data.grammar = json_schema_to_grammar (inputs.json_schema );
1025+ } else {
1026+ data.grammar = inputs.grammar ;
1027+ }
1028+ }
1029+
1030+ return data;
1031+ }
1032+
9711033static void common_chat_parse_mistral_nemo (common_chat_msg_parser & builder) {
9721034 if (!builder.syntax ().parse_tool_calls ) {
9731035 builder.add_content (builder.consume_rest ());
@@ -978,6 +1040,18 @@ static void common_chat_parse_mistral_nemo(common_chat_msg_parser & builder) {
9781040 parse_prefixed_json_tool_call_array (builder, prefix);
9791041}
9801042
1043+ static void common_chat_parse_magistral (common_chat_msg_parser & builder) {
1044+ builder.try_parse_reasoning (" [THINK]" , " [/THINK]" );
1045+
1046+ if (!builder.syntax ().parse_tool_calls ) {
1047+ builder.add_content (builder.consume_rest ());
1048+ return ;
1049+ }
1050+
1051+ static const common_regex prefix (regex_escape (" [TOOL_CALLS]" ));
1052+ parse_prefixed_json_tool_call_array (builder, prefix);
1053+ }
1054+
9811055static common_chat_params common_chat_params_init_command_r7b (const common_chat_template & tmpl, const struct templates_params & inputs) {
9821056 common_chat_params data;
9831057
@@ -1250,6 +1324,75 @@ static common_chat_params common_chat_params_init_nemotron_v2(const common_chat_
12501324 }
12511325 return data;
12521326}
1327+
1328+ static common_chat_params common_chat_params_init_apertus (const common_chat_template & tmpl, const struct templates_params & inputs) {
1329+ common_chat_params data;
1330+
1331+ // Generate the prompt using the apply() function with the template
1332+ data.prompt = apply (tmpl, inputs);
1333+ data.format = COMMON_CHAT_FORMAT_APERTUS;
1334+
1335+ // Handle thinking tags appropriately based on inputs.enable_thinking
1336+ if (string_ends_with (data.prompt , " <|inner_prefix|>" )) {
1337+ if (!inputs.enable_thinking ) {
1338+ data.prompt += " <|inner_suffix|>" ;
1339+ } else {
1340+ data.thinking_forced_open = true ;
1341+ }
1342+ }
1343+
1344+ // When tools are present, build grammar for the <|tools_prefix|> format
1345+ if (!inputs.tools .is_null () && inputs.tools .is_array () && !inputs.tools .empty ()) {
1346+ data.grammar_lazy = true ;
1347+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
1348+ auto schemas = json::array ();
1349+ foreach_function (inputs.tools , [&](const json & tool) {
1350+ const auto & function = tool.at (" function" );
1351+ schemas.push_back ({
1352+ { " type" , " object" },
1353+ { " properties" ,
1354+ {
1355+ { function.at (" name" ), function.at (" parameters" ) }
1356+ } },
1357+ { " required" , json::array ({ function.at (" name" ) }) },
1358+ });
1359+ });
1360+ auto schema = json{
1361+ { " type" , " array" },
1362+ { " items" , schemas.size () == 1 ? schemas[0 ] : json{ { " anyOf" , schemas } } },
1363+ { " minItems" , 1 },
1364+ };
1365+ if (!inputs.parallel_tool_calls ) {
1366+ schema[" maxItems" ] = 1 ;
1367+ }
1368+ builder.add_rule (" root" ,
1369+ std::string (data.thinking_forced_open ? " ( \" <|inner_suffix|>\" space )? " : " " ) +
1370+ " \" <|tools_prefix|>\" " + builder.add_schema (" tool_calls" , schema) + " \" <|tools_suffix|>\" " );
1371+ });
1372+ data.grammar_triggers .push_back ({ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
1373+ // If thinking_forced_open, then we capture the <|inner_suffix|> tag in the grammar,
1374+ // (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
1375+ std::string (data.thinking_forced_open ?
1376+ " [\\ s\\ S]*?(<\\ |inner_suffix\\ |>\\ s*)" :
1377+ " (?:<\\ |inner_prefix\\ |>[\\ s\\ S]*?<\\ |inner_suffix\\ |>\\ s*)?" ) +
1378+ " (<\\ |tools_prefix\\ |>)[\\ s\\ S]*" });
1379+ data.preserved_tokens = {
1380+ " <|system_start|>" ,
1381+ " <|system_end|>" ,
1382+ " <|developer_start|>" ,
1383+ " <|developer_end|>" ,
1384+ " <|user_start|>" ,
1385+ " <|user_end|>" ,
1386+ " <|assistant_start|>" ,
1387+ " <|assistant_end|>" ,
1388+ " <|inner_prefix|>" ,
1389+ " <|inner_suffix|>" ,
1390+ " <|tools_prefix|>" ,
1391+ " <|tools_suffix|>" ,
1392+ };
1393+ }
1394+ return data;
1395+ }
12531396static void common_chat_parse_llama_3_1 (common_chat_msg_parser & builder, bool with_builtin_tools = false ) {
12541397 if (!builder.syntax ().parse_tool_calls ) {
12551398 builder.add_content (builder.consume_rest ());
@@ -2309,6 +2452,37 @@ static void common_chat_parse_nemotron_v2(common_chat_msg_parser & builder) {
23092452 builder.add_content (builder.consume_rest ());
23102453}
23112454
2455+ static void common_chat_parse_apertus (common_chat_msg_parser & builder) {
2456+ // Parse thinking tags
2457+ builder.try_parse_reasoning (" <|inner_prefix|>" , " <|inner_suffix|>" );
2458+ if (!builder.syntax ().parse_tool_calls ) {
2459+ builder.add_content (builder.consume_rest ());
2460+ return ;
2461+ }
2462+
2463+ // Look for tool calls
2464+ static const common_regex tool_call_regex (regex_escape (" <|tools_prefix|>" ));
2465+ if (auto res = builder.try_find_regex (tool_call_regex)) {
2466+ builder.move_to (res->groups [0 ].end );
2467+
2468+ auto tool_calls_data = builder.consume_json ();
2469+ if (tool_calls_data.json .is_array ()) {
2470+ builder.consume_spaces ();
2471+ if (!builder.try_consume_literal (" <|tools_suffix|>" )) {
2472+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2473+ }
2474+ for (const auto & value : tool_calls_data.json ) {
2475+ if (value.is_object ()) {
2476+ builder.add_tool_call_short_form (value);
2477+ }
2478+ }
2479+ } else {
2480+ throw common_chat_msg_partial_exception (" Incomplete tool call" );
2481+ }
2482+ }
2483+ builder.add_content (builder.consume_rest ());
2484+ }
2485+
23122486static void common_chat_parse_seed_oss (common_chat_msg_parser & builder) {
23132487 // Parse thinking tags first - this handles the main reasoning content
23142488 builder.try_parse_reasoning (" <seed:think>" , " </seed:think>" );
@@ -2553,6 +2727,11 @@ static common_chat_params common_chat_templates_apply_jinja(
25532727 return common_chat_params_init_nemotron_v2 (tmpl, params);
25542728 }
25552729
2730+ // Apertus format detection
2731+ if (src.find (" <|system_start|>" ) != std::string::npos && src.find (" <|tools_prefix|>" ) != std::string::npos) {
2732+ return common_chat_params_init_apertus (tmpl, params);
2733+ }
2734+
25562735 // Use generic handler when mixing tools + JSON schema.
25572736 // TODO: support that mix in handlers below.
25582737 if ((params.tools .is_array () && params.json_schema .is_object ())) {
@@ -2581,6 +2760,10 @@ static common_chat_params common_chat_templates_apply_jinja(
25812760 return common_chat_params_init_llama_3_x (tmpl, params, allow_python_tag_builtin_tools);
25822761 }
25832762
2763+ if (src.find (" [THINK]" ) != std::string::npos && src.find (" [/THINK]" ) != std::string::npos) {
2764+ return common_chat_params_init_magistral (tmpl, params);
2765+ }
2766+
25842767 // Plain handler (no tools)
25852768 if (params.tools .is_null () || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
25862769 return common_chat_params_init_without_tools (tmpl, params);
@@ -2681,6 +2864,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
26812864 case COMMON_CHAT_FORMAT_MISTRAL_NEMO:
26822865 common_chat_parse_mistral_nemo (builder);
26832866 break ;
2867+ case COMMON_CHAT_FORMAT_MAGISTRAL:
2868+ common_chat_parse_magistral (builder);
2869+ break ;
26842870 case COMMON_CHAT_FORMAT_LLAMA_3_X:
26852871 common_chat_parse_llama_3_1 (builder);
26862872 break ;
@@ -2720,6 +2906,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
27202906 case COMMON_CHAT_FORMAT_NEMOTRON_V2:
27212907 common_chat_parse_nemotron_v2 (builder);
27222908 break ;
2909+ case COMMON_CHAT_FORMAT_APERTUS:
2910+ common_chat_parse_apertus (builder);
2911+ break ;
27232912 default :
27242913 throw std::runtime_error (std::string (" Unsupported format: " ) + common_chat_format_name (builder.syntax ().format ));
27252914 }
0 commit comments