@@ -296,6 +296,7 @@ json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & msg
296296 }
297297 if (!msg.reasoning_content .empty ()) {
298298 jmsg[" reasoning_content" ] = msg.reasoning_content ;
299+ jmsg[" thinking" ] = msg.reasoning_content ; // gpt-oss
299300 }
300301 if (!msg.tool_name .empty ()) {
301302 jmsg[" name" ] = msg.tool_name ;
@@ -472,11 +473,12 @@ std::string common_chat_format_single(
472473 return ss.str ();
473474}
474475
475- std::string common_chat_format_example (const struct common_chat_templates * tmpls, bool use_jinja) {
476+ std::string common_chat_format_example (const struct common_chat_templates * tmpls, bool use_jinja, const std::map<std::string, std::string> & chat_template_kwargs ) {
476477 common_chat_templates_inputs inputs;
477478 inputs.use_jinja = use_jinja;
478479 inputs.add_bos = tmpls->add_bos ;
479480 inputs.add_eos = tmpls->add_eos ;
481+ inputs.chat_template_kwargs = chat_template_kwargs;
480482 auto add_simple_msg = [&](auto role, auto content) {
481483 common_chat_msg msg;
482484 msg.role = role;
@@ -1338,16 +1340,164 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp
13381340 data.prompt = prompt;
13391341 data.format = COMMON_CHAT_FORMAT_GPT_OSS;
13401342
1341- // TODO: support tool calls in GPT-OSS?
1343+ // These special tokens are required to parse properly, so we include them
1344+ // even if parse_tool_calls is false.
1345+ data.preserved_tokens = {
1346+ " <|channel|>" ,
1347+ " <|constrain|>" ,
1348+ " <|message|>" ,
1349+ " <|start|>" ,
1350+ " <|end|>" ,
1351+ };
1352+
1353+ if (inputs.tools .is_array () && !inputs.tools .empty ()) {
1354+ data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
1355+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
1356+ // tool calls can appear in commentary or analysis channels
1357+ auto channel = builder.add_rule (" channel" , " \" <|channel|>\" ( \" commentary\" | \" analysis\" )" );
1358+
1359+ std::vector<std::string> tool_rules_recipient_in_role;
1360+ std::vector<std::string> tool_rules_recipient_in_channel;
1361+ foreach_function (inputs.tools , [&](const json & tool) {
1362+ const auto & function = tool.at (" function" );
1363+ std::string name = function.at (" name" );
1364+ auto parameters = function.at (" parameters" );
1365+ builder.resolve_refs (parameters);
1366+
1367+ tool_rules_recipient_in_role.push_back (
1368+ builder.add_rule (name + " -call" ,
1369+ " \" " + name + " \" " + channel + " \" <|constrain|>json\" ? \" <|message|>\" " +
1370+ builder.add_schema (name + " -args" , parameters)
1371+ )
1372+ );
1373+
1374+ tool_rules_recipient_in_channel.push_back (
1375+ builder.add_rule (name + " -call" ,
1376+ " \" " + name + " \" " + " \" <|constrain|>json\" ? \" <|message|>\" " +
1377+ builder.add_schema (name + " -args" , parameters)
1378+ )
1379+ );
1380+ });
1381+
1382+ auto recipient_in_role = builder.add_rule (" recipient_in_role" ,
1383+ " \" <|start|>assistant\" ? \" to=functions.\" ( " +
1384+ string_join (tool_rules_recipient_in_role, " | " ) + " )"
1385+ );
1386+
1387+ auto recipient_in_channel = builder.add_rule (" recipient_in_channel" ,
1388+ channel + " \" to=functions.\" ( " +
1389+ string_join (tool_rules_recipient_in_channel, " | " ) + " )"
1390+ );
1391+
1392+ builder.add_rule (" root" , recipient_in_role + " | " + recipient_in_channel);
1393+
1394+ // Trigger on tool calls that appear in the commentary channel
1395+ data.grammar_triggers .push_back ({
1396+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
1397+ " <\\ |channel\\ |>(commentary|analysis) to"
1398+ });
1399+
1400+ // Trigger tool calls that appear in the role section, either at the
1401+ // start or in the middle.
1402+ data.grammar_triggers .push_back ({
1403+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL,
1404+ " ^ to"
1405+ });
1406+
1407+ data.grammar_triggers .push_back ({
1408+ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN,
1409+ " <\\ |start\\ |>assistant to"
1410+ });
1411+ });
1412+ }
13421413
13431414 return data;
13441415}
13451416static void common_chat_parse_gpt_oss (common_chat_msg_parser & builder) {
1346- // TODO @ngxson : this won't work with --special enabled, we should fix that
1347- builder.try_parse_reasoning (" <|channel|>analysis<|message|>" , " <|start|>assistant<|channel|>final<|message|>" );
1348- if (!builder.syntax ().parse_tool_calls ) {
1349- builder.add_content (builder.consume_rest ());
1350- return ;
1417+ static const std::string constraint = " (?: (<\\ |constrain\\ |>)?([a-zA-Z0-9_-]+))" ;
1418+ static const std::string recipient (" (?: to=functions\\ .([^<\\ s]+))" );
1419+
1420+ static const common_regex start_regex (" <\\ |start\\ |>assistant" );
1421+ static const common_regex analysis_regex (" <\\ |channel\\ |>analysis" );
1422+ static const common_regex final_regex (" <\\ |channel\\ |>final" + constraint + " ?" );
1423+ static const common_regex preamble_regex (" <\\ |channel\\ |>commentary" );
1424+ static const common_regex tool_call1_regex (recipient + " <\\ |channel\\ |>(analysis|commentary)" + constraint + " ?" );
1425+ static const common_regex tool_call2_regex (" <\\ |channel\\ |>(analysis|commentary)" + recipient + constraint + " ?" );
1426+
1427+ auto consume_end = [&](bool include_end = false ) {
1428+ if (auto res = builder.try_find_literal (" <|end|>" )) {
1429+ return res->prelude + (include_end ? builder.str (res->groups [0 ]) : " " );
1430+ }
1431+ return builder.consume_rest ();
1432+ };
1433+
1434+ auto handle_tool_call = [&](const std::string & name) {
1435+ if (auto args = builder.try_consume_json_with_dumped_args ({{}})) {
1436+ if (builder.syntax ().parse_tool_calls ) {
1437+ if (!builder.add_tool_call (name, " " , args->value ) || args->is_partial ) {
1438+ throw common_chat_msg_partial_exception (" incomplete tool call" );
1439+ }
1440+ } else if (args->is_partial ) {
1441+ throw common_chat_msg_partial_exception (" incomplete tool call" );
1442+ }
1443+ }
1444+ };
1445+
1446+ auto regex_match = [](const common_regex & regex, const std::string & input) -> std::optional<common_regex_match> {
1447+ auto match = regex.search (input, 0 , true );
1448+ if (match.type == COMMON_REGEX_MATCH_TYPE_FULL) {
1449+ return match;
1450+ }
1451+ return std::nullopt ;
1452+ };
1453+
1454+ do {
1455+ auto header_start_pos = builder.pos ();
1456+ auto content_start = builder.try_find_literal (" <|message|>" );
1457+ if (!content_start) {
1458+ throw common_chat_msg_partial_exception (" incomplete header" );
1459+ }
1460+
1461+ auto header = content_start->prelude ;
1462+
1463+ if (auto match = regex_match (tool_call1_regex, header)) {
1464+ auto group = match->groups [1 ];
1465+ auto name = header.substr (group.begin , group.end - group.begin );
1466+ handle_tool_call (name);
1467+ continue ;
1468+ }
1469+
1470+ if (auto match = regex_match (tool_call2_regex, header)) {
1471+ auto group = match->groups [2 ];
1472+ auto name = header.substr (group.begin , group.end - group.begin );
1473+ handle_tool_call (name);
1474+ continue ;
1475+ }
1476+
1477+ if (regex_match (analysis_regex, header)) {
1478+ builder.move_to (header_start_pos);
1479+ if (builder.syntax ().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax ().reasoning_in_content ) {
1480+ builder.add_content (consume_end (true ));
1481+ } else {
1482+ builder.try_parse_reasoning (" <|channel|>analysis<|message|>" , " <|end|>" );
1483+ }
1484+ continue ;
1485+ }
1486+
1487+ if (regex_match (final_regex, header) || regex_match (preamble_regex, header)) {
1488+ builder.add_content (consume_end ());
1489+ continue ;
1490+ }
1491+
1492+ // Possibly a malformed message, attempt to recover by rolling
1493+ // back to pick up the next <|start|>
1494+ LOG_DBG (" %s: unknown header from message: %s\n " , __func__, header.c_str ());
1495+ builder.move_to (header_start_pos);
1496+ } while (builder.try_find_regex (start_regex, std::string::npos, false ));
1497+
1498+ auto remaining = builder.consume_rest ();
1499+ if (!remaining.empty ()) {
1500+ LOG_DBG (" %s: content after last message: %s\n " , __func__, remaining.c_str ());
13511501 }
13521502}
13531503
@@ -1911,8 +2061,8 @@ static common_chat_params common_chat_templates_apply_jinja(
19112061 params.enable_thinking = inputs.enable_thinking ;
19122062 params.grammar = inputs.grammar ;
19132063 params.now = inputs.now ;
1914- params.add_bos = inputs. add_bos ;
1915- params.add_eos = inputs. add_eos ;
2064+ params.add_bos = tmpls-> add_bos ;
2065+ params.add_eos = tmpls-> add_eos ;
19162066
19172067 params.extra_context = json::object ();
19182068 for (auto el : inputs.chat_template_kwargs ) {
0 commit comments