mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-28 17:56:13 +08:00
* chat : strip trailing whitespace from parsed reasoning and content Assisted-by: Claude Fable 5.1 * chat : stop qwen3-coder reasoning and content before template separators Assisted-by: Claude * tests : remove the Qwen parser case from the chat session tests Assisted-by: Claude * common : handle partial prefix until match at end of generation --------- Co-authored-by: Zeshawn Zahid <zzshawnn@gmail.com> Co-authored-by: Alde Rojas <hello@alde.dev>
1956 lines
81 KiB
C++
1956 lines
81 KiB
C++
#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 "parsers/parsers.h"
|
||
#include "sampling.h"
|
||
|
||
#include "jinja/value.h"
|
||
#include "jinja/runtime.h"
|
||
#include "jinja/caps.h"
|
||
#include "peg-parser.h"
|
||
|
||
#include <algorithm>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <ctime>
|
||
#include <exception>
|
||
#include <functional>
|
||
#include <iomanip>
|
||
#include <map>
|
||
|
||
#include <optional>
|
||
#include <sstream>
|
||
#include <stdexcept>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
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 "";
|
||
}
|
||
|
||
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<size_t, size_t> & skips) const {
|
||
std::vector<std::pair<common_chat_role, size_t>> 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) {
|
||
if (part.type == "tool_reference") {
|
||
parts.push_back({
|
||
{"type", part.type},
|
||
{"name", part.name},
|
||
});
|
||
} else {
|
||
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> common_chat_msg_diff::compute_diffs(const common_chat_msg & msg_prv,
|
||
const common_chat_msg & msg_new) {
|
||
std::vector<common_chat_msg_diff> 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<common_chat_template> template_default; // always set (defaults to chatml)
|
||
std::unique_ptr<common_chat_template> template_tool_use;
|
||
std::unique_ptr<common_chat_template> template_tts; // the prompt of a text to speak
|
||
};
|
||
|
||
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_msg> common_chat_msgs_parse_oaicompat(const json & messages) {
|
||
std::vector<common_chat_msg> 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");
|
||
common_chat_msg_content_part msg_part;
|
||
msg_part.type = type;
|
||
if (type == "text" || type == "media_marker") {
|
||
msg_part.text = part.at("text");
|
||
} else if (type == "tool_reference") {
|
||
// names a request tool, the template renders its definition
|
||
msg_part.name = part.at("name");
|
||
} else {
|
||
throw std::invalid_argument("Unsupported content part type: " + type.dump());
|
||
}
|
||
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");
|
||
}
|
||
{
|
||
json extra = json::object();
|
||
for (const auto & [k, v] : message.items()) {
|
||
if (k != "role" && k != "content" && k != "tool_calls" && k != "reasoning_content" &&
|
||
k != "name" && k != "tool_call_id") {
|
||
extra[k] = v;
|
||
}
|
||
}
|
||
if (has_tool_calls) {
|
||
// per tool call: keys besides type/function/id, and function keys besides name/arguments
|
||
json tc_extra = json::array();
|
||
bool any = false;
|
||
for (const auto & tool_call : message.at("tool_calls")) {
|
||
json e = json::object();
|
||
for (const auto & [k, v] : tool_call.items()) {
|
||
if (k != "type" && k != "function" && k != "id") {
|
||
e[k] = v;
|
||
}
|
||
}
|
||
for (const auto & [k, v] : tool_call.at("function").items()) {
|
||
if (k != "name" && k != "arguments") {
|
||
e["function"][k] = v;
|
||
}
|
||
}
|
||
any = any || !e.empty();
|
||
tc_extra.push_back(e);
|
||
}
|
||
if (any) {
|
||
extra["__tool_call_extras"] = tc_extra;
|
||
}
|
||
}
|
||
if (!extra.empty()) {
|
||
msg.extra_json = extra.dump();
|
||
}
|
||
}
|
||
|
||
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<std::string>()},
|
||
}
|
||
});
|
||
} 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<common_chat_msg> & 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);
|
||
}
|
||
|
||
// DeepSeek V4.1 reads non-OpenAI message keys and joins content blocks with "\n\n" around images,
|
||
// so keep media markers as separate text parts and restore the extra keys
|
||
static json render_message_to_json_deepseek_v4_1(const std::vector<common_chat_msg> & msgs, const jinja::caps & c) {
|
||
json messages = json::array();
|
||
for (const auto & msg : msgs) {
|
||
json jmsg;
|
||
if (msg.contains_media() && msg.content.empty()) {
|
||
common_chat_msg typed = msg;
|
||
for (auto & part : typed.content_parts) {
|
||
part.type = "text";
|
||
}
|
||
jmsg = typed.to_json_oaicompat(/* concat_typed_text= */ false);
|
||
} else {
|
||
jmsg = msg.to_json_oaicompat(/* concat_typed_text= */ false);
|
||
}
|
||
if (!msg.extra_json.empty()) {
|
||
const json extra = json::parse(msg.extra_json);
|
||
for (const auto & [k, v] : extra.items()) {
|
||
if (k == "__tool_call_extras") {
|
||
if (!jmsg.contains("tool_calls")) {
|
||
continue;
|
||
}
|
||
auto & tcs = jmsg.at("tool_calls");
|
||
for (size_t i = 0; i < tcs.size() && i < v.size(); i++) {
|
||
for (const auto & [tk, tv] : v[i].items()) {
|
||
if (tk == "function") {
|
||
for (const auto & [fk, fv] : tv.items()) {
|
||
tcs[i]["function"][fk] = fv;
|
||
}
|
||
} else {
|
||
tcs[i][tk] = tv;
|
||
}
|
||
}
|
||
}
|
||
} else if (!jmsg.contains(k)) {
|
||
jmsg[k] = v;
|
||
}
|
||
}
|
||
}
|
||
messages.push_back(std::move(jmsg));
|
||
}
|
||
return messages_inp_normalizer(c).normalize(messages);
|
||
}
|
||
|
||
// DEPRECATED: only used in tests
|
||
json common_chat_msgs_to_json_oaicompat(const std::vector<common_chat_msg> & 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<common_chat_tool> & tools) {
|
||
if (tools.empty()) {
|
||
return json();
|
||
}
|
||
|
||
auto result = json::array();
|
||
for (const auto & tool : tools) {
|
||
json function {
|
||
{ "name", tool.name },
|
||
{ "description", tool.description },
|
||
{ "parameters", json::parse(tool.parameters) },
|
||
};
|
||
if (tool.defer_loading) {
|
||
function["defer_loading"] = true;
|
||
}
|
||
result.push_back({
|
||
{ "type", "function" },
|
||
{ "function", function },
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
|
||
json common_chat_tool_parameters(const json & function) {
|
||
if (function.contains("parameters")) {
|
||
const auto & params = function.at("parameters");
|
||
if (!params.is_null() && !(params.is_object() && params.empty())) {
|
||
return params;
|
||
}
|
||
}
|
||
return json{{"type", "object"}, {"properties", json::object()}};
|
||
}
|
||
|
||
std::vector<common_chat_tool> common_chat_tools_parse_oaicompat(const json & tools) {
|
||
std::vector<common_chat_tool> 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");
|
||
|
||
// "defer_loading" sits on the function, or on the tool next to "type"; the function wins
|
||
const json & defer = function.contains("defer_loading") ? function.at("defer_loading")
|
||
: tool.value("defer_loading", json(false));
|
||
if (!defer.is_boolean()) {
|
||
throw std::invalid_argument("Expected 'defer_loading' to be a boolean, got " + defer.dump());
|
||
}
|
||
|
||
result.push_back({
|
||
/* .name = */ function.at("name"),
|
||
/* .description = */ function.value("description", ""),
|
||
/* .parameters = */ function.value("parameters", json::object()).dump(),
|
||
/* .defer_loading = */ defer.get<bool>(),
|
||
});
|
||
}
|
||
}
|
||
} catch (const std::exception & e) {
|
||
throw std::runtime_error("Failed to parse tools: " + std::string(e.what()) + "; tools = " + tools.dump(2));
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
common_chat_continuation common_chat_continuation_parse(const common_json & value) {
|
||
if (value.is_null()) {
|
||
return COMMON_CHAT_CONTINUATION_NONE;
|
||
}
|
||
if (value.is_boolean()) {
|
||
return value.get<bool>() ? COMMON_CHAT_CONTINUATION_AUTO : COMMON_CHAT_CONTINUATION_NONE;
|
||
}
|
||
if (value.is_string()) {
|
||
auto value_str = value.get<std::string>();
|
||
if (value_str == "reasoning_content") {
|
||
return COMMON_CHAT_CONTINUATION_REASONING;
|
||
}
|
||
if (value_str == "content") {
|
||
return COMMON_CHAT_CONTINUATION_CONTENT;
|
||
}
|
||
}
|
||
throw std::invalid_argument("Invalid continue_final_message: expected a boolean, \"content\" or \"reasoning_content\"");
|
||
}
|
||
|
||
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<common_chat_msg> & 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.str();
|
||
}
|
||
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.str();
|
||
// 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<std::string, std::string> & 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.str();
|
||
}
|
||
|
||
#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;
|
||
}
|
||
|
||
// renders a task template (e.g. "tts") with the given variables
|
||
static std::string common_chat_template_render(const common_chat_template & tmpl, const json & vars) {
|
||
jinja::context ctx(tmpl.source());
|
||
jinja::global_from_json(ctx, vars, false);
|
||
ctx.set_val("bos_token", jinja::mk_val<jinja::value_string>(tmpl.bos_token()));
|
||
ctx.set_val("eos_token", jinja::mk_val<jinja::value_string>(tmpl.eos_token()));
|
||
jinja::runtime runtime(ctx);
|
||
return jinja::runtime::gather_string_parts(runtime.execute(tmpl.prog))->as_string().str();
|
||
}
|
||
|
||
// LFM3 format detection: template renders deferred tools inside <additional_tools>...</additional_tools>
|
||
static bool is_lfm3_template(const std::string & src) {
|
||
return src.find("<additional_tools>") != std::string::npos;
|
||
}
|
||
|
||
// LFM3-Audio: a known language code maps to its name, no language lets the model detect it
|
||
static std::string lfm3_asr_system_prompt(const std::string & language) {
|
||
static const std::map<std::string, std::string> names = {
|
||
{ "ar", "Arabic" }, { "da", "Danish" }, { "de", "German" }, { "el", "Greek" }, { "en", "English" },
|
||
{ "es", "Spanish" }, { "fi", "Finnish" }, { "fr", "French" }, { "he", "Hebrew" }, { "it", "Italian" },
|
||
{ "ja", "Japanese" }, { "ko", "Korean" }, { "nl", "Dutch" }, { "pl", "Polish" }, { "pt", "Portuguese" },
|
||
{ "sv", "Swedish" }, { "tr", "Turkish" }, { "zh", "Chinese" },
|
||
};
|
||
if (language.empty()) {
|
||
return "Perform ASR.";
|
||
}
|
||
std::string code = language;
|
||
std::transform(code.begin(), code.end(), code.begin(), [](unsigned char c) { return (char) std::tolower(c); });
|
||
const auto it = names.find(code);
|
||
return "Perform ASR in " + (it != names.end() ? it->second : language) + ".";
|
||
}
|
||
|
||
common_chat_prompt_preset common_chat_get_asr_prompt(const common_chat_templates * chat_templates, const std::string & language) {
|
||
common_chat_prompt_preset asr_preset;
|
||
asr_preset.system = "";
|
||
asr_preset.user = "Transcribe audio to text";
|
||
|
||
if (chat_templates && chat_templates->template_default) {
|
||
const std::string src = chat_templates->template_default->source();
|
||
if (is_lfm2_template(src)) {
|
||
asr_preset.system = "Perform ASR.";
|
||
asr_preset.user = "";
|
||
} else if (is_lfm3_template(src)) {
|
||
asr_preset.system = lfm3_asr_system_prompt(language);
|
||
asr_preset.user = "";
|
||
asr_preset.has_language = true;
|
||
}
|
||
}
|
||
|
||
return asr_preset;
|
||
}
|
||
|
||
std::string common_chat_get_tts_prompt(const common_chat_templates * chat_templates, const std::string & text,
|
||
const std::string & language, const std::string & voice) {
|
||
if (!chat_templates || !chat_templates->template_tts) {
|
||
return "";
|
||
}
|
||
return common_chat_template_render(*chat_templates->template_tts, {
|
||
{"messages", json::array({{{"role", "user"}, {"content", text}}})},
|
||
{"language", language},
|
||
{"voice", voice},
|
||
});
|
||
}
|
||
|
||
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<common_chat_template>(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<common_chat_template>(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());
|
||
}
|
||
}
|
||
// task templates of the model, kept with a chat template override
|
||
if (model) {
|
||
if (const char * src = llama_model_chat_template(model, "tts")) {
|
||
try {
|
||
tmpls->template_tts = std::make_unique<common_chat_template>(src, token_bos, token_eos);
|
||
} catch (const std::exception & e) {
|
||
LOG_ERR("%s: failed to parse the tts 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);
|
||
}
|
||
|
||
common_chat_prompt common_chat_template_direct_apply_impl(
|
||
const common_chat_template & tmpl,
|
||
const autoparser::generation_params & inputs,
|
||
const std::optional<json> & messages_override,
|
||
const std::optional<json> & tools_override,
|
||
const std::optional<json> & 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<bool>();
|
||
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<std::string>();
|
||
jinja::caps_apply_reasoning_effort(ctx, reasoning_effort);
|
||
}
|
||
|
||
jinja::global_from_json(ctx, inp, inputs.mark_input);
|
||
|
||
// bos/eos come from the model, not from the user
|
||
ctx.set_val("bos_token", jinja::mk_val<jinja::value_string>(tmpl.bos_token()));
|
||
ctx.set_val("eos_token", jinja::mk_val<jinja::value_string>(tmpl.eos_token()));
|
||
|
||
// render
|
||
jinja::runtime runtime(ctx);
|
||
const jinja::value results = runtime.execute(tmpl.prog);
|
||
auto parts = jinja::runtime::gather_string_parts(results);
|
||
|
||
common_chat_prompt result;
|
||
for (auto & part : parts->val_str.parts) {
|
||
result.parts.push_back({std::move(part.val), part.is_input});
|
||
}
|
||
|
||
// TODO: improve this later
|
||
if (inputs.add_bos && !result.parts.empty()) {
|
||
auto & first = result.parts.front();
|
||
if (!first.is_input && string_starts_with(first.text, tmpl.bos_token())) {
|
||
first.text.erase(0, tmpl.bos_token().size());
|
||
}
|
||
}
|
||
if (inputs.add_eos && !result.parts.empty()) {
|
||
auto & last = result.parts.back();
|
||
if (!last.is_input && string_ends_with(last.text, tmpl.eos_token())) {
|
||
last.text.resize(last.text.size() - tmpl.eos_token().size());
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
common_chat_prompt 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<json> & messages_override,
|
||
const std::optional<json> & tools_override,
|
||
const std::optional<json> & 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).str();
|
||
params.add_generation_prompt = true;
|
||
std::string gen_prompt = common_chat_template_direct_apply_impl(tmpl, params, messages_override, tools_override, additional_context).str();
|
||
|
||
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<std::string>()
|
||
+ "\n" + second_msg.at("content").get<std::string>();
|
||
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<std::string>());
|
||
} 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<common_chat_msg> & 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;
|
||
}
|
||
|
||
static common_chat_params common_chat_params_init_lfm2_tokens(const common_chat_template & tmpl, const autoparser::generation_params & inputs) {
|
||
return common_chat_params_init_lfm2(tmpl, inputs, /* tool_list_tokens = */ true);
|
||
}
|
||
|
||
static common_chat_params common_chat_params_init_lfm2_5(const common_chat_template & tmpl, const autoparser::generation_params & inputs) {
|
||
return common_chat_params_init_lfm2(tmpl, inputs, /* tool_list_tokens = */ false);
|
||
}
|
||
|
||
// Older gemma4 templates need their tool responses rewritten before rendering
|
||
static common_chat_params common_chat_params_init_gemma4_legacy(const common_chat_template & tmpl, const autoparser::generation_params & inputs) {
|
||
auto adjusted = inputs;
|
||
workaround::convert_tool_responses_gemma4(adjusted.messages);
|
||
return common_chat_params_init_gemma4(tmpl, adjusted);
|
||
}
|
||
|
||
// Pick the dedicated handler for a template from its source, or null for the autoparser.
|
||
// Order matters: the first match wins, and later checks assume the earlier ones did not match.
|
||
static common_chat_params_init_fn common_chat_template_detect_params_init(const std::string & src) {
|
||
// 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;
|
||
}
|
||
|
||
// LLM-jp-4.1 - GPT-OSS dialect (spaces after special tokens, <|end|>-separated parallel calls)
|
||
if (src.find("chat_format=llm-jp-harmony-v1") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: LLM-jp Harmony v1\n");
|
||
return common_chat_params_init_llm_jp_harmony;
|
||
}
|
||
|
||
// Harmony XML tool calls, checked before the GPT-OSS JSON calls
|
||
if (src.find("<|channel|>") != std::string::npos && src.find("<|constrain|>xml") != std::string::npos &&
|
||
src.find("rfl:parameter") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: Harmony XML\n");
|
||
return common_chat_params_init_harmony_xml;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Muse Spark shares the Muse recipient protocol and adds a commentary recipient.
|
||
if (src.find("to=commentary") != std::string::npos && src.find("<|eom|>") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: Muse Spark\n");
|
||
return common_chat_params_init_muse_spark;
|
||
}
|
||
|
||
// Muse Glimmer format using " to=<recipient>" recipients and <|eom|>/<|eot|> message terminators.
|
||
if (src.find("<atem:function_calls>") != std::string::npos && src.find("<|eom|>") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: Muse Glimmer\n");
|
||
return common_chat_params_init_muse_glimmer;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Kimi K2 Thinking - uses unique tool call ID format: functions.<name>:<index>
|
||
// 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;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// K2 Horizon - <|ifm|im_start|> turns, <ifm|think*> reasoning picked by reasoning_effort and
|
||
// <ifm|tool_calls> sections; the three think tag pairs defeat the autoparser's reasoning detection
|
||
if (src.find("<|ifm|im_start|>") != std::string::npos &&
|
||
src.find("<ifm|tool_calls>") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: K2 Horizon\n");
|
||
return common_chat_params_init_k2_horizon;
|
||
}
|
||
|
||
// Ling 3.0 / Bailing V3 - <role>X</role> sections with <arg_key>/<arg_value> tagged
|
||
// tool calls. <role> sections are unique to this family among the tagged-arg templates.
|
||
if (src.find("<role>ASSISTANT</role>") != std::string::npos &&
|
||
src.find("<arg_key>") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: Ling 3.0 (Bailing V3)\n");
|
||
return common_chat_params_init_ling3;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
if (is_lfm2_template(src)) {
|
||
LOG_DBG("Using specialized template: LFM2\n");
|
||
return common_chat_params_init_lfm2_tokens;
|
||
}
|
||
|
||
// 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_5;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 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("<tool_call>") != std::string::npos &&
|
||
src.find("<invoke name=") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: MiniMax-M3\n");
|
||
return common_chat_params_init_minimax_m3;
|
||
}
|
||
|
||
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
|
||
// The template source contains the token as a variable assignment, not as a literal in markup.
|
||
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
|
||
// V4.1 tag names have a leading space and its template does not name the dsml_token variable.
|
||
if ((src.find("dsml_token") != std::string::npos &&
|
||
src.find("DSML") != std::string::npos &&
|
||
(src.find("function_calls") != std::string::npos ||
|
||
src.find("tool_calls") != std::string::npos)) ||
|
||
src.find("|DSML| calls>") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: DeepSeek V3.2/V4/V4.1\n");
|
||
return common_chat_params_init_deepseek_v3_2;
|
||
}
|
||
|
||
// Gemma4 format detection
|
||
if (src.find("'<|tool_call>call:'") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: Gemma4\n");
|
||
if (src.find("{#- OpenAI Chat Completions:") == std::string::npos) {
|
||
LOG_WRN("%s: detected an outdated gemma4 chat template, applying compatibility workarounds. "
|
||
"Consider updating to the official template.\n", __func__);
|
||
return common_chat_params_init_gemma4_legacy;
|
||
}
|
||
return common_chat_params_init_gemma4;
|
||
}
|
||
|
||
// MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function>
|
||
if (src.find("Tool usage guidelines:") != std::string::npos &&
|
||
src.find("<function name=\"") != std::string::npos &&
|
||
src.find("<param name=\"") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: MiniCPM5\n");
|
||
return common_chat_params_init_minicpm5;
|
||
}
|
||
|
||
// TranslateGemma - user content must follow a custom schema with language codes
|
||
if (src.find("[source_lang_code]") != std::string::npos &&
|
||
src.find("[target_lang_code]") != std::string::npos) {
|
||
LOG_DBG("Using specialized template: TranslateGemma\n");
|
||
return common_chat_params_init_translate_gemma;
|
||
}
|
||
|
||
// Qwen3-Coder XML tool calls, also used by Nemotron Nano 3, Qwen3.5 and StepFun-3.5-Flash
|
||
if (src.find("<tool_call>") != std::string::npos &&
|
||
src.find("<function=") != std::string::npos &&
|
||
src.find("<parameter=") != std::string::npos &&
|
||
// Exclude models that don't use \n between tags
|
||
src.find("'<tool_call><function=' ~ tool_call.name ~ '>'") == std::string::npos) {
|
||
LOG_DBG("Using specialized template: Qwen3-Coder\n");
|
||
return common_chat_params_init_qwen3_coder;
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
common_chat_template::common_chat_template(const std::string & src, const std::string & bos_token, const std::string & eos_token) {
|
||
jinja::lexer lexer;
|
||
auto lexer_res = lexer.tokenize(src);
|
||
this->prog = jinja::parse_from_tokens(lexer_res);
|
||
|
||
this->src = lexer_res.source;
|
||
this->bos_tok = bos_token;
|
||
this->eos_tok = eos_token;
|
||
|
||
this->caps = jinja::caps_get(prog);
|
||
// LOG_INF("%s: caps:\n%s\n", __func__, this->caps.to_string().c_str());
|
||
|
||
this->params_init = common_chat_template_detect_params_init(this->src);
|
||
if (this->params_init) {
|
||
return;
|
||
}
|
||
|
||
// The analysis depends only on the template, so run it once here instead of on every apply.
|
||
// A failure is kept for apply to report, so a bad template still loads like it did before.
|
||
try {
|
||
analysis = std::make_unique<autoparser::autoparser>();
|
||
analysis->analyze_template(*this);
|
||
} catch (const std::exception & e) {
|
||
analysis.reset();
|
||
analysis_error = e.what();
|
||
}
|
||
}
|
||
|
||
common_chat_template::~common_chat_template() = default;
|
||
common_chat_template::common_chat_template(common_chat_template &&) = default;
|
||
common_chat_template & common_chat_template::operator=(common_chat_template &&) = default;
|
||
|
||
std::optional<common_chat_params> common_chat_try_specialized_template(
|
||
const common_chat_template & tmpl,
|
||
const autoparser::generation_params & params) {
|
||
if (!tmpl.params_init) {
|
||
return std::nullopt;
|
||
}
|
||
return tmpl.params_init(tmpl, params);
|
||
}
|
||
|
||
static common_chat_params common_chat_templates_apply_jinja(const struct common_chat_templates * tmpls,
|
||
const struct common_chat_templates_inputs & inputs) {
|
||
autoparser::generation_params params;
|
||
params.tools = common_chat_tools_to_json_oaicompat(inputs.tools);
|
||
const auto & tmpl =
|
||
params.tools.is_array() && tmpls->template_tool_use ? *tmpls->template_tool_use : *tmpls->template_default;
|
||
const auto & src = tmpl.source();
|
||
const auto & caps = tmpl.original_caps();
|
||
std::vector<common_chat_msg> trimmed_messages;
|
||
const std::vector<common_chat_msg> * 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 = src.find("|DSML| calls>") != std::string::npos ?
|
||
render_message_to_json_deepseek_v4_1(*messages_to_render, tmpl.original_caps()) :
|
||
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;
|
||
data.parser = build_chat_peg_parser([&data](common_chat_peg_builder &p) {
|
||
return p.literal(data.generation_prompt) << p.content(p.rest());
|
||
});
|
||
return data;
|
||
}
|
||
|
||
if (auto result = common_chat_try_specialized_template(tmpl, params)) {
|
||
return *result;
|
||
}
|
||
|
||
if (!tmpl.analysis) {
|
||
throw std::invalid_argument("Unable to generate parser for this template. Automatic parser generation failed: " + tmpl.analysis_error);
|
||
}
|
||
|
||
try {
|
||
LOG_DBG("%s: using differential autoparser\n", __func__);
|
||
const auto & autoparser = *tmpl.analysis;
|
||
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)};
|
||
}
|
||
}
|
||
const auto & arena = 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<llama_chat_message> chat;
|
||
std::vector<std::string> 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<char> 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 = common_chat_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);
|
||
}
|
||
|
||
std::string common_chat_prompt::str() const {
|
||
if (parts.size() == 1) {
|
||
return parts[0].text;
|
||
}
|
||
size_t len = 0;
|
||
for (const auto & part : parts) {
|
||
len += part.text.size();
|
||
}
|
||
std::string out;
|
||
out.reserve(len);
|
||
for (const auto & part : parts) {
|
||
out += part.text;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
void common_chat_prompt::append(const std::string & text, bool is_input) {
|
||
if (text.empty()) {
|
||
return;
|
||
}
|
||
if (!parts.empty() && parts.back().is_input == is_input) {
|
||
parts.back().text += text;
|
||
} else {
|
||
parts.push_back({text, is_input});
|
||
}
|
||
}
|
||
|
||
void common_chat_prompt::replace_last(const std::string & from, const std::string & to) {
|
||
for (auto it = parts.rbegin(); it != parts.rend(); ++it) {
|
||
if (it->is_input) {
|
||
continue;
|
||
}
|
||
size_t pos = it->text.rfind(from);
|
||
if (pos != std::string::npos) {
|
||
it->text.replace(pos, from.size(), to);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void common_chat_input::append(const std::string & piece, llama_token token) {
|
||
if (piece.empty()) {
|
||
return;
|
||
}
|
||
tokens.push_back(token);
|
||
tokens.resize(tokens.size() + piece.size() - 1, LLAMA_TOKEN_NULL);
|
||
text += piece;
|
||
}
|
||
|
||
void common_chat_input::append(const common_chat_input & chunk) {
|
||
tokens.insert(tokens.end(), chunk.tokens.begin(), chunk.tokens.end());
|
||
text += chunk.text;
|
||
}
|
||
|
||
void common_chat_input::truncate(size_t pos) {
|
||
if (pos < text.size()) {
|
||
text.erase(pos);
|
||
tokens.resize(pos);
|
||
}
|
||
}
|
||
|
||
common_chat_input common_chat_input::substr(size_t pos, size_t n) const {
|
||
common_chat_input out;
|
||
out.text = text.substr(pos, n);
|
||
out.tokens.assign(tokens.begin() + pos, tokens.begin() + pos + out.size());
|
||
return out;
|
||
}
|
||
|
||
void common_chat_input::prepend(const std::string & prefix) {
|
||
tokens.insert(tokens.begin(), prefix.size(), LLAMA_TOKEN_NULL);
|
||
text = prefix + text;
|
||
}
|
||
|
||
void common_chat_input::prepend(const common_chat_input & prefix) {
|
||
tokens.insert(tokens.begin(), prefix.tokens.begin(), prefix.tokens.end());
|
||
text = prefix.text + text;
|
||
}
|
||
|
||
common_chat_input common_chat_input_tokenize(const llama_vocab * vocab, const std::string & text) {
|
||
common_chat_input input;
|
||
auto tokens = common_tokenize(vocab, text, false, true);
|
||
for (size_t i = 0; i < tokens.size(); i++) {
|
||
std::string piece = common_token_to_piece(vocab, tokens[i], true);
|
||
if (i == 0 && std::isspace(piece[0]) && !std::isspace(text[0])) {
|
||
// Some tokenizers will add a space before the first special token, need to exclude
|
||
continue;
|
||
}
|
||
input.append(piece, tokens[i]);
|
||
}
|
||
if (input.text != text) {
|
||
// the pieces do not give back the same text, keep the text without tokens
|
||
return common_chat_input(text);
|
||
}
|
||
return input;
|
||
}
|
||
|
||
common_chat_msg common_chat_parse(const common_chat_input & 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 common_chat_input & input,
|
||
bool is_partial,
|
||
const common_chat_parser_params & params) {
|
||
// both branches must be lvalues, a temporary here would copy the arena on every call
|
||
static const common_peg_arena content_only =
|
||
build_chat_peg_parser([](common_chat_peg_builder & p) { return p.content(p.rest()) + p.end(); });
|
||
const common_peg_arena & parser = src_parser.empty() ? content_only : src_parser;
|
||
|
||
if (src_parser.empty()) {
|
||
LOG_DBG("No parser definition detected, assuming pure content parser.");
|
||
}
|
||
|
||
common_chat_input effective_input = input;
|
||
effective_input.prepend(params.generation_prompt);
|
||
|
||
//LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.text.c_str());
|
||
|
||
common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT;
|
||
if (!is_partial) {
|
||
flags |= COMMON_PEG_PARSE_FLAG_COMPLETE;
|
||
}
|
||
if (params.debug) {
|
||
flags |= COMMON_PEG_PARSE_FLAG_DEBUG;
|
||
}
|
||
|
||
common_peg_parse_context ctx(std::move(effective_input.text), std::move(effective_input.tokens), 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<common_chat_peg_mapper> mapper;
|
||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||
} else {
|
||
mapper = std::make_unique<common_chat_peg_mapper>(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), ctx.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), ctx.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<common_chat_peg_mapper> mapper;
|
||
if (params.format == COMMON_CHAT_FORMAT_PEG_GEMMA4) {
|
||
mapper = std::make_unique<common_chat_peg_gemma4_mapper>(msg);
|
||
} else if (params.format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3) {
|
||
mapper = std::make_unique<common_chat_peg_minimax_m3_mapper>(msg);
|
||
} else {
|
||
mapper = std::make_unique<common_chat_peg_mapper>(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;
|
||
}
|
||
|
||
common_chat_session::common_chat_session(const common_chat_templates * tmpls,
|
||
const llama_vocab * vocab,
|
||
const common_chat_templates_inputs & inputs,
|
||
const common_chat_session_params & params) {
|
||
auto applied = common_chat_templates_apply(tmpls, inputs);
|
||
|
||
templated = true;
|
||
prompt_parts = std::move(applied.prompt);
|
||
result.role = "assistant";
|
||
|
||
grammar_text = std::move(applied.grammar);
|
||
grammar_lazy = applied.grammar_lazy;
|
||
stops = std::move(applied.additional_stops);
|
||
generation_prompt_text = applied.generation_prompt;
|
||
thinking_start = std::move(applied.thinking_start_tag);
|
||
thinking_ends = std::move(applied.thinking_end_tags);
|
||
|
||
parser_params.format = applied.format;
|
||
parser_params.generation_prompt = vocab ? common_chat_input_tokenize(vocab, applied.generation_prompt)
|
||
: common_chat_input(applied.generation_prompt);
|
||
parser_params.debug = params.debug;
|
||
parser_params.parser = std::move(applied.parser);
|
||
|
||
delimiters = std::move(applied.message_delimiters);
|
||
|
||
if (vocab) {
|
||
common_params_sampling resolved;
|
||
resolved.grammar_lazy = applied.grammar_lazy;
|
||
common_sampling_add_preserved_tokens(resolved, vocab, applied.preserved_tokens);
|
||
common_sampling_add_grammar_triggers(resolved, vocab, std::move(applied.grammar_triggers));
|
||
preserved_tokens = std::move(resolved.preserved_tokens);
|
||
grammar_triggers = std::move(resolved.grammar_triggers);
|
||
|
||
delimiters.tokenize(vocab);
|
||
} else {
|
||
grammar_triggers = std::move(applied.grammar_triggers);
|
||
}
|
||
|
||
if (inputs.continue_final_message != COMMON_CHAT_CONTINUATION_NONE && !params.echo) {
|
||
// start from the prefill so it is not emitted as part of the first delta
|
||
result = common_chat_parse(input, true, parser_params);
|
||
}
|
||
}
|
||
|
||
void common_chat_session::apply_sampling(common_params_sampling & sampling) const {
|
||
if (!templated) {
|
||
return;
|
||
}
|
||
if (!grammar_text.empty()) {
|
||
sampling.grammar = {COMMON_GRAMMAR_TYPE_TOOL_CALLS, grammar_text};
|
||
}
|
||
sampling.grammar_lazy = grammar_lazy;
|
||
sampling.generation_prompt = generation_prompt_text;
|
||
sampling.preserved_tokens.insert(preserved_tokens.begin(), preserved_tokens.end());
|
||
sampling.grammar_triggers.insert(sampling.grammar_triggers.end(), grammar_triggers.begin(), grammar_triggers.end());
|
||
}
|
||
|
||
const common_chat_msg & common_chat_session::feed(const common_chat_input & chunk) {
|
||
GGML_ASSERT(!finished && "feed() after finish()");
|
||
input.append(chunk);
|
||
auto msg = common_chat_parse(input, true, parser_params);
|
||
if (!msg.empty()) {
|
||
result = std::move(msg);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
const common_chat_msg & common_chat_session::finish(const common_chat_input & chunk) {
|
||
GGML_ASSERT(!finished && "finish() called twice");
|
||
finished = true;
|
||
input.append(chunk);
|
||
auto msg = common_chat_parse(input, false, parser_params);
|
||
if (!msg.empty()) {
|
||
result = std::move(msg);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
common_json common_chat_templates_get_caps(const common_chat_templates * chat_templates) {
|
||
GGML_ASSERT(chat_templates != nullptr);
|
||
GGML_ASSERT(chat_templates->template_default != nullptr);
|
||
// take the more expressive template when available
|
||
const auto & caps = chat_templates->template_tool_use != nullptr ? chat_templates->template_tool_use->caps
|
||
: chat_templates->template_default->caps;
|
||
common_json result = caps.to_map();
|
||
result["reasoning_effort_levels"] = caps.reasoning_effort_levels;
|
||
return result;
|
||
}
|