#include "chat.h" #include "chat-auto-parser-helpers.h" #include "chat-auto-parser.h" #include "chat-peg-parser.h" #include "common.h" #include "ggml.h" #include "json-schema-to-grammar.h" #include "json.h" #include "log.h" #include "reasoning-budget.h" #include "chat-auto-parser-generator.cpp" #include "chat-auto-parser-helpers.cpp" #include "chat-diff-analyzer.cpp" #include "parsers/parsers.cpp" #include "parsers/cohere2moe.cpp" #include "parsers/deepseek.cpp" #include "parsers/functionary-v3-2.cpp" #include "parsers/gemma4.cpp" #include "parsers/gigachat-v3.cpp" #include "parsers/gpt-oss.cpp" #include "parsers/kimi-k2.cpp" #include "parsers/kimi-k3.cpp" #include "parsers/lfm2.cpp" #include "parsers/minicpm5.cpp" #include "parsers/minimax-m3.cpp" #include "parsers/ministral3.cpp" #include "parsers/muse-glimmer.cpp" #include "parsers/qwen3-coder.cpp" #include "jinja/lexer.cpp" #include "jinja/parser.cpp" #include "jinja/runtime.cpp" #include "jinja/value.cpp" #include "jinja/string.cpp" #include "jinja/caps.cpp" #include "peg-parser.h" #include #include "nlohmann/json.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using json = common_json; static std::string format_time(const std::chrono::system_clock::time_point & now, const std::string & format) { auto time = std::chrono::system_clock::to_time_t(now); auto local_time = *std::localtime(&time); std::ostringstream ss; ss << std::put_time(&local_time, format.c_str()); auto res = ss.str(); return res; } static json safe_args_parse(const std::string & to_parse) { std::string stripped = to_parse; if (to_parse.at(0) == '"' && to_parse.at(to_parse.length() - 1) == '"') { stripped = to_parse.substr(1, to_parse.length() - 1); } try { return json::parse(stripped); } catch (const common_json_error & e) { return stripped; } } static std::string string_diff(const std::string & last, const std::string & current) { if (last.empty()) { return current; } if (!string_starts_with(current, last)) { if (string_starts_with(last, current)) { // This happens if the last generation ended on a partial stop word (not erased), // and the current ended on a stop word (erased). return ""; } throw std::runtime_error("Invalid diff: '" + last + "' not found at start of '" + current + "'"); } return current.substr(last.size()); } static bool has_content_or_tool_calls(const common_chat_msg & msg) { return !msg.content.empty() || !msg.tool_calls.empty(); } std::string common_chat_msg::render_content(const std::string & delimiter) const { if (!content.empty() && !content_parts.empty()) { throw std::runtime_error("Cannot specify both content and content_parts"); } if (!content.empty()) { return content; } std::string text; for (const auto & part : content_parts) { if (part.type == "text") { if (!text.empty()) { text += delimiter; } text += part.text; } } return text; } common_chat_role common_chat_role_from_string(const std::string & role) { if (role == "system") { return COMMON_CHAT_ROLE_SYSTEM; } if (role == "assistant") { return COMMON_CHAT_ROLE_ASSISTANT; } if (role == "user") { return COMMON_CHAT_ROLE_USER; } if (role == "tool") { return COMMON_CHAT_ROLE_TOOL; } return COMMON_CHAT_ROLE_UNKNOWN; } const char * common_chat_role_to_string(common_chat_role role) { switch (role) { case COMMON_CHAT_ROLE_SYSTEM: return "system"; case COMMON_CHAT_ROLE_ASSISTANT: return "assistant"; case COMMON_CHAT_ROLE_USER: return "user"; case COMMON_CHAT_ROLE_TOOL: return "tool"; case COMMON_CHAT_ROLE_UNKNOWN: return ""; } return ""; } json common_chat_msg_delimiters::to_json() const { json result = json::array(); for (const auto & d : delimiters) { result.push_back({ { "role", common_chat_role_to_string(d.role) }, { "delimiter", d.delimiter }, }); } return result; } common_chat_msg_delimiters common_chat_msg_delimiters_parse(const json & delimiters) { common_chat_msg_delimiters result; if (!delimiters.is_array()) { return result; } result.delimiters.reserve(delimiters.size()); for (const auto & d : delimiters) { if (!d.is_object()) { continue; } result.delimiters.push_back({ common_chat_role_from_string(d.value("role", std::string())), d.value("delimiter", std::string()), }); } return result; } void common_chat_msg_delimiters::tokenize(const llama_vocab * vocab) { for (auto & d : delimiters) { d.tokens = common_tokenize(vocab, d.delimiter, false, true); } } common_chat_msg_spans common_chat_msg_delimiters::split(const llama_tokens & tokens, const std::map & skips) const { std::vector> matches; auto skip = skips.begin(); for (size_t i = 0; i < tokens.size();) { if (skip != skips.end() && i == skip->first) { i += skip->second; ++skip; continue; } for (const auto & d : delimiters) { if (i + d.tokens.size() > tokens.size()) { continue; } if (std::equal(d.tokens.begin(), d.tokens.end(), tokens.begin() + i)) { matches.emplace_back(d.role, i); break; } } i++; } matches.emplace_back(COMMON_CHAT_ROLE_UNKNOWN, tokens.size()); common_chat_msg_spans spans; for (size_t i = 0; i + 1 < matches.size(); i++) { const auto & curr = matches[i]; const auto & next = matches[i + 1]; spans.add(curr.first, curr.second, next.second - curr.second); } return spans; } json common_chat_msg::to_json_oaicompat(bool concat_typed_text) const { if (!content.empty() && !content_parts.empty()) { throw std::runtime_error("Cannot specify both content and content_parts"); } json jmsg { {"role", role}, }; if (!content.empty()) { jmsg["content"] = content; } else if (!content_parts.empty()) { if (concat_typed_text || contains_media()) { std::string text; bool last_was_media_marker = false; // join parts with newline, do not add newline before or after media markers for (const auto & part : content_parts) { bool add_new_line = true; if (part.type == "text") { add_new_line = !last_was_media_marker && !text.empty(); last_was_media_marker = false; } else if (part.type == "media_marker") { add_new_line = false; last_was_media_marker = true; } else { LOG_WRN("Ignoring content part type: %s\n", part.type.c_str()); continue; } if (add_new_line) { text += '\n'; } text += part.text; } jmsg["content"] = text; } else { auto & parts = jmsg["content"] = json::array(); for (const auto & part : content_parts) { parts.push_back({ {"type", part.type}, {"text", part.text}, }); } } } else { jmsg["content"] = ""; } if (!reasoning_content.empty()) { jmsg["reasoning_content"] = reasoning_content; } if (!tool_name.empty()) { jmsg["name"] = tool_name; } if (!tool_call_id.empty()) { jmsg["tool_call_id"] = tool_call_id; } if (!tool_calls.empty()) { jmsg["tool_calls"] = json::array(); auto & jtool_calls = jmsg["tool_calls"]; for (const auto & tool_call : tool_calls) { json tc { {"type", "function"}, {"function", { {"name", tool_call.name}, {"arguments", json(tool_call.arguments)}, }}, }; if (!tool_call.id.empty()) { tc["id"] = tool_call.id; } // Some templates generate and require an id (sometimes in a very specific format, e.g. Mistral Nemo). // We only generate a random id for the ones that don't generate one by themselves // (they also won't get to see it as their template likely doesn't use it, so it's all for the client) // {"id", tc.id.empty() ? gen_tool_call_id() : tc.id}, jtool_calls.push_back(tc); } } return jmsg; } std::vector common_chat_msg_diff::compute_diffs(const common_chat_msg & msg_prv, const common_chat_msg & msg_new) { std::vector diffs; if (msg_new.tool_calls.size() > msg_prv.tool_calls.size()) { diffs.reserve(msg_new.tool_calls.size() - msg_prv.tool_calls.size() + 3); } else { diffs.reserve(3); } // TODO: these can become expensive for long messages - how to optimize? if (msg_prv.reasoning_content != msg_new.reasoning_content) { auto & diff = diffs.emplace_back(); diff.reasoning_content_delta = string_diff(msg_prv.reasoning_content, msg_new.reasoning_content); } if (msg_prv.content != msg_new.content) { auto & diff = diffs.emplace_back(); diff.content_delta = string_diff(msg_prv.content, msg_new.content); } if (msg_new.tool_calls.size() < msg_prv.tool_calls.size()) { std::string err = "Invalid diff: now finding less tool calls!\n"; err += " Previous (" + std::to_string(msg_prv.tool_calls.size()) + "):\n"; for (const auto & tc : msg_prv.tool_calls) { err += " - name: '" + tc.name + "', args: '" + tc.arguments + "'\n"; } err += " Current (" + std::to_string(msg_new.tool_calls.size()) + "):\n"; for (const auto & tc : msg_new.tool_calls) { err += " - name: '" + tc.name + "', args: '" + tc.arguments + "'\n"; } err += " Current msg text content:\n" + msg_new.content + "\n"; throw std::runtime_error(err); } if (!msg_prv.tool_calls.empty()) { const auto idx = msg_prv.tool_calls.size() - 1; const auto & pref = msg_prv.tool_calls[idx]; const auto & newf = msg_new.tool_calls[idx]; // Allow tool name to change during incremental parsing: // - empty -> non-empty (initial discovery) // - prefix -> longer string (name grows as more input is parsed) if (pref.name != newf.name && !pref.name.empty() && !newf.name.empty()) { // Check if one is a prefix of the other (for incremental parsing where names grow or shrink) bool is_prefix = (newf.name.rfind(pref.name, 0) == 0); if (!is_prefix) { LOG_ERR("Tool call mismatch: prev='%s' new='%s'\n", pref.name.c_str(), newf.name.c_str()); throw std::runtime_error("Invalid diff: tool call mismatch!"); } } const auto args_diff = string_diff(pref.arguments, newf.arguments); if (!args_diff.empty() || pref.id != newf.id || pref.name != newf.name) { auto & diff = diffs.emplace_back(); diff.tool_call_index = idx; if (pref.id != newf.id || pref.name != newf.name) { diff.tool_call_delta.id = newf.id; diff.tool_call_delta.name = newf.name; } diff.tool_call_delta.arguments = args_diff; } } for (size_t idx = msg_prv.tool_calls.size(); idx < msg_new.tool_calls.size(); ++idx) { auto & diff = diffs.emplace_back(); diff.tool_call_index = idx; diff.tool_call_delta = msg_new.tool_calls[idx]; } return diffs; } using chat_template_caps = jinja::caps; struct common_chat_templates { bool add_bos; bool add_eos; bool has_explicit_template; // Model had builtin template or template overridden was specified. std::unique_ptr template_default; // always set (defaults to chatml) std::unique_ptr template_tool_use; }; common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice) { if (tool_choice == "auto") { return COMMON_CHAT_TOOL_CHOICE_AUTO; } if (tool_choice == "none") { return COMMON_CHAT_TOOL_CHOICE_NONE; } if (tool_choice == "required") { return COMMON_CHAT_TOOL_CHOICE_REQUIRED; } throw std::invalid_argument("Invalid tool_choice: " + tool_choice); } bool common_chat_templates_support_enable_thinking(const common_chat_templates * chat_templates) { common_chat_templates_inputs inputs; inputs.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK; common_chat_msg msg; msg.role = "user"; msg.content = "test"; inputs.messages = { msg }; inputs.enable_thinking = true; inputs.add_generation_prompt = true; inputs.reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK; auto params = common_chat_templates_apply(chat_templates, inputs); return params.supports_thinking; } std::vector common_chat_msgs_parse_oaicompat(const json & messages) { std::vector msgs; try { if (!messages.is_array()) { throw std::invalid_argument("Expected 'messages' to be an array, got " + messages.dump()); } for (const auto & message : messages) { if (!message.is_object()) { throw std::invalid_argument("Expected 'message' to be an object, got " + message.dump()); } common_chat_msg msg; if (!message.contains("role")) { throw std::invalid_argument("Missing 'role' in message: " + message.dump()); } msg.role = message.at("role"); auto has_content = message.contains("content"); auto has_tool_calls = message.contains("tool_calls"); if (has_content) { const auto & content = message.at("content"); if (content.is_string()) { msg.content = content; } else if (content.is_array()) { for (const auto & part : content) { if (!part.contains("type")) { throw std::invalid_argument("Missing content part type: " + part.dump()); } const auto & type = part.at("type"); if (type != "text" && type != "media_marker") { throw std::invalid_argument("Unsupported content part type: " + type.dump()); } common_chat_msg_content_part msg_part; msg_part.type = type; msg_part.text = part.at("text"); msg.content_parts.push_back(msg_part); } } else if (!content.is_null()) { throw std::invalid_argument("Invalid 'content' type: expected string or array, got " + content.dump() + " (ref: https://github.com/ggml-org/llama.cpp/issues/8367)"); } } if (has_tool_calls) { for (const auto & tool_call : message.at("tool_calls")) { common_chat_tool_call tc; if (!tool_call.contains("type")) { throw std::invalid_argument("Missing tool call type: " + tool_call.dump()); } const auto & type = tool_call.at("type"); if (type != "function") { throw std::invalid_argument("Unsupported tool call type: " + tool_call.dump()); } if (!tool_call.contains("function")) { throw std::invalid_argument("Missing tool call function: " + tool_call.dump()); } const auto & fc = tool_call.at("function"); if (!fc.contains("name")) { throw std::invalid_argument("Missing tool call name: " + tool_call.dump()); } tc.name = fc.at("name"); const auto & args = fc.at("arguments"); if (args.is_string()) { tc.arguments = args; } else { tc.arguments = args.dump(); } if (tool_call.contains("id")) { tc.id = tool_call.at("id"); } msg.tool_calls.push_back(tc); } } if (!has_content && !has_tool_calls) { throw std::invalid_argument( "Expected 'content' or 'tool_calls' (ref: https://github.com/ggml-org/llama.cpp/issues/8367 & " "https://github.com/ggml-org/llama.cpp/issues/12279)"); } if (message.contains("reasoning_content")) { msg.reasoning_content = message.at("reasoning_content"); } if (message.contains("name")) { msg.tool_name = message.at("name"); } if (message.contains("tool_call_id")) { msg.tool_call_id = message.at("tool_call_id"); } msgs.push_back(msg); } } catch (const std::exception & e) { // @ngxson : disable otherwise it's bloating the API response // printf("%s\n", std::string("; messages = ") + messages.dump(2)); throw std::runtime_error("Failed to parse messages: " + std::string(e.what())); } return msgs; } struct messages_inp_normalizer { const jinja::caps & caps; messages_inp_normalizer(const jinja::caps & c) : caps(c) {} // handle supports_string_content / supports_typed_content // if string=true and array=false, convert array to string // if string=false and array=true, convert string to array // if both are true, do nothing json normalize(const json & messages) { bool only_string = caps.supports_string_content && !caps.supports_typed_content; bool only_typed = !caps.supports_string_content && caps.supports_typed_content; if ((!only_string && !only_typed) || !messages.is_array()) { return messages; } json normalized = json::array(); for (const auto & msg : messages) { json copy = msg; if (copy.contains("content")) { json & it = copy.at("content"); if (only_typed && it.is_string()) { it = json::array({ json{ {"type", "text"}, {"text", it.get()}, } }); } else if (only_string && it.is_array()) { it = concat_content_parts(it); } } normalized.push_back(std::move(copy)); } return normalized; } // join parts with newline, do not add newline before or after media markers static std::string concat_content_parts(const json & parts) { std::string text; bool last_was_media_marker = false; for (const auto & part : parts) { std::string type = part.value("type", ""); bool add_new_line = true; if (type == "text") { add_new_line = !last_was_media_marker && !text.empty(); last_was_media_marker = false; } else if (type == "media_marker") { add_new_line = false; last_was_media_marker = true; } else { LOG_WRN("Ignoring content part type: %s\n", type.c_str()); continue; } if (add_new_line) { text += '\n'; } text += part.value("text", ""); } return text; } }; static json render_message_to_json(const std::vector & msgs, const jinja::caps & c) { if (!c.supports_string_content && !c.supports_typed_content) { LOG_WRN("%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues.\n", __func__); } json messages = json::array(); for (const auto & msg : msgs) { messages.push_back(msg.to_json_oaicompat(/* concat_typed_text= */ false)); } return messages_inp_normalizer(c).normalize(messages); } // DEPRECATED: only used in tests json common_chat_msgs_to_json_oaicompat(const std::vector & msgs, bool concat_typed_text) { jinja::caps c; c.supports_string_content = true; c.supports_typed_content = !concat_typed_text; return render_message_to_json(msgs, c); } json common_chat_tools_to_json_oaicompat(const std::vector & tools) { if (tools.empty()) { return json(); } auto result = json::array(); for (const auto & tool : tools) { result.push_back({ { "type", "function" }, { "function", { { "name", tool.name }, { "description", tool.description }, { "parameters", json::parse(tool.parameters) }, }}, }); } return result; } std::vector common_chat_tools_parse_oaicompat(const json & tools) { std::vector result; try { if (!tools.is_null()) { if (!tools.is_array()) { throw std::invalid_argument("Expected 'tools' to be an array, got " + tools.dump()); } for (const auto & tool : tools) { if (!tool.contains("type")) { throw std::invalid_argument("Missing tool type: " + tool.dump()); } const auto & type = tool.at("type"); if (!type.is_string() || type != "function") { throw std::invalid_argument("Unsupported tool type: " + tool.dump()); } if (!tool.contains("function")) { throw std::invalid_argument("Missing tool function: " + tool.dump()); } const auto & function = tool.at("function"); result.push_back({ /* .name = */ function.at("name"), /* .description = */ function.value("description", ""), /* .parameters = */ function.value("parameters", json::object()).dump(), }); } } } catch (const std::exception & e) { throw std::runtime_error("Failed to parse tools: " + std::string(e.what()) + "; tools = " + tools.dump(2)); } return result; } #include "common/unicode.h" #include "trie.cpp" #include "peg-parser.cpp" #include "chat-peg-parser.cpp" common_chat_continuation common_chat_continuation_parse(const common_json & value) { if (value.is_boolean() && value.get()) { return COMMON_CHAT_CONTINUATION_AUTO; } if (value.is_string()) { auto value_str = value.get(); if (value_str == "reasoning_content") { return COMMON_CHAT_CONTINUATION_REASONING; } if (value_str == "content") { return COMMON_CHAT_CONTINUATION_CONTENT; } } return COMMON_CHAT_CONTINUATION_NONE; } bool common_chat_verify_template(const std::string & tmpl, bool use_jinja) { if (use_jinja) { try { common_chat_msg msg; msg.role = "user"; msg.content = "test"; auto tmpls = common_chat_templates_init(/* model= */ nullptr, tmpl); common_chat_templates_inputs inputs; inputs.messages = { msg }; common_chat_templates_apply(tmpls.get(), inputs); return true; } catch (const std::exception & e) { LOG_ERR("%s: failed to apply template: %s\n", __func__, e.what()); return false; } } llama_chat_message chat[] = { { "user", "test" } }; const int res = llama_chat_apply_template(tmpl.c_str(), chat, 1, true, nullptr, 0); return res >= 0; } std::string common_chat_format_single(const struct common_chat_templates * tmpls, const std::vector & past_msg, const common_chat_msg & new_msg, bool add_ass, bool use_jinja) { common_chat_templates_inputs inputs; inputs.use_jinja = use_jinja; inputs.add_bos = tmpls->add_bos; inputs.add_eos = tmpls->add_eos; std::string fmt_past_msg; if (!past_msg.empty()) { inputs.messages = past_msg; inputs.add_generation_prompt = false; fmt_past_msg = common_chat_templates_apply(tmpls, inputs).prompt; } std::ostringstream ss; // if the past_msg ends with a newline, we must preserve it in the formatted version if (add_ass && !fmt_past_msg.empty() && fmt_past_msg.back() == '\n') { ss << "\n"; }; // format chat with new_msg inputs.messages.push_back(new_msg); inputs.add_generation_prompt = add_ass; auto fmt_new_msg = common_chat_templates_apply(tmpls, inputs).prompt; // get the diff part ss << fmt_new_msg.substr(fmt_past_msg.size(), fmt_new_msg.size() - fmt_past_msg.size()); return ss.str(); } std::string common_chat_format_example(const struct common_chat_templates * tmpls, bool use_jinja, const std::map & chat_template_kwargs) { common_chat_templates_inputs inputs; inputs.use_jinja = use_jinja; inputs.add_bos = tmpls->add_bos; inputs.add_eos = tmpls->add_eos; inputs.chat_template_kwargs = chat_template_kwargs; auto add_simple_msg = [&](auto role, auto content) { common_chat_msg msg; msg.role = role; msg.content = content; inputs.messages.push_back(msg); }; add_simple_msg("system", "You are a helpful assistant"); add_simple_msg("user", "Hello"); add_simple_msg("assistant", "Hi there"); add_simple_msg("user", "How are you?"); return common_chat_templates_apply(tmpls, inputs).prompt; } #define CHATML_TEMPLATE_SRC \ "{%- for message in messages -%}\n" \ " {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' -}}\n" \ "{%- endfor -%}\n" \ "{%- if add_generation_prompt -%}\n" \ " {{- '<|im_start|>assistant\n' -}}\n" \ "{%- endif -%}" void common_chat_templates_free(struct common_chat_templates * tmpls) { delete tmpls; } bool common_chat_templates_was_explicit(const struct common_chat_templates * tmpls) { return tmpls->has_explicit_template; } common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates) { common_chat_prompt_preset asr_preset; asr_preset.system = ""; asr_preset.user = "Transcribe audio to text"; if (chat_templates && chat_templates->template_default && is_lfm2_template(chat_templates->template_default->source())) { asr_preset.system = "Perform ASR."; asr_preset.user = ""; } return asr_preset; } std::string common_chat_templates_source(const struct common_chat_templates * tmpls, const std::string & variant) { if (!variant.empty()) { if (variant == "tool_use") { if (tmpls->template_tool_use) { return tmpls->template_tool_use->source(); } return ""; } LOG_DBG("%s: unknown template variant: %s\n", __func__, variant.c_str()); } return tmpls->template_default->source(); } common_chat_templates_ptr common_chat_templates_init(const struct llama_model * model, const std::string & chat_template_override, const std::string & bos_token_override, const std::string & eos_token_override) { std::string default_template_src; std::string template_tool_use_src; bool has_explicit_template = !chat_template_override.empty(); if (chat_template_override.empty()) { GGML_ASSERT(model != nullptr); const auto * str = llama_model_chat_template(model, /* name */ nullptr); if (str) { default_template_src = str; has_explicit_template = true; } str = llama_model_chat_template(model, /* name */ "tool_use"); if (str) { template_tool_use_src = str; has_explicit_template = true; } } else { default_template_src = chat_template_override; } if (default_template_src.empty() || default_template_src == "chatml") { if (!template_tool_use_src.empty()) { default_template_src = template_tool_use_src; } else { default_template_src = CHATML_TEMPLATE_SRC; } } // TODO @ngxson : this is a temporary hack to prevent chat template from throwing an error // Ref: https://github.com/ggml-org/llama.cpp/pull/15230#issuecomment-3173959633 if (default_template_src.find("<|channel|>") != std::string::npos // search for the error message and patch it && default_template_src.find("in message.content or") != std::string::npos) { string_replace_all(default_template_src, "{%- if \"<|channel|>analysis<|message|>\" in message.content or " "\"<|channel|>final<|message|>\" in message.content %}", "{%- if false %}"); } // TODO @aldehir : this is a temporary fix, pending Minja changes // Ref: https://github.com/ggml-org/llama.cpp/pull/17713#issuecomment-3631342664 if (default_template_src.find("[TOOL_CALLS]") != std::string::npos // search for the error message and patch it && default_template_src.find("if (message['content'] is none or") != std::string::npos) { string_replace_all(default_template_src, "{%- if (message['content'] is none or message['content'] == '' or " "message['content']|length == 0) and (message['tool_calls'] is not defined or " "message['tool_calls'] is none or message['tool_calls']|length == 0) %}", "{%- if false %}"); } std::string token_bos = bos_token_override; std::string token_eos = eos_token_override; bool add_bos = false; bool add_eos = false; if (model) { const auto * vocab = llama_model_get_vocab(model); const auto get_token = [&](llama_token token, const char * name, const char * jinja_variable_name) { if (token == LLAMA_TOKEN_NULL) { if (default_template_src.find(jinja_variable_name) != std::string::npos || template_tool_use_src.find(jinja_variable_name) != std::string::npos) { LOG_WRN( "common_chat_templates_init: warning: vocab does not have a %s token, jinja template won't " "work as intended.\n", name); } return std::string(); } return common_token_to_piece(vocab, token, true); }; token_bos = get_token(llama_vocab_bos(vocab), "BOS", "bos_token"); token_eos = get_token(llama_vocab_eos(vocab), "EOS", "eos_token"); add_bos = llama_vocab_get_add_bos(vocab); add_eos = llama_vocab_get_add_eos(vocab); } common_chat_templates_ptr tmpls(new common_chat_templates()); tmpls->has_explicit_template = has_explicit_template; tmpls->add_bos = add_bos; tmpls->add_eos = add_eos; try { tmpls->template_default = std::make_unique(default_template_src, token_bos, token_eos); } catch (const std::exception & e) { LOG_ERR("%s: error: %s\n", __func__, e.what()); LOG_ERR("%s: failed to initialize chat template\n", __func__); LOG_ERR("%s: please consider disabling jinja via --no-jinja, or using another chat template\n", __func__); throw e; } if (!template_tool_use_src.empty()) { try { tmpls->template_tool_use = std::make_unique(template_tool_use_src, token_bos, token_eos); } catch (const std::exception & e) { LOG_ERR("%s: failed to parse tool use chat template (ignoring it): %s\n", __func__, e.what()); } } return tmpls; } const char * common_chat_format_name(common_chat_format format) { switch (format) { case COMMON_CHAT_FORMAT_CONTENT_ONLY: return "Content-only"; case COMMON_CHAT_FORMAT_PEG_SIMPLE: return "peg-simple"; case COMMON_CHAT_FORMAT_PEG_NATIVE: return "peg-native"; case COMMON_CHAT_FORMAT_PEG_GEMMA4: return "peg-gemma4"; case COMMON_CHAT_FORMAT_PEG_MINIMAX_M3: return "peg-minimax-m3"; default: throw std::runtime_error("Unknown chat format"); } } const char * common_reasoning_format_name(common_reasoning_format format) { switch (format) { case COMMON_REASONING_FORMAT_NONE: return "none"; case COMMON_REASONING_FORMAT_AUTO: return "auto"; case COMMON_REASONING_FORMAT_DEEPSEEK: return "deepseek"; case COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY: return "deepseek-legacy"; default: throw std::runtime_error("Unknown reasoning format"); } } common_reasoning_format common_reasoning_format_from_name(const std::string & format) { if (format == "none") { return COMMON_REASONING_FORMAT_NONE; } if (format == "auto") { return COMMON_REASONING_FORMAT_AUTO; } if (format == "deepseek") { return COMMON_REASONING_FORMAT_DEEPSEEK; } if (format == "deepseek-legacy") { return COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY; } throw std::runtime_error("Unknown reasoning format: " + format); } std::string common_chat_template_direct_apply_impl( const common_chat_template & tmpl, const autoparser::generation_params & inputs, const std::optional & messages_override, const std::optional & tools_override, const std::optional & additional_context) { jinja::context ctx(tmpl.source()); // messages_override is already built for this template, do not touch its content parts json inp = json{ {"messages", messages_override.has_value() ? *messages_override : messages_inp_normalizer(tmpl.original_caps()).normalize(inputs.messages)}, {"bos_token", tmpl.bos_token()}, {"eos_token", tmpl.eos_token()}, {"enable_thinking", inputs.enable_thinking}, }; if (tools_override.has_value() || !inputs.tools.empty()) { inp["tools"] = tools_override.has_value() ? *tools_override : inputs.tools; } if (inputs.extra_context.is_object()) { // TODO: do we need to merge, or replacing is fine? for (const auto & [k, v] : inputs.extra_context.items()) { inp[k] = v; } } if (additional_context.has_value()) { // TODO: merge properly instead of overwriting (matching old behavior) for (const auto & [k, v] : additional_context->items()) { inp[k] = v; } } if (inputs.add_generation_prompt) { inp["add_generation_prompt"] = true; } if (inp.contains("preserve_reasoning") && inp["preserve_reasoning"].is_boolean()) { bool enabled = inp["preserve_reasoning"].get(); jinja::caps_apply_preserve_reasoning(ctx, enabled); } if (inp.contains("reasoning_effort") && inp["reasoning_effort"].is_string() && !inp["reasoning_effort"].empty()) { std::string reasoning_effort = inp["reasoning_effort"].get(); jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } jinja::global_from_json(ctx, inp, inputs.mark_input); // render jinja::runtime runtime(ctx); const jinja::value results = runtime.execute(tmpl.prog); auto parts = jinja::runtime::gather_string_parts(results); std::string result = parts->as_string().str(); // TODO: improve this later if (inputs.add_bos && string_starts_with(result, tmpl.bos_token())) { result = result.substr(tmpl.bos_token().size()); } if (inputs.add_eos && string_ends_with(result, tmpl.eos_token())) { result = result.substr(0, result.size() - tmpl.eos_token().size()); } return result; } std::string common_chat_template_direct_apply( const common_chat_template & tmpl, const autoparser::generation_params & inputs) { return common_chat_template_direct_apply_impl(tmpl, inputs, std::nullopt, std::nullopt, std::nullopt); } std::string common_chat_template_generation_prompt_impl( const common_chat_template & tmpl, const autoparser::generation_params & inputs, const std::optional & messages_override, const std::optional & tools_override, const std::optional & additional_context) { autoparser::generation_params params = inputs; params.add_generation_prompt = false; params.continue_final_message = COMMON_CHAT_CONTINUATION_NONE; std::string no_gen_prompt = common_chat_template_direct_apply_impl(tmpl, params, messages_override, tools_override, additional_context); params.add_generation_prompt = true; std::string gen_prompt = common_chat_template_direct_apply_impl(tmpl, params, messages_override, tools_override, additional_context); size_t prefix_len = 0; size_t min_size = std::min(no_gen_prompt.size(), gen_prompt.size()); while (prefix_len < min_size && no_gen_prompt[prefix_len] == gen_prompt[prefix_len]) { prefix_len++; } return gen_prompt.substr(prefix_len); } std::string common_chat_template_generation_prompt( const common_chat_template & tmpl, const autoparser::generation_params & inputs) { return common_chat_template_generation_prompt_impl(tmpl, inputs, std::nullopt, std::nullopt, std::nullopt); } namespace workaround { static void map_developer_role_to_system(json & messages) { for (auto & message : messages) { if (message.contains("role")) { if (message["role"] == "developer") { message["role"] = "system"; } } } } // if first message is system and template does not support it, merge it with next message static void system_message_not_supported(json & messages) { if (!messages.empty() && messages.front().at("role") == "system") { if (messages.size() > 1) { LOG_DBG("Merging system prompt into next message\n"); auto & first_msg = messages.front(); auto & second_msg = messages[1]; second_msg["content"] = first_msg.at("content").get() + "\n" + second_msg.at("content").get(); messages.erase(0); } else { LOG_WRN("Removing system prompt due to template not supporting system role\n"); messages.erase(0); } } } static void requires_non_null_content(json & messages) { GGML_ASSERT(messages.is_array()); for (auto & message : messages) { if (message.contains("tool_calls") && !message.contains("content")) { message["content"] = ""; } } } static void func_args_not_string(json & messages) { GGML_ASSERT(messages.is_array()); for (auto & message : messages) { if (message.contains("tool_calls")) { for (auto & tool_call : message["tool_calls"]) { if (tool_call.contains("function") && tool_call["function"].contains("arguments")) { auto & args = tool_call["function"]["arguments"]; if (args.is_string()) { try { args = json::parse(args.get()); } catch (const std::exception & e) { throw std::runtime_error("Failed to parse tool call arguments as JSON: " + std::string(e.what())); } } } } } } } // Trim leading/trailing whitespace from message contents before rendering. This // has to run on the messages (not on the rendered JSON) because templates with // string-only content caps concatenate typed content parts into a single string // during rendering, after which the per-part whitespace can no longer be reached. // Both the plain string content and the text of typed content parts are trimmed. static void trim_all_content(std::vector & messages) { for (auto & message : messages) { message.content = trim_whitespace(message.content); message.reasoning_content = trim_whitespace(message.reasoning_content); for (auto & part : message.content_parts) { if (part.type == "text") { part.text = trim_whitespace(part.text); } } } } } static json common_chat_extra_context() { json ctx = json::object(); std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); std::string datetime_str = format_time(now, "%b %d %Y"); std::string date_str = format_time(now, "%d %b %Y"); ctx["datetime"] = datetime_str; ctx["date_string"] = date_str; return ctx; } std::optional common_chat_try_specialized_template( const common_chat_template & tmpl, const std::string & src, autoparser::generation_params & params) { // Ministral/Mistral Large 3 - uses special reasoning structure fixes, can't use autoparser // Note: Mistral Small 3.2 uses [CALL_ID] which Ministral doesn't have, so we can distinguish them if (src.find("[SYSTEM_PROMPT]") != std::string::npos && src.find("[TOOL_CALLS]") != std::string::npos && src.find("[ARGS]") != std::string::npos && src.find("[CALL_ID]") == std::string::npos) { LOG_DBG("Using specialized template: Ministral/Magistral Large 3\n"); return common_chat_params_init_ministral_3(tmpl, params); } // GPT-OSS - has unique channel-based structure that needs dedicated handler if (src.find("<|channel|>") != std::string::npos) { LOG_DBG("Using specialized template: GPT-OSS\n"); return common_chat_params_init_gpt_oss(tmpl, params); } // Muse Glimmer format using " to=" recipients and <|eom|>/<|eot|> message terminators. if (src.find("") != std::string::npos && src.find("<|eom|>") != std::string::npos) { LOG_DBG("Using specialized template: Muse Glimmer\n"); return common_chat_params_init_muse_glimmer(tmpl, params); } // Functionary v3.2 - uses recipient-based format with >>>recipient\n{content} // Detection: template has ">>>all" for content and ">>>" prefix for tool calls if (src.find(">>>all") != std::string::npos && src.find(">>>${recipient}") != std::string::npos) { LOG_DBG("Using specialized template: Functionary v3.2\n"); return common_chat_params_init_functionary_v3_2(tmpl, params); } // Kimi K2 Thinking - uses unique tool call ID format: functions.: // Detection: template has "<|tool_calls_section_begin|>" and "functions." prefix in tool call IDs if (src.find("<|tool_calls_section_begin|>") != std::string::npos && src.find("<|tool_call_begin|>") != std::string::npos) { LOG_DBG("Using specialized template: Kimi K2 Thinking\n"); return common_chat_params_init_kimi_k2(tmpl, params); } // Kimi K3 - the <|open|>/<|close|>/<|end_of_msg|> markers are unique to it if (src.find("<|open|>") != std::string::npos && src.find("<|close|>") != std::string::npos && src.find("<|end_of_msg|>") != std::string::npos) { LOG_DBG("Using specialized template: Kimi K3\n"); return common_chat_params_init_kimi_k3(tmpl, params); } // Cohere2 MoE / North Code - marker-wrapped format with <|START_TEXT|> content and // <|START_ACTION|> JSON tool calls. <|START_TEXT|> is unique to this template (the older // Command-R templates use <|START_RESPONSE|>). if (src.find("<|START_TEXT|>") != std::string::npos && src.find("<|START_ACTION|>") != std::string::npos) { LOG_DBG("Using specialized template: Cohere2 MoE\n"); return common_chat_params_init_cohere2moe(tmpl, params); } if (is_lfm2_template(src)) { LOG_DBG("Using specialized template: LFM2\n"); return common_chat_params_init_lfm2(tmpl, params, /* tool_list_tokens = */ true); } // LFM2.5 format detection: template uses plain "List of tools: [...]" with no special tokens if (src.find("List of tools: [") != std::string::npos && src.find("<|tool_list_start|>") == std::string::npos) { LOG_DBG("Using specialized template: LFM2.5\n"); return common_chat_params_init_lfm2(tmpl, params, /* tool_list_tokens = */ false); } // GigaChatV3 format detection if (src.find("<|role_sep|>") != std::string::npos && src.find("<|message_sep|>") != std::string::npos && src.find("<|function_call|>") == std::string::npos) { LOG_DBG("Using specialized template: GigaChatV3\n"); return common_chat_params_init_gigachat_v3(tmpl, params); } // MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's // markup delimiters, so detect the template and use a dedicated parser. if (src.find("]<]minimax[>[") != std::string::npos && src.find("") != std::string::npos && src.find("call:'") != std::string::npos) { if (src.find("{#- OpenAI Chat Completions:") == std::string::npos) { // apply workarounds if using the older gemma4 templates LOG_WRN("%s: detected an outdated gemma4 chat template, applying compatibility workarounds. " "Consider updating to the official template.\n", __func__); workaround::convert_tool_responses_gemma4(params.messages); } return common_chat_params_init_gemma4(tmpl, params); } // MiniCPM5 - XML tool calls with ... if (src.find("Tool usage guidelines:") != std::string::npos && src.find("") != std::string::npos && src.find("template_tool_use ? *tmpls->template_tool_use : *tmpls->template_default; const auto & src = tmpl.source(); const auto & caps = tmpl.original_caps(); std::vector trimmed_messages; const std::vector * messages_to_render = &inputs.messages; if (src.find("You have access to the following functions in JSONSchema format") != std::string::npos) { // StepFun: trim message contents (including typed content parts) before rendering, // otherwise leftover whitespace drives the model into reasoning loops (issue #24181) trimmed_messages = inputs.messages; workaround::trim_all_content(trimmed_messages); messages_to_render = &trimmed_messages; } params.messages = render_message_to_json(*messages_to_render, tmpl.original_caps()); params.tool_choice = inputs.tool_choice; params.reasoning_format = inputs.reasoning_format; params.enable_thinking = inputs.enable_thinking; params.grammar = inputs.grammar; params.now = inputs.now; params.add_generation_prompt = inputs.add_generation_prompt; params.add_bos = tmpls->add_bos; params.add_eos = tmpls->add_eos; params.continue_final_message = inputs.continue_final_message; if (params.continue_final_message != COMMON_CHAT_CONTINUATION_NONE) { params.add_generation_prompt = false; if (!inputs.messages.empty()) { // Render messages[:-1] and store continuation message separately params.continue_msg = inputs.messages.back(); params.messages.erase(params.messages.size() - 1); } if (params.continue_final_message == COMMON_CHAT_CONTINUATION_AUTO && !inputs.messages.empty()) { // Resolve based on message content params.continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT; if (!params.continue_msg.reasoning_content.empty() && params.continue_msg.content.empty() && params.continue_msg.content_parts.empty()) { params.continue_final_message = COMMON_CHAT_CONTINUATION_REASONING; } } } if (src.find("<|channel|>") == std::string::npos) { // map developer to system for all models except for GPT-OSS workaround::map_developer_role_to_system(params.messages); } if (!tmpl.original_caps().supports_system_role) { workaround::system_message_not_supported(params.messages); } if (tmpl.original_caps().supports_tool_calls) { // some templates will require the content field in tool call messages // to still be non-null, this puts an empty string everywhere where the // content field is null workaround::requires_non_null_content(params.messages); } if (tmpl.original_caps().supports_object_arguments) { workaround::func_args_not_string(params.messages); } params.extra_context = common_chat_extra_context(); for (auto el : inputs.chat_template_kwargs) { params.extra_context[el.first] = json::parse(el.second); } if (!inputs.json_schema.empty()) { params.json_schema = json::parse(inputs.json_schema); } params.parallel_tool_calls = inputs.parallel_tool_calls; if (params.tools.is_array()) { if (params.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && !params.grammar.empty()) { throw std::runtime_error("Cannot specify grammar with tools"); } if (caps.supports_tool_calls && !caps.supports_tools) { LOG_WRN( "Template supports tool calls but does not natively describe tools. The fallback behaviour used may " "produce bad results, inspect prompt w/ --verbose & consider overriding the template.\n"); } } if (inputs.force_pure_content) { LOG_WRN("Forcing pure content template, will not render reasoning or tools separately."); // Create the result structure common_chat_params data; auto params_copy = params; params_copy.reasoning_format = COMMON_REASONING_FORMAT_NONE; data.prompt = common_chat_template_direct_apply_impl(tmpl, params_copy); data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, params); data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; auto parser = build_chat_peg_parser([&data](common_chat_peg_builder &p) { return p.literal(data.generation_prompt) << p.content(p.rest()); }); data.parser = parser.save(); return data; } if (auto result = common_chat_try_specialized_template(tmpl, src, params)) { return *result; } try { LOG_DBG("%s: using differential autoparser\n", __func__); struct autoparser::autoparser autoparser; autoparser.analyze_template(tmpl); auto auto_params = autoparser::peg_generator::generate_parser(tmpl, params, autoparser); common_chat_msg_delimiters delimiters; if (!autoparser.assistant_start.empty()) { delimiters.add(COMMON_CHAT_ROLE_ASSISTANT, autoparser.assistant_start); } if (!autoparser.user_start.empty()) { delimiters.add(COMMON_CHAT_ROLE_USER, autoparser.user_start); } auto_params.message_delimiters = std::move(delimiters); auto_params.supports_thinking = autoparser.reasoning.mode != autoparser::reasoning_mode::NONE; if (auto_params.supports_thinking) { auto_params.thinking_start_tag = trim_whitespace(autoparser.reasoning.start); auto end_tag = trim_whitespace(autoparser.reasoning.end); if (!end_tag.empty()) { auto_params.thinking_end_tags = {std::move(end_tag)}; } } common_peg_arena arena; arena.load(auto_params.parser); LOG_DBG("%s: generated parser:\n%s\n\nparser generation prompt: %s\n", __func__, arena.dump(arena.root()).c_str(), auto_params.generation_prompt.c_str()); return auto_params; } catch (const std::exception & e) { throw std::invalid_argument(std::string("Unable to generate parser for this template. Automatic parser generation failed: ") + e.what()); } } // Legacy template route (adhoc C++ implementation of known templates), forward to llama_chat_apply_template. static common_chat_params common_chat_templates_apply_legacy(const struct common_chat_templates * tmpls, const struct common_chat_templates_inputs & inputs) { size_t alloc_size = 0; std::vector chat; std::vector contents; for (const auto & msg : inputs.messages) { auto content = msg.content; for (const auto & part : msg.content_parts) { if (part.type != "text" && part.type != "media_marker") { LOG_WRN("Ignoring non-text content part: %s\n", part.type.c_str()); continue; } if (!content.empty()) { content += "\n"; ; } content += part.text; } contents.emplace_back(std::move(content)); } for (size_t i = 0; i < contents.size(); ++i) { const auto & msg = inputs.messages[i]; const auto & content = contents[i]; chat.push_back({ msg.role.c_str(), content.c_str() }); size_t msg_size = msg.role.size() + content.size(); alloc_size += msg_size + (msg_size / 4); // == msg_size * 1.25 but avoiding float ops } std::vector buf(alloc_size); // run the first time to get the total output length const auto & src = tmpls->template_default->source(); int32_t res = llama_chat_apply_template(src.c_str(), chat.data(), chat.size(), inputs.add_generation_prompt, buf.data(), buf.size()); // error: chat template is not supported if (res < 0) { // if the custom "tmpl" is not supported, we throw an error // this is a bit redundant (for good), since we're not sure if user validated the custom template with llama_chat_verify_template() throw std::runtime_error("this custom template is not supported, try using --jinja"); } // if it turns out that our buffer is too small, we resize it if ((size_t) res > buf.size()) { buf.resize(res); res = llama_chat_apply_template(src.c_str(), chat.data(), chat.size(), inputs.add_generation_prompt, buf.data(), buf.size()); } // for safety, we check the result again if (res < 0 || (size_t) res > buf.size()) { throw std::runtime_error("failed to apply chat template, try using --jinja"); } common_chat_params params; params.prompt = std::string(buf.data(), res); if (!inputs.json_schema.empty()) { params.grammar = json_schema_to_grammar(json::parse(inputs.json_schema)); } else { params.grammar = inputs.grammar; } return params; } common_chat_params common_chat_templates_apply(const struct common_chat_templates * tmpls, const struct common_chat_templates_inputs & inputs) { GGML_ASSERT(tmpls != nullptr); return inputs.use_jinja ? common_chat_templates_apply_jinja(tmpls, inputs) : common_chat_templates_apply_legacy(tmpls, inputs); } common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_parser_params & params) { return common_chat_peg_parse(params.parser, input, is_partial, params); } common_chat_msg common_chat_peg_parse(const common_peg_arena & src_parser, const std::string & input, bool is_partial, const common_chat_parser_params & params) { const common_peg_arena & parser = src_parser.empty() ? build_chat_peg_parser([](common_chat_peg_builder & p) { return p.content(p.rest()) + p.end(); }) : src_parser; if (src_parser.empty()) { LOG_DBG("No parser definition detected, assuming pure content parser."); } const std::string effective_input = params.generation_prompt.empty() ? input : params.generation_prompt + input; //LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.c_str()); common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT; if (params.debug) { flags |= COMMON_PEG_PARSE_FLAG_DEBUG; } common_peg_parse_context ctx(effective_input, flags); auto result = parser.parse(ctx); if (result.fail()) { // During partial parsing, return partial results if any AST nodes were captured // This allows streaming to work correctly for formats like FUNC_MARKDOWN_CODE_BLOCK if (is_partial && result.end > 0) { // Try to extract any partial results from what was successfully parsed common_chat_msg msg; msg.role = "assistant"; std::unique_ptr mapper; if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) { mapper = std::make_unique(msg); } else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) { mapper = std::make_unique(msg); } else { mapper = std::make_unique(msg); } mapper->from_ast(ctx.ast, result); if (ctx.is_debug()) { fprintf(stderr, "\nAST for partial parse (fail):\n%s\n", ctx.ast.dump().c_str()); fflush(stderr); } return msg; } LOG_WRN("%s: unparsed %s output: %s\n", __func__, common_chat_format_name(params.format), effective_input.substr(result.end).c_str()); LOG_DBG("%s: full %s output triggering error:\n=== BEGIN ===\n%s\n=== END ===\n", __func__, common_chat_format_name(params.format), effective_input.c_str()); throw std::runtime_error(std::string("The model produced output that does not match the expected ") + common_chat_format_name(params.format) + " format"); } common_chat_msg msg; msg.role = "assistant"; std::unique_ptr mapper; if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) { mapper = std::make_unique(msg); } else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) { mapper = std::make_unique(msg); } else { mapper = std::make_unique(msg); } mapper->from_ast(ctx.ast, result); if (ctx.is_debug()) { fprintf(stderr, "\nAST for %s parse:\n%s\n", is_partial ? "partial" : "full", ctx.ast.dump().c_str()); fflush(stderr); } if (!is_partial) { LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat({ msg }).at(0).dump().c_str()); } return msg; } std::map common_chat_templates_get_caps(const common_chat_templates * chat_templates) { GGML_ASSERT(chat_templates != nullptr); GGML_ASSERT(chat_templates->template_default != nullptr); if (chat_templates->template_tool_use != nullptr) { // take the more expressive template when available return chat_templates->template_tool_use->caps.to_map(); } return chat_templates->template_default->caps.to_map(); }