llama.cpp/common/chat-auto-parser-helpers.cpp

1420 lines
63 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "chat-auto-parser-helpers.h"
#include "chat-auto-parser.h"
#include "chat.h"
#include "log.h"
#include "nlohmann/json.hpp"
using json = nlohmann::ordered_json;
bool string_ends_with(const std::string & str, const std::string & suffix) {
return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0;
}
void trim_whitespace(std::string & str) {
if (str.empty()) {
return;
}
size_t first = str.find_first_not_of(" \n\t\r");
if (first == std::string::npos) {
str.clear();
return;
}
size_t last = str.find_last_not_of(" \n\t\r");
str = str.substr(first, (last - first + 1));
}
void trim_trailing_newlines(std::string & str) {
while (!str.empty() && (str.back() == '\n' || str.back() == '\r')) {
str.pop_back();
}
}
size_t count_non_whitespace(const std::string & str) {
size_t count = 0;
for (char c : str) {
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
count++;
}
}
return count;
}
size_t find_last_of_any(const std::string & str, const std::string & chars, size_t start_pos) {
size_t last_pos = std::string::npos;
for (char c : chars) {
size_t pos = str.rfind(c, start_pos);
if (pos != std::string::npos && (last_pos == std::string::npos || pos > last_pos)) {
last_pos = pos;
}
}
return last_pos;
}
std::string extract_tag_name(const std::string & tag) {
if (tag.empty() || tag[0] != '<') {
return "";
}
std::string tag_name = tag.substr(1);
size_t end_bracket = tag_name.find_first_of(" >");
if (end_bracket != std::string::npos) {
tag_name = tag_name.substr(0, end_bracket);
}
return tag_name;
}
std::string create_closing_tag(const std::string & opening_tag) {
if (opening_tag.empty()) {
return "";
}
if (opening_tag[0] == '<') {
std::string name = extract_tag_name(opening_tag);
return "</" + name + ">";
}
if (opening_tag.front() == '[' && opening_tag.back() == ']') {
std::string name = opening_tag.substr(1, opening_tag.length() - 2);
return "[/" + name + "]";
}
return "";
}
std::string find_common_prefix(const std::vector<std::string> & strings) {
if (strings.empty()) {
return "";
}
if (strings.size() == 1) {
return strings[0];
}
std::string common = strings[0];
for (size_t i = 1; i < strings.size(); ++i) {
const std::string & current = strings[i];
std::string temp_common;
for (size_t j = 0; j < common.length() && j < current.length(); ++j) {
if (common[j] == current[j]) {
temp_common += common[j];
} else {
break;
}
}
common = temp_common;
}
return common;
}
std::string find_common_suffix_generic(const std::vector<std::string> & strings) {
if (strings.empty()) {
return "";
}
if (strings.size() == 1) {
return strings[0];
}
std::string common = strings[0];
for (size_t i = 1; i < strings.size(); ++i) {
const std::string & current = strings[i];
std::string temp_common;
size_t min_len = std::min(common.length(), current.length());
for (size_t j = 0; j < min_len; ++j) {
size_t pos_common = common.length() - j - 1;
size_t pos_current = current.length() - j - 1;
if (common[pos_common] == current[pos_current]) {
temp_common = common[pos_common] + temp_common;
} else {
break;
}
}
common = temp_common;
}
return common;
}
std::string find_common_substring_limited(const std::vector<std::string> & strings,
size_t max_length,
const std::string & delimiters) {
std::string common = find_common_prefix(strings);
if (common.length() > max_length) {
size_t pos = find_last_of_any(common, delimiters, common.length() - 1);
if (pos != std::string::npos && pos > 0) {
return common.substr(0, pos + 1);
}
return common.substr(0, max_length);
}
return common;
}
std::string apply_template(common_chat_template & tmpl,
const struct templates_params & inputs,
const std::optional<json> & messages_override,
const std::optional<json> & tools_override,
const std::optional<json> & additional_context) {
struct templates_params final_inputs(inputs);
final_inputs.messages = messages_override ? *messages_override : inputs.messages;
if (tools_override) {
final_inputs.tools = *tools_override;
} else {
final_inputs.tools = inputs.tools.empty() ? json() : inputs.tools;
}
final_inputs.add_generation_prompt = inputs.add_generation_prompt;
final_inputs.extra_context = inputs.extra_context;
final_inputs.extra_context["enable_thinking"] = inputs.enable_thinking;
if (additional_context) {
final_inputs.extra_context.merge_patch(*additional_context);
}
try {
return common_chat_template_direct_apply(tmpl, inputs);
} catch (const std::exception & e) {
LOG_ERR("Template application failed: %s\n", e.what());
return "";
}
}
std::string adjust_to_token_boundary(const std::string & str) {
if (str.empty()) {
return str;
}
// Check if the string ends in the middle of a <|...|> token
// Look for unmatched <| at the end
// Find the last <| in the string
size_t last_open = str.rfind("<|");
if (last_open == std::string::npos) {
return str; // No special tokens
}
// Find if there's a |> after the last <|
size_t matching_close = str.find("|>", last_open + 2);
if (matching_close != std::string::npos) {
// The token is complete, return as-is
return str;
}
// The string is truncated mid-token
// Truncate to just before the incomplete token
std::string result = str.substr(0, last_open);
// Trim any trailing whitespace
while (!result.empty() && (result.back() == ' ' || result.back() == '\t' || result.back() == '\n')) {
result.pop_back();
}
return result;
}
// Fullwidth vertical bar: (U+FF5C) is 3 bytes in UTF-8: 0xEF 0xBD 0x9C
static const std::string FULLWIDTH_PIPE = "\xef\xbd\x9c"; //
static const std::string TOKEN_OPENER_STD = "<|";
static const std::string TOKEN_OPENER_FW = "<" + FULLWIDTH_PIPE; // <
static const std::string TOKEN_CLOSER_STD = "|>";
static const std::string TOKEN_CLOSER_FW = FULLWIDTH_PIPE + ">"; // >
size_t find_token_opener(const std::string & str, size_t start_pos) {
size_t pos_std = str.find(TOKEN_OPENER_STD, start_pos);
size_t pos_fw = str.find(TOKEN_OPENER_FW, start_pos);
if (pos_std == std::string::npos) {
return pos_fw;
}
if (pos_fw == std::string::npos) {
return pos_std;
}
return std::min(pos_std, pos_fw);
}
size_t find_token_closer(const std::string & str, size_t start_pos) {
size_t pos_std = str.find(TOKEN_CLOSER_STD, start_pos);
size_t pos_fw = str.find(TOKEN_CLOSER_FW, start_pos);
if (pos_std == std::string::npos) {
return pos_fw;
}
if (pos_fw == std::string::npos) {
return pos_std;
}
return std::min(pos_std, pos_fw);
}
size_t get_token_opener_length(const std::string & str, size_t pos) {
if (pos >= str.length()) {
return 0;
}
if (str.compare(pos, TOKEN_OPENER_FW.length(), TOKEN_OPENER_FW) == 0) {
return TOKEN_OPENER_FW.length(); // 4 bytes for <
}
if (str.compare(pos, TOKEN_OPENER_STD.length(), TOKEN_OPENER_STD) == 0) {
return TOKEN_OPENER_STD.length(); // 2 bytes for <|
}
return 0;
}
size_t get_token_closer_length(const std::string & str, size_t pos) {
if (pos >= str.length()) {
return 0;
}
if (str.compare(pos, TOKEN_CLOSER_FW.length(), TOKEN_CLOSER_FW) == 0) {
return TOKEN_CLOSER_FW.length(); // 4 bytes for >
}
if (str.compare(pos, TOKEN_CLOSER_STD.length(), TOKEN_CLOSER_STD) == 0) {
return TOKEN_CLOSER_STD.length(); // 2 bytes for |>
}
return 0;
}
std::string strip_eos_token(const std::string & str) {
if (str.empty()) {
return str;
}
// Find the last token in the string
// We need to find a token that looks like an EOS marker
// Common patterns:
// - <|eot_id|>, <|eos|>, <|end|>, <|endoftext|>
// - <end▁of▁sentence> (DeepSeek fullwidth)
size_t last_closer = std::string::npos;
size_t search_pos = str.length();
// Search backwards for the last token closer
while (search_pos > 0) {
// Check for fullwidth closer first (it's longer)
if (search_pos >= TOKEN_CLOSER_FW.length()) {
size_t check_pos = search_pos - TOKEN_CLOSER_FW.length();
if (str.compare(check_pos, TOKEN_CLOSER_FW.length(), TOKEN_CLOSER_FW) == 0) {
last_closer = check_pos;
break;
}
}
// Check for standard closer
if (search_pos >= TOKEN_CLOSER_STD.length()) {
size_t check_pos = search_pos - TOKEN_CLOSER_STD.length();
if (str.compare(check_pos, TOKEN_CLOSER_STD.length(), TOKEN_CLOSER_STD) == 0) {
last_closer = check_pos;
break;
}
}
search_pos--;
}
if (last_closer == std::string::npos) {
return str; // No token closer found
}
// Find the corresponding opener
size_t opener_search_start = (last_closer > 100) ? last_closer - 100 : 0;
size_t last_opener = std::string::npos;
size_t opener_len = 0;
for (size_t pos = opener_search_start; pos < last_closer; pos++) {
size_t len = get_token_opener_length(str, pos);
if (len > 0) {
last_opener = pos;
opener_len = len;
}
}
if (last_opener == std::string::npos) {
return str; // No matching opener found
}
// Extract the token content to check if it's an EOS marker
size_t closer_len = get_token_closer_length(str, last_closer);
size_t content_start = last_opener + opener_len;
size_t content_length = last_closer - content_start;
if (content_length == 0 || content_length > 50) {
return str; // Invalid or too long token content
}
std::string token_content = str.substr(content_start, content_length);
// Convert to lowercase for comparison (ASCII only, sufficient for EOS markers)
std::string lower_content;
for (char c : token_content) {
lower_content += (c >= 'A' && c <= 'Z') ? (c + 32) : c;
}
// Check if this looks like an EOS token
// True EOS tokens:
// - <|eos|>, <|eot_id|>, <|end_of_text|>, <|endoftext|>
// - <end▁of▁sentence> (DeepSeek fullwidth)
// NOT EOS tokens (structural markers):
// - <|END_ACTION|>, <|TOOL_CALL_END|>, <|end_thinking|>, etc.
bool is_eos = false;
// Check for specific EOS patterns
if (lower_content == "eos" || lower_content == "eot_id" || lower_content == "eot" ||
lower_content == "end_of_text" || lower_content == "endoftext") {
is_eos = true;
}
// DeepSeek's end_of_sentence uses fullwidth underscore (▁) which is preserved in lower_content
// The token content would be "end▁of▁sentence" (with ▁ = U+2581)
else if (token_content.find("sentence") != std::string::npos ||
token_content.find("\xe2\x96\x81of\xe2\x96\x81sentence") != std::string::npos) {
is_eos = true;
}
if (!is_eos) {
return str; // Not an EOS token
}
// Strip the EOS token
std::string result = str.substr(0, last_opener);
LOG_DBG("Stripped EOS token '%s' from string\n",
str.substr(last_opener, last_closer + closer_len - last_opener).c_str());
return result;
}
std::string find_string_difference(const std::string & base, const std::string & extended) {
size_t common_prefix = 0;
while (common_prefix < base.length() && common_prefix < extended.length() &&
base[common_prefix] == extended[common_prefix]) {
common_prefix++;
}
return extended.substr(common_prefix);
}
std::string extract_json_field_name(const std::string & opener,
const std::string & default_name,
const std::vector<std::string> & candidates) {
for (const auto & candidate : candidates) {
std::string pattern = "\"" + candidate + "\"";
if (opener.find(pattern) != std::string::npos) {
LOG_DBG("Found JSON field name '%s' in opener\n", candidate.c_str());
return candidate;
}
}
return default_name;
}
std::string find_closing_pattern(const std::string & diff, size_t func_pos) {
std::vector<std::string> closers = { "</", "}", "]", ">", " " };
std::string best_pattern;
size_t best_pos = std::string::npos;
for (const auto & pattern : closers) {
size_t pos = diff.find(pattern, func_pos);
if (pos != std::string::npos) {
if (pos < best_pos) {
if (pattern == "</") {
size_t end_pos = diff.find('>', pos);
if (end_pos != std::string::npos) {
best_pattern = diff.substr(pos, end_pos - pos + 1);
best_pos = pos;
}
} else {
best_pattern = pattern;
best_pos = pos;
}
}
}
}
return best_pattern;
}
std::string find_tool_call_start(const std::string & diff) {
std::vector<std::string> start_patterns = { "<", "[", "{", "call", "func", "tool", "TOOL" };
for (const auto & pattern : start_patterns) {
size_t pos = diff.find(pattern);
if (pos < 5) {
if (pattern == "<") {
size_t end_pos = diff.find('>', pos);
if (end_pos != std::string::npos) {
return diff.substr(pos, end_pos - pos + 1);
}
}
if (pattern == "[" || pattern == "{") {
size_t chunk_len = std::min(diff.length() - pos, (size_t) 60);
return diff.substr(pos, chunk_len);
}
size_t end_pos = diff.find_first_of(">]} \n", pos);
if (end_pos != std::string::npos) {
if (diff[end_pos] == '>' || diff[end_pos] == ']' || diff[end_pos] == '}') {
return diff.substr(pos, end_pos - pos + 1);
}
return diff.substr(pos, end_pos - pos);
}
return diff.substr(pos, pattern.length());
}
}
return "";
}
std::string find_tool_call_end(const std::string & diff, size_t func_pos) {
char opener_char = 0;
std::string start_tag_name;
std::string openers = "[{<";
size_t last_opener_pos = std::string::npos;
for (char c : openers) {
size_t p = diff.rfind(c, func_pos);
if (p != std::string::npos) {
if (last_opener_pos == std::string::npos || p > last_opener_pos) {
last_opener_pos = p;
opener_char = c;
}
}
}
size_t unclosed_bracket = diff.rfind('[', func_pos);
if (unclosed_bracket != std::string::npos) {
size_t closer = diff.find(']', unclosed_bracket);
if (closer == std::string::npos || closer > func_pos) {
opener_char = '[';
}
}
if (opener_char == '<') {
size_t tag_start = diff.find('<', last_opener_pos);
if (tag_start != std::string::npos) {
// Include '=' in search to handle <function=name> style tags
// where the closing tag is </function>, not </function=name>
size_t tag_end = diff.find_first_of(" >=\n", tag_start);
if (tag_end != std::string::npos) {
start_tag_name = diff.substr(tag_start + 1, tag_end - (tag_start + 1));
}
}
}
if (!start_tag_name.empty()) {
std::string expected_closer = "</" + start_tag_name + ">";
size_t pos = diff.find(expected_closer, func_pos);
if (pos != std::string::npos) {
if (opener_char == '[') {
size_t bracket_pos = diff.rfind(']', pos);
if (bracket_pos != std::string::npos && bracket_pos > func_pos) {
return diff.substr(bracket_pos, (pos + expected_closer.length()) - bracket_pos);
}
}
return expected_closer;
}
}
std::vector<std::string> end_patterns = { "</", "]", "}", ">", "```", "\n", " " };
std::string best_pattern;
size_t best_pos = std::string::npos;
auto is_structural = [](const std::string & s) {
if (s.empty()) {
return false;
}
return s[0] == ']' || s[0] == '}' || s[0] == '>' || (s.size() >= 2 && s.substr(0, 2) == "</") ||
(s.size() >= 3 && s.substr(0, 3) == "```");
};
for (const auto & pattern : end_patterns) {
size_t pos = diff.find(pattern, func_pos);
if (pos == std::string::npos) {
continue;
}
bool current_is_struct = is_structural(pattern);
bool best_is_struct = is_structural(best_pattern);
bool better = false;
if (best_pattern.empty()) {
better = true;
} else if (pos < best_pos) {
better = !(best_is_struct && !current_is_struct) &&
!(opener_char == '[' && best_pattern[0] == ']' && pattern[0] == '}');
} else {
if (!best_is_struct && current_is_struct && pos < best_pos + 400) {
better = true;
} else if (best_is_struct && current_is_struct && opener_char == '[' && pattern[0] == ']' &&
best_pattern[0] == '}') {
if (pos < best_pos + 100) {
better = true;
}
}
}
if (better) {
best_pattern = pattern;
best_pos = pos;
if (current_is_struct && (pattern == "]" || pattern == "}" || pattern == "```")) {
size_t tag_start = diff.find('<', best_pos + pattern.length());
if (tag_start != std::string::npos && tag_start < best_pos + pattern.length() + 5) {
size_t tag_end = diff.find('>', tag_start);
if (tag_end != std::string::npos) {
best_pattern = diff.substr(best_pos, tag_end - best_pos + 1);
}
}
}
}
}
return best_pattern;
}
std::string infer_tool_call_opener(const std::string & diff1, const std::string & diff2, const std::string & diff3) {
std::vector<std::string> differences = { diff1, diff2, diff3 };
return find_common_prefix(differences);
}
std::string infer_tool_call_closer(const std::string & diff1, const std::string & diff2, const std::string & diff3) {
std::vector<std::string> differences = { diff1, diff2, diff3 };
return find_common_suffix_generic(differences);
}
internal_discovered_pattern extract_patterns_from_differences(const std::string & tool1_diff,
const std::string & tool2_diff,
const std::string & tool3_diff,
const std::string & tool1_full) {
LOG_DBG("%s\n", __func__);
internal_discovered_pattern patterns;
size_t func1_pos = tool1_diff.rfind("test_function_name");
size_t func2_pos = tool2_diff.rfind("test_function_name");
if (func1_pos != std::string::npos && func2_pos != std::string::npos) {
patterns.tool_call_opener = tool1_diff.substr(0, func1_pos);
if (tool1_full.length() >= tool1_diff.length()) {
size_t diff_start = tool1_full.length() - tool1_diff.length();
if (diff_start > 0 && tool1_full[diff_start - 1] == '<' && !patterns.tool_call_opener.empty() &&
patterns.tool_call_opener[0] != '<') {
patterns.tool_call_opener = "<" + patterns.tool_call_opener;
}
}
if (func1_pos == 0 && !tool1_full.empty()) {
size_t func_in_full = tool1_full.rfind("test_function_name");
if (func_in_full != std::string::npos && func_in_full > 0) {
// Look backwards from function name to find prefix pattern
// Find where the prefix ends (skip whitespace immediately before function name)
size_t prefix_end = func_in_full;
while (prefix_end > 0 && (tool1_full[prefix_end - 1] == ' ' || tool1_full[prefix_end - 1] == '\t')) {
prefix_end--;
}
// Find where the prefix starts by looking for newline or alphanumeric boundary
size_t prefix_start = prefix_end;
while (prefix_start > 0) {
char c = tool1_full[prefix_start - 1];
// Stop at newline
if (c == '\n' || c == '\r') {
break;
}
// Stop if we hit alphanumeric (probably content, not a prefix delimiter)
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
prefix_start = prefix_end; // Reset - no valid prefix found
break;
}
prefix_start--;
}
// Extract the prefix if we found something meaningful
if (prefix_start < prefix_end) {
std::string prefix = tool1_full.substr(prefix_start, prefix_end - prefix_start);
// Validate: prefix should contain non-whitespace and be reasonable length
bool has_content = false;
for (char c : prefix) {
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
has_content = true;
break;
}
}
if (has_content && prefix.length() >= 2 && prefix.length() <= 20) {
LOG_DBG("Found prefix pattern in full output: '%s'\n", prefix.c_str());
patterns.function_opener = prefix;
patterns.tool_call_start_marker = prefix;
}
}
}
}
patterns.tool_name_field = extract_json_field_name(patterns.tool_call_opener, "name",
{ "tool_name", "name", "function_name", "function" });
patterns.tool_args_field =
extract_json_field_name(patterns.tool_call_opener + tool1_diff.substr(func1_pos), "arguments",
{ "parameters", "arguments", "args", "params", "input" });
patterns.tool_id_field =
extract_json_field_name(tool1_diff, "", { "tool_call_id", "tool_id", "id", "call_id" });
size_t param1_pos = tool2_diff.find("\"param1\"");
bool param_has_quotes = (param1_pos != std::string::npos);
size_t param2_pos = tool2_diff.find("\"param2\"");
size_t value1_pos = tool2_diff.find("\"value1\"");
if (param1_pos == std::string::npos) {
param1_pos = tool2_diff.find("param1");
}
if (param_has_quotes && param1_pos != std::string::npos) {
param1_pos++;
}
if (param2_pos == std::string::npos) {
param2_pos = tool2_diff.find("param2");
}
if (param_has_quotes && param2_pos != std::string::npos) {
param2_pos++;
}
if (value1_pos == std::string::npos) {
value1_pos = tool2_diff.find("value1");
}
// Only skip quote if value was actually found quoted
bool value_has_quotes = (value1_pos != std::string::npos && tool2_diff[value1_pos] == '"');
if (value_has_quotes) {
value1_pos++;
}
if (param1_pos != std::string::npos && value1_pos != std::string::npos) {
size_t search_start = (param1_pos > 20) ? param1_pos - 20 : 0;
std::string pre_param = tool2_diff.substr(search_start, param1_pos - search_start);
size_t delim_pos = pre_param.find_last_of('\n');
if (delim_pos == std::string::npos) {
delim_pos = pre_param.find_last_of('>');
}
if (delim_pos != std::string::npos) {
patterns.parameter_key_prefix = pre_param.substr(delim_pos + 1);
// If prefix is empty after '>', check for GLM-style key-value tags
// Pattern: <arg_key>param1</arg_key><arg_value>value1</arg_value>
// In this case, the '>' ends the opening tag, and we should include the whole tag
if (patterns.parameter_key_prefix.empty() && delim_pos > 0) {
// Look for matching '<' before the '>'
size_t open_bracket = pre_param.rfind('<', delim_pos);
if (open_bracket != std::string::npos) {
// Extract the whole tag as the prefix
patterns.parameter_key_prefix = pre_param.substr(open_bracket);
}
}
} else {
size_t start_marker = pre_param.find_last_of("<{[ \"");
if (start_marker != std::string::npos) {
patterns.parameter_key_prefix = pre_param.substr(start_marker);
} else {
patterns.parameter_key_prefix = pre_param;
}
}
trim_whitespace(patterns.parameter_key_prefix);
size_t key_end = param1_pos + std::string("param1").length();
if (value1_pos > key_end) {
patterns.parameter_key_suffix = tool2_diff.substr(key_end, value1_pos - key_end);
}
size_t value1_end = value1_pos + std::string("value1").length();
if (value1_end < tool2_diff.length()) {
// Try to find XML-style closing tag like </parameter>
size_t close_start = tool2_diff.find("</", value1_end);
if (close_start != std::string::npos) {
size_t close_end = tool2_diff.find('>', close_start);
if (close_end != std::string::npos) {
patterns.parameter_closer = tool2_diff.substr(close_start, close_end - close_start + 1);
}
}
}
}
const std::string & func_context = tool1_diff;
size_t open_pos = func_context.rfind('<', func1_pos);
if (open_pos != std::string::npos && open_pos < func1_pos) {
size_t close_pos = func_context.find('>', open_pos);
if (close_pos != std::string::npos && close_pos < func1_pos) {
bool is_adjacent = true;
for (size_t k = close_pos + 1; k < func1_pos; ++k) {
char c = func_context[k];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
is_adjacent = false;
break;
}
}
if (is_adjacent) {
patterns.function_opener = func_context.substr(open_pos, close_pos - open_pos + 1);
}
} else {
patterns.function_opener = func_context.substr(open_pos, func1_pos - open_pos);
}
}
if (func1_pos > 0 && patterns.function_opener.empty()) {
size_t prefix_end = func1_pos;
// Skip whitespace immediately before function name
while (prefix_end > 0 && (func_context[prefix_end - 1] == ' ' || func_context[prefix_end - 1] == '\t')) {
prefix_end--;
}
// Find prefix start - look for newline or alphanumeric boundary
size_t prefix_start = prefix_end;
while (prefix_start > 0) {
char c = func_context[prefix_start - 1];
if (c == '\n' || c == '\r') {
break;
}
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
prefix_start = prefix_end; // Reset - no valid prefix
break;
}
prefix_start--;
}
if (prefix_start < prefix_end) {
// ...
}
}
// Fallback: look for standard delimiters
if (patterns.function_opener.empty()) {
for (int i = (int) func1_pos - 1; i >= 0; i--) {
if (func_context[i] == '{' || func_context[i] == '[' || func_context[i] == '(' ||
func_context[i] == '<') {
patterns.function_opener = func_context.substr(i, func1_pos - i);
break;
}
}
}
size_t func_name_end = func1_pos + std::string("test_function_name").length();
if (func_name_end < func_context.length()) {
char next_char = func_context[func_name_end];
if (next_char == '>' || next_char == ']' || next_char == '}') {
patterns.function_name_suffix = std::string(1, next_char);
} else if (next_char == '"') {
if (func_name_end + 1 < func_context.length() && func_context[func_name_end + 1] == '>') {
patterns.function_name_suffix = "\">";
} else {
patterns.function_name_suffix = "\"";
}
} else if (next_char == '<') {
// Check if it's an XML-like tag suffix (e.g. <|tool_call_argument_begin|>)
// But NOT if it's a closing tag (e.g., </tool_call>) - that should be function_closer
if (func_name_end + 1 < func_context.length() && func_context[func_name_end + 1] == '/') {
// This is a closing tag like </tool_call>, not a suffix
// Leave function_name_suffix empty; function_closer will capture this
} else {
size_t tag_close = func_context.find('>', func_name_end);
if (tag_close != std::string::npos) {
// It seems to be a tag, use it as suffix
patterns.function_name_suffix = func_context.substr(func_name_end, tag_close - func_name_end + 1);
}
}
} else if (next_char == '[') {
// Bracket-tag format: [CALL_ID]id[ARGS] (Mistral Small 3.2 style)
// Find where the JSON arguments start (at '{')
size_t json_start = func_context.find('{', func_name_end);
if (json_start != std::string::npos) {
patterns.function_name_suffix = func_context.substr(func_name_end, json_start - func_name_end);
LOG_DBG("Found bracket-tag suffix: '%s'\n", patterns.function_name_suffix.c_str());
}
} else if (next_char == ':') {
// Indexed format: function_name:0<|marker|> or function_name:0{args}
// Find where the suffix ends - either at a tag marker or at the JSON args start
size_t suffix_end = func_name_end + 1;
// Skip the index digits
while (suffix_end < func_context.length() &&
std::isdigit(static_cast<unsigned char>(func_context[suffix_end]))) {
suffix_end++;
}
if (suffix_end < func_context.length()) {
char after_index = func_context[suffix_end];
if (after_index == '<') {
// There's a marker after the index (e.g., :0<|tool_call_argument_begin|>)
size_t tag_close = func_context.find('>', suffix_end);
if (tag_close != std::string::npos) {
patterns.function_name_suffix =
func_context.substr(func_name_end, tag_close - func_name_end + 1);
} else {
patterns.function_name_suffix =
func_context.substr(func_name_end, suffix_end - func_name_end);
}
} else {
// Just the index part (e.g., :0)
patterns.function_name_suffix = func_context.substr(func_name_end, suffix_end - func_name_end);
}
}
} else if (next_char == '\n' || next_char == '\r') {
// Check for markdown code block pattern (e.g., DeepSeek R1): \n```json\n{...}\n```<end>
size_t code_block_start = func_context.find("```", func_name_end);
if (code_block_start != std::string::npos && code_block_start < func_name_end + 10) {
// Found code block start after function name
// Skip the optional language tag (e.g., "json")
size_t newline_after_lang = func_context.find('\n', code_block_start + 3);
if (newline_after_lang != std::string::npos) {
// function_name_suffix should include everything up to (and including) the newline after language tag
patterns.function_name_suffix =
func_context.substr(func_name_end, newline_after_lang - func_name_end + 1);
LOG_DBG("Found markdown code block suffix: '%s'\n", patterns.function_name_suffix.c_str());
}
}
}
}
// Function closer
size_t search_start = func_name_end;
if (!patterns.function_name_suffix.empty()) {
search_start += patterns.function_name_suffix.length();
}
patterns.function_closer = find_closing_pattern(func_context, search_start);
// Fix for XML-style tag formats where function_closer was detected as "}" (JSON closing)
// but should be the actual tag closer (e.g., <|tool_call_end|> or <tool▁call▁end>)
if (patterns.function_closer == "}" && !patterns.function_opener.empty() &&
patterns.function_opener[0] == '<') {
// This is an XML-style tag format, so the closer should be a tag, not just "}"
// Find the next tag marker after the search position
size_t next_tag = func_context.find('<', search_start);
if (next_tag != std::string::npos) {
// Handle both standard <|...|> and fullwidth <...> formats
size_t closer_pos = find_token_closer(func_context, next_tag);
if (closer_pos != std::string::npos) {
size_t closer_len = get_token_closer_length(func_context, closer_pos);
patterns.function_closer = func_context.substr(next_tag, closer_pos - next_tag + closer_len);
LOG_DBG("Adjusted function_closer from '}' to tag '%s' for XML-style format\n",
patterns.function_closer.c_str());
}
}
}
if (patterns.function_closer == "}" && !patterns.function_name_suffix.empty() &&
patterns.function_name_suffix.find("```") != std::string::npos) {
// function_name_suffix contains a code block opener, look for the closing code block
size_t code_block_end = func_context.find("```", search_start);
if (code_block_end != std::string::npos) {
// Found closing code block, extract everything from ``` to end of tool call
// The closer should be \n```<per_call_end> (everything from ``` to the end marker)
size_t after_block = code_block_end + 3;
// Find the next tag marker (e.g., <|tool_call_end|>)
size_t next_tag = func_context.find('<', after_block);
if (next_tag != std::string::npos) {
size_t tag_end = func_context.find('>', next_tag);
if (tag_end != std::string::npos) {
// Don't include leading newline - the JSON args parser consumes trailing whitespace
// So start exactly at the ``` (code_block_end)
patterns.function_closer = func_context.substr(code_block_end, tag_end - code_block_end + 1);
LOG_DBG("Detected markdown code block args, adjusted function_closer to: '%s'\n",
patterns.function_closer.c_str());
}
}
}
}
// Tool call start marker
if (patterns.function_opener.length() > 0 &&
patterns.tool_call_opener.length() > patterns.function_opener.length()) {
size_t opener_start = patterns.tool_call_opener.length() - patterns.function_opener.length();
if (opener_start > 0) {
std::string before_func = patterns.tool_call_opener.substr(0, opener_start);
size_t last_bracket = before_func.find_last_of('[');
size_t tool_obj_brace = std::string::npos;
if (last_bracket != std::string::npos && last_bracket + 1 < before_func.length()) {
tool_obj_brace = before_func.find('{', last_bracket + 1);
}
if (tool_obj_brace != std::string::npos) {
patterns.tool_call_start_marker = before_func.substr(0, tool_obj_brace);
} else if (last_bracket != std::string::npos) {
patterns.tool_call_start_marker = before_func.substr(0, last_bracket + 1);
} else {
patterns.tool_call_start_marker = before_func;
}
}
} else if (patterns.tool_call_start_marker.empty()) {
// Only search if not already set (e.g., by >>> prefix detection)
patterns.tool_call_start_marker = find_tool_call_start(tool1_diff);
}
if (patterns.tool_call_opener.empty()) {
patterns.tool_call_opener = infer_tool_call_opener(tool1_diff, tool2_diff, tool3_diff);
if (func1_pos != std::string::npos && patterns.tool_call_opener.length() > func1_pos) {
patterns.tool_call_opener = patterns.tool_call_opener.substr(0, func1_pos);
}
}
if (patterns.tool_call_closer.empty()) {
patterns.tool_call_closer = infer_tool_call_closer(tool1_diff, tool2_diff, tool3_diff);
}
patterns.tool_call_end_marker = find_tool_call_end(func_context, func1_pos);
if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.length() > 1) {
size_t eos_pos = patterns.tool_call_end_marker.find("<|");
if (eos_pos == 1) {
// Check if there's a bracket/brace before the token
char first_char = patterns.tool_call_end_marker[0];
if (first_char == ']' || first_char == '}') {
// Check if this is an actual EOS token (contains "eot_id" or "eos")
std::string token_content = patterns.tool_call_end_marker.substr(eos_pos);
if (token_content.find("eot_id") != std::string::npos ||
token_content.find("eos") != std::string::npos) {
// This is an EOS token, strip it
patterns.tool_call_end_marker = patterns.tool_call_end_marker.substr(0, 1);
}
}
}
}
// Trim whitespace
if (!patterns.tool_call_end_marker.empty()) {
size_t first = patterns.tool_call_end_marker.find_first_not_of(" \n\t");
size_t last = patterns.tool_call_end_marker.find_last_not_of(" \n\t");
if (first != std::string::npos && last != std::string::npos) {
patterns.tool_call_end_marker = patterns.tool_call_end_marker.substr(first, (last - first + 1));
}
}
// If tool_call_end_marker matches function_closer, it found the wrong tag.
// Use tool_call_closer instead which is derived from common suffix of diffs.
if (!patterns.function_closer.empty() && patterns.tool_call_end_marker == patterns.function_closer) {
if (!patterns.tool_call_closer.empty()) {
// Try to extract a proper closing tag from tool_call_closer
// Use rfind to get the LAST closing tag (e.g., not </function>)
size_t close_start = patterns.tool_call_closer.rfind("</");
if (close_start != std::string::npos) {
size_t close_end = patterns.tool_call_closer.find('>', close_start);
if (close_end != std::string::npos) {
patterns.tool_call_end_marker =
patterns.tool_call_closer.substr(close_start, close_end - close_start + 1);
}
}
}
} else if (patterns.tool_call_end_marker == ">" && !patterns.tool_call_closer.empty() &&
patterns.tool_call_closer.length() > 3) {
// If the specific end marker is just ">", but the common suffix (tool_call_closer) is substantial (e.g. <|tool_calls_section_end|>)
// then prefer the common suffix, as finding ">" might just be hitting the end of the last function call
if (patterns.tool_call_closer.find(patterns.tool_call_end_marker) != std::string::npos) {
patterns.tool_call_end_marker = patterns.tool_call_closer;
}
}
if (patterns.tool_call_start_marker.empty()) {
std::vector<std::string> diffs = { tool1_diff, tool2_diff, tool3_diff };
patterns.tool_call_start_marker = find_common_substring_limited(diffs, 20, " \n\t<[{");
}
// Truncate if needed, but skip if func_pos is 0 (marker found via full output)
if (func1_pos != std::string::npos && func1_pos > 0 && patterns.tool_call_start_marker.length() > func1_pos) {
std::string candidate = patterns.tool_call_start_marker.substr(0, func1_pos);
size_t last_opener = candidate.find_last_of("{[");
if (last_opener != std::string::npos) {
patterns.tool_call_start_marker = candidate.substr(0, last_opener);
} else {
patterns.tool_call_start_marker = candidate;
}
}
// Ensure we don't truncate in the middle of <|...|> tokens
patterns.tool_call_start_marker = adjust_to_token_boundary(patterns.tool_call_start_marker);
patterns.tool_call_end_marker = adjust_to_token_boundary(patterns.tool_call_end_marker);
// Final trim
if (!patterns.tool_call_start_marker.empty()) {
size_t first = patterns.tool_call_start_marker.find_first_not_of(" \n\t\r");
size_t last = patterns.tool_call_start_marker.find_last_not_of(" \n\t\r");
if (first != std::string::npos && last != std::string::npos) {
patterns.tool_call_start_marker = patterns.tool_call_start_marker.substr(first, (last - first + 1));
}
}
}
return patterns;
}
internal_tool_format determine_format_from_patterns(const internal_discovered_pattern & patterns) {
LOG_DBG("%s\n", __func__);
if (patterns.tool_call_opener.empty() && patterns.tool_call_closer.empty() && patterns.function_opener.empty() &&
patterns.function_closer.empty() && patterns.parameter_opener.empty() && patterns.parameter_closer.empty() &&
patterns.argument_separator.empty() && patterns.tool_call_start_marker.empty() &&
patterns.tool_call_end_marker.empty()) {
LOG_DBG("All patterns are empty - template doesn't support tool calls\n");
return FORMAT_UNKNOWN;
}
// Check for markdown code block format (Cohere Command-R Plus)
// STRUCTURAL PATTERN: Action:\n```json\n[...]\n```
// Key indicators:
// 1. tool_call_start_marker contains "Action:" or similar plain text marker
// 2. function_name_suffix or tool_call_closer contains "```" (markdown code fence)
// 3. tool_call_opener starts with "[" indicating JSON array
bool has_code_fence = false;
if (!patterns.function_name_suffix.empty() && patterns.function_name_suffix.find("```") != std::string::npos) {
has_code_fence = true;
}
if (!patterns.tool_call_closer.empty() && patterns.tool_call_closer.find("```") != std::string::npos) {
has_code_fence = true;
}
bool has_action_marker = false;
if (!patterns.tool_call_start_marker.empty()) {
std::string marker_lower = patterns.tool_call_start_marker;
std::transform(marker_lower.begin(), marker_lower.end(), marker_lower.begin(), ::tolower);
if (marker_lower.find("action") != std::string::npos) {
has_action_marker = true;
}
}
if (has_code_fence && has_action_marker) {
LOG_DBG("Detected MARKDOWN_CODE_BLOCK format (Action: + ```json code fence)\n");
return FORMAT_MARKDOWN_CODE_BLOCK;
}
// Check for recipient-based routing format (e.g., Functionary v3.2)
// STRUCTURAL PATTERN: The same marker is used for both content routing and tool routing
// Key indicators:
// 1. tool_call_start_marker == function_opener (same marker used for both)
// 2. No parameter markers (arguments are plain dict/JSON, not wrapped in tags)
// 3. No XML-style tags (differentiates from FUNC_TAG_WITH_NAME)
// 4. function_opener doesn't start with structural chars like {, [, < (differentiates from other formats)
if (!patterns.tool_call_start_marker.empty() && !patterns.function_opener.empty() &&
patterns.tool_call_start_marker == patterns.function_opener) {
// Check this isn't an XML-tagged format (opener would start with '<')
if (patterns.function_opener[0] != '<' && patterns.function_opener[0] != '{' &&
patterns.function_opener[0] != '[') {
// Check there are no parameter markers
if (patterns.parameter_opener.empty() && patterns.parameter_closer.empty()) {
LOG_DBG("Detected RECIPIENT_BASED format (tool_call_start_marker == function_opener = '%s')\n",
patterns.tool_call_start_marker.c_str());
return FORMAT_RECIPIENT_BASED;
}
}
}
if (!patterns.tool_call_opener.empty()) {
if (patterns.tool_call_opener.find("{\"name\":") != std::string::npos ||
patterns.tool_call_opener.find("{&quot;name&quot;:") != std::string::npos) {
LOG_DBG("Detected JSON_NATIVE format from tool_call_opener JSON structure\n");
return FORMAT_JSON_NATIVE;
}
}
if (!patterns.function_opener.empty() && patterns.function_opener.find('<') == 0) {
bool has_substantial_param_markers = false;
if (!patterns.parameter_opener.empty()) {
has_substantial_param_markers = (count_non_whitespace(patterns.parameter_opener) > 1);
}
if (!has_substantial_param_markers && !patterns.parameter_closer.empty()) {
has_substantial_param_markers = (count_non_whitespace(patterns.parameter_closer) > 1);
}
if (!has_substantial_param_markers) {
if ((!patterns.tool_call_opener.empty() && (patterns.tool_call_opener.find('[') != std::string::npos ||
patterns.tool_call_opener.find('{') != std::string::npos)) ||
(!patterns.tool_call_start_marker.empty() &&
(patterns.tool_call_start_marker.find('[') != std::string::npos ||
patterns.tool_call_start_marker.find('{') != std::string::npos))) {
LOG_DBG("Detected JSON_NATIVE format (XML markers but JSON structure)\n");
return FORMAT_JSON_NATIVE;
}
}
LOG_DBG("Detected XML_CONSTRUCTED format from function_opener\n");
return FORMAT_XML_CONSTRUCTED;
}
if (!patterns.function_opener.empty() && patterns.function_opener.find('{') == 0) {
LOG_DBG("Detected JSON_NATIVE format from function_opener\n");
return FORMAT_JSON_NATIVE;
}
// Check for bracket-tag format: [TOOL_CALLS]name[CALL_ID]id[ARGS]{...}
// Detected when function_name_suffix contains bracket tags like [CALL_ID]...[ARGS]
if (!patterns.function_name_suffix.empty() && patterns.function_name_suffix.find('[') != std::string::npos &&
patterns.function_name_suffix.find(']') != std::string::npos) {
LOG_DBG("Detected BRACKET_TAG format from function_name_suffix containing bracket tags\n");
return FORMAT_BRACKET_TAG;
}
if (!patterns.tool_call_start_marker.empty() &&
(patterns.tool_call_start_marker.find('<') == 0 || patterns.tool_call_start_marker.find('[') == 0)) {
bool is_prefix_marker =
patterns.tool_call_start_marker.find("<|") == 0 || patterns.tool_call_start_marker.find("[|") == 0;
// Check for bracket-tag format: [TAG] style without | (e.g., [TOOL_CALLS])
bool is_bracket_tag = patterns.tool_call_start_marker.find('[') == 0 &&
patterns.tool_call_start_marker.find("[|") != 0 &&
patterns.tool_call_start_marker.find(']') != std::string::npos;
if (is_bracket_tag) {
LOG_DBG("Detected BRACKET_TAG format from tool_call_start_marker\n");
return FORMAT_BRACKET_TAG;
}
if (is_prefix_marker) {
LOG_DBG("Detected JSON_NATIVE format from tool_call_start_marker (instruction-based)\n");
return FORMAT_JSON_NATIVE;
}
LOG_DBG("Detected XML_CONSTRUCTED format from tool_call_start_marker\n");
return FORMAT_XML_CONSTRUCTED;
}
if (!patterns.tool_call_start_marker.empty() && patterns.tool_call_start_marker.find('{') == 0) {
LOG_DBG("Detected JSON_NATIVE format from tool_call_start_marker\n");
return FORMAT_JSON_NATIVE;
}
if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.find('>') == 0) {
LOG_DBG("Detected XML_CONSTRUCTED format from tool_call_end_marker\n");
return FORMAT_XML_CONSTRUCTED;
}
if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.find('}') == 0) {
LOG_DBG("Detected JSON_NATIVE format from tool_call_end_marker\n");
return FORMAT_JSON_NATIVE;
}
LOG_DBG("Format could not be determined from patterns\n");
return FORMAT_UNKNOWN;
}
internal_discovered_pattern analyze_by_differential(const common_chat_template & tmpl) {
internal_discovered_pattern patterns;
try {
LOG_DBG("%s\n", __func__);
auto caps = tmpl.original_caps();
bool minja_supports_tool_calls = caps.supports_tool_calls;
if (!minja_supports_tool_calls) {
LOG_DBG("Template doesn't support standard tool calls (per minja caps detection)\n");
}
// Define tools for testing
json tools = {
{ { "type", "function" },
{ "function",
{ { "name", "test_function_name" },
{ "description", "A test function" },
{ "parameters",
{ { "type", "object" },
{ "properties",
{ { "param1", { { "type", "string" }, { "description", "First parameter" } } },
{ "param2", { { "type", "string" }, { "description", "Second parameter" } } } } },
{ "required", json::array({ "param1", "param2" }) } } } } } },
{ { "type", "function" },
{ "function",
{ { "name", "another_test_function" },
{ "description", "Another test function" },
{ "parameters",
{ { "type", "object" },
{ "properties",
{ { "param1", { { "type", "string" }, { "description", "First parameter" } } } } },
{ "required", json::array({ "param1" }) } } } } } }
};
// Test payload 1: Tool definitions + user + assistant with content only (no tool calls)
json user_msg = {
{ "role", "user" },
{ "content", "Please help me with a task." }
};
json assistant_content_only = {
{ "role", "assistant" },
{ "content", "I'll help you with that task right away." }
};
// Test payload 2: Tool definitions + user + assistant with content + tool calls
json assistant_content_with_tool = {
{ "role", "assistant" },
{ "content", "I'll help you with that task right away." },
{ "tool_calls",
json::array(
{ { { "id", "call_0001" },
{ "type", "function" },
{ "function",
{ { "name", "test_function_name" },
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
};
// Also test with content = null + tool calls (some templates check for this)
json assistant_null_content_with_tool = {
{ "role", "assistant" },
{ "content", nullptr },
{ "tool_calls",
json::array(
{ { { "id", "call_0001" },
{ "type", "function" },
{ "function",
{ { "name", "test_function_name" },
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
};
struct templates_params inputs;
inputs.tools = tools;
inputs.add_generation_prompt = false;
// Helper function to safely render template, handling null content issues
auto safe_render = [&](const json & messages) -> std::string {
try {
// First try with the original messages
inputs.messages = messages;
return common_chat_template_direct_apply(tmpl, inputs);
} catch (const std::exception & e) {
// If it fails, try replacing null content with empty string
json fixed_messages = messages;
for (auto & msg : fixed_messages) {
if (msg.contains("content") && msg["content"].is_null()) {
msg["content"] = "";
}
}
inputs.messages = fixed_messages;
try {
return common_chat_template_direct_apply(tmpl, inputs);
} catch (...) {
return "";
}
}
};
// Render payload 1: content only
std::string output_content_only = safe_render({ user_msg, assistant_content_only });
// Render payload 2: content + tool calls
std::string output_content_with_tool = safe_render({ user_msg, assistant_content_with_tool });
// Render payload 3: null content + tool calls
std::string output_null_content_with_tool = safe_render({ user_msg, assistant_null_content_with_tool });
LOG_DBG("Output 1 (content only): %s\n", output_content_only.c_str());
LOG_DBG("Output 2 (content + tools): %s\n", output_content_with_tool.c_str());
LOG_DBG("Output 3 (null + tools): %s\n", output_null_content_with_tool.c_str());
// Check if the template renders tool calls in any scenario
// Test 1: content vs content+tool_calls (for templates that render both)
// Test 2: content vs null+tool_calls (for templates that only render tools when content is null)
bool renders_tool_calls_with_content = (output_content_only != output_content_with_tool);
bool renders_tool_calls_without_content = (output_content_only != output_null_content_with_tool);
if (!renders_tool_calls_with_content && !renders_tool_calls_without_content) {
LOG_DBG("Template does NOT render tool calls in any scenario\n");
// Return empty patterns to indicate no tool support
return patterns;
}
LOG_DBG("Template renders tool calls, proceeding with differential analysis\n");
// If we get here, the template does support tool calls
// Use the original differential analysis approach but now we know it's valid
json base_msg = {
{ "role", "assistant" },
{ "content", "MARKER" }
};
// Use nullptr for content to trigger tool_calls branch in templates that check "content is none"
// Include "id" field as some templates (e.g., Mistral Nemo) require it
json tool_msg1 = {
{ "role", "assistant" },
{ "content", nullptr },
{ "tool_calls",
json::array(
{ { { "id", "call_0001" },
{ "type", "function" },
{ "function", { { "name", "test_function_name" }, { "arguments", json::object() } } } } }) }
};
json tool_msg2 = {
{ "role", "assistant" },
{ "content", nullptr },
{ "tool_calls",
json::array(
{ { { "id", "call_0001" },
{ "type", "function" },
{ "function",
{ { "name", "test_function_name" },
{ "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) }
};
json tool_msg3 = {
{ "role", "assistant" },
{ "content", nullptr },
{ "tool_calls",
json::array(
{ { { "id", "call_0001" },
{ "type", "function" },
{ "function", { { "name", "test_function_name" }, { "arguments", json::object() } } } },
{ { "id", "call_0002" },
{ "type", "function" },
{ "function", { { "name", "another_test_function" }, { "arguments", json::object() } } } } }) }
};
inputs.messages = { user_msg, base_msg };
auto base_output = safe_render({ user_msg, base_msg });
inputs.messages = { user_msg, tool_msg1 };
auto tool1_output = safe_render({ user_msg, tool_msg1 });
// Detect if template renders null content as "None" (Python/Jinja string representation)
// This happens when templates concatenate content without null checks, e.g.:
// {{ '<|im_start|>' + message.role + '\n' + content }}
// Check if "None" appears in the tool output where it shouldn't
if (tool1_output.find("None") != std::string::npos) {
// Verify this is actually from null content by checking if it goes away with empty string
json tool_msg1_empty_content = tool_msg1;
tool_msg1_empty_content["content"] = "";
auto tool1_output_empty = safe_render({ user_msg, tool_msg1_empty_content });
if (tool1_output_empty.find("None") == std::string::npos) {
LOG_DBG("Template renders null content as 'None', switching to empty string\n");
patterns.requires_nonnull_content = true;
tool1_output = tool1_output_empty;
// Update tool messages to use empty string instead of null
tool_msg1["content"] = "";
tool_msg2["content"] = "";
tool_msg3["content"] = "";
}
}
inputs.messages = { user_msg, tool_msg2 };
auto tool2_output = safe_render({ user_msg, tool_msg2 });
inputs.messages = { user_msg, tool_msg3 };
auto tool3_output = safe_render({ user_msg, tool_msg3 });
std::string tool1_diff = find_string_difference(base_output, tool1_output);
std::string tool2_diff = find_string_difference(base_output, tool2_output);
std::string tool3_diff = find_string_difference(base_output, tool3_output);
LOG_DBG("Tool1 diff length: %zu\n", tool1_diff.length());
LOG_DBG("Tool2 diff length: %zu\n", tool2_diff.length());
LOG_DBG("Tool3 diff length: %zu\n", tool3_diff.length());
if (tool1_diff.empty() && tool2_diff.empty() && tool3_diff.empty()) {
LOG_DBG("All diffs are empty - trying without add_generation_prompt\n");
// Try with add_generation_prompt variations
json alternative_base_msg = {
{ "role", "assistant" },
{ "content", "MARKER" }
};
templates_params alt_inputs;
alt_inputs.tools = tools;
alt_inputs.messages = { user_msg, alternative_base_msg };
alt_inputs.add_generation_prompt = false;
auto alt_base = common_chat_template_direct_apply(tmpl, alt_inputs);
alt_inputs.messages = { user_msg, tool_msg1 };
auto alt_tool1 = common_chat_template_direct_apply(tmpl, alt_inputs);
tool1_diff = find_string_difference(alt_base, alt_tool1);
if (!tool1_diff.empty()) {
// If we found a diff using the alternative approach, we must use the corresponding
// full output for pattern extraction (otherwise diff indices will be invalid)
tool1_output = alt_tool1;
alt_inputs.messages = { user_msg, tool_msg2 };
tool2_diff = find_string_difference(alt_base, common_chat_template_direct_apply(tmpl, inputs));
alt_inputs.messages = { user_msg, tool_msg3 };
tool3_diff = find_string_difference(alt_base, common_chat_template_direct_apply(tmpl, inputs));
}
}
patterns = extract_patterns_from_differences(tool1_diff, tool2_diff, tool3_diff, tool1_output);
LOG_DBG("=== ENDING TEMPLATE DIFFERENTIAL ANALYSIS ===\n");
} catch (const std::exception & e) {
LOG_DBG("Template differential analysis failed: %s\n", e.what());
}
return patterns;
}