@@ -638,6 +638,7 @@ const char * common_chat_format_name(common_chat_format format) {
638638        case  COMMON_CHAT_FORMAT_GPT_OSS: return  " GPT-OSS" 
639639        case  COMMON_CHAT_FORMAT_SEED_OSS: return  " Seed-OSS" 
640640        case  COMMON_CHAT_FORMAT_NEMOTRON_V2: return  " Nemotron V2" 
641+         case  COMMON_CHAT_FORMAT_APERTUS: return  " Apertus" 
641642        default :
642643            throw  std::runtime_error (" Unknown chat format" 
643644    }
@@ -801,6 +802,7 @@ static std::string apply(
801802    }
802803    tmpl_inputs.add_generation_prompt  = inputs.add_generation_prompt ;
803804    tmpl_inputs.extra_context  = inputs.extra_context ;
805+     tmpl_inputs.extra_context [" enable_thinking" enable_thinking ;
804806    if  (additional_context) {
805807        tmpl_inputs.extra_context .merge_patch (*additional_context);
806808    }
@@ -1264,6 +1266,75 @@ static common_chat_params common_chat_params_init_nemotron_v2(const common_chat_
12641266    }
12651267    return  data;
12661268}
1269+ 
1270+ static  common_chat_params common_chat_params_init_apertus (const  common_chat_template & tmpl, const  struct  templates_params  & inputs) {
1271+     common_chat_params data;
1272+ 
1273+     //  Generate the prompt using the apply() function with the template
1274+     data.prompt  = apply (tmpl, inputs);
1275+     data.format  = COMMON_CHAT_FORMAT_APERTUS;
1276+ 
1277+     //  Handle thinking tags appropriately based on inputs.enable_thinking
1278+     if  (string_ends_with (data.prompt , " <|inner_prefix|>" 
1279+         if  (!inputs.enable_thinking ) {
1280+             data.prompt  += " <|inner_suffix|>" 
1281+         } else  {
1282+             data.thinking_forced_open  = true ;
1283+         }
1284+     }
1285+ 
1286+     //  When tools are present, build grammar for the <|tools_prefix|> format
1287+     if  (!inputs.tools .is_null () && inputs.tools .is_array () && !inputs.tools .empty ()) {
1288+         data.grammar_lazy  = true ;
1289+         data.grammar       = build_grammar ([&](const  common_grammar_builder & builder) {
1290+             auto  schemas = json::array ();
1291+             foreach_function (inputs.tools , [&](const  json & tool) {
1292+                 const  auto  & function = tool.at (" function" 
1293+                 schemas.push_back ({
1294+                     { " type" " object" 
1295+                     { " properties" 
1296+                         {
1297+                             { function.at (" name" at (" parameters" 
1298+                         }                                                                        },
1299+                     { " required" json::array ({ function.at (" name" 
1300+                 });
1301+             });
1302+             auto  schema = json{
1303+                         { " type" " array" 
1304+                         { " items" size () == 1  ? schemas[0 ] : json{ { " anyOf" 
1305+                         { " minItems" 1                                                                },
1306+             };
1307+             if  (!inputs.parallel_tool_calls ) {
1308+                 schema[" maxItems" 1 ;
1309+             }
1310+             builder.add_rule (" root" 
1311+                                 std::string (data.thinking_forced_open  ? " ( \" <|inner_suffix|>\"  space )? " " " 
1312+                                     " \" <|tools_prefix|>\" " add_schema (" tool_calls" " \" <|tools_suffix|>\" " 
1313+                             });
1314+         data.grammar_triggers .push_back ({ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
1315+             //  If thinking_forced_open, then we capture the <|inner_suffix|> tag in the grammar,
1316+             //  (important for required tool choice) and in the trigger's first capture (decides what is sent to the grammar)
1317+             std::string (data.thinking_forced_open  ?
1318+                             " [\\ s\\ S]*?(<\\ |inner_suffix\\ |>\\ s*)" 
1319+                             " (?:<\\ |inner_prefix\\ |>[\\ s\\ S]*?<\\ |inner_suffix\\ |>\\ s*)?" 
1320+                 " (<\\ |tools_prefix\\ |>)[\\ s\\ S]*" 
1321+         data.preserved_tokens  = {
1322+             " <|system_start|>" 
1323+             " <|system_end|>" 
1324+             " <|developer_start|>" 
1325+             " <|developer_end|>" 
1326+             " <|user_start|>" 
1327+             " <|user_end|>" 
1328+             " <|assistant_start|>" 
1329+             " <|assistant_end|>" 
1330+             " <|inner_prefix|>" 
1331+             " <|inner_suffix|>" 
1332+             " <|tools_prefix|>" 
1333+             " <|tools_suffix|>" 
1334+         };
1335+     }
1336+     return  data;
1337+ }
12671338static  void  common_chat_parse_llama_3_1 (common_chat_msg_parser & builder, bool  with_builtin_tools = false ) {
12681339    if  (!builder.syntax ().parse_tool_calls ) {
12691340        builder.add_content (builder.consume_rest ());
@@ -2323,6 +2394,37 @@ static void common_chat_parse_nemotron_v2(common_chat_msg_parser & builder) {
23232394    builder.add_content (builder.consume_rest ());
23242395}
23252396
2397+ static  void  common_chat_parse_apertus (common_chat_msg_parser & builder) {
2398+     //  Parse thinking tags
2399+     builder.try_parse_reasoning (" <|inner_prefix|>" " <|inner_suffix|>" 
2400+     if  (!builder.syntax ().parse_tool_calls ) {
2401+         builder.add_content (builder.consume_rest ());
2402+         return ;
2403+     }
2404+ 
2405+     //  Look for tool calls
2406+     static  const  common_regex tool_call_regex (regex_escape (" <|tools_prefix|>" 
2407+     if  (auto  res = builder.try_find_regex (tool_call_regex)) {
2408+         builder.move_to (res->groups [0 ].end );
2409+ 
2410+         auto  tool_calls_data = builder.consume_json ();
2411+         if  (tool_calls_data.json .is_array ()) {
2412+             builder.consume_spaces ();
2413+             if  (!builder.try_consume_literal (" <|tools_suffix|>" 
2414+                 throw  common_chat_msg_partial_exception (" Incomplete tool call" 
2415+             }
2416+             for  (const  auto  & value : tool_calls_data.json ) {
2417+                 if  (value.is_object ()) {
2418+                     builder.add_tool_call_short_form (value);
2419+                 }
2420+             }
2421+         } else  {
2422+             throw  common_chat_msg_partial_exception (" Incomplete tool call" 
2423+         }
2424+     }
2425+     builder.add_content (builder.consume_rest ());
2426+ }
2427+ 
23262428static  void  common_chat_parse_seed_oss (common_chat_msg_parser & builder) {
23272429    //  Parse thinking tags first - this handles the main reasoning content
23282430    builder.try_parse_reasoning (" <seed:think>" " </seed:think>" 
@@ -2567,6 +2669,11 @@ static common_chat_params common_chat_templates_apply_jinja(
25672669        return  common_chat_params_init_nemotron_v2 (tmpl, params);
25682670    }
25692671
2672+     //  Apertus format detection
2673+     if  (src.find (" <|system_start|>" find (" <|tools_prefix|>" 
2674+         return  common_chat_params_init_apertus (tmpl, params);
2675+     }
2676+ 
25702677    //  Use generic handler when mixing tools + JSON schema.
25712678    //  TODO: support that mix in handlers below.
25722679    if  ((params.tools .is_array () && params.json_schema .is_object ())) {
@@ -2734,6 +2841,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
27342841        case  COMMON_CHAT_FORMAT_NEMOTRON_V2:
27352842            common_chat_parse_nemotron_v2 (builder);
27362843            break ;
2844+         case  COMMON_CHAT_FORMAT_APERTUS:
2845+             common_chat_parse_apertus (builder);
2846+             break ;
27372847        default :
27382848            throw  std::runtime_error (std::string (" Unsupported format: " common_chat_format_name (builder.syntax ().format ));
27392849    }
0 commit comments