@@ -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" ] = inputs.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" ), function.at (" parameters" ) }
1298+ } },
1299+ { " required" , json::array ({ function.at (" name" ) }) },
1300+ });
1301+ });
1302+ auto schema = json{
1303+ { " type" , " array" },
1304+ { " items" , schemas.size () == 1 ? schemas[0 ] : json{ { " anyOf" , schemas } } },
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|>\" " + builder.add_schema (" tool_calls" , schema) + " \" <|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|>" ) != std::string::npos && src.find (" <|tools_prefix|>" ) != std::string::npos) {
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