using json = nlohmann::ordered_json;
+static void parse_prefixed_json_tool_call_array(common_chat_msg_parser & builder,
+ const common_regex & prefix,
+ size_t rstrip_prefix = 0) {
+ static const std::vector<std::vector<std::string>> args_paths = { { "arguments" } };
+ if (auto res = builder.try_find_regex(prefix)) {
+ builder.move_back(rstrip_prefix);
+ auto tool_calls = builder.consume_json_with_dumped_args(args_paths);
+ if (!builder.add_tool_calls(tool_calls.value) || tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call array");
+ }
+ } else {
+ builder.add_content(builder.consume_rest());
+ }
+}
+
+static std::string wrap_code_as_arguments(common_chat_msg_parser & builder, const std::string & code) {
+ std::string arguments;
+ if (builder.is_partial()) {
+ arguments = (json{
+ { "code", code + builder.healing_marker() }
+ })
+ .dump();
+ auto idx = arguments.find(builder.healing_marker());
+ if (idx != std::string::npos) {
+ arguments.resize(idx);
+ }
+ } else {
+ arguments = (json{
+ { "code", code }
+ })
+ .dump();
+ }
+ return arguments;
+}
+
+/**
+ * Takes a prefix regex that must have 1 group to capture the function name, a closing suffix, and expects json parameters in between.
+ * Aggregates the prefix, suffix and in-between text into the content.
+ */
+static void parse_json_tool_calls(
+ common_chat_msg_parser & builder,
+ const std::optional<common_regex> & block_open,
+ const std::optional<common_regex> & function_regex_start_only,
+ const std::optional<common_regex> & function_regex,
+ const common_regex & close_regex,
+ const std::optional<common_regex> & block_close,
+ bool allow_raw_python = false,
+ const std::function<std::string(const common_chat_msg_parser::find_regex_result & fres)> & get_function_name =
+ nullptr) {
+ auto parse_tool_calls = [&]() {
+ size_t from = std::string::npos;
+ auto first = true;
+ while (true) {
+ auto start_pos = builder.pos();
+ auto res = function_regex_start_only && first ? builder.try_consume_regex(*function_regex_start_only) :
+ function_regex ? builder.try_find_regex(*function_regex, from) :
+ std::nullopt;
+
+ if (res) {
+ std::string name;
+ if (get_function_name) {
+ name = get_function_name(*res);
+ } else {
+ GGML_ASSERT(res->groups.size() == 2);
+ name = builder.str(res->groups[1]);
+ }
+ first = false;
+ if (name.empty()) {
+ // get_function_name signalled us that we should skip this match and treat it as content.
+ from = res->groups[0].begin + 1;
+ continue;
+ }
+ from = std::string::npos;
+
+ auto maybe_raw_python = name == "python" && allow_raw_python;
+ if (builder.input()[builder.pos()] == '{' || !maybe_raw_python) {
+ if (auto arguments = builder.try_consume_json_with_dumped_args({ {} })) {
+ if (!builder.add_tool_call(name, "", arguments->value) || arguments->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_regex(close_regex);
+ }
+ continue;
+ }
+ if (maybe_raw_python) {
+ auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
+ if (!builder.add_tool_call(name, "", arguments)) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ return;
+ }
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ } else {
+ builder.move_to(start_pos);
+ }
+ break;
+ }
+ if (block_close) {
+ builder.consume_regex(*block_close);
+ }
+ builder.consume_spaces();
+ builder.add_content(builder.consume_rest());
+ };
+ if (block_open) {
+ if (auto res = builder.try_find_regex(*block_open)) {
+ parse_tool_calls();
+ } else {
+ builder.add_content(builder.consume_rest());
+ }
+ } else {
+ parse_tool_calls();
+ }
+}
+
common_chat_msg_parser::common_chat_msg_parser(const std::string & input, bool is_partial, const common_chat_syntax & syntax)
: input_(input), is_partial_(is_partial), syntax_(syntax)
{
void common_chat_msg_parser::clear_tools() {
result_.tool_calls.clear();
}
+
+/**
+ * All common_chat_parse_* moved from chat.cpp to chat-parser.cpp below
+ * to reduce incremental compile time for parser changes.
+ */
+static void common_chat_parse_generic(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ static const std::vector<std::vector<std::string>> content_paths = {
+ {"response"},
+ };
+ static const std::vector<std::vector<std::string>> args_paths = {
+ {"tool_call", "arguments"},
+ {"tool_calls", "arguments"},
+ };
+ auto data = builder.consume_json_with_dumped_args(args_paths, content_paths);
+ if (data.value.contains("tool_calls")) {
+ if (!builder.add_tool_calls(data.value.at("tool_calls")) || data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool calls");
+ }
+ } else if (data.value.contains("tool_call")) {
+ if (!builder.add_tool_call(data.value.at("tool_call")) || data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ } else if (data.value.contains("response")) {
+ const auto & response = data.value.at("response");
+ builder.add_content(response.is_string() ? response.template get<std::string>() : response.dump(2));
+ if (data.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete response");
+ }
+ } else {
+ throw common_chat_msg_partial_exception("Expected 'tool_call', 'tool_calls' or 'response' in JSON");
+ }
+}
+
+static void common_chat_parse_mistral_nemo(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex prefix(regex_escape("[TOOL_CALLS]"));
+ parse_prefixed_json_tool_call_array(builder, prefix);
+}
+
+static void common_chat_parse_magistral(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("[THINK]", "[/THINK]");
+
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex prefix(regex_escape("[TOOL_CALLS]"));
+ parse_prefixed_json_tool_call_array(builder, prefix);
+}
+
+static void common_chat_parse_command_r7b(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("<|START_THINKING|>", "<|END_THINKING|>");
+
+ static const common_regex start_action_regex("<\\|START_ACTION\\|>");
+ static const common_regex end_action_regex("<\\|END_ACTION\\|>");
+ static const common_regex start_response_regex("<\\|START_RESPONSE\\|>");
+ static const common_regex end_response_regex("<\\|END_RESPONSE\\|>");
+
+ if (auto res = builder.try_find_regex(start_action_regex)) {
+ // If we didn't extract thoughts, prelude includes them.
+ auto tool_calls = builder.consume_json_with_dumped_args({{"parameters"}});
+ for (const auto & tool_call : tool_calls.value) {
+ std::string name = tool_call.contains("tool_name") ? tool_call.at("tool_name") : "";
+ std::string id = tool_call.contains("tool_call_id") ? tool_call.at("tool_call_id") : "";
+ std::string arguments = tool_call.contains("parameters") ? tool_call.at("parameters") : "";
+ if (!builder.add_tool_call(name, id, arguments) || tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ }
+ if (tool_calls.is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_regex(end_action_regex);
+ } else if (auto res = builder.try_find_regex(start_response_regex)) {
+ if (!builder.try_find_regex(end_response_regex)) {
+ builder.add_content(builder.consume_rest());
+ throw common_chat_msg_partial_exception(end_response_regex.str());
+ }
+ } else {
+ builder.add_content(builder.consume_rest());
+ }
+}
+
+static void common_chat_parse_llama_3_1(common_chat_msg_parser & builder, bool with_builtin_tools = false) {
+ builder.try_parse_reasoning("<think>", "</think>");
+
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex function_regex(
+ "\\s*\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\"([^\"]+)\"\\s*,\\s*\"parameters\"\\s*: ");
+ static const common_regex close_regex("\\}\\s*");
+
+ static const common_regex function_name_regex("\\s*(\\w+)\\s*\\.\\s*call\\(");
+ static const common_regex arg_name_regex("\\s*(\\w+)\\s*=\\s*");
+
+ if (with_builtin_tools) {
+ static const common_regex builtin_call_regex("<\\|python_tag\\|>");
+ if (auto res = builder.try_find_regex(builtin_call_regex)) {
+ auto fun_res = builder.consume_regex(function_name_regex);
+ auto function_name = builder.str(fun_res.groups[1]);
+
+ common_healing_marker healing_marker;
+ json args = json::object();
+ while (true) {
+ if (auto arg_res = builder.try_consume_regex(arg_name_regex)) {
+ auto arg_name = builder.str(arg_res->groups[1]);
+ auto partial = builder.consume_json();
+ args[arg_name] = partial.json;
+ healing_marker.marker = partial.healing_marker.marker;
+ healing_marker.json_dump_marker = partial.healing_marker.json_dump_marker;
+ builder.consume_spaces();
+ if (!builder.try_consume_literal(",")) {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+ builder.consume_literal(")");
+ builder.consume_spaces();
+
+ auto arguments = args.dump();
+ if (!builder.add_tool_call(function_name, "", arguments)) {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ return;
+ }
+ }
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ std::nullopt,
+ /* function_regex_start_only= */ function_regex,
+ /* function_regex= */ std::nullopt,
+ close_regex,
+ std::nullopt);
+
+}
+
+static void common_chat_parse_deepseek_r1(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("<think>", "</think>");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)");
+ static const common_regex tool_calls_end("<|tool▁calls▁end|>");
+ static const common_regex function_regex("(?:<|tool▁call▁begin|>)?function<|tool▁sep|>([^\n]+)\n```json\n");
+ static const common_regex close_regex("```[\\s\\r\\n]*<|tool▁call▁end|>");
+
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ tool_calls_begin,
+ /* function_regex_start_only= */ std::nullopt,
+ function_regex,
+ close_regex,
+ tool_calls_end);
+}
+
+static void common_chat_parse_deepseek_v3_1_content(common_chat_msg_parser & builder) {
+ static const common_regex function_regex("(?:<|tool▁call▁begin|>)?([^\\n<]+)(?:<|tool▁sep|>)");
+
+ static const common_regex close_regex("(?:[\\s]*)?<|tool▁call▁end|>");
+ static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)");
+ static const common_regex tool_calls_end("<|tool▁calls▁end|>");
+
+ if (!builder.syntax().parse_tool_calls) {
+ LOG_DBG("%s: not parse_tool_calls\n", __func__);
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ LOG_DBG("%s: parse_tool_calls\n", __func__);
+
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ tool_calls_begin,
+ /* function_regex_start_only= */ std::nullopt,
+ function_regex,
+ close_regex,
+ tool_calls_end);
+}
+
+static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) {
+ // DeepSeek V3.1 outputs reasoning content between "<think>" and "</think>" tags, followed by regular content
+ // First try to parse using the standard reasoning parsing method
+ LOG_DBG("%s: thinking_forced_open: %s\n", __func__, std::to_string(builder.syntax().thinking_forced_open).c_str());
+
+ auto start_pos = builder.pos();
+ auto found_end_think = builder.try_find_literal("</think>");
+ builder.move_to(start_pos);
+
+ if (builder.syntax().thinking_forced_open && !builder.is_partial() && !found_end_think) {
+ LOG_DBG("%s: no end_think, not partial, adding content\n", __func__);
+ common_chat_parse_deepseek_v3_1_content(builder);
+ } else if (builder.try_parse_reasoning("<think>", "</think>")) {
+ // If reasoning was parsed successfully, the remaining content is regular content
+ LOG_DBG("%s: parsed reasoning, adding content\n", __func__);
+ // </think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>NAME\n```json\nJSON\n```<|tool▁call▁end|><|tool▁calls▁end|>
+ common_chat_parse_deepseek_v3_1_content(builder);
+ } else {
+ if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE) {
+ LOG_DBG("%s: reasoning_format none, adding content\n", __func__);
+ common_chat_parse_deepseek_v3_1_content(builder);
+ return;
+ }
+ // If no reasoning tags found, check if we should treat everything as reasoning
+ if (builder.syntax().thinking_forced_open) {
+ // If thinking is forced open but no tags found, treat everything as reasoning
+ LOG_DBG("%s: thinking_forced_open, adding reasoning content\n", __func__);
+ builder.add_reasoning_content(builder.consume_rest());
+ } else {
+ LOG_DBG("%s: no thinking_forced_open, adding content\n", __func__);
+ // <|tool▁call▁begin|>NAME<|tool▁sep|>JSON<|tool▁call▁end|>
+ common_chat_parse_deepseek_v3_1_content(builder);
+ }
+ }
+}
+
+static void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form {
+ /* form.scope_start = */ "<minimax:tool_call>",
+ /* form.tool_start = */ "<invoke name=\"",
+ /* form.tool_sep = */ "\">",
+ /* form.key_start = */ "<parameter name=\"",
+ /* form.key_val_sep = */ "\">",
+ /* form.val_end = */ "</parameter>",
+ /* form.tool_end = */ "</invoke>",
+ /* form.scope_end = */ "</minimax:tool_call>",
+ };
+ builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
+}
+
+static void common_chat_parse_qwen3_coder_xml(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form = ([]() {
+ xml_tool_call_format form {};
+ form.scope_start = "<tool_call>";
+ form.tool_start = "<function=";
+ form.tool_sep = ">";
+ form.key_start = "<parameter=";
+ form.key_val_sep = ">";
+ form.val_end = "</parameter>";
+ form.tool_end = "</function>";
+ form.scope_end = "</tool_call>";
+ form.trim_raw_argval = true;
+ return form;
+ })();
+ builder.consume_reasoning_with_xml_tool_calls(form);
+}
+
+static void common_chat_parse_kimi_k2(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form = ([]() {
+ xml_tool_call_format form {};
+ form.scope_start = "<|tool_calls_section_begin|>";
+ form.tool_start = "<|tool_call_begin|>";
+ form.tool_sep = "<|tool_call_argument_begin|>{";
+ form.key_start = "\"";
+ form.key_val_sep = "\": ";
+ form.val_end = ", ";
+ form.tool_end = "}<|tool_call_end|>";
+ form.scope_end = "<|tool_calls_section_end|>";
+ form.raw_argval = false;
+ form.last_val_end = "";
+ return form;
+ })();
+ builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
+}
+
+static void common_chat_parse_apriel_1_5(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form = ([]() {
+ xml_tool_call_format form {};
+ form.scope_start = "<tool_calls>[";
+ form.tool_start = "{\"name\": \"";
+ form.tool_sep = "\", \"arguments\": {";
+ form.key_start = "\"";
+ form.key_val_sep = "\": ";
+ form.val_end = ", ";
+ form.tool_end = "}, ";
+ form.scope_end = "]</tool_calls>";
+ form.raw_argval = false;
+ form.last_val_end = "";
+ form.last_tool_end = "}";
+ return form;
+ })();
+ builder.consume_reasoning_with_xml_tool_calls(form, "<thinking>", "</thinking>");
+}
+
+static void common_chat_parse_xiaomi_mimo(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form = ([]() {
+ xml_tool_call_format form {};
+ form.scope_start = "";
+ form.tool_start = "<tool_call>\n{\"name\": \"";
+ form.tool_sep = "\", \"arguments\": {";
+ form.key_start = "\"";
+ form.key_val_sep = "\": ";
+ form.val_end = ", ";
+ form.tool_end = "}\n</tool_call>";
+ form.scope_end = "";
+ form.raw_argval = false;
+ form.last_val_end = "";
+ return form;
+ })();
+ builder.consume_reasoning_with_xml_tool_calls(form);
+}
+
+static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
+ static const std::string constraint = "(?: (<\\|constrain\\|>)?([a-zA-Z0-9_-]+))";
+ static const std::string recipient("(?: to=functions\\.([^<\\s]+))");
+
+ static const common_regex start_regex("<\\|start\\|>assistant");
+ static const common_regex analysis_regex("<\\|channel\\|>analysis");
+ static const common_regex final_regex("<\\|channel\\|>final" + constraint + "?");
+ static const common_regex preamble_regex("<\\|channel\\|>commentary");
+ static const common_regex tool_call1_regex(recipient + "<\\|channel\\|>(analysis|commentary)" + constraint + "?");
+ static const common_regex tool_call2_regex("<\\|channel\\|>(analysis|commentary)" + recipient + constraint + "?");
+
+ auto consume_end = [&](bool include_end = false) {
+ if (auto res = builder.try_find_literal("<|end|>")) {
+ return res->prelude + (include_end ? builder.str(res->groups[0]) : "");
+ }
+ return builder.consume_rest();
+ };
+
+ auto handle_tool_call = [&](const std::string & name) {
+ if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
+ if (builder.syntax().parse_tool_calls) {
+ if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ } else if (args->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ }
+ };
+
+ auto regex_match = [](const common_regex & regex, const std::string & input) -> std::optional<common_regex_match> {
+ auto match = regex.search(input, 0, true);
+ if (match.type == COMMON_REGEX_MATCH_TYPE_FULL) {
+ return match;
+ }
+ return std::nullopt;
+ };
+
+ do {
+ auto header_start_pos = builder.pos();
+ auto content_start = builder.try_find_literal("<|message|>");
+ if (!content_start) {
+ throw common_chat_msg_partial_exception("incomplete header");
+ }
+
+ auto header = content_start->prelude;
+
+ if (auto match = regex_match(tool_call1_regex, header)) {
+ auto group = match->groups[1];
+ auto name = header.substr(group.begin, group.end - group.begin);
+ handle_tool_call(name);
+ continue;
+ }
+
+ if (auto match = regex_match(tool_call2_regex, header)) {
+ auto group = match->groups[2];
+ auto name = header.substr(group.begin, group.end - group.begin);
+ handle_tool_call(name);
+ continue;
+ }
+
+ if (regex_match(analysis_regex, header)) {
+ builder.move_to(header_start_pos);
+ if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax().reasoning_in_content) {
+ builder.add_content(consume_end(true));
+ } else {
+ builder.try_parse_reasoning("<|channel|>analysis<|message|>", "<|end|>");
+ }
+ continue;
+ }
+
+ if(regex_match(final_regex, header) || regex_match(preamble_regex, header)) {
+ builder.add_content(consume_end());
+ continue;
+ }
+
+ // Possibly a malformed message, attempt to recover by rolling
+ // back to pick up the next <|start|>
+ LOG_DBG("%s: unknown header from message: %s\n", __func__, header.c_str());
+ builder.move_to(header_start_pos);
+ } while (builder.try_find_regex(start_regex, std::string::npos, false));
+
+ auto remaining = builder.consume_rest();
+ if (!remaining.empty()) {
+ LOG_DBG("%s: content after last message: %s\n", __func__, remaining.c_str());
+ }
+}
+
+static void common_chat_parse_glm_4_5(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form {
+ /* form.scope_start = */ "",
+ /* form.tool_start = */ "<tool_call>",
+ /* form.tool_sep = */ "",
+ /* form.key_start = */ "<arg_key>",
+ /* form.key_val_sep = */ "</arg_key>",
+ /* form.val_end = */ "</arg_value>",
+ /* form.tool_end = */ "</tool_call>",
+ /* form.scope_end = */ "",
+ /* form.key_val_sep2 = */ "<arg_value>",
+ };
+ builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
+}
+
+static void common_chat_parse_firefunction_v2(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ static const common_regex prefix(regex_escape(" functools["));
+ parse_prefixed_json_tool_call_array(builder, prefix, /* rstrip_prefix= */ 1);
+}
+
+static void common_chat_parse_functionary_v3_2(common_chat_msg_parser & builder) {
+ static const common_regex function_regex_start_only(R"((\w+\n\{|python\n|all\n))");
+ static const common_regex function_regex(R"(>>>(\w+\n\{|python\n|all\n))");
+ static const common_regex close_regex(R"(\s*)");
+
+ parse_json_tool_calls(
+ builder,
+ std::nullopt,
+ function_regex_start_only,
+ function_regex,
+ close_regex,
+ std::nullopt,
+ /* allow_raw_python= */ true,
+ /* get_function_name= */ [&](const auto & res) -> std::string {
+ auto at_start = res.groups[0].begin == 0;
+ auto name = builder.str(res.groups[1]);
+ if (!name.empty() && name.back() == '{') {
+ // Unconsume the opening brace '{' to ensure the JSON parsing goes well.
+ builder.move_back(1);
+ }
+ auto idx = name.find_last_not_of("\n{");
+ name = name.substr(0, idx + 1);
+ if (at_start && name == "all") {
+ return "";
+ }
+ return name;
+ });
+}
+
+static void common_chat_parse_functionary_v3_1_llama_3_1(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+ // This version of Functionary still supports the llama 3.1 tool call format for the python tool.
+ static const common_regex python_tag_regex(regex_escape("<|python_tag|>"));
+
+ static const common_regex function_regex(R"(<function=(\w+)>)");
+ static const common_regex close_regex(R"(</function>)");
+
+ parse_json_tool_calls(
+ builder,
+ /* block_open= */ std::nullopt,
+ /* function_regex_start_only= */ std::nullopt,
+ function_regex,
+ close_regex,
+ std::nullopt);
+
+ if (auto res = builder.try_find_regex(python_tag_regex)) {
+ auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
+ builder.add_tool_call("python", "", arguments);
+ return;
+ }
+}
+
+static void common_chat_parse_hermes_2_pro(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("<think>", "</think>");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ static const common_regex open_regex(
+ "(?:"
+ "(```(?:xml|json)?\\n\\s*)?" // match 1 (block_start)
+ "(" // match 2 (open_tag)
+ "<tool_call>"
+ "|<function_call>"
+ "|<tool>"
+ "|<tools>"
+ "|<response>"
+ "|<json>"
+ "|<xml>"
+ "|<JSON>"
+ ")?"
+ "(\\s*\\{\\s*\"name\")" // match 3 (named tool call)
+ ")"
+ "|<function=([^>]+)>" // match 4 (function name)
+ "|<function name=\"([^\"]+)\">" // match 5 (function name again)
+ );
+
+ while (auto res = builder.try_find_regex(open_regex)) {
+ const auto & block_start = res->groups[1];
+ std::string block_end = block_start.empty() ? "" : "```";
+
+ const auto & open_tag = res->groups[2];
+ std::string close_tag;
+
+ if (!res->groups[3].empty()) {
+ builder.move_to(res->groups[3].begin);
+ close_tag = open_tag.empty() ? "" : "</" + builder.str(open_tag).substr(1);
+
+ if (auto tool_call = builder.try_consume_json_with_dumped_args({{"arguments"}})) {
+ if (!builder.add_tool_call(tool_call->value) || tool_call->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_spaces();
+ builder.consume_literal(close_tag);
+ builder.consume_spaces();
+ if (!block_end.empty()) {
+ builder.consume_literal(block_end);
+ builder.consume_spaces();
+ }
+ } else {
+ throw common_chat_msg_partial_exception("failed to parse tool call");
+ }
+ } else {
+ auto function_name = builder.str(res->groups[4]);
+ if (function_name.empty()) {
+ function_name = builder.str(res->groups[5]);
+ }
+ GGML_ASSERT(!function_name.empty());
+
+ close_tag = "</function>";
+
+ if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
+ if (!builder.add_tool_call(function_name, "", arguments->value) || arguments->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ builder.consume_spaces();
+ builder.consume_literal(close_tag);
+ builder.consume_spaces();
+ if (!block_end.empty()) {
+ builder.consume_literal(block_end);
+ builder.consume_spaces();
+ }
+ }
+ }
+ }
+
+ builder.add_content(builder.consume_rest());
+}
+
+static void common_chat_parse_granite(common_chat_msg_parser & builder) {
+ // Parse thinking tags
+ static const common_regex start_think_regex(regex_escape("<think>"));
+ static const common_regex end_think_regex(regex_escape("</think>"));
+ // Granite models output partial tokens such as "<" and "<think".
+ // By leveraging try_consume_regex()/try_find_regex() throwing
+ // common_chat_msg_partial_exception for these partial tokens,
+ // processing is interrupted and the tokens are not passed to add_content().
+ if (auto res = builder.try_consume_regex(start_think_regex)) {
+ // Restore position for try_parse_reasoning()
+ builder.move_to(res->groups[0].begin);
+ builder.try_find_regex(end_think_regex, std::string::npos, false);
+ // Restore position for try_parse_reasoning()
+ builder.move_to(res->groups[0].begin);
+ }
+ builder.try_parse_reasoning("<think>", "</think>");
+
+ // Parse response tags
+ static const common_regex start_response_regex(regex_escape("<response>"));
+ static const common_regex end_response_regex(regex_escape("</response>"));
+ // Granite models output partial tokens such as "<" and "<response".
+ // Same hack as reasoning parsing.
+ if (builder.try_consume_regex(start_response_regex)) {
+ builder.try_find_regex(end_response_regex);
+ }
+
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ // Look for tool calls
+ static const common_regex tool_call_regex(regex_escape("<|tool_call|>"));
+ if (auto res = builder.try_find_regex(tool_call_regex)) {
+ builder.move_to(res->groups[0].end);
+
+ // Expect JSON array of tool calls
+ if (auto tool_call = builder.try_consume_json_with_dumped_args({{{"arguments"}}})) {
+ if (!builder.add_tool_calls(tool_call->value) || tool_call->is_partial) {
+ throw common_chat_msg_partial_exception("incomplete tool call");
+ }
+ }
+ } else {
+ builder.add_content(builder.consume_rest());
+ }
+}
+
+static void common_chat_parse_nemotron_v2(common_chat_msg_parser & builder) {
+ // Parse thinking tags
+ builder.try_parse_reasoning("<think>", "</think>");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ // Look for tool calls
+ static const common_regex tool_call_regex(regex_escape("<TOOLCALL>"));
+ if (auto res = builder.try_find_regex(tool_call_regex)) {
+ builder.move_to(res->groups[0].end);
+
+ // Expect JSON array of tool calls
+ auto tool_calls_data = builder.consume_json();
+ if (tool_calls_data.json.is_array()) {
+ if (!builder.try_consume_literal("</TOOLCALL>")) {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ builder.add_tool_calls(tool_calls_data.json);
+ } else {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ }
+ builder.add_content(builder.consume_rest());
+}
+
+static void common_chat_parse_apertus(common_chat_msg_parser & builder) {
+ // Parse thinking tags
+ builder.try_parse_reasoning("<|inner_prefix|>", "<|inner_suffix|>");
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ // Look for tool calls
+ static const common_regex tool_call_regex(regex_escape("<|tools_prefix|>"));
+ if (auto res = builder.try_find_regex(tool_call_regex)) {
+ builder.move_to(res->groups[0].end);
+
+ auto tool_calls_data = builder.consume_json();
+ if (tool_calls_data.json.is_array()) {
+ builder.consume_spaces();
+ if (!builder.try_consume_literal("<|tools_suffix|>")) {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ for (const auto & value : tool_calls_data.json) {
+ if (value.is_object()) {
+ builder.add_tool_call_short_form(value);
+ }
+ }
+ } else {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ }
+ builder.add_content(builder.consume_rest());
+}
+
+
+static void common_chat_parse_lfm2(common_chat_msg_parser & builder) {
+ if (!builder.syntax().parse_tool_calls) {
+ builder.add_content(builder.consume_rest());
+ return;
+ }
+
+ // LFM2 format: <|tool_call_start|>[{"name": "get_current_time", "arguments": {"location": "Paris"}}]<|tool_call_end|>
+ static const common_regex tool_call_start_regex(regex_escape("<|tool_call_start|>"));
+ static const common_regex tool_call_end_regex(regex_escape("<|tool_call_end|>"));
+
+ // Loop through all tool calls
+ while (auto res = builder.try_find_regex(tool_call_start_regex, std::string::npos, /* add_prelude_to_content= */ true)) {
+ builder.move_to(res->groups[0].end);
+
+ // Parse JSON array format: [{"name": "...", "arguments": {...}}]
+ auto tool_calls_data = builder.consume_json();
+
+ // Consume end marker
+ builder.consume_spaces();
+ if (!builder.try_consume_regex(tool_call_end_regex)) {
+ throw common_chat_msg_partial_exception("Expected <|tool_call_end|>");
+ }
+
+ // Process each tool call in the array
+ if (tool_calls_data.json.is_array()) {
+ for (const auto & tool_call : tool_calls_data.json) {
+ if (!tool_call.is_object()) {
+ throw common_chat_msg_partial_exception("Tool call must be an object");
+ }
+
+ if (!tool_call.contains("name")) {
+ throw common_chat_msg_partial_exception("Tool call missing 'name' field");
+ }
+
+ std::string function_name = tool_call.at("name");
+ std::string arguments = "{}";
+
+ if (tool_call.contains("arguments")) {
+ if (tool_call.at("arguments").is_object()) {
+ arguments = tool_call.at("arguments").dump();
+ } else if (tool_call.at("arguments").is_string()) {
+ arguments = tool_call.at("arguments");
+ }
+ }
+
+ if (!builder.add_tool_call(function_name, "", arguments)) {
+ throw common_chat_msg_partial_exception("Incomplete tool call");
+ }
+ }
+ } else {
+ throw common_chat_msg_partial_exception("Expected JSON array for tool calls");
+ }
+
+ // Consume any trailing whitespace after this tool call
+ builder.consume_spaces();
+ }
+
+ // Consume any remaining content after all tool calls
+ auto remaining = builder.consume_rest();
+ if (!string_strip(remaining).empty()) {
+ builder.add_content(remaining);
+ }
+}
+
+static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
+ static const xml_tool_call_format form {
+ /* form.scope_start = */ "<seed:tool_call>",
+ /* form.tool_start = */ "<function=",
+ /* form.tool_sep = */ ">",
+ /* form.key_start = */ "<parameter=",
+ /* form.key_val_sep = */ ">",
+ /* form.val_end = */ "</parameter>",
+ /* form.tool_end = */ "</function>",
+ /* form.scope_end = */ "</seed:tool_call>",
+ };
+ builder.consume_reasoning_with_xml_tool_calls(form, "<seed:think>", "</seed:think>");
+}
+
+static void common_chat_parse_content_only(common_chat_msg_parser & builder) {
+ builder.try_parse_reasoning("<think>", "</think>");
+ builder.add_content(builder.consume_rest());
+}
+
+static void common_chat_parse(common_chat_msg_parser & builder) {
+ LOG_DBG("Parsing input with format %s: %s\n", common_chat_format_name(builder.syntax().format), builder.input().c_str());
+
+ switch (builder.syntax().format) {
+ case COMMON_CHAT_FORMAT_CONTENT_ONLY:
+ common_chat_parse_content_only(builder);
+ break;
+ case COMMON_CHAT_FORMAT_GENERIC:
+ common_chat_parse_generic(builder);
+ break;
+ case COMMON_CHAT_FORMAT_MISTRAL_NEMO:
+ common_chat_parse_mistral_nemo(builder);
+ break;
+ case COMMON_CHAT_FORMAT_MAGISTRAL:
+ common_chat_parse_magistral(builder);
+ break;
+ case COMMON_CHAT_FORMAT_LLAMA_3_X:
+ common_chat_parse_llama_3_1(builder);
+ break;
+ case COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS:
+ common_chat_parse_llama_3_1(builder, /* with_builtin_tools= */ true);
+ break;
+ case COMMON_CHAT_FORMAT_DEEPSEEK_R1:
+ common_chat_parse_deepseek_r1(builder);
+ break;
+ case COMMON_CHAT_FORMAT_DEEPSEEK_V3_1:
+ common_chat_parse_deepseek_v3_1(builder);
+ break;
+ case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2:
+ common_chat_parse_functionary_v3_2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1:
+ common_chat_parse_functionary_v3_1_llama_3_1(builder);
+ break;
+ case COMMON_CHAT_FORMAT_HERMES_2_PRO:
+ common_chat_parse_hermes_2_pro(builder);
+ break;
+ case COMMON_CHAT_FORMAT_FIREFUNCTION_V2:
+ common_chat_parse_firefunction_v2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_COMMAND_R7B:
+ common_chat_parse_command_r7b(builder);
+ break;
+ case COMMON_CHAT_FORMAT_GRANITE:
+ common_chat_parse_granite(builder);
+ break;
+ case COMMON_CHAT_FORMAT_GPT_OSS:
+ common_chat_parse_gpt_oss(builder);
+ break;
+ case COMMON_CHAT_FORMAT_SEED_OSS:
+ common_chat_parse_seed_oss(builder);
+ break;
+ case COMMON_CHAT_FORMAT_NEMOTRON_V2:
+ common_chat_parse_nemotron_v2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_APERTUS:
+ common_chat_parse_apertus(builder);
+ break;
+ case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS:
+ common_chat_parse_lfm2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_MINIMAX_M2:
+ common_chat_parse_minimax_m2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_GLM_4_5:
+ common_chat_parse_glm_4_5(builder);
+ break;
+ case COMMON_CHAT_FORMAT_KIMI_K2:
+ common_chat_parse_kimi_k2(builder);
+ break;
+ case COMMON_CHAT_FORMAT_QWEN3_CODER_XML:
+ common_chat_parse_qwen3_coder_xml(builder);
+ break;
+ case COMMON_CHAT_FORMAT_APRIEL_1_5:
+ common_chat_parse_apriel_1_5(builder);
+ break;
+ case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
+ common_chat_parse_xiaomi_mimo(builder);
+ break;
+ default:
+ throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
+ }
+ builder.finish();
+}
+
+common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax) {
+ common_chat_msg_parser builder(input, is_partial, syntax);
+ try {
+ common_chat_parse(builder);
+ } catch (const common_chat_msg_partial_exception & ex) {
+ LOG_DBG("Partial parse: %s\n", ex.what());
+ if (!is_partial) {
+ builder.clear_tools();
+ builder.move_to(0);
+ common_chat_parse_content_only(builder);
+ }
+ }
+ auto msg = builder.result();
+ if (!is_partial) {
+ LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat<json>({msg}).at(0).dump().c_str());
+ }
+ return msg;
+}
throw std::runtime_error("Unknown reasoning format: " + format);
}
-static std::string wrap_code_as_arguments(common_chat_msg_parser & builder, const std::string & code) {
- std::string arguments;
- if (builder.is_partial()) {
- arguments = (json {{"code", code + builder.healing_marker()}}).dump();
- auto idx = arguments.find(builder.healing_marker());
- if (idx != std::string::npos) {
- arguments.resize(idx);
- }
- } else {
- arguments = (json {{"code", code}}).dump();
- }
- return arguments;
-}
-
-/**
- * Takes a prefix regex that must have 1 group to capture the function name, a closing suffix, and expects json parameters in between.
- * Aggregates the prefix, suffix and in-between text into the content.
- */
-static void parse_json_tool_calls(
- common_chat_msg_parser & builder,
- const std::optional<common_regex> & block_open,
- const std::optional<common_regex> & function_regex_start_only,
- const std::optional<common_regex> & function_regex,
- const common_regex & close_regex,
- const std::optional<common_regex> & block_close,
- bool allow_raw_python = false,
- const std::function<std::string(const common_chat_msg_parser::find_regex_result & fres)> & get_function_name = nullptr) {
-
- auto parse_tool_calls = [&]() {
- size_t from = std::string::npos;
- auto first = true;
- while (true) {
- auto start_pos = builder.pos();
- auto res = function_regex_start_only && first
- ? builder.try_consume_regex(*function_regex_start_only)
- : function_regex
- ? builder.try_find_regex(*function_regex, from)
- : std::nullopt;
-
- if (res) {
- std::string name;
- if (get_function_name) {
- name = get_function_name(*res);
- } else {
- GGML_ASSERT(res->groups.size() == 2);
- name = builder.str(res->groups[1]);
- }
- first = false;
- if (name.empty()) {
- // get_function_name signalled us that we should skip this match and treat it as content.
- from = res->groups[0].begin + 1;
- continue;
- }
- from = std::string::npos;
-
- auto maybe_raw_python = name == "python" && allow_raw_python;
- if (builder.input()[builder.pos()] == '{' || !maybe_raw_python) {
- if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
- if (!builder.add_tool_call(name, "", arguments->value) || arguments->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- builder.consume_regex(close_regex);
- }
- continue;
- }
- if (maybe_raw_python) {
- auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
- if (!builder.add_tool_call(name, "", arguments)) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- return;
- }
- throw common_chat_msg_partial_exception("incomplete tool call");
- } else {
- builder.move_to(start_pos);
- }
- break;
- }
- if (block_close) {
- builder.consume_regex(*block_close);
- }
- builder.consume_spaces();
- builder.add_content(builder.consume_rest());
- };
- if (block_open) {
- if (auto res = builder.try_find_regex(*block_open)) {
- parse_tool_calls();
- } else {
- builder.add_content(builder.consume_rest());
- }
- } else {
- parse_tool_calls();
- }
-}
-
-static void parse_prefixed_json_tool_call_array(common_chat_msg_parser & builder, const common_regex & prefix, size_t rstrip_prefix = 0) {
- static const std::vector<std::vector<std::string>> args_paths = {{"arguments"}};
- if (auto res = builder.try_find_regex(prefix)) {
- builder.move_back(rstrip_prefix);
- auto tool_calls = builder.consume_json_with_dumped_args(args_paths);
- if (!builder.add_tool_calls(tool_calls.value) || tool_calls.is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call array");
- }
- } else {
- builder.add_content(builder.consume_rest());
- }
-}
-
static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) {
for (const auto & tool : tools) {
if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) {
data.format = COMMON_CHAT_FORMAT_GENERIC;
return data;
}
-static void common_chat_parse_generic(common_chat_msg_parser & builder) {
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
- static const std::vector<std::vector<std::string>> content_paths = {
- {"response"},
- };
- static const std::vector<std::vector<std::string>> args_paths = {
- {"tool_call", "arguments"},
- {"tool_calls", "arguments"},
- };
- auto data = builder.consume_json_with_dumped_args(args_paths, content_paths);
- if (data.value.contains("tool_calls")) {
- if (!builder.add_tool_calls(data.value.at("tool_calls")) || data.is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool calls");
- }
- } else if (data.value.contains("tool_call")) {
- if (!builder.add_tool_call(data.value.at("tool_call")) || data.is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- } else if (data.value.contains("response")) {
- const auto & response = data.value.at("response");
- builder.add_content(response.is_string() ? response.template get<std::string>() : response.dump(2));
- if (data.is_partial) {
- throw common_chat_msg_partial_exception("incomplete response");
- }
- } else {
- throw common_chat_msg_partial_exception("Expected 'tool_call', 'tool_calls' or 'response' in JSON");
- }
-}
static common_chat_params common_chat_params_init_mistral_nemo(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_mistral_nemo(common_chat_msg_parser & builder) {
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- static const common_regex prefix(regex_escape("[TOOL_CALLS]"));
- parse_prefixed_json_tool_call_array(builder, prefix);
-}
-
-static void common_chat_parse_magistral(common_chat_msg_parser & builder) {
- builder.try_parse_reasoning("[THINK]", "[/THINK]");
-
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- static const common_regex prefix(regex_escape("[TOOL_CALLS]"));
- parse_prefixed_json_tool_call_array(builder, prefix);
-}
-
static common_chat_params common_chat_params_init_command_r7b(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_command_r7b(common_chat_msg_parser & builder) {
- builder.try_parse_reasoning("<|START_THINKING|>", "<|END_THINKING|>");
-
- static const common_regex start_action_regex("<\\|START_ACTION\\|>");
- static const common_regex end_action_regex("<\\|END_ACTION\\|>");
- static const common_regex start_response_regex("<\\|START_RESPONSE\\|>");
- static const common_regex end_response_regex("<\\|END_RESPONSE\\|>");
-
- if (auto res = builder.try_find_regex(start_action_regex)) {
- // If we didn't extract thoughts, prelude includes them.
- auto tool_calls = builder.consume_json_with_dumped_args({{"parameters"}});
- for (const auto & tool_call : tool_calls.value) {
- std::string name = tool_call.contains("tool_name") ? tool_call.at("tool_name") : "";
- std::string id = tool_call.contains("tool_call_id") ? tool_call.at("tool_call_id") : "";
- std::string arguments = tool_call.contains("parameters") ? tool_call.at("parameters") : "";
- if (!builder.add_tool_call(name, id, arguments) || tool_calls.is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- }
- if (tool_calls.is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- builder.consume_regex(end_action_regex);
- } else if (auto res = builder.try_find_regex(start_response_regex)) {
- if (!builder.try_find_regex(end_response_regex)) {
- builder.add_content(builder.consume_rest());
- throw common_chat_msg_partial_exception(end_response_regex.str());
- }
- } else {
- builder.add_content(builder.consume_rest());
- }
-}
-
static void expect_tool_parameters(const std::string & name, const json & parameters, const std::vector<std::string> & expected_properties) {
if (!parameters.is_object() || !parameters.contains("type") || parameters.at("type") != "object" || !parameters.contains("properties") || !parameters.contains("required")) {
throw std::runtime_error("Parameters of tool " + name + " must be an object w/ required properties");
}
return data;
}
-static void common_chat_parse_llama_3_1(common_chat_msg_parser & builder, bool with_builtin_tools = false) {
- builder.try_parse_reasoning("<think>", "</think>");
-
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- static const common_regex function_regex(
- "\\s*\\{\\s*(?:\"type\"\\s*:\\s*\"function\"\\s*,\\s*)?\"name\"\\s*:\\s*\"([^\"]+)\"\\s*,\\s*\"parameters\"\\s*: ");
- static const common_regex close_regex("\\}\\s*");
-
- static const common_regex function_name_regex("\\s*(\\w+)\\s*\\.\\s*call\\(");
- static const common_regex arg_name_regex("\\s*(\\w+)\\s*=\\s*");
-
- if (with_builtin_tools) {
- static const common_regex builtin_call_regex("<\\|python_tag\\|>");
- if (auto res = builder.try_find_regex(builtin_call_regex)) {
- auto fun_res = builder.consume_regex(function_name_regex);
- auto function_name = builder.str(fun_res.groups[1]);
-
- common_healing_marker healing_marker;
- json args = json::object();
- while (true) {
- if (auto arg_res = builder.try_consume_regex(arg_name_regex)) {
- auto arg_name = builder.str(arg_res->groups[1]);
- auto partial = builder.consume_json();
- args[arg_name] = partial.json;
- healing_marker.marker = partial.healing_marker.marker;
- healing_marker.json_dump_marker = partial.healing_marker.json_dump_marker;
- builder.consume_spaces();
- if (!builder.try_consume_literal(",")) {
- break;
- }
- } else {
- break;
- }
- }
- builder.consume_literal(")");
- builder.consume_spaces();
-
- auto arguments = args.dump();
- if (!builder.add_tool_call(function_name, "", arguments)) {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- return;
- }
- }
- parse_json_tool_calls(
- builder,
- /* block_open= */ std::nullopt,
- /* function_regex_start_only= */ function_regex,
- /* function_regex= */ std::nullopt,
- close_regex,
- std::nullopt);
-
-}
static common_chat_params common_chat_params_init_deepseek_r1(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_deepseek_r1(common_chat_msg_parser & builder) {
- builder.try_parse_reasoning("<think>", "</think>");
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)");
- static const common_regex tool_calls_end("<|tool▁calls▁end|>");
- static const common_regex function_regex("(?:<|tool▁call▁begin|>)?function<|tool▁sep|>([^\n]+)\n```json\n");
- static const common_regex close_regex("```[\\s\\r\\n]*<|tool▁call▁end|>");
-
- parse_json_tool_calls(
- builder,
- /* block_open= */ tool_calls_begin,
- /* function_regex_start_only= */ std::nullopt,
- function_regex,
- close_regex,
- tool_calls_end);
-}
-
-static void common_chat_parse_deepseek_v3_1_content(common_chat_msg_parser & builder) {
- static const common_regex function_regex("(?:<|tool▁call▁begin|>)?([^\\n<]+)(?:<|tool▁sep|>)");
-
- static const common_regex close_regex("(?:[\\s]*)?<|tool▁call▁end|>");
- static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)");
- static const common_regex tool_calls_end("<|tool▁calls▁end|>");
-
- if (!builder.syntax().parse_tool_calls) {
- LOG_DBG("%s: not parse_tool_calls\n", __func__);
- builder.add_content(builder.consume_rest());
- return;
- }
-
- LOG_DBG("%s: parse_tool_calls\n", __func__);
-
- parse_json_tool_calls(
- builder,
- /* block_open= */ tool_calls_begin,
- /* function_regex_start_only= */ std::nullopt,
- function_regex,
- close_regex,
- tool_calls_end);
-}
-
-static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) {
- // DeepSeek V3.1 outputs reasoning content between "<think>" and "</think>" tags, followed by regular content
- // First try to parse using the standard reasoning parsing method
- LOG_DBG("%s: thinking_forced_open: %s\n", __func__, std::to_string(builder.syntax().thinking_forced_open).c_str());
-
- auto start_pos = builder.pos();
- auto found_end_think = builder.try_find_literal("</think>");
- builder.move_to(start_pos);
-
- if (builder.syntax().thinking_forced_open && !builder.is_partial() && !found_end_think) {
- LOG_DBG("%s: no end_think, not partial, adding content\n", __func__);
- common_chat_parse_deepseek_v3_1_content(builder);
- } else if (builder.try_parse_reasoning("<think>", "</think>")) {
- // If reasoning was parsed successfully, the remaining content is regular content
- LOG_DBG("%s: parsed reasoning, adding content\n", __func__);
- // </think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>NAME\n```json\nJSON\n```<|tool▁call▁end|><|tool▁calls▁end|>
- common_chat_parse_deepseek_v3_1_content(builder);
- } else {
- if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE) {
- LOG_DBG("%s: reasoning_format none, adding content\n", __func__);
- common_chat_parse_deepseek_v3_1_content(builder);
- return;
- }
- // If no reasoning tags found, check if we should treat everything as reasoning
- if (builder.syntax().thinking_forced_open) {
- // If thinking is forced open but no tags found, treat everything as reasoning
- LOG_DBG("%s: thinking_forced_open, adding reasoning content\n", __func__);
- builder.add_reasoning_content(builder.consume_rest());
- } else {
- LOG_DBG("%s: no thinking_forced_open, adding content\n", __func__);
- // <|tool▁call▁begin|>NAME<|tool▁sep|>JSON<|tool▁call▁end|>
- common_chat_parse_deepseek_v3_1_content(builder);
- }
- }
-}
-
-
static common_chat_params common_chat_params_init_minimax_m2(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
return data;
}
-static void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form {
- /* form.scope_start = */ "<minimax:tool_call>",
- /* form.tool_start = */ "<invoke name=\"",
- /* form.tool_sep = */ "\">",
- /* form.key_start = */ "<parameter name=\"",
- /* form.key_val_sep = */ "\">",
- /* form.val_end = */ "</parameter>",
- /* form.tool_end = */ "</invoke>",
- /* form.scope_end = */ "</minimax:tool_call>",
- };
- builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
-}
-
static common_chat_params common_chat_params_init_qwen3_coder_xml(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
return data;
}
-static void common_chat_parse_qwen3_coder_xml(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form = ([]() {
- xml_tool_call_format form {};
- form.scope_start = "<tool_call>";
- form.tool_start = "<function=";
- form.tool_sep = ">";
- form.key_start = "<parameter=";
- form.key_val_sep = ">";
- form.val_end = "</parameter>";
- form.tool_end = "</function>";
- form.scope_end = "</tool_call>";
- form.trim_raw_argval = true;
- return form;
- })();
- builder.consume_reasoning_with_xml_tool_calls(form);
-}
-
static common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
return data;
}
-static void common_chat_parse_kimi_k2(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form = ([]() {
- xml_tool_call_format form {};
- form.scope_start = "<|tool_calls_section_begin|>";
- form.tool_start = "<|tool_call_begin|>";
- form.tool_sep = "<|tool_call_argument_begin|>{";
- form.key_start = "\"";
- form.key_val_sep = "\": ";
- form.val_end = ", ";
- form.tool_end = "}<|tool_call_end|>";
- form.scope_end = "<|tool_calls_section_end|>";
- form.raw_argval = false;
- form.last_val_end = "";
- return form;
- })();
- builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
-}
-
static common_chat_params common_chat_params_init_apriel_1_5(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
return data;
}
-static void common_chat_parse_apriel_1_5(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form = ([]() {
- xml_tool_call_format form {};
- form.scope_start = "<tool_calls>[";
- form.tool_start = "{\"name\": \"";
- form.tool_sep = "\", \"arguments\": {";
- form.key_start = "\"";
- form.key_val_sep = "\": ";
- form.val_end = ", ";
- form.tool_end = "}, ";
- form.scope_end = "]</tool_calls>";
- form.raw_argval = false;
- form.last_val_end = "";
- form.last_tool_end = "}";
- return form;
- })();
- builder.consume_reasoning_with_xml_tool_calls(form, "<thinking>", "</thinking>");
-}
-
static common_chat_params common_chat_params_init_xiaomi_mimo(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
return data;
}
-static void common_chat_parse_xiaomi_mimo(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form = ([]() {
- xml_tool_call_format form {};
- form.scope_start = "";
- form.tool_start = "<tool_call>\n{\"name\": \"";
- form.tool_sep = "\", \"arguments\": {";
- form.key_start = "\"";
- form.key_val_sep = "\": ";
- form.val_end = ", ";
- form.tool_end = "}\n</tool_call>";
- form.scope_end = "";
- form.raw_argval = false;
- form.last_val_end = "";
- return form;
- })();
- builder.consume_reasoning_with_xml_tool_calls(form);
-}
-
static common_chat_params common_chat_params_init_gpt_oss(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
- static const std::string constraint = "(?: (<\\|constrain\\|>)?([a-zA-Z0-9_-]+))";
- static const std::string recipient("(?: to=functions\\.([^<\\s]+))");
-
- static const common_regex start_regex("<\\|start\\|>assistant");
- static const common_regex analysis_regex("<\\|channel\\|>analysis");
- static const common_regex final_regex("<\\|channel\\|>final" + constraint + "?");
- static const common_regex preamble_regex("<\\|channel\\|>commentary");
- static const common_regex tool_call1_regex(recipient + "<\\|channel\\|>(analysis|commentary)" + constraint + "?");
- static const common_regex tool_call2_regex("<\\|channel\\|>(analysis|commentary)" + recipient + constraint + "?");
-
- auto consume_end = [&](bool include_end = false) {
- if (auto res = builder.try_find_literal("<|end|>")) {
- return res->prelude + (include_end ? builder.str(res->groups[0]) : "");
- }
- return builder.consume_rest();
- };
-
- auto handle_tool_call = [&](const std::string & name) {
- if (auto args = builder.try_consume_json_with_dumped_args({{}})) {
- if (builder.syntax().parse_tool_calls) {
- if (!builder.add_tool_call(name, "", args->value) || args->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- } else if (args->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- }
- };
-
- auto regex_match = [](const common_regex & regex, const std::string & input) -> std::optional<common_regex_match> {
- auto match = regex.search(input, 0, true);
- if (match.type == COMMON_REGEX_MATCH_TYPE_FULL) {
- return match;
- }
- return std::nullopt;
- };
-
- do {
- auto header_start_pos = builder.pos();
- auto content_start = builder.try_find_literal("<|message|>");
- if (!content_start) {
- throw common_chat_msg_partial_exception("incomplete header");
- }
-
- auto header = content_start->prelude;
-
- if (auto match = regex_match(tool_call1_regex, header)) {
- auto group = match->groups[1];
- auto name = header.substr(group.begin, group.end - group.begin);
- handle_tool_call(name);
- continue;
- }
-
- if (auto match = regex_match(tool_call2_regex, header)) {
- auto group = match->groups[2];
- auto name = header.substr(group.begin, group.end - group.begin);
- handle_tool_call(name);
- continue;
- }
-
- if (regex_match(analysis_regex, header)) {
- builder.move_to(header_start_pos);
- if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE || builder.syntax().reasoning_in_content) {
- builder.add_content(consume_end(true));
- } else {
- builder.try_parse_reasoning("<|channel|>analysis<|message|>", "<|end|>");
- }
- continue;
- }
-
- if(regex_match(final_regex, header) || regex_match(preamble_regex, header)) {
- builder.add_content(consume_end());
- continue;
- }
-
- // Possibly a malformed message, attempt to recover by rolling
- // back to pick up the next <|start|>
- LOG_DBG("%s: unknown header from message: %s\n", __func__, header.c_str());
- builder.move_to(header_start_pos);
- } while (builder.try_find_regex(start_regex, std::string::npos, false));
-
- auto remaining = builder.consume_rest();
- if (!remaining.empty()) {
- LOG_DBG("%s: content after last message: %s\n", __func__, remaining.c_str());
- }
-}
static common_chat_params common_chat_params_init_glm_4_5(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_glm_4_5(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form {
- /* form.scope_start = */ "",
- /* form.tool_start = */ "<tool_call>",
- /* form.tool_sep = */ "",
- /* form.key_start = */ "<arg_key>",
- /* form.key_val_sep = */ "</arg_key>",
- /* form.val_end = */ "</arg_value>",
- /* form.tool_end = */ "</tool_call>",
- /* form.scope_end = */ "",
- /* form.key_val_sep2 = */ "<arg_value>",
- };
- builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
-}
-
static common_chat_params common_chat_params_init_firefunction_v2(const common_chat_template & tmpl, const struct templates_params & inputs) {
LOG_DBG("%s\n", __func__);
common_chat_params data;
}
return data;
}
-static void common_chat_parse_firefunction_v2(common_chat_msg_parser & builder) {
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
- static const common_regex prefix(regex_escape(" functools["));
- parse_prefixed_json_tool_call_array(builder, prefix, /* rstrip_prefix= */ 1);
-}
static common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_template & tmpl, const struct templates_params & inputs) {
// >>>all\nlet's call functions>>>fn1\n{"arg1": 1...}\n>>>fn2\n{"arg1": 1...}...
}
return data;
}
-static void common_chat_parse_functionary_v3_2(common_chat_msg_parser & builder) {
- static const common_regex function_regex_start_only(R"((\w+\n\{|python\n|all\n))");
- static const common_regex function_regex(R"(>>>(\w+\n\{|python\n|all\n))");
- static const common_regex close_regex(R"(\s*)");
-
- parse_json_tool_calls(
- builder,
- std::nullopt,
- function_regex_start_only,
- function_regex,
- close_regex,
- std::nullopt,
- /* allow_raw_python= */ true,
- /* get_function_name= */ [&](const auto & res) -> std::string {
- auto at_start = res.groups[0].begin == 0;
- auto name = builder.str(res.groups[1]);
- if (!name.empty() && name.back() == '{') {
- // Unconsume the opening brace '{' to ensure the JSON parsing goes well.
- builder.move_back(1);
- }
- auto idx = name.find_last_not_of("\n{");
- name = name.substr(0, idx + 1);
- if (at_start && name == "all") {
- return "";
- }
- return name;
- });
-}
static common_chat_params common_chat_params_init_functionary_v3_1_llama_3_1(const common_chat_template & tmpl, const struct templates_params & inputs) {
// https://github.com/MeetKai/functionary/blob/main/tests/prompt_test_v3-llama3.1.txt
// TODO: if (has_raw_python)
return data;
}
-static void common_chat_parse_functionary_v3_1_llama_3_1(common_chat_msg_parser & builder) {
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
- // This version of Functionary still supports the llama 3.1 tool call format for the python tool.
- static const common_regex python_tag_regex(regex_escape("<|python_tag|>"));
-
- static const common_regex function_regex(R"(<function=(\w+)>)");
- static const common_regex close_regex(R"(</function>)");
-
- parse_json_tool_calls(
- builder,
- /* block_open= */ std::nullopt,
- /* function_regex_start_only= */ std::nullopt,
- function_regex,
- close_regex,
- std::nullopt);
-
- if (auto res = builder.try_find_regex(python_tag_regex)) {
- auto arguments = wrap_code_as_arguments(builder, builder.consume_rest());
- builder.add_tool_call("python", "", arguments);
- return;
- }
-}
static common_chat_params common_chat_params_init_hermes_2_pro(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_hermes_2_pro(common_chat_msg_parser & builder) {
- builder.try_parse_reasoning("<think>", "</think>");
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- static const common_regex open_regex(
- "(?:"
- "(```(?:xml|json)?\\n\\s*)?" // match 1 (block_start)
- "(" // match 2 (open_tag)
- "<tool_call>"
- "|<function_call>"
- "|<tool>"
- "|<tools>"
- "|<response>"
- "|<json>"
- "|<xml>"
- "|<JSON>"
- ")?"
- "(\\s*\\{\\s*\"name\")" // match 3 (named tool call)
- ")"
- "|<function=([^>]+)>" // match 4 (function name)
- "|<function name=\"([^\"]+)\">" // match 5 (function name again)
- );
-
- while (auto res = builder.try_find_regex(open_regex)) {
- const auto & block_start = res->groups[1];
- std::string block_end = block_start.empty() ? "" : "```";
-
- const auto & open_tag = res->groups[2];
- std::string close_tag;
-
- if (!res->groups[3].empty()) {
- builder.move_to(res->groups[3].begin);
- close_tag = open_tag.empty() ? "" : "</" + builder.str(open_tag).substr(1);
-
- if (auto tool_call = builder.try_consume_json_with_dumped_args({{"arguments"}})) {
- if (!builder.add_tool_call(tool_call->value) || tool_call->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- builder.consume_spaces();
- builder.consume_literal(close_tag);
- builder.consume_spaces();
- if (!block_end.empty()) {
- builder.consume_literal(block_end);
- builder.consume_spaces();
- }
- } else {
- throw common_chat_msg_partial_exception("failed to parse tool call");
- }
- } else {
- auto function_name = builder.str(res->groups[4]);
- if (function_name.empty()) {
- function_name = builder.str(res->groups[5]);
- }
- GGML_ASSERT(!function_name.empty());
-
- close_tag = "</function>";
-
- if (auto arguments = builder.try_consume_json_with_dumped_args({{}})) {
- if (!builder.add_tool_call(function_name, "", arguments->value) || arguments->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- builder.consume_spaces();
- builder.consume_literal(close_tag);
- builder.consume_spaces();
- if (!block_end.empty()) {
- builder.consume_literal(block_end);
- builder.consume_spaces();
- }
- }
- }
- }
-
- builder.add_content(builder.consume_rest());
-}
static common_chat_params common_chat_params_init_granite(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
return data;
}
-static void common_chat_parse_granite(common_chat_msg_parser & builder) {
- // Parse thinking tags
- static const common_regex start_think_regex(regex_escape("<think>"));
- static const common_regex end_think_regex(regex_escape("</think>"));
- // Granite models output partial tokens such as "<" and "<think".
- // By leveraging try_consume_regex()/try_find_regex() throwing
- // common_chat_msg_partial_exception for these partial tokens,
- // processing is interrupted and the tokens are not passed to add_content().
- if (auto res = builder.try_consume_regex(start_think_regex)) {
- // Restore position for try_parse_reasoning()
- builder.move_to(res->groups[0].begin);
- builder.try_find_regex(end_think_regex, std::string::npos, false);
- // Restore position for try_parse_reasoning()
- builder.move_to(res->groups[0].begin);
- }
- builder.try_parse_reasoning("<think>", "</think>");
-
- // Parse response tags
- static const common_regex start_response_regex(regex_escape("<response>"));
- static const common_regex end_response_regex(regex_escape("</response>"));
- // Granite models output partial tokens such as "<" and "<response".
- // Same hack as reasoning parsing.
- if (builder.try_consume_regex(start_response_regex)) {
- builder.try_find_regex(end_response_regex);
- }
-
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- // Look for tool calls
- static const common_regex tool_call_regex(regex_escape("<|tool_call|>"));
- if (auto res = builder.try_find_regex(tool_call_regex)) {
- builder.move_to(res->groups[0].end);
-
- // Expect JSON array of tool calls
- if (auto tool_call = builder.try_consume_json_with_dumped_args({{{"arguments"}}})) {
- if (!builder.add_tool_calls(tool_call->value) || tool_call->is_partial) {
- throw common_chat_msg_partial_exception("incomplete tool call");
- }
- }
- } else {
- builder.add_content(builder.consume_rest());
- }
-}
-
-static void common_chat_parse_nemotron_v2(common_chat_msg_parser & builder) {
- // Parse thinking tags
- builder.try_parse_reasoning("<think>", "</think>");
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- // Look for tool calls
- static const common_regex tool_call_regex(regex_escape("<TOOLCALL>"));
- if (auto res = builder.try_find_regex(tool_call_regex)) {
- builder.move_to(res->groups[0].end);
-
- // Expect JSON array of tool calls
- auto tool_calls_data = builder.consume_json();
- if (tool_calls_data.json.is_array()) {
- if (!builder.try_consume_literal("</TOOLCALL>")) {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- builder.add_tool_calls(tool_calls_data.json);
- } else {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- }
- builder.add_content(builder.consume_rest());
-}
-
-static void common_chat_parse_apertus(common_chat_msg_parser & builder) {
- // Parse thinking tags
- builder.try_parse_reasoning("<|inner_prefix|>", "<|inner_suffix|>");
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- // Look for tool calls
- static const common_regex tool_call_regex(regex_escape("<|tools_prefix|>"));
- if (auto res = builder.try_find_regex(tool_call_regex)) {
- builder.move_to(res->groups[0].end);
-
- auto tool_calls_data = builder.consume_json();
- if (tool_calls_data.json.is_array()) {
- builder.consume_spaces();
- if (!builder.try_consume_literal("<|tools_suffix|>")) {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- for (const auto & value : tool_calls_data.json) {
- if (value.is_object()) {
- builder.add_tool_call_short_form(value);
- }
- }
- } else {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- }
- builder.add_content(builder.consume_rest());
-}
-
-
-static void common_chat_parse_lfm2(common_chat_msg_parser & builder) {
- if (!builder.syntax().parse_tool_calls) {
- builder.add_content(builder.consume_rest());
- return;
- }
-
- // LFM2 format: <|tool_call_start|>[{"name": "get_current_time", "arguments": {"location": "Paris"}}]<|tool_call_end|>
- static const common_regex tool_call_start_regex(regex_escape("<|tool_call_start|>"));
- static const common_regex tool_call_end_regex(regex_escape("<|tool_call_end|>"));
-
- // Loop through all tool calls
- while (auto res = builder.try_find_regex(tool_call_start_regex, std::string::npos, /* add_prelude_to_content= */ true)) {
- builder.move_to(res->groups[0].end);
-
- // Parse JSON array format: [{"name": "...", "arguments": {...}}]
- auto tool_calls_data = builder.consume_json();
-
- // Consume end marker
- builder.consume_spaces();
- if (!builder.try_consume_regex(tool_call_end_regex)) {
- throw common_chat_msg_partial_exception("Expected <|tool_call_end|>");
- }
-
- // Process each tool call in the array
- if (tool_calls_data.json.is_array()) {
- for (const auto & tool_call : tool_calls_data.json) {
- if (!tool_call.is_object()) {
- throw common_chat_msg_partial_exception("Tool call must be an object");
- }
-
- if (!tool_call.contains("name")) {
- throw common_chat_msg_partial_exception("Tool call missing 'name' field");
- }
-
- std::string function_name = tool_call.at("name");
- std::string arguments = "{}";
-
- if (tool_call.contains("arguments")) {
- if (tool_call.at("arguments").is_object()) {
- arguments = tool_call.at("arguments").dump();
- } else if (tool_call.at("arguments").is_string()) {
- arguments = tool_call.at("arguments");
- }
- }
-
- if (!builder.add_tool_call(function_name, "", arguments)) {
- throw common_chat_msg_partial_exception("Incomplete tool call");
- }
- }
- } else {
- throw common_chat_msg_partial_exception("Expected JSON array for tool calls");
- }
-
- // Consume any trailing whitespace after this tool call
- builder.consume_spaces();
- }
-
- // Consume any remaining content after all tool calls
- auto remaining = builder.consume_rest();
- if (!string_strip(remaining).empty()) {
- builder.add_content(remaining);
- }
-}
-
-static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form {
- /* form.scope_start = */ "<seed:tool_call>",
- /* form.tool_start = */ "<function=",
- /* form.tool_sep = */ ">",
- /* form.key_start = */ "<parameter=",
- /* form.key_val_sep = */ ">",
- /* form.val_end = */ "</parameter>",
- /* form.tool_end = */ "</function>",
- /* form.scope_end = */ "</seed:tool_call>",
- };
- builder.consume_reasoning_with_xml_tool_calls(form, "<seed:think>", "</seed:think>");
-}
-
static common_chat_params common_chat_params_init_without_tools(const common_chat_template & tmpl, const struct templates_params & inputs) {
common_chat_params data;
data.prompt = apply(tmpl, inputs);
? common_chat_templates_apply_jinja(tmpls, inputs)
: common_chat_templates_apply_legacy(tmpls, inputs);
}
-
-static void common_chat_parse_content_only(common_chat_msg_parser & builder) {
- builder.try_parse_reasoning("<think>", "</think>");
- builder.add_content(builder.consume_rest());
-}
-
-static void common_chat_parse(common_chat_msg_parser & builder) {
- LOG_DBG("Parsing input with format %s: %s\n", common_chat_format_name(builder.syntax().format), builder.input().c_str());
-
- switch (builder.syntax().format) {
- case COMMON_CHAT_FORMAT_CONTENT_ONLY:
- common_chat_parse_content_only(builder);
- break;
- case COMMON_CHAT_FORMAT_GENERIC:
- common_chat_parse_generic(builder);
- break;
- case COMMON_CHAT_FORMAT_MISTRAL_NEMO:
- common_chat_parse_mistral_nemo(builder);
- break;
- case COMMON_CHAT_FORMAT_MAGISTRAL:
- common_chat_parse_magistral(builder);
- break;
- case COMMON_CHAT_FORMAT_LLAMA_3_X:
- common_chat_parse_llama_3_1(builder);
- break;
- case COMMON_CHAT_FORMAT_LLAMA_3_X_WITH_BUILTIN_TOOLS:
- common_chat_parse_llama_3_1(builder, /* with_builtin_tools= */ true);
- break;
- case COMMON_CHAT_FORMAT_DEEPSEEK_R1:
- common_chat_parse_deepseek_r1(builder);
- break;
- case COMMON_CHAT_FORMAT_DEEPSEEK_V3_1:
- common_chat_parse_deepseek_v3_1(builder);
- break;
- case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_2:
- common_chat_parse_functionary_v3_2(builder);
- break;
- case COMMON_CHAT_FORMAT_FUNCTIONARY_V3_1_LLAMA_3_1:
- common_chat_parse_functionary_v3_1_llama_3_1(builder);
- break;
- case COMMON_CHAT_FORMAT_HERMES_2_PRO:
- common_chat_parse_hermes_2_pro(builder);
- break;
- case COMMON_CHAT_FORMAT_FIREFUNCTION_V2:
- common_chat_parse_firefunction_v2(builder);
- break;
- case COMMON_CHAT_FORMAT_COMMAND_R7B:
- common_chat_parse_command_r7b(builder);
- break;
- case COMMON_CHAT_FORMAT_GRANITE:
- common_chat_parse_granite(builder);
- break;
- case COMMON_CHAT_FORMAT_GPT_OSS:
- common_chat_parse_gpt_oss(builder);
- break;
- case COMMON_CHAT_FORMAT_SEED_OSS:
- common_chat_parse_seed_oss(builder);
- break;
- case COMMON_CHAT_FORMAT_NEMOTRON_V2:
- common_chat_parse_nemotron_v2(builder);
- break;
- case COMMON_CHAT_FORMAT_APERTUS:
- common_chat_parse_apertus(builder);
- break;
- case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS:
- common_chat_parse_lfm2(builder);
- break;
- case COMMON_CHAT_FORMAT_MINIMAX_M2:
- common_chat_parse_minimax_m2(builder);
- break;
- case COMMON_CHAT_FORMAT_GLM_4_5:
- common_chat_parse_glm_4_5(builder);
- break;
- case COMMON_CHAT_FORMAT_KIMI_K2:
- common_chat_parse_kimi_k2(builder);
- break;
- case COMMON_CHAT_FORMAT_QWEN3_CODER_XML:
- common_chat_parse_qwen3_coder_xml(builder);
- break;
- case COMMON_CHAT_FORMAT_APRIEL_1_5:
- common_chat_parse_apriel_1_5(builder);
- break;
- case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
- common_chat_parse_xiaomi_mimo(builder);
- break;
- default:
- throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
- }
- builder.finish();
-}
-
-common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax) {
- common_chat_msg_parser builder(input, is_partial, syntax);
- try {
- common_chat_parse(builder);
- } catch (const common_chat_msg_partial_exception & ex) {
- LOG_DBG("Partial parse: %s\n", ex.what());
- if (!is_partial) {
- builder.clear_tools();
- builder.move_to(0);
- common_chat_parse_content_only(builder);
- }
- }
- auto msg = builder.result();
- if (!is_partial) {
- LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat<json>({msg}).at(0).dump().c_str());
- }
- return msg;
-}