diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp
index e9fe71c1d6..13ec14fb64 100644
--- a/common/chat-auto-parser-generator.cpp
+++ b/common/chat-auto-parser-generator.cpp
@@ -30,8 +30,8 @@ common_chat_params universal_peg_generator::generate_parser(const common_chat_te
const struct templates_params & inputs,
const diff_analysis_result & analysis) {
// Check for thinking forced open
- bool thinking_forced_open = (analysis.reasoning == reasoning_mode::FORCED_OPEN);
- bool thinking_forced_closed = (analysis.reasoning == reasoning_mode::FORCED_CLOSED);
+ bool thinking_forced_open = (analysis.reasoning.mode == reasoning_mode::FORCED_OPEN);
+ bool thinking_forced_closed = (analysis.reasoning.mode == reasoning_mode::FORCED_CLOSED);
// Build the parser using the analysis results
auto parser = build_parser(analysis, inputs, thinking_forced_open, thinking_forced_closed);
@@ -44,7 +44,7 @@ common_chat_params universal_peg_generator::generate_parser(const common_chat_te
data.parser = parser.save();
// Build grammar if tools are present
- bool has_tools = inputs.tools.is_array() && !inputs.tools.empty();
+ bool has_tools = analysis.tools.format.mode != tool_format::NONE && inputs.tools.is_array() && !inputs.tools.empty();
bool include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
if (include_grammar) {
@@ -60,9 +60,9 @@ common_chat_params universal_peg_generator::generate_parser(const common_chat_te
});
// Set grammar triggers based on tool section markers (fall back to per-call markers)
- std::string trigger_marker = !analysis.markers.tool_section_start.empty()
- ? analysis.markers.tool_section_start
- : analysis.markers.per_call_start;
+ std::string trigger_marker = !analysis.tools.format.section_start.empty()
+ ? analysis.tools.format.section_start
+ : analysis.tools.format.per_call_start;
if (!trigger_marker.empty()) {
data.grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_marker }
@@ -79,26 +79,24 @@ common_peg_arena universal_peg_generator::build_parser(const diff_analysis_resul
bool thinking_forced_closed) {
return build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) {
p.set_allow_python_dict_format(true);
- const auto & m = analysis.markers;
-
common_peg_parser reasoning = p.eps();
bool extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
bool enable_thinking = inputs.enable_thinking;
- if (extract_reasoning && enable_thinking && analysis.reasoning != reasoning_mode::NONE) {
+ if (extract_reasoning && enable_thinking && analysis.reasoning.mode != reasoning_mode::NONE) {
if (thinking_forced_open || thinking_forced_closed) {
// Thinking is forced open OR forced closed with enable_thinking=true
// In both cases, expect only the closing tag (opening was in template)
- reasoning = p.reasoning(p.until(m.reasoning_end)) + m.reasoning_end;
- } else if (analysis.reasoning == reasoning_mode::TAG_BASED ||
- analysis.reasoning == reasoning_mode::TOOLS_ONLY) {
+ reasoning = p.reasoning(p.until(analysis.reasoning.end)) + analysis.reasoning.end;
+ } else if (analysis.reasoning.mode == reasoning_mode::TAG_BASED ||
+ analysis.reasoning.mode == reasoning_mode::TOOLS_ONLY) {
// Standard tag-based reasoning OR tools-only mode (reasoning appears with tools)
// Both use the same tag-based pattern if markers are available
- if (!m.reasoning_start.empty() && !m.reasoning_end.empty()) {
- reasoning = p.optional(m.reasoning_start + p.reasoning(p.until(m.reasoning_end)) + m.reasoning_end);
+ if (!analysis.reasoning.start.empty() && !analysis.reasoning.end.empty()) {
+ reasoning = p.optional(analysis.reasoning.start + p.reasoning(p.until(analysis.reasoning.end)) + analysis.reasoning.end);
}
- } else if (analysis.reasoning == reasoning_mode::DELIMITER) {
- reasoning = p.optional(p.reasoning(p.until(m.reasoning_end)) + m.reasoning_end);
+ } else if (analysis.reasoning.mode == reasoning_mode::DELIMITER) {
+ reasoning = p.optional(p.reasoning(p.until(analysis.reasoning.end)) + analysis.reasoning.end);
}
}
@@ -109,19 +107,19 @@ common_peg_arena universal_peg_generator::build_parser(const diff_analysis_resul
return reasoning + p.space() + p.content(p.schema(p.json(), "response-format", inputs.json_schema)) + p.end();
}
- if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && analysis.supports_tools) {
+ if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && analysis.jinja_caps.supports_tool_calls) {
return build_tool_parser(p, analysis, inputs, reasoning);
}
- if (analysis.content == content_mode::ALWAYS_WRAPPED &&
- !m.content_start.empty() && !m.content_end.empty()) {
+ if (analysis.content.mode == content_mode::ALWAYS_WRAPPED &&
+ !analysis.content.start.empty() && !analysis.content.end.empty()) {
- bool extracting_reasoning = extract_reasoning && enable_thinking && analysis.reasoning != reasoning_mode::NONE;
+ bool extracting_reasoning = extract_reasoning && enable_thinking && analysis.reasoning.mode != reasoning_mode::NONE;
if (extracting_reasoning) {
- return reasoning + m.content_start + p.content(p.until(m.content_end)) + m.content_end + p.end();
+ return reasoning + analysis.content.start + p.content(p.until(analysis.content.end)) + analysis.content.end + p.end();
}
- return p.content(p.until(m.content_start)) + m.content_start + p.content(p.until(m.content_end)) + m.content_end + p.end();
+ return p.content(p.until(analysis.content.start)) + analysis.content.start + p.content(p.until(analysis.content.end)) + analysis.content.end + p.end();
}
return reasoning + p.content(p.rest()) + p.end();
});
@@ -133,7 +131,7 @@ common_peg_parser universal_peg_generator::build_tool_parser(
const templates_params & inputs,
const common_peg_parser & reasoning) {
- switch (analysis.tools) {
+ switch (analysis.tools.format.mode) {
case tool_format::JSON_NATIVE:
return build_tool_parser_json_native(p, analysis, inputs, reasoning);
case tool_format::TAG_WITH_JSON:
@@ -151,42 +149,40 @@ common_peg_parser universal_peg_generator::build_tool_parser_json_native(
const templates_params & inputs,
const common_peg_parser & reasoning) {
- const auto & m = analysis.markers;
-
// Build effective field names with dot notation if function_field is set
- std::string name_field = analysis.name_field;
- std::string args_field = analysis.args_field;
+ std::string name_field = analysis.tools.format.name_field;
+ std::string args_field = analysis.tools.format.args_field;
- if (!analysis.function_field.empty() &&
- analysis.function_field != "function" &&
+ if (!analysis.tools.format.function_field.empty() &&
+ analysis.tools.format.function_field != "function" &&
name_field.find('.') == std::string::npos) {
- name_field = analysis.function_field + "." + name_field;
- args_field = analysis.function_field + "." + args_field;
+ name_field = analysis.tools.format.function_field + "." + name_field;
+ args_field = analysis.tools.format.function_field + "." + args_field;
}
auto tools_parser = p.standard_json_tools(
- m.tool_section_start,
- m.tool_section_end,
+ analysis.tools.format.section_start,
+ analysis.tools.format.section_end,
inputs.tools,
inputs.parallel_tool_calls,
inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED,
name_field,
args_field,
- analysis.tools_array_wrapped,
- analysis.fun_name_is_key,
- analysis.id_field,
- analysis.gen_id_field,
- analysis.parameter_order
+ analysis.tools.format.tools_array_wrapped,
+ analysis.tools.format.fun_name_is_key,
+ analysis.tools.format.id_field,
+ analysis.tools.format.gen_id_field,
+ analysis.tools.format.parameter_order
);
// Handle content wrappers if present
- if (analysis.content == content_mode::ALWAYS_WRAPPED &&
- !m.content_start.empty() && !m.content_end.empty()) {
- auto wrapped_content = p.optional(m.content_start + p.content(p.until(m.content_end)) + m.content_end);
+ if (analysis.content.mode == content_mode::ALWAYS_WRAPPED &&
+ !analysis.content.start.empty() && !analysis.content.end.empty()) {
+ auto wrapped_content = p.optional(analysis.content.start + p.content(p.until(analysis.content.end)) + analysis.content.end);
return reasoning + wrapped_content + tools_parser + p.end();
}
- auto content_before_tools = m.tool_section_start.empty() ? p.eps() : p.until(m.tool_section_start);
+ auto content_before_tools = analysis.tools.format.section_start.empty() ? p.eps() : p.until(analysis.tools.format.section_start);
return reasoning + p.optional(p.content(content_before_tools)) + tools_parser + p.end();
}
@@ -196,7 +192,6 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_json(
const templates_params & inputs,
const common_peg_parser & reasoning) {
- const auto & m = analysis.markers;
common_peg_parser tool_choice = p.choice();
foreach_function(inputs.tools, [&](const json & tool) {
@@ -206,17 +201,17 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_json(
// Build call_id parser based on position (if supported)
common_peg_parser call_id_section = p.eps();
- if (analysis.call_id_pos == call_id_position::BETWEEN_FUNC_AND_ARGS &&
- !m.call_id_prefix.empty() && !m.call_id_suffix.empty()) {
- call_id_section = p.optional(m.call_id_prefix + p.tool_id(p.until(m.call_id_suffix))) + m.call_id_suffix;
+ if (analysis.tools.call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS &&
+ !analysis.tools.call_id.prefix.empty() && !analysis.tools.call_id.suffix.empty()) {
+ call_id_section = p.optional(analysis.tools.call_id.prefix + p.tool_id(p.until(analysis.tools.call_id.suffix))) + analysis.tools.call_id.suffix;
}
- auto func_parser = p.tool_open(m.func_name_prefix + p.tool_name(p.literal(name)) + m.func_name_suffix) +
+ auto func_parser = p.tool_open(analysis.tools.function.name_prefix + p.tool_name(p.literal(name)) + analysis.tools.function.name_suffix) +
call_id_section +
p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema));
- if (!m.func_close.empty()) {
- func_parser = func_parser + m.func_close;
+ if (!analysis.tools.function.close.empty()) {
+ func_parser = func_parser + analysis.tools.function.close;
}
tool_choice |= p.rule("tool-" + name, func_parser);
@@ -226,30 +221,26 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_json(
common_peg_parser tool_calls = p.eps();
- if (!m.per_call_start.empty()) {
- auto wrapped_call = m.per_call_start + tool_choice + m.per_call_end;
+ if (!analysis.tools.format.per_call_start.empty()) {
+ auto wrapped_call = analysis.tools.format.per_call_start + tool_choice + analysis.tools.format.per_call_end;
if (inputs.parallel_tool_calls) {
tool_calls = p.trigger_rule("tool-call",
wrapped_call + p.zero_or_more(p.space() + wrapped_call));
} else {
tool_calls = p.trigger_rule("tool-call", wrapped_call);
}
- if (!m.tool_section_start.empty()) {
- tool_calls = p.trigger_rule("tool-calls", p.literal(m.tool_section_start) + p.space() +
- tool_calls + p.space() + (m.tool_section_end.empty() ? p.end() : p.literal(m.tool_section_end)));
+ if (!analysis.tools.format.section_start.empty()) {
+ tool_calls = p.trigger_rule("tool-calls", p.literal(analysis.tools.format.section_start) + p.space() +
+ tool_calls + p.space() + (analysis.tools.format.section_end.empty() ? p.end() : p.literal(analysis.tools.format.section_end)));
}
} else {
- std::string separator = m.call_separator;
- if (separator.empty()) {
- separator = ", "; // Default
- }
-
+ std::string separator = ", "; // Default
if (inputs.parallel_tool_calls) {
tool_calls = p.trigger_rule("tool-call",
- m.tool_section_start + tool_choice + p.zero_or_more(separator + tool_choice) + m.tool_section_end);
+ analysis.tools.format.section_start + tool_choice + p.zero_or_more(separator + tool_choice) + analysis.tools.format.section_end);
} else {
tool_calls = p.trigger_rule("tool-call",
- m.tool_section_start + tool_choice + m.tool_section_end);
+ analysis.tools.format.section_start + tool_choice + analysis.tools.format.section_end);
}
}
@@ -257,7 +248,7 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_json(
tool_calls = p.optional(tool_calls);
}
- std::string trigger_marker = !m.tool_section_start.empty() ? m.tool_section_start : m.per_call_start;
+ std::string trigger_marker = !analysis.tools.format.section_start.empty() ? analysis.tools.format.section_start : analysis.tools.format.per_call_start;
auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker);
return reasoning + p.optional(p.content(content_before_tools)) + tool_calls + p.end();
}
@@ -268,7 +259,6 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_tagged(
const templates_params & inputs,
const common_peg_parser & reasoning) {
- const auto & m = analysis.markers;
common_peg_parser tool_choice = p.choice();
foreach_function(inputs.tools, [&](const json & tool) {
@@ -293,13 +283,13 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_tagged(
auto type = param_schema.value("type", "object");
auto arg = p.tool_arg(
- p.tool_arg_open(m.arg_name_prefix + p.tool_arg_name(p.literal(param_name)) + m.arg_name_suffix) + m.arg_value_prefix +
+ p.tool_arg_open(analysis.tools.arguments.name_prefix + p.tool_arg_name(p.literal(param_name)) + analysis.tools.arguments.name_suffix) + analysis.tools.arguments.value_prefix +
(type == "string" ?
- p.tool_arg_string_value(p.schema(p.until(m.arg_value_suffix),
+ p.tool_arg_string_value(p.schema(p.until(analysis.tools.arguments.value_suffix),
"tool-" + name + "-arg-" + param_name + "-schema", param_schema, true)) :
p.tool_arg_json_value(p.schema(p.json(),
"tool-" + name + "-arg-" + param_name + "-schema", param_schema)) + p.space()) +
- p.tool_arg_close(p.literal(m.arg_value_suffix))
+ p.tool_arg_close(p.literal(analysis.tools.arguments.value_suffix))
);
if (is_required) {
@@ -320,23 +310,23 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_tagged(
// Build call_id parser based on position (if supported)
common_peg_parser call_id_section = p.eps();
- if (analysis.call_id_pos == call_id_position::BETWEEN_FUNC_AND_ARGS &&
- !m.call_id_prefix.empty() && !m.call_id_suffix.empty()) {
- call_id_section = p.optional(m.call_id_prefix + p.tool_id(p.until(m.call_id_suffix))) + m.call_id_suffix;
+ if (analysis.tools.call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS &&
+ !analysis.tools.call_id.prefix.empty() && !analysis.tools.call_id.suffix.empty()) {
+ call_id_section = p.optional(analysis.tools.call_id.prefix + p.tool_id(p.until(analysis.tools.call_id.suffix))) + analysis.tools.call_id.suffix;
}
- auto func_parser = p.tool_open(m.func_name_prefix + p.tool_name(p.literal(name)) + m.func_name_suffix) +
+ auto func_parser = p.tool_open(analysis.tools.function.name_prefix + p.tool_name(p.literal(name)) + analysis.tools.function.name_suffix) +
call_id_section +
p.space() + args_seq;
- if (!m.func_close.empty()) {
- func_parser = func_parser + p.space() + p.tool_close(p.literal(m.func_close));
- } else if (!m.per_call_end.empty()) {
+ if (!analysis.tools.function.close.empty()) {
+ func_parser = func_parser + p.space() + p.tool_close(p.literal(analysis.tools.function.close));
+ } else if (!analysis.tools.format.per_call_end.empty()) {
// When there's no func_close but there is a per_call_end marker, use peek() to ensure
// we only emit tool_close when we can actually see the closing marker. This prevents
// premature closing during partial parsing when we've seen e.g. "" which could be
// either "" (end) or "" prefix that failed to match.
- func_parser = func_parser + p.tool_close(p.peek(p.literal(m.per_call_end)));
+ func_parser = func_parser + p.tool_close(p.peek(p.literal(analysis.tools.format.per_call_end)));
} else {
func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper
}
@@ -348,29 +338,26 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_tagged(
common_peg_parser tool_calls = p.eps();
- if (!m.per_call_start.empty()) {
- auto wrapped_call = m.per_call_start + p.space() + tool_choice + p.space() + m.per_call_end;
+ if (!analysis.tools.format.per_call_start.empty()) {
+ auto wrapped_call = analysis.tools.format.per_call_start + p.space() + tool_choice + p.space() + analysis.tools.format.per_call_end;
if (inputs.parallel_tool_calls) {
tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call));
} else {
tool_calls = p.trigger_rule("tool-call", wrapped_call);
}
- if (!m.tool_section_start.empty()) {
- tool_calls = p.trigger_rule("tool-calls", p.literal(m.tool_section_start) + p.space() +
- tool_calls + p.space() + (m.tool_section_end.empty() ? p.end() : p.literal(m.tool_section_end)));
+ if (!analysis.tools.format.section_start.empty()) {
+ tool_calls = p.trigger_rule("tool-calls", p.literal(analysis.tools.format.section_start) + p.space() +
+ tool_calls + p.space() + (analysis.tools.format.section_end.empty() ? p.end() : p.literal(analysis.tools.format.section_end)));
}
} else {
- std::string separator = m.call_separator;
- if (separator.empty()) {
- separator = ", "; // Default
- }
+ std::string separator = ", "; // Default
if (inputs.parallel_tool_calls) {
tool_calls = p.trigger_rule("tool-call",
- m.tool_section_start + p.space() + tool_choice + p.zero_or_more(separator + tool_choice) + p.space() + m.tool_section_end);
+ analysis.tools.format.section_start + p.space() + tool_choice + p.zero_or_more(separator + tool_choice) + p.space() + analysis.tools.format.section_end);
} else {
tool_calls = p.trigger_rule("tool-call",
- m.tool_section_start + p.space() + tool_choice + p.space() + m.tool_section_end);
+ analysis.tools.format.section_start + p.space() + tool_choice + p.space() + analysis.tools.format.section_end);
}
}
@@ -378,7 +365,7 @@ common_peg_parser universal_peg_generator::build_tool_parser_tag_tagged(
tool_calls = p.optional(tool_calls);
}
- std::string trigger_marker = !m.tool_section_start.empty() ? m.tool_section_start : m.per_call_start;
+ std::string trigger_marker = !analysis.tools.format.section_start.empty() ? analysis.tools.format.section_start : analysis.tools.format.per_call_start;
auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker);
return reasoning + p.optional(p.content(content_before_tools)) + tool_calls + p.end();
}
diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp
index 080fecfce2..03978f6e57 100644
--- a/common/chat-diff-analyzer.cpp
+++ b/common/chat-diff-analyzer.cpp
@@ -20,76 +20,77 @@ static std::vector void {
- if (tmpl.src.find("content.split('')") != std::string::npos && analysis.reasoning == reasoning_mode::NONE) {
- analysis.reasoning = reasoning_mode::FORCED_OPEN;
- analysis.markers.reasoning_start = "";
- analysis.markers.reasoning_end = "";
- analysis.preserved_tokens.push_back("");
- analysis.preserved_tokens.push_back("");
- LOG_DBG(ANSI_ORANGE "[Patch: old Qwen/Deepseek thinking template]\n" ANSI_RESET);
+ if (tmpl.src.find("content.split('')") != std::string::npos &&
+ analysis.reasoning.mode == reasoning_mode::NONE) {
+ analysis.reasoning.mode = reasoning_mode::FORCED_OPEN;
+ analysis.reasoning.start = "";
+ analysis.reasoning.end = "";
+ analysis.preserved_tokens.push_back("");
+ analysis.preserved_tokens.push_back("");
+ LOG_DBG(ANSI_ORANGE "[Patch: old Qwen/Deepseek thinking template]\n" ANSI_RESET);
}
},
// Granite 3.3, with separate reasoning and content markers
[](const common_chat_template & tmpl, diff_analysis_result & analysis) -> void {
if (tmpl.src.find("Write your thoughts between and write your response between "
"") != std::string::npos) {
- analysis.reasoning = reasoning_mode::TAG_BASED;
- analysis.markers.reasoning_start = "";
- analysis.markers.reasoning_end = "";
- analysis.preserved_tokens.push_back("");
- analysis.preserved_tokens.push_back("");
- analysis.content = content_mode::WRAPPED_WITH_REASONING;
- analysis.markers.content_start = "";
- analysis.markers.content_end = "";
- analysis.preserved_tokens.push_back("");
- analysis.preserved_tokens.push_back("");
- LOG_DBG(ANSI_ORANGE "[Patch: Granite 3.3]\n" ANSI_RESET);
+ analysis.reasoning.mode = reasoning_mode::TAG_BASED;
+ analysis.reasoning.start = "";
+ analysis.reasoning.end = "";
+ analysis.preserved_tokens.push_back("");
+ analysis.preserved_tokens.push_back("");
+ analysis.content.mode = content_mode::WRAPPED_WITH_REASONING;
+ analysis.content.start = "";
+ analysis.content.end = "";
+ analysis.preserved_tokens.push_back("");
+ analysis.preserved_tokens.push_back("");
+ LOG_DBG(ANSI_ORANGE "[Patch: Granite 3.3]\n" ANSI_RESET);
}
},
// Cohere Command R+ - content wrapped in <|CHATBOT_TOKEN|>...<|END_OF_TURN_TOKEN|>
[](const common_chat_template & tmpl, diff_analysis_result & analysis) -> void {
if (tmpl.src.find("<|CHATBOT_TOKEN|>") != std::string::npos &&
- tmpl.src.find("<|END_OF_TURN_TOKEN|>") != std::string::npos &&
- analysis.markers.content_start.empty()) {
- analysis.content = content_mode::ALWAYS_WRAPPED;
- analysis.markers.content_start = "<|CHATBOT_TOKEN|>";
- analysis.markers.content_end = "<|END_OF_TURN_TOKEN|>";
- analysis.preserved_tokens.push_back("<|CHATBOT_TOKEN|>");
- analysis.preserved_tokens.push_back("<|END_OF_TURN_TOKEN|>");
- LOG_DBG(ANSI_ORANGE "[Patch: Cohere Command R+]\n" ANSI_RESET);
+ tmpl.src.find("<|END_OF_TURN_TOKEN|>") != std::string::npos && analysis.content.start.empty()) {
+ analysis.content.mode = content_mode::ALWAYS_WRAPPED;
+ analysis.content.start = "<|CHATBOT_TOKEN|>";
+ analysis.content.end = "<|END_OF_TURN_TOKEN|>";
+ analysis.preserved_tokens.push_back("<|CHATBOT_TOKEN|>");
+ analysis.preserved_tokens.push_back("<|END_OF_TURN_TOKEN|>");
+ LOG_DBG(ANSI_ORANGE "[Patch: Cohere Command R+]\n" ANSI_RESET);
}
},
// Functionary - no tool call section delimiter
[](const common_chat_template & tmpl, diff_analysis_result & analysis) -> void {
if (tmpl.src.find("set has_code_interpreter = tools | selectattr(\"type\", \"equalto\", "
"\"code_interpreter\") | list | length > 0") != std::string::npos) {
- analysis.content = content_mode::PLAIN;
- analysis.markers.content_end = "";
- analysis.markers.func_name_prefix = "";
- analysis.markers.tool_section_start = "";
- analysis.markers.tool_section_end = "";
- analysis.markers.per_call_start = "");
- analysis.preserved_tokens.push_back("<|eom_id|>");
- analysis.preserved_tokens.push_back("");
- analysis.preserved_tokens.push_back("");
- LOG_DBG(ANSI_ORANGE "[Patch: Functionary 3.1]\n" ANSI_RESET);
- }
+ analysis.content.mode = content_mode::PLAIN;
+ analysis.content.end = "";
+ analysis.tools.function.name_prefix = "";
+ analysis.tools.format.section_start = "";
+ analysis.tools.format.section_end = "";
+ analysis.tools.format.per_call_start = "");
+ analysis.preserved_tokens.push_back("<|eom_id|>");
+ analysis.preserved_tokens.push_back("");
+ analysis.preserved_tokens.push_back("");
+ LOG_DBG(ANSI_ORANGE "[Patch: Functionary 3.1]\n" ANSI_RESET);
+ }
},
// DeepSeek-R1-Distill-Qwen
[](const common_chat_template & tmpl, diff_analysis_result & analysis) -> void {
- if (tmpl.src.find("{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>'") !=
+ if (tmpl.src.find(
+ "{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>'") !=
std::string::npos) {
- analysis.markers.tool_section_start = "<|tool▁calls▁begin|>";
- analysis.markers.tool_section_end = "<|tool▁calls▁end|>";
- analysis.markers.per_call_start = "<|tool▁call▁begin|>function";
- analysis.markers.func_name_prefix = "<|tool▁sep|>";
- analysis.markers.per_call_end = "<|tool▁call▁end|>";
- analysis.markers.func_close = "```";
+ analysis.tools.format.section_start = "<|tool▁calls▁begin|>";
+ analysis.tools.format.section_end = "<|tool▁calls▁end|>";
+ analysis.tools.format.per_call_start = "<|tool▁call▁begin|>function";
+ analysis.tools.function.name_prefix = "<|tool▁sep|>";
+ analysis.tools.format.per_call_end = "<|tool▁call▁end|>";
+ analysis.tools.function.close = "```";
}
} });
@@ -191,14 +192,12 @@ diff_analysis_result differential_analyzer::analyze(const common_chat_template &
LOG_DBG(ANSI_PURPLE "=== Starting differential analysis ===\n" ANSI_RESET);
- auto caps = tmpl.original_caps();
- result.supports_tools = caps.supports_tools || caps.supports_tool_calls;
- result.supports_parallel_calls = caps.supports_parallel_tool_calls;
+ result.jinja_caps = tmpl.original_caps();
- analyze_reasoning(tmpl, result);
- analyze_content(tmpl, result);
- if (result.supports_tools) {
- analyze_tools(tmpl, result);
+ result.reasoning = analyze_reasoning(tmpl, result.jinja_caps.supports_tool_calls);
+ result.content = analyze_content(tmpl, result.reasoning);
+ if (result.jinja_caps.supports_tool_calls) {
+ result.tools = analyze_tools(tmpl, result.jinja_caps, result.reasoning);
}
collect_preserved_tokens(result);
@@ -211,18 +210,21 @@ diff_analysis_result differential_analyzer::analyze(const common_chat_template &
return result;
}
-void differential_analyzer::analyze_reasoning(const common_chat_template & tmpl, diff_analysis_result & result) {
+reasoning_analysis differential_analyzer::analyze_reasoning(const common_chat_template & tmpl, bool supports_tools) {
LOG_DBG(ANSI_ORANGE "Phase 1: Reasoning analysis\n" ANSI_RESET);
+ reasoning_analysis result;
+
compare_reasoning_presence(tmpl, result);
compare_thinking_enabled(tmpl, result);
- if (result.supports_tools) {
+ if (supports_tools) {
compare_reasoning_scope(tmpl, result);
}
+
+ return result;
}
-void differential_analyzer::compare_reasoning_presence(const common_chat_template & tmpl,
- diff_analysis_result & result) {
+void differential_analyzer::compare_reasoning_presence(const common_chat_template & tmpl, reasoning_analysis & reasoning) {
json user_msg = json{
{ "role", "user" },
{ "content", "Hello" }
@@ -248,38 +250,34 @@ void differential_analyzer::compare_reasoning_presence(const common_chat_templat
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_reasoning }); });
if (!comparison) {
- LOG_DBG(ANSI_ORANGE "R1: Template application failed, skipping reasoning detection\n" ANSI_RESET);
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed, skipping reasoning detection\n" ANSI_RESET, __func__);
return;
}
const auto & diff = comparison->diff;
- LOG_DBG(ANSI_ORANGE "R1 diff - suffix: " ANSI_RESET "'%s', " ANSI_ORANGE "left: " ANSI_RESET "'%s', " ANSI_ORANGE
- "right: " ANSI_ORANGE "'%s'\n" ANSI_RESET,
- diff.suffix.c_str(), diff.left.c_str(), diff.right.c_str());
-
const std::string reasoning_content = "Let me think about this.";
if (!diff.right.empty() && diff.right.find(reasoning_content) != std::string::npos) {
auto seg = prune_whitespace_segments(segmentize_markers(diff.right));
if (seg.size() >= 3 && trim_whitespace(seg[1].value) == reasoning_content) {
// easy one: opening marker - reasoning - closing marker (possibly with trailing whitespace)
- result.reasoning = reasoning_mode::TAG_BASED;
- result.markers.reasoning_start = trim_whitespace(seg[0].value);
- result.markers.reasoning_end = trim_leading_whitespace(seg[2].value);
+ reasoning.mode = reasoning_mode::TAG_BASED;
+ reasoning.start = trim_whitespace(seg[0].value);
+ reasoning.end = trim_leading_whitespace(seg[2].value);
for (size_t i = 3; i < seg.size(); i++) {
- result.markers.reasoning_end += seg[i].value;
+ reasoning.end += seg[i].value;
}
// we always truncate because this doesn't really influence correctness but model might not always generate newline
- result.markers.reasoning_end = trim_whitespace(result.markers.reasoning_end);
+ reasoning.end = trim_whitespace(reasoning.end);
} else if (seg.size() >= 2 && trim_whitespace(seg[0].value) == reasoning_content) {
// delimited
- result.reasoning = reasoning_mode::DELIMITER;
- result.markers.reasoning_end = trim_leading_whitespace(seg[1].value);
+ reasoning.mode = reasoning_mode::DELIMITER;
+ reasoning.end = trim_leading_whitespace(seg[1].value);
for (size_t i = 2; i < seg.size(); i++) {
- result.markers.reasoning_end += seg[i].value;
+ reasoning.end += seg[i].value;
}
- result.markers.reasoning_end = trim_whitespace(result.markers.reasoning_end);
+ reasoning.end = trim_whitespace(reasoning.end);
} else if (seg.size() == 1 && trim_whitespace(seg[0].value) == reasoning_content) {
// the marker might be in the prefix actually, let's check for case of
// left: empty
@@ -297,16 +295,16 @@ void differential_analyzer::compare_reasoning_presence(const common_chat_templat
if (marker_seg.type == segment_type::TEXT) {
marker_seg = pre_seg[pre_seg.size() - 2];
}
- result.reasoning = reasoning_mode::FORCED_CLOSED;
- result.markers.reasoning_start = trim_whitespace(marker_seg.value);
- result.markers.reasoning_end = trim_whitespace(suf_seg[0].value);
+ reasoning.mode = reasoning_mode::FORCED_CLOSED;
+ reasoning.start = trim_whitespace(marker_seg.value);
+ reasoning.end = trim_whitespace(suf_seg[0].value);
}
}
}
}
}
-void differential_analyzer::compare_thinking_enabled(const common_chat_template & tmpl, diff_analysis_result & result) {
+void differential_analyzer::compare_thinking_enabled(const common_chat_template & tmpl, reasoning_analysis & reasoning) {
json user_msg = json{
{ "role", "user" },
{ "content", "Hello" }
@@ -320,15 +318,12 @@ void differential_analyzer::compare_thinking_enabled(const common_chat_template
auto comparison = compare_variants(tmpl, params, [&](template_params & p) { p.enable_thinking = true; });
if (!comparison) {
- LOG_DBG("R2: Template application failed\n");
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET , __func__);
return;
}
const auto & diff = comparison->diff;
- LOG_DBG("R2 diff - suffix: '%s', left: '%s', right: '%s'\n", diff.suffix.c_str(), diff.left.c_str(),
- diff.right.c_str());
-
std::string left_trimmed = diff.left;
trim_whitespace(left_trimmed);
@@ -337,18 +332,15 @@ void differential_analyzer::compare_thinking_enabled(const common_chat_template
trim_whitespace(right_trimmed);
if (!right_trimmed.empty() && string_ends_with(comparison->output_B, right_trimmed)) {
- if (result.markers.reasoning_start.empty()) {
- result.markers.reasoning_start = right_trimmed;
- result.reasoning = reasoning_mode::FORCED_OPEN;
- LOG_DBG("R2: Detected forced-open reasoning with start marker: '%s'\n", right_trimmed.c_str());
+ if (reasoning.start.empty()) {
+ reasoning.start = right_trimmed;
+ reasoning.mode = reasoning_mode::FORCED_OPEN;
}
}
}
- if (result.markers.reasoning_start.empty() && !result.markers.reasoning_end.empty()) {
- result.reasoning = reasoning_mode::DELIMITER;
- LOG_DBG("R2: Delimiter-based reasoning detected (empty start, end: '%s')\n",
- result.markers.reasoning_end.c_str());
+ if (reasoning.start.empty() && !reasoning.end.empty()) {
+ reasoning.mode = reasoning_mode::DELIMITER;
}
// Check for FORCED_CLOSED: when enable_thinking=false produces both start and end markers,
@@ -360,33 +352,30 @@ void differential_analyzer::compare_thinking_enabled(const common_chat_template
// Both should end with the assistant role marker
// Check if output_A has both reasoning_start and reasoning_end markers
// while output_B has only reasoning_start
- if (!result.markers.reasoning_start.empty()) {
+ if (!reasoning.start.empty()) {
// Check if output_A contains both start and end markers
- bool A_has_start = output_A.find(result.markers.reasoning_start) != std::string::npos;
- bool A_has_end = !result.markers.reasoning_end.empty() &&
- output_A.find(result.markers.reasoning_end) != std::string::npos;
+ bool A_has_start = output_A.find(reasoning.start) != std::string::npos;
+ bool A_has_end = !reasoning.end.empty() && output_A.find(reasoning.end) != std::string::npos;
// Check if output_B contains only the start marker (and not the end marker)
- bool B_has_start = output_B.find(result.markers.reasoning_start) != std::string::npos;
- bool B_has_end = !result.markers.reasoning_end.empty() &&
- output_B.find(result.markers.reasoning_end) != std::string::npos;
+ bool B_has_start = output_B.find(reasoning.start) != std::string::npos;
+ bool B_has_end = !reasoning.end.empty() && output_B.find(reasoning.end) != std::string::npos;
// For FORCED_CLOSED: A should have both, B should have only start
if (A_has_start && A_has_end && B_has_start && !B_has_end) {
- result.reasoning = reasoning_mode::FORCED_CLOSED;
- LOG_DBG("R2: Detected forced-closed reasoning\n");
+ reasoning.mode = reasoning_mode::FORCED_CLOSED;
}
- } else if (!result.markers.reasoning_end.empty()) {
+ } else if (!reasoning.end.empty()) {
// We might not have detected the reasoning open marker until now,
// but this is another chance to do so
auto diff = comparison->diff;
auto diff_rt = trim_whitespace(diff.right);
auto diff_lt = trim_whitespace(diff.left);
- if (diff_rt.empty() && diff_lt == result.markers.reasoning_end) {
+ if (diff_rt.empty() && diff_lt == reasoning.end) {
auto seg = segmentize_markers(trim_whitespace(diff.prefix));
if (!seg.empty() && seg[seg.size() - 1].type == MARKER) { // this is FORCED_CLOSED
- result.markers.reasoning_start = seg[seg.size() - 1].value;
- result.reasoning = reasoning_mode::FORCED_CLOSED;
+ reasoning.start = seg[seg.size() - 1].value;
+ reasoning.mode = reasoning_mode::FORCED_CLOSED;
}
}
}
@@ -394,21 +383,21 @@ void differential_analyzer::compare_thinking_enabled(const common_chat_template
// Check for slash-in-tag pattern: vs
// diff shows: suffix="think>", left="/", right="" (or vice versa)
- if (result.markers.reasoning_start.empty() && result.markers.reasoning_end.empty()) {
+ if (reasoning.start.empty() && reasoning.end.empty()) {
if (diff.right.empty() && trim_whitespace(diff.left) == "/") {
auto seg_A = segmentize_markers(trim_trailing_whitespace(comparison->output_A));
auto seg_B = segmentize_markers(trim_trailing_whitespace(comparison->output_B));
if (!seg_A.empty() && !seg_B.empty() && seg_A[seg_A.size() - 1].type == segment_type::MARKER &&
seg_B[seg_B.size() - 1].type == segment_type::MARKER) {
- result.reasoning = reasoning_mode::FORCED_CLOSED;
- result.markers.reasoning_start = seg_B[seg_B.size() - 1].value;
- result.markers.reasoning_end = seg_A[seg_A.size() - 1].value;
+ reasoning.mode = reasoning_mode::FORCED_CLOSED;
+ reasoning.start = seg_B[seg_B.size() - 1].value;
+ reasoning.end = seg_A[seg_A.size() - 1].value;
}
}
}
}
-void differential_analyzer::compare_reasoning_scope(const common_chat_template & tmpl, diff_analysis_result & result) {
+void differential_analyzer::compare_reasoning_scope(const common_chat_template & tmpl, reasoning_analysis & reasoning) {
json assistant_reasoning_content = json{
{ "role", "assistant" },
{ "content", "Here is my response." },
@@ -433,23 +422,18 @@ void differential_analyzer::compare_reasoning_scope(const common_chat_template &
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_reasoning_tools }); });
if (!comparison) {
- LOG_DBG("R3: Template application failed\n");
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
return;
}
- const auto & diff = comparison->diff;
-
std::string reasoning_content = "Let me think.";
- LOG_DBG("R3 diff - prefix: '%s', suffix: '%s', left: '%s', right: '%s'\n", diff.prefix.c_str(), diff.suffix.c_str(),
- diff.left.c_str(), diff.right.c_str());
-
// Check if reasoning only appears in variant B (with tools)
bool reasoning_in_A = comparison->output_A.find(reasoning_content) != std::string::npos;
bool reasoning_in_B = comparison->output_B.find(reasoning_content) != std::string::npos;
if (!reasoning_in_A && reasoning_in_B) {
- result.reasoning = reasoning_mode::TOOLS_ONLY;
+ reasoning.mode = reasoning_mode::TOOLS_ONLY;
LOG_DBG("R3: Detected TOOLS_ONLY reasoning mode\n");
// Extract reasoning markers from output_B
@@ -464,8 +448,7 @@ void differential_analyzer::compare_reasoning_scope(const common_chat_template &
for (auto & segment : segments_before) {
if (segment.type == segment_type::MARKER) {
- result.markers.reasoning_start = segment.value;
- LOG_DBG("R3: Found reasoning_start: '%s'\n", result.markers.reasoning_start.c_str());
+ reasoning.start = segment.value;
break;
}
}
@@ -477,30 +460,25 @@ void differential_analyzer::compare_reasoning_scope(const common_chat_template &
if (!after_reasoning.empty()) {
// Try to find matching end marker
- if (!result.markers.reasoning_start.empty()) {
+ if (!reasoning.start.empty()) {
auto segments = segmentize_markers(after_reasoning);
for (auto & segment : segments) {
if (segment.type == segment_type::MARKER) {
- result.markers.reasoning_end = segment.value;
+ reasoning.end = segment.value;
break;
}
}
- if (!result.markers.reasoning_end.empty()) {
- LOG_DBG("R3: Found reasoning_end (matched): '%s'\n", result.markers.reasoning_end.c_str());
- }
}
}
}
}
}
-void differential_analyzer::analyze_content(const common_chat_template & tmpl, diff_analysis_result & result) {
+content_analysis differential_analyzer::analyze_content(const common_chat_template & tmpl, const reasoning_analysis & reasoning) {
LOG_DBG(ANSI_ORANGE "Phase 2: Content analysis\n" ANSI_RESET);
- compare_content_values(tmpl, result);
-}
-
-void differential_analyzer::compare_content_values(const common_chat_template & tmpl, diff_analysis_result & result) {
+ content_analysis result;
+
json assistant_content_only = json{
{ "role", "assistant" },
{ "content", "Response text" }
@@ -533,8 +511,7 @@ void differential_analyzer::compare_content_values(const common_chat_template &
});
if (!comparison_with_tools || !comparison_with_reasoning) {
- LOG_DBG("C1: Template application failed\n");
- return;
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
}
const auto & diff_tools = comparison_with_tools->diff;
@@ -548,52 +525,115 @@ void differential_analyzer::compare_content_values(const common_chat_template &
if (trim_whitespace(diff_reasoning.left) == response ||
(segments.size() == 2 && trim_whitespace(segments[0].value) == response)) {
// We only have the content text in the diff (possibly with a stray EOG marker), so no markers
- LOG_DBG("C1: No content markers\n");
- result.content = content_mode::PLAIN;
+ result.mode = content_mode::PLAIN;
found_plain_content = true;
- } else if (result.reasoning != reasoning_mode::NONE && !result.markers.reasoning_end.empty() &&
- diff_reasoning.left.find(result.markers.reasoning_end) != std::string::npos) {
+ } else if (reasoning.mode != reasoning_mode::NONE && !reasoning.end.empty() &&
+ diff_reasoning.left.find(reasoning.end) != std::string::npos) {
std::string post_closed_reasoning = diff_reasoning.left.substr(
- diff_reasoning.left.find(result.markers.reasoning_end) + result.markers.reasoning_end.length());
+ diff_reasoning.left.find(reasoning.end) + reasoning.end.length());
if (trim_whitespace(post_closed_reasoning) == "Response text") {
LOG_DBG("C1: No content markers after stripping reasoning close marker\n");
- result.content = content_mode::PLAIN;
+ result.mode = content_mode::PLAIN;
found_plain_content = true;
}
}
}
if (!found_plain_content) {
std::string rdiff = diff_reasoning.left;
- if (!result.markers.reasoning_end.empty() && rdiff.find(result.markers.reasoning_end) != std::string::npos) {
- rdiff = rdiff.substr(rdiff.find(result.markers.reasoning_end) + result.markers.reasoning_end.length());
+ if (!reasoning.end.empty() && rdiff.find(reasoning.end) != std::string::npos) {
+ rdiff = rdiff.substr(rdiff.find(reasoning.end) + reasoning.end.length());
}
// Take the more promising diff
std::string pure_content = rdiff.length() > diff_tools.left.length() ? rdiff : diff_tools.left;
size_t pos = pure_content.find("Response text");
if (pos == std::string::npos) {
- LOG_DBG("C1: Error: response text not found - improper template application?");
- return;
+ LOG_DBG(ANSI_ORANGE "%s: Error: response text not found - improper template application?\n" ANSI_RESET, __func__);
+ return result;
}
- result.markers.content_start = trim_leading_whitespace(pure_content.substr(0, pos));
- result.markers.content_end =
- trim_leading_whitespace(pure_content.substr(pos + 13)); // 13 - len of "Response text"
+ result.start = trim_leading_whitespace(pure_content.substr(0, pos));
+ result.end = trim_leading_whitespace(pure_content.substr(pos + 13)); // 13 - len of "Response text"
// TODO: WRAPPED_WITH_REASONING
}
// Determine content mode
- if (!result.markers.content_start.empty() || !result.markers.content_end.empty()) {
- result.content = content_mode::ALWAYS_WRAPPED;
- LOG_DBG("C1: Content is ALWAYS_WRAPPED\n");
+ if (!result.start.empty() || !result.end.empty()) {
+ result.mode = content_mode::ALWAYS_WRAPPED;
// TODO: END_DELIMITED content mode - delimited at end but not at start?
}
+
+ return result;
}
-void differential_analyzer::analyze_tool_call_format(const std::string & haystack,
- const std::string & fun_name_needle,
- const std::string & arg_name_needle,
- diff_analysis_result & result) {
+tool_analysis differential_analyzer::analyze_tools(const common_chat_template & tmpl,
+ const jinja::caps & caps,
+ const reasoning_analysis & reasoning) {
+ tool_analysis result;
+ LOG_DBG(ANSI_ORANGE "Phase 3: Tool call analysis\n" ANSI_RESET);
+
+ result.format = analyze_tool_calls(tmpl, reasoning);
+
+ if (result.format.mode != tool_format::NONE && result.format.mode != tool_format::JSON_NATIVE) {
+ if (caps.supports_parallel_tool_calls) {
+ check_per_call_markers(tmpl, result.format);
+ }
+ result.function = extract_function_markers(tmpl, result.format);
+ if (result.format.mode == tool_format::TAG_WITH_TAGGED) {
+ result.arguments = analyze_arguments(tmpl, result);
+ }
+ extract_argument_separator(tmpl, result.arguments);
+ extract_args_markers(tmpl, result, result.arguments);
+ result.call_id = extract_call_id_markers(tmpl, result.format);
+ }
+
+ return result;
+}
+
+tool_format_analysis differential_analyzer::analyze_tool_calls(const common_chat_template & tmpl,
+ const reasoning_analysis & reasoning) {
+ json assistant_no_tools = json{
+ { "role", "assistant" },
+ { "content", "Response." }
+ };
+
+ json assistant_with_tools = json{
+ { "role", "assistant" },
+ { "content", "" },
+ { "tool_calls", json::array({ first_tool_call }) }
+ };
+
+ template_params params;
+ params.messages = json::array({ user_msg, assistant_no_tools });
+ params.tools = tools;
+ params.add_generation_prompt = false;
+ params.enable_thinking = true;
+
+ auto comparison = compare_variants(
+ tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_tools }); });
+
+ if (!comparison) {
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
+ return tool_format_analysis();
+ }
+
+ const auto & diff = comparison->diff;
+
+ std::string tool_section = diff.right;
+
+ if (tool_section.empty()) {
+ return tool_format_analysis();
+ }
+
+ return analyze_tool_call_format(tool_section, "foofoo", "first", reasoning);
+}
+
+tool_format_analysis differential_analyzer::analyze_tool_call_format(const std::string & haystack,
+ const std::string & fun_name_needle,
+ const std::string & arg_name_needle,
+ const reasoning_analysis & reasoning) {
+ tool_format_analysis result;
+
if (fun_name_needle.empty() || arg_name_needle.empty() || haystack.empty()) {
- return;
+ return result;
}
auto in_json_haystack = [&haystack](const std::string & needle) -> bool {
@@ -618,43 +658,44 @@ void differential_analyzer::analyze_tool_call_format(const std::string & hays
if (in_json_haystack(fun_name_needle)) {
// no need to check further, we're in JSON land
- result.tools = tool_format::JSON_NATIVE;
+ result.mode = tool_format::JSON_NATIVE;
} else if (in_json_haystack(arg_name_needle)) {
- result.tools = tool_format::TAG_WITH_JSON;
+ result.mode = tool_format::TAG_WITH_JSON;
} else {
- result.tools = tool_format::TAG_WITH_TAGGED;
+ result.mode = tool_format::TAG_WITH_TAGGED;
}
// first, remove any reasoning markers
std::string clean_haystack = haystack;
- if (!result.markers.reasoning_start.empty()) {
- auto pos = haystack.find(result.markers.reasoning_start);
+ if (!reasoning.start.empty()) {
+ auto pos = haystack.find(reasoning.start);
if (pos != std::string::npos) {
- clean_haystack = haystack.substr(0, pos) + haystack.substr(pos + result.markers.reasoning_start.length());
+ clean_haystack = haystack.substr(0, pos) + haystack.substr(pos + reasoning.start.length());
}
}
- if (!result.markers.reasoning_end.empty()) {
- auto pos = clean_haystack.find(result.markers.reasoning_end);
+ if (!reasoning.end.empty()) {
+ auto pos = clean_haystack.find(reasoning.end);
if (pos != std::string::npos) {
- clean_haystack =
- clean_haystack.substr(0, pos) + clean_haystack.substr(pos + result.markers.reasoning_end.length());
+ clean_haystack = clean_haystack.substr(0, pos) + clean_haystack.substr(pos + reasoning.end.length());
}
}
- if (result.tools == tool_format::JSON_NATIVE) {
+ if (result.mode == tool_format::JSON_NATIVE) {
analyze_tool_call_format_json_native(clean_haystack, fun_name_needle, arg_name_needle, result);
} else {
analyze_tool_call_format_non_json(clean_haystack, fun_name_needle, result);
}
// always relax whitespace requirements on ending markers since they don't influence content
- result.markers.tool_section_end = trim_whitespace(result.markers.tool_section_end);
- result.markers.per_call_end = trim_whitespace(result.markers.per_call_end);
+ result.section_end = trim_whitespace(result.section_end);
+ result.per_call_end = trim_whitespace(result.per_call_end);
+
+ return result;
}
void differential_analyzer::analyze_tool_call_format_json_native(const std::string & clean_haystack,
const std::string & fun_name_needle,
const std::string & arg_name_needle,
- diff_analysis_result & result) {
+ tool_format_analysis & format) {
// we might not have the typical OpenAI tool calling structure
int json_start = clean_haystack.find_first_of('{');
int json_end = clean_haystack.find_last_of('}');
@@ -662,28 +703,28 @@ void differential_analyzer::analyze_tool_call_format_json_native(const std::stri
json call_struct = json::parse(cut);
auto register_field = [&](const std::string & prefix, const nlohmann::detail::iteration_proxy_value & subel) {
if (subel.value().is_string() && std::string(subel.value()).find("call0000") != std::string::npos) {
- result.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
+ format.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
} else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) {
- result.name_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
+ format.name_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
} else if (subel.value().dump().find(arg_name_needle) !=
std::string::npos) { // handle both string and JSON obj variants
- result.args_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
+ format.args_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
} else if (subel.key().find("id") != std::string::npos) {
// heuristics for generated id field
- result.gen_id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
+ format.gen_id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();
}
};
for (const auto & el : call_struct.items()) {
if (el.key() == fun_name_needle) {
- result.fun_name_is_key = true;
+ format.fun_name_is_key = true;
// When function name is the key, there's no name field and args are direct
- result.name_field.clear();
- result.args_field.clear();
+ format.name_field.clear();
+ format.args_field.clear();
// Don't register this element - the function name IS the key, not a field
} else {
if (el.value().is_object() &&
el.value().dump().find(arg_name_needle) == std::string::npos) { // not the args object
- result.function_field = el.key();
+ format.function_field = el.key();
for (const auto & subel : el.value().items()) {
register_field(el.key(), subel);
}
@@ -700,11 +741,11 @@ void differential_analyzer::analyze_tool_call_format_json_native(const std::stri
if (json_start > 0 && space_or_bracket(true, clean_haystack[json_start - 1])) {
for (--json_start; space_or_bracket(true, clean_haystack[json_start]) && json_start >= 0; json_start--) {
if (clean_haystack[json_start] == '[') {
- result.tools_array_wrapped = true;
+ format.tools_array_wrapped = true;
break;
}
}
- if (!result.tools_array_wrapped) {
+ if (!format.tools_array_wrapped) {
json_start++; // we ate into the last pre-json character
}
}
@@ -716,35 +757,35 @@ void differential_analyzer::analyze_tool_call_format_json_native(const std::stri
}
std::vector> located_params;
- if (!result.name_field.empty()) {
- located_params.push_back({ clean_haystack.find(result.name_field), result.name_field });
+ if (!format.name_field.empty()) {
+ located_params.push_back({ clean_haystack.find(format.name_field), format.name_field });
}
- if (!result.args_field.empty()) {
- located_params.push_back({ clean_haystack.find(result.args_field), result.args_field });
+ if (!format.args_field.empty()) {
+ located_params.push_back({ clean_haystack.find(format.args_field), format.args_field });
}
- if (!result.id_field.empty()) {
- located_params.push_back({ clean_haystack.find(result.id_field), result.id_field });
+ if (!format.id_field.empty()) {
+ located_params.push_back({ clean_haystack.find(format.id_field), format.id_field });
}
- if (!result.gen_id_field.empty()) {
- located_params.push_back({ clean_haystack.find(result.gen_id_field), result.gen_id_field });
+ if (!format.gen_id_field.empty()) {
+ located_params.push_back({ clean_haystack.find(format.gen_id_field), format.gen_id_field });
}
std::sort(located_params.begin(), located_params.end());
for (auto & pair : located_params) {
- result.parameter_order.push_back(pair.second);
+ format.parameter_order.push_back(pair.second);
}
// we can immediately extract tool calling markers too
- result.markers.tool_section_start = trim_leading_whitespace(clean_haystack.substr(0, json_start));
- result.markers.tool_section_end = trim_whitespace(clean_haystack.substr(json_end));
+ format.section_start = trim_leading_whitespace(clean_haystack.substr(0, json_start));
+ format.section_end = trim_whitespace(clean_haystack.substr(json_end));
// When tools_array_wrapped is true, the closing bracket is part of the array structure,
// not a separate section end marker. Clear tool_section_end to avoid duplicate brackets.
- if (result.tools_array_wrapped && result.markers.tool_section_end == "]") {
- result.markers.tool_section_end.clear();
+ if (format.tools_array_wrapped && format.section_end == "]") {
+ format.section_end.clear();
}
}
void differential_analyzer::analyze_tool_call_format_non_json(const std::string & clean_haystack,
const std::string & fun_name_needle,
- diff_analysis_result & result) {
+ tool_format_analysis & format) {
// we need to split by markers...
auto haystack_split = segmentize_markers(trim_leading_whitespace(clean_haystack));
int where_is_nemo = 0;
@@ -776,17 +817,15 @@ void differential_analyzer::analyze_tool_call_format_non_json(const std::string
if (haystack_split[seg].type == MARKER) {
if (!had_marker) {
had_marker = true;
- result.markers.per_call_start = haystack_split[seg].value + result.markers.per_call_start;
+ format.per_call_start = haystack_split[seg].value + format.per_call_start;
} else {
- result.markers.tool_section_start =
- haystack_split[seg].value + result.markers.tool_section_start;
+ format.section_start = haystack_split[seg].value + format.section_start;
}
} else {
if (had_marker) {
- result.markers.tool_section_start =
- haystack_split[seg].value + result.markers.tool_section_start;
+ format.section_start = haystack_split[seg].value + format.section_start;
} else {
- result.markers.per_call_start = haystack_split[seg].value + result.markers.per_call_start;
+ format.per_call_start = haystack_split[seg].value + format.per_call_start;
}
}
}
@@ -797,15 +836,15 @@ void differential_analyzer::analyze_tool_call_format_non_json(const std::string
backtracked_so_far++;
if (!had_marker) {
had_marker = true;
- result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end;
+ format.section_end = haystack_split[seg].value + format.section_end;
} else {
- result.markers.per_call_end = haystack_split[seg].value + result.markers.per_call_end;
+ format.per_call_end = haystack_split[seg].value + format.per_call_end;
}
} else {
if (had_marker) {
- result.markers.per_call_end = haystack_split[seg].value + result.markers.per_call_end;
+ format.per_call_end = haystack_split[seg].value + format.per_call_end;
} else {
- result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end;
+ format.section_end = haystack_split[seg].value + format.section_end;
}
}
if (backtracked_so_far >= how_many_markers) {
@@ -814,19 +853,19 @@ void differential_analyzer::analyze_tool_call_format_non_json(const std::string
}
} else {
for (int seg = 0; seg < where_is_nemo; seg++) {
- result.markers.tool_section_start += haystack_split[seg].value;
+ format.section_start += haystack_split[seg].value;
}
for (size_t seg = haystack_split.size() - 1; seg > (size_t) where_is_nemo; seg--) {
- result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end;
+ format.section_end = haystack_split[seg].value + format.section_end;
if (haystack_split[seg].type == segment_type::MARKER) {
break;
}
}
}
} else {
- result.markers.tool_section_start += haystack_split[0].value;
+ format.section_start += haystack_split[0].value;
for (size_t seg = haystack_split.size() - 1; seg > (size_t) where_is_nemo; seg--) {
- result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end;
+ format.section_end = haystack_split[seg].value + format.section_end;
if (haystack_split[seg].type == segment_type::MARKER) {
break;
}
@@ -834,28 +873,7 @@ void differential_analyzer::analyze_tool_call_format_non_json(const std::string
}
}
-void differential_analyzer::analyze_tools(const common_chat_template & tmpl, diff_analysis_result & result) {
- LOG_DBG(ANSI_ORANGE "Phase 3: Tool call analysis\n" ANSI_RESET);
- analyze_tool_calls(tmpl, result);
-
- if (result.tools == tool_format::NONE) {
- LOG_DBG("T1: No tool support found\n");
- // Continue anyway - we may still have useful markers
- } else if (result.tools != tool_format::JSON_NATIVE) {
- if (result.supports_parallel_calls) {
- check_per_call_markers(tmpl, result);
- }
- extract_function_markers(tmpl, result);
- extract_argument_separator(tmpl, result);
- extract_args_markers(tmpl, result);
- extract_call_id_markers(tmpl, result);
- if (result.tools == tool_format::TAG_WITH_TAGGED) {
- analyze_arguments(tmpl, result);
- }
- }
-}
-
-void differential_analyzer::check_per_call_markers(const common_chat_template & tmpl, diff_analysis_result & result) {
+void differential_analyzer::check_per_call_markers(const common_chat_template & tmpl, tool_format_analysis & result) {
json assistant_one_tool = json{
{ "role", "assistant" },
{ "content", "" },
@@ -878,112 +896,23 @@ void differential_analyzer::check_per_call_markers(const common_chat_template &
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_two_tools }); });
if (!one_vs_two) {
- LOG_DBG("T2: Generating double tool call comparison failed\n");
+ LOG_DBG(ANSI_ORANGE "%s: Generating double tool call comparison failed\n" ANSI_RESET, __func__);
return;
}
std::string second_tool_content = trim_leading_whitespace(one_vs_two->diff.right);
- if (!result.markers.tool_section_start.empty() &&
- second_tool_content.find(result.markers.tool_section_start) == 0) {
- result.markers.per_call_start = result.markers.tool_section_start;
- result.markers.per_call_end = result.markers.tool_section_end;
- result.markers.tool_section_start.clear();
- result.markers.tool_section_end.clear();
+ if (!result.section_start.empty() &&
+ second_tool_content.find(result.section_start) == 0) {
+ result.per_call_start = result.section_start;
+ result.per_call_end = result.section_end;
+ result.section_start.clear();
+ result.section_end.clear();
}
}
-void differential_analyzer::analyze_tool_calls(const common_chat_template & tmpl, diff_analysis_result & result) {
- json assistant_no_tools = json{
- { "role", "assistant" },
- { "content", "Response." }
- };
+tool_function_analysis differential_analyzer::extract_function_markers(const common_chat_template & tmpl, const tool_format_analysis & analysis) {
+ tool_function_analysis result;
- json assistant_with_tools = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call }) }
- };
-
- template_params params;
- params.messages = json::array({ user_msg, assistant_no_tools });
- params.tools = tools;
- params.add_generation_prompt = false;
- params.enable_thinking = true;
-
- auto comparison = compare_variants(
- tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_tools }); });
-
- if (!comparison) {
- LOG_DBG("T1: Template application failed\n");
- return;
- }
-
- const auto & diff = comparison->diff;
- LOG_DBG("T1 diff - prefix: '%s', suffix: '%s'\n", diff.prefix.c_str(), diff.suffix.c_str());
- LOG_DBG("T1 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
-
- std::string tool_section = diff.right;
-
- if (tool_section.empty()) {
- return;
- }
-
- analyze_tool_call_format(tool_section, "foofoo", "first", result);
-
- LOG_DBG("T1: tool_section_start='%s', tool_section_end='%s'\n", result.markers.tool_section_start.c_str(),
- result.markers.tool_section_end.c_str());
-}
-
-void differential_analyzer::extract_call_separator(const common_chat_template & tmpl,
- diff_analysis_result & result,
- std::string & second_call_content) {
- json assistant_one_call = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call }) }
- };
-
- json assistant_two_calls = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call, second_tool_call }) }
- };
-
- template_params params;
- params.messages = json::array({ user_msg, assistant_one_call });
- params.tools = tools;
- params.add_generation_prompt = false;
- params.enable_thinking = true;
-
- auto comparison = compare_variants(
- tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_two_calls }); });
-
- if (!comparison) {
- LOG_DBG("T2: Template application failed\n");
- return;
- }
-
- const auto & diff = comparison->diff;
- LOG_DBG("T2 diff - prefix: '%s', suffix: '%s'\n", diff.prefix.c_str(), diff.suffix.c_str());
- LOG_DBG("T2 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
-
- if (!diff.right.empty()) {
- std::string first_func_name = "foofoo";
- std::string second_func_name = "barbar";
-
- std::string separator = until_common_prefix(diff.right, first_func_name, second_func_name);
- result.markers.call_separator = trim_whitespace(separator);
-
- LOG_DBG("T2: call_separator='%s'\n", result.markers.call_separator.c_str());
-
- result.supports_parallel_calls = true;
- second_call_content = diff.right;
-
- LOG_DBG("T2: second_call_content='%s', supports_parallel_calls=true\n", second_call_content.c_str());
- }
-}
-
-void differential_analyzer::extract_function_markers(const common_chat_template & tmpl, diff_analysis_result & result) {
json assistant_nocall = json{
{ "role", "assistant" },
{ "content", "BBBB" },
@@ -1011,8 +940,8 @@ void differential_analyzer::extract_function_markers(const common_chat_template
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_barbar }); });
if (!comparison) {
- LOG_DBG("T3: Template application failed\n");
- return;
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
+ return result;
}
const auto & diff = comparison->diff;
@@ -1021,26 +950,26 @@ void differential_analyzer::extract_function_markers(const common_chat_template
if (diff.left.find("foofoo") != std::string::npos && diff.right.find("barbar") != std::string::npos) {
std::string prefix_marker;
- if (!result.markers.per_call_start.empty()) {
- prefix_marker = result.markers.per_call_start;
+ if (!analysis.per_call_start.empty()) {
+ prefix_marker = analysis.per_call_start;
} else {
- prefix_marker = result.markers.tool_section_start;
+ prefix_marker = analysis.section_start;
}
if (!prefix_marker.empty() && diff.prefix.rfind(prefix_marker) != std::string::npos) {
- result.markers.func_name_prefix =
+ result.name_prefix =
diff.prefix.substr(diff.prefix.rfind(prefix_marker) + prefix_marker.size());
}
auto seg = segmentize_markers(diff.left);
for (const auto & s : seg) {
if (s.value.find("foofoo") == std::string::npos) {
- result.markers.func_name_prefix += s.value;
+ result.name_prefix += s.value;
} else {
size_t pos = s.value.find("foofoo");
std::string pre = s.value.substr(0, pos);
std::string post = s.value.substr(pos + 6); // 6 = len("foofoo")
- result.markers.func_name_prefix += pre;
- result.markers.func_name_suffix += post;
+ result.name_prefix += pre;
+ result.name_suffix += post;
break;
}
}
@@ -1050,7 +979,7 @@ void differential_analyzer::extract_function_markers(const common_chat_template
size_t stop_internal_pos = 0;
for (const auto & ss : seg_suf) {
bool has_needle = false;
- if (result.tools == tool_format::TAG_WITH_JSON) {
+ if (analysis.mode == tool_format::TAG_WITH_JSON) {
has_needle = (ss.type == segment_type::TEXT && ss.value.find_first_of("{[") != std::string::npos);
if (has_needle) {
stop_internal_pos = ss.value.find_first_of("{[");
@@ -1066,7 +995,7 @@ void differential_analyzer::extract_function_markers(const common_chat_template
stop++;
}
if (stop < seg_suf.size() - 1) {
- if (result.tools == tool_format::TAG_WITH_TAGGED) {
+ if (analysis.mode == tool_format::TAG_WITH_TAGGED) {
size_t how_far = 0;
if (stop > 0) {
if (seg_suf[stop].type == segment_type::MARKER) {
@@ -1075,24 +1004,24 @@ void differential_analyzer::extract_function_markers(const common_chat_template
how_far = stop - 1;
}
for (size_t i = 0; i < how_far; i++) {
- result.markers.func_name_suffix += seg_suf[i].value;
+ result.name_suffix += seg_suf[i].value;
}
}
} else {
for (size_t i = 0; i < stop; i++) {
- result.markers.func_name_suffix += seg_suf[i].value;
+ result.name_suffix += seg_suf[i].value;
}
const std::string & stopper = seg_suf[stop].value;
- result.markers.func_name_suffix += stopper.substr(0, stop_internal_pos);
+ result.name_suffix += stopper.substr(0, stop_internal_pos);
}
}
// now just to find the closer
std::string suffix_marker;
- if (!result.markers.per_call_end.empty()) {
- suffix_marker = result.markers.per_call_end;
+ if (!analysis.per_call_end.empty()) {
+ suffix_marker = analysis.per_call_end;
} else {
- suffix_marker = result.markers.tool_section_end;
+ suffix_marker = analysis.section_end;
}
std::string closer_suffix;
if (suffix_marker.empty()) {
@@ -1106,18 +1035,18 @@ void differential_analyzer::extract_function_markers(const common_chat_template
}
if (!closer_suffix.empty()) {
auto closer_seg = segmentize_markers(closer_suffix);
- bool need_to_eat_arg_marker = (result.tools == tool_format::TAG_WITH_TAGGED);
+ bool need_to_eat_arg_marker = (analysis.mode == tool_format::TAG_WITH_TAGGED);
size_t last_arg_seg = closer_seg.size() - 1;
for (int i = (int) closer_seg.size() - 1; i >= 0; i--) {
if (closer_seg[i].value.find("YYYY") != std::string::npos) {
last_arg_seg = i;
}
}
- if (result.tools == tool_format::TAG_WITH_JSON) {
+ if (analysis.mode == tool_format::TAG_WITH_JSON) {
const auto & entire_seg = closer_seg[last_arg_seg].value;
size_t pos = entire_seg.find_last_of("}]");
if (pos != std::string::npos && pos < entire_seg.size() - 1) {
- result.markers.func_close = trim_leading_whitespace(entire_seg.substr(pos + 1));
+ result.close = trim_leading_whitespace(entire_seg.substr(pos + 1));
}
}
for (size_t i = last_arg_seg + 1; i < closer_seg.size(); i++) {
@@ -1125,23 +1054,204 @@ void differential_analyzer::extract_function_markers(const common_chat_template
if (need_to_eat_arg_marker) {
need_to_eat_arg_marker = false;
} else {
- result.markers.func_close += closer_seg[i].value;
+ result.close += closer_seg[i].value;
}
} else if (!need_to_eat_arg_marker) {
- result.markers.func_close += closer_seg[i].value;
+ result.close += closer_seg[i].value;
}
}
}
- result.markers.func_close = trim_leading_whitespace(result.markers.func_close);
+ result.close = trim_leading_whitespace(result.close);
+ }
+ return result;
+}
- LOG_DBG("T3: func_name_prefix='%s', func_name_suffix='%s', func_close='%s'\n",
- result.markers.func_name_prefix.c_str(), result.markers.func_name_suffix.c_str(),
- result.markers.func_close.c_str());
+tool_arguments_analysis differential_analyzer::analyze_arguments(const common_chat_template & tmpl, const tool_analysis & tool_analysis) {
+ LOG_DBG(ANSI_ORANGE "Phase 4: Argument analysis\n" ANSI_RESET);
+
+ tool_arguments_analysis result;
+
+ extract_argument_name_markers(tmpl, result);
+ extract_argument_value_markers(tmpl, tool_analysis, result);
+
+ return result;
+}
+
+void differential_analyzer::extract_argument_name_markers(const common_chat_template & tmpl,
+ tool_arguments_analysis & args_analysis) {
+ json assistant_first_arg = json{
+ { "role", "assistant" },
+ { "content", "" },
+ { "tool_calls", json::array({ first_tool_call_one_arg }) }
+ };
+
+ json assistant_second_arg = json{
+ { "role", "assistant" },
+ { "content", "" },
+ { "tool_calls", json::array({ first_tool_call_other_arg }) }
+ };
+
+ template_params params;
+ params.messages = json::array({ user_msg, assistant_first_arg });
+ params.tools = tools;
+ params.add_generation_prompt = false;
+ params.enable_thinking = true;
+
+ auto comparison = compare_variants(
+ tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_second_arg }); });
+
+ if (!comparison) {
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
+ return;
+ }
+
+ const auto & diff = comparison->diff;
+
+ if (!diff.left.empty() && !diff.right.empty()) {
+ size_t common_len = 0;
+ size_t min_len = std::min(diff.left.length(), diff.right.length());
+ while (common_len < min_len && diff.left[common_len] == diff.right[common_len]) {
+ common_len++;
+ }
+
+ if (common_len > 0) { // we have a marker structure with the name *inside* the marker
+ std::string common_prefix = diff.left.substr(0, common_len);
+ std::string left_remainder = diff.left.substr(common_len);
+ std::string right_remainder = diff.right.substr(common_len);
+ size_t left_close =
+ left_remainder.find_first_of("\"X"); // because arg-val is XXXX, can be quoted or unquoted
+ size_t right_close = right_remainder.find_first_of("\"Y"); // here arg-val is YYYY
+
+ if (left_close != std::string::npos && right_close != std::string::npos) {
+ std::string left_name = left_remainder.substr(0, 5); // 5 = len("first")
+ std::string right_name = right_remainder.substr(0, 6); // 6 = len("second")
+
+ if (left_name == "first" && right_name == "second") {
+ args_analysis.name_prefix = trim_whitespace(common_prefix);
+ std::string suffix_left = left_remainder.substr(5, left_close - 5);
+ std::string suffix_right = right_remainder.substr(6, right_close - 6);
+ if (suffix_left == suffix_right) {
+ args_analysis.name_suffix = trim_leading_whitespace(suffix_left);
+ }
+ }
+ }
+ } else if (diff.left.substr(0, 5) == "first" && diff.right.substr(0, 6) == "second") {
+ // we most likely have actual markers for argument names
+ auto pre_seg = segmentize_markers(diff.prefix);
+ for (int i = pre_seg.size() - 1; i >= 0; i--) {
+ args_analysis.name_prefix = args_analysis.name_prefix + pre_seg[i].value;
+ if (pre_seg[i].type == segment_type::MARKER) {
+ break;
+ }
+ }
+ auto left_seg = segmentize_markers(diff.left);
+ if (left_seg.size() == 1) { // only the name + maybe extra whitespace / normal chars in differing part
+ args_analysis.name_suffix = diff.left.substr(5);
+ auto suf_seg= segmentize_markers(diff.suffix);
+ for (size_t i = 0; i < suf_seg.size(); i++) {
+ args_analysis.name_suffix += suf_seg[i].value;
+ if (suf_seg[i].type == segment_type::MARKER) {
+ if (i < suf_seg.size() - 2 && suf_seg[i + 1].type == segment_type::TEXT &&
+ trim_whitespace(suf_seg[i + 1].value).empty()) {
+ // we need to include post-marker whitespace/newlines as well
+ args_analysis.name_suffix += suf_seg[i + 1].value;
+ }
+ break;
+ }
+ }
+ } else {
+ for (size_t i = 0; i < left_seg.size(); i++) {
+ std::string to_add;
+ if (i == 0) {
+ to_add = left_seg[i].value.substr(5);
+ } else {
+ to_add = left_seg[i].value;
+ }
+ args_analysis.name_suffix += to_add;
+ if (left_seg[i].type == segment_type::MARKER) {
+ if (i < left_seg.size() - 2 && left_seg[i + 1].type == segment_type::TEXT &&
+ trim_whitespace(left_seg[i + 1].value).empty()) {
+ // we need to include post-marker whitespace/newlines as well
+ args_analysis.name_suffix += left_seg[i + 1].value;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+}
+
+void differential_analyzer::extract_argument_value_markers(const common_chat_template & tmpl,
+ const tool_analysis & analysis,
+ tool_arguments_analysis & args_analysis) {
+ json assistant_val_X = json{
+ { "role", "assistant" },
+ { "content", "" },
+ { "tool_calls", json::array({ first_tool_call_one_arg }) }
+ };
+
+ json assistant_val_Y = json{
+ { "role", "assistant" },
+ { "content", "" },
+ { "tool_calls", json::array({ first_tool_call_one_arg_other_val }) }
+ };
+
+ template_params params;
+ params.messages = json::array({ user_msg, assistant_val_X });
+ params.tools = tools;
+ params.add_generation_prompt = false;
+ params.enable_thinking = true;
+
+ auto comparison = compare_variants(
+ tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_val_Y }); });
+
+ if (!comparison) {
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
+ return;
+ }
+
+ const auto & diff = comparison->diff;
+
+ if (diff.left == "XXXX" && diff.right == "YYYY") {
+ std::string arg_name_ending = "first" + args_analysis.name_suffix;
+ std::string prefix = diff.prefix;
+ if (prefix.rfind(arg_name_ending) != std::string::npos) {
+ prefix = prefix.substr(prefix.rfind(arg_name_ending) + arg_name_ending.size());
+ }
+ if (!prefix.empty()) {
+ auto seg_pre = segmentize_markers(prefix);
+ for (int i = seg_pre.size() - 1; i >= 0; i--) {
+ args_analysis.value_prefix = seg_pre[i].value + args_analysis.value_prefix;
+ if (seg_pre[i].type == segment_type::MARKER) {
+ break;
+ }
+ }
+ }
+
+ std::string value_suffix = diff.suffix;
+ if (!analysis.function.close.empty()) {
+ size_t func_close_pos = value_suffix.find(analysis.function.close);
+ if (func_close_pos != std::string::npos) {
+ value_suffix = value_suffix.substr(0, func_close_pos);
+ }
+ } else if (!analysis.format.per_call_end.empty() || !analysis.format.section_end.empty()) {
+ std::string end_marker =
+ !analysis.format.per_call_end.empty() ? analysis.format.per_call_end : analysis.format.section_end;
+ size_t end_marker_pos = value_suffix.find(end_marker);
+ if (end_marker_pos != std::string::npos) {
+ value_suffix = value_suffix.substr(0, end_marker_pos);
+ }
+ }
+ value_suffix = trim_leading_whitespace(value_suffix);
+ if (!value_suffix.empty()) {
+ args_analysis.value_suffix = value_suffix;
+ }
}
}
void differential_analyzer::extract_argument_separator(const common_chat_template & tmpl,
- diff_analysis_result & result) {
+ tool_arguments_analysis & args_analysis) {
json assistant_one_arg = json{
{ "role", "assistant" },
{ "content", "" },
@@ -1164,31 +1274,30 @@ void differential_analyzer::extract_argument_separator(const common_chat_templat
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_two_args }); });
if (!comparison) {
- LOG_DBG("T4: Template application failed\n");
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
return;
}
const auto & diff = comparison->diff;
- LOG_DBG("T4 diff - suffix: '%s'\n", diff.suffix.c_str());
- LOG_DBG("T4 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
if (!diff.right.empty()) {
std::string separator = until_common_prefix(diff.right, "first", "second");
- result.markers.arg_separator = separator;
- LOG_DBG("T4: arg_separator='%s'\n", result.markers.arg_separator.c_str());
+ args_analysis.separator = separator;
}
}
-void differential_analyzer::extract_args_markers(const common_chat_template & tmpl, diff_analysis_result & result) {
+void differential_analyzer::extract_args_markers(const common_chat_template & tmpl,
+ const tool_analysis & analysis,
+ tool_arguments_analysis & args_analysis) {
json assistant_no_args = json{
- { "role", "assistant" },
- { "content", "" },
+ { "role", "assistant"},
+ { "content", "" },
{ "tool_calls", json::array({ first_tool_call_zero_args }) }
};
json assistant_with_args = json{
- { "role", "assistant" },
- { "content", "" },
+ { "role", "assistant"},
+ { "content", "" },
{ "tool_calls", json::array({ first_tool_call_one_arg }) }
};
@@ -1202,19 +1311,15 @@ void differential_analyzer::extract_args_markers(const common_chat_template & tm
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_args }); });
if (!comparison) {
- LOG_DBG("T5: Template application failed\n");
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);
return;
}
const auto & diff = comparison->diff;
- LOG_DBG("T5 diff - suffix: '%s'\n", diff.suffix.c_str());
- LOG_DBG("T5 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
- if (result.markers.args_start.empty() && result.tools != tool_format::JSON_NATIVE) {
- std::string prefix_marker = !result.markers.tool_section_start.empty() ? result.markers.tool_section_start :
- result.markers.per_call_start;
- std::string suffix_marker =
- !result.markers.tool_section_end.empty() ? result.markers.tool_section_end : result.markers.per_call_end;
+ if (analysis.format.mode != tool_format::JSON_NATIVE) {
+ std::string prefix_marker = !analysis.format.section_start.empty() ? analysis.format.section_start : analysis.format.per_call_start;
+ std::string suffix_marker = !analysis.format.section_end.empty() ? analysis.format.section_end : analysis.format.per_call_end;
// these might happen earlier in the tools section as an example or somewhere else, so we need to find the closest ones
size_t prefix_pos = prefix_marker.empty() ? 0 : diff.prefix.rfind(prefix_marker);
size_t suffix_pos = suffix_marker.empty() ? diff.suffix.size() : diff.suffix.find(suffix_marker);
@@ -1230,15 +1335,15 @@ void differential_analyzer::extract_args_markers(const common_chat_template & tm
std::string args_end = after_common_suffix(suffix_cut, "{}", "\"XXXX\"}");
if (!args_start.empty() || !args_end.empty()) {
- result.markers.args_start = args_start;
- result.markers.args_end = args_end;
- LOG_DBG("T5: Custom argument container detected: start='%s', end='%s'\n", args_start.c_str(),
- args_end.c_str());
+ args_analysis.start = args_start;
+ args_analysis.end = args_end;
}
}
}
-void differential_analyzer::extract_call_id_markers(const common_chat_template & tmpl, diff_analysis_result & result) {
+tool_id_analysis differential_analyzer::extract_call_id_markers(const common_chat_template & tmpl, tool_format_analysis & analysis) {
+ tool_id_analysis result;
+
json assistant_id1 = json{
{ "role", "assistant" },
{ "content", "" },
@@ -1261,8 +1366,8 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_id2 }); });
if (!comparison) {
- LOG_DBG("T6: Template application failed for call_id detection\n");
- return;
+ LOG_DBG(ANSI_ORANGE "%s: Template application failed for call_id detection\n" ANSI_RESET, __func__);
+ return result;
}
const auto & diff = comparison->diff;
@@ -1270,8 +1375,7 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
LOG_DBG("T6 diff (call_id) - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
if (diff.left.empty() && diff.right.empty()) {
- LOG_DBG("T6: No call_id difference detected\n");
- return;
+ return result;
}
std::string id_value_1 = "call00001";
@@ -1302,8 +1406,7 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
if (args_in_suffix != std::string::npos &&
(args_in_prefix == std::string::npos || args_in_prefix > diff.prefix.length())) {
// Args are in suffix, so call_id is BETWEEN_FUNC_AND_ARGS
- result.call_id_pos = call_id_position::BETWEEN_FUNC_AND_ARGS;
- LOG_DBG("T6: Detected BETWEEN_FUNC_AND_ARGS position\n");
+ result.pos = call_id_position::BETWEEN_FUNC_AND_ARGS;
// The prefix ends with: ...
// Segmentize to find the call_id_prefix marker
@@ -1328,14 +1431,12 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
}
if (!marker_before_id.empty()) {
- result.markers.call_id_prefix = marker_before_id;
- LOG_DBG("T6: call_id_prefix='%s'\n", result.markers.call_id_prefix.c_str());
+ result.prefix = marker_before_id;
} else {
// Fallback: look for the last marker in after_func
for (int i = (int) segments.size() - 1; i >= 0; i--) {
if (segments[i].type == segment_type::MARKER) {
- result.markers.call_id_prefix = segments[i].value;
- LOG_DBG("T6: call_id_prefix (fallback)='%s'\n", result.markers.call_id_prefix.c_str());
+ result.prefix = segments[i].value;
break;
}
}
@@ -1345,8 +1446,8 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
auto suffix_segments = segmentize_markers(diff.suffix);
for (size_t i = 0; i < suffix_segments.size(); i++) {
if (suffix_segments[i].type == segment_type::MARKER) {
- result.markers.call_id_suffix = suffix_segments[i].value;
- LOG_DBG("T6: call_id_suffix='%s'\n", result.markers.call_id_suffix.c_str());
+ result.suffix = suffix_segments[i].value;
+ LOG_DBG("T6: call_id_suffix='%s'\n", result.suffix.c_str());
break;
}
// Stop if we hit the args
@@ -1356,8 +1457,7 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
}
} else if (args_in_prefix != std::string::npos) {
// Args are in prefix, so call_id is POST_ARGS
- result.call_id_pos = call_id_position::POST_ARGS;
- LOG_DBG("T6: POST_ARGS call_id position detected\n");
+ result.pos = call_id_position::POST_ARGS;
// Extract markers from between args and the ID
std::string after_args = diff.prefix.substr(args_in_prefix);
@@ -1367,8 +1467,8 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
auto segments = segmentize_markers(between_args_and_id);
for (int i = (int) segments.size() - 1; i >= 0; i--) {
if (segments[i].type == segment_type::MARKER) {
- result.markers.call_id_prefix = segments[i].value;
- LOG_DBG("T6: call_id_prefix='%s'\n", result.markers.call_id_prefix.c_str());
+ result.prefix = segments[i].value;
+ LOG_DBG("T6: call_id_prefix='%s'\n", result.prefix.c_str());
break;
}
}
@@ -1378,23 +1478,20 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
auto suffix_segments = segmentize_markers(diff.suffix);
for (const auto & seg : suffix_segments) {
if (seg.type == segment_type::MARKER) {
- result.markers.call_id_suffix = seg.value;
- LOG_DBG("T6: call_id_suffix='%s'\n", result.markers.call_id_suffix.c_str());
+ result.suffix = seg.value;
break;
}
}
}
} else if (func_name_in_suffix != std::string::npos && func_name_in_prefix == std::string::npos) {
// Function name is only in suffix - call_id is PRE_FUNC_NAME
- result.call_id_pos = call_id_position::PRE_FUNC_NAME;
- LOG_DBG("T6: PRE_FUNC_NAME call_id position detected\n");
+ result.pos = call_id_position::PRE_FUNC_NAME;
// Extract call_id_prefix from prefix (last marker before the common_id_part)
auto prefix_segments = segmentize_markers(diff.prefix);
for (int i = (int) prefix_segments.size() - 1; i >= 0; i--) {
if (prefix_segments[i].type == segment_type::MARKER) {
- result.markers.call_id_prefix = prefix_segments[i].value;
- LOG_DBG("T6: call_id_prefix='%s'\n", result.markers.call_id_prefix.c_str());
+ result.prefix = prefix_segments[i].value;
break;
}
}
@@ -1404,210 +1501,20 @@ void differential_analyzer::extract_call_id_markers(const common_chat_template &
auto suffix_segments = segmentize_markers(before_func);
for (const auto & seg : suffix_segments) {
if (seg.type == segment_type::MARKER) {
- result.markers.call_id_suffix = seg.value;
- LOG_DBG("T6: call_id_suffix='%s'\n", result.markers.call_id_suffix.c_str());
+ result.suffix = seg.value;
break;
}
}
- } else {
- LOG_DBG("T6: Unable to determine call_id position\n");
}
// When call_id is detected, per_call_end may have been incorrectly set to include
// the call_id_suffix and sample args. Clear it if it starts with call_id_suffix.
- if (result.call_id_pos != call_id_position::NONE && !result.markers.call_id_suffix.empty() &&
- result.markers.per_call_end.find(result.markers.call_id_suffix) == 0) {
- result.markers.per_call_end.clear();
- LOG_DBG("T6: Cleared per_call_end (was incorrectly including call_id_suffix)\n");
- }
-}
-
-void differential_analyzer::analyze_arguments(const common_chat_template & tmpl, diff_analysis_result & result) {
- LOG_DBG(ANSI_ORANGE "Phase 4: Argument analysis\n" ANSI_RESET);
-
- extract_argument_name_markers(tmpl, result);
- extract_argument_value_markers(tmpl, result);
-}
-
-void differential_analyzer::extract_argument_name_markers(const common_chat_template & tmpl,
- diff_analysis_result & result) {
- json assistant_first_arg = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call_one_arg }) }
- };
-
- json assistant_second_arg = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call_other_arg }) }
- };
-
- template_params params;
- params.messages = json::array({ user_msg, assistant_first_arg });
- params.tools = tools;
- params.add_generation_prompt = false;
- params.enable_thinking = true;
-
- auto comparison = compare_variants(
- tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_second_arg }); });
-
- if (!comparison) {
- LOG_DBG("A1: Template application failed\n");
- return;
+ if (result.pos != call_id_position::NONE && !result.suffix.empty() &&
+ analysis.per_call_end.find(result.suffix) == 0) {
+ analysis.per_call_end.clear();
}
- const auto & diff = comparison->diff;
- LOG_DBG("A1 diff - suffix: '%s', left: '%s', right: '%s'\n", diff.suffix.c_str(), diff.left.c_str(),
- diff.right.c_str());
-
- if (!diff.left.empty() && !diff.right.empty()) {
- size_t common_len = 0;
- size_t min_len = std::min(diff.left.length(), diff.right.length());
- while (common_len < min_len && diff.left[common_len] == diff.right[common_len]) {
- common_len++;
- }
-
- if (common_len > 0) { // we have a marker structure with the name *inside* the marker
- std::string common_prefix = diff.left.substr(0, common_len);
- std::string left_remainder = diff.left.substr(common_len);
- std::string right_remainder = diff.right.substr(common_len);
- size_t left_close =
- left_remainder.find_first_of("\"X"); // because arg-val is XXXX, can be quoted or unquoted
- size_t right_close = right_remainder.find_first_of("\"Y"); // here arg-val is YYYY
-
- if (left_close != std::string::npos && right_close != std::string::npos) {
- std::string left_name = left_remainder.substr(0, 5); // 5 = len("first")
- std::string right_name = right_remainder.substr(0, 6); // 6 = len("second")
-
- if (left_name == "first" && right_name == "second") {
- result.markers.arg_name_prefix = trim_whitespace(common_prefix);
- std::string suffix_left = left_remainder.substr(5, left_close - 5);
- std::string suffix_right = right_remainder.substr(6, right_close - 6);
- if (suffix_left == suffix_right) {
- result.markers.arg_name_suffix = trim_leading_whitespace(suffix_left);
- }
- LOG_DBG("A1: arg_name_prefix='%s', arg_name_suffix='%s'\n", result.markers.arg_name_prefix.c_str(),
- result.markers.arg_name_suffix.c_str());
- }
- }
- } else if (diff.left.substr(0, 5) == "first" && diff.right.substr(0, 6) == "second") {
- // we most likely have actual markers for argument names
- auto pre_seg = segmentize_markers(diff.prefix);
- for (int i = pre_seg.size() - 1; i >= 0; i--) {
- result.markers.arg_name_prefix = result.markers.arg_name_prefix + pre_seg[i].value;
- if (pre_seg[i].type == segment_type::MARKER) {
- break;
- }
- }
- auto left_seg = segmentize_markers(diff.left);
- if (left_seg.size() == 1) { // only the name + maybe extra whitespace / normal chars in differing part
- result.markers.arg_name_suffix = diff.left.substr(5);
- auto suf_seg= segmentize_markers(diff.suffix);
- for (size_t i = 0; i < suf_seg.size(); i++) {
- result.markers.arg_name_suffix += suf_seg[i].value;
- if (suf_seg[i].type == segment_type::MARKER) {
- if (i < suf_seg.size() - 2 && suf_seg[i + 1].type == segment_type::TEXT &&
- trim_whitespace(suf_seg[i + 1].value).empty()) {
- // we need to include post-marker whitespace/newlines as well
- result.markers.arg_name_suffix += suf_seg[i + 1].value;
- }
- break;
- }
- }
- } else {
- for (size_t i = 0; i < left_seg.size(); i++) {
- std::string to_add;
- if (i == 0) {
- to_add = left_seg[i].value.substr(5);
- } else {
- to_add = left_seg[i].value;
- }
- result.markers.arg_name_suffix += to_add;
- if (left_seg[i].type == segment_type::MARKER) {
- if (i < left_seg.size() - 2 && left_seg[i + 1].type == segment_type::TEXT &&
- trim_whitespace(left_seg[i + 1].value).empty()) {
- // we need to include post-marker whitespace/newlines as well
- result.markers.arg_name_suffix += left_seg[i + 1].value;
- }
- break;
- }
- }
- }
- }
- }
-}
-
-void differential_analyzer::extract_argument_value_markers(const common_chat_template & tmpl,
- diff_analysis_result & result) {
- json assistant_val_X = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call_one_arg }) }
- };
-
- json assistant_val_Y = json{
- { "role", "assistant" },
- { "content", "" },
- { "tool_calls", json::array({ first_tool_call_one_arg_other_val }) }
- };
-
- template_params params;
- params.messages = json::array({ user_msg, assistant_val_X });
- params.tools = tools;
- params.add_generation_prompt = false;
- params.enable_thinking = true;
-
- auto comparison = compare_variants(
- tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_val_Y }); });
-
- if (!comparison) {
- LOG_DBG("A2: Template application failed\n");
- return;
- }
-
- const auto & diff = comparison->diff;
- LOG_DBG("A2 diff - suffix: '%s'\n", diff.suffix.c_str());
- LOG_DBG("A2 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str());
-
- if (diff.left == "XXXX" && diff.right == "YYYY") {
- std::string arg_name_ending = "first" + result.markers.arg_name_suffix;
- std::string prefix = diff.prefix;
- if (prefix.rfind(arg_name_ending) != std::string::npos) {
- prefix = prefix.substr(prefix.rfind(arg_name_ending) + arg_name_ending.size());
- }
- if (!prefix.empty()) {
- auto seg_pre = segmentize_markers(prefix);
- for (int i = seg_pre.size() - 1; i >= 0; i--) {
- result.markers.arg_value_prefix = seg_pre[i].value + result.markers.arg_value_prefix;
- if (seg_pre[i].type == segment_type::MARKER) {
- break;
- }
- }
- }
-
- std::string value_suffix = diff.suffix;
- if (!result.markers.func_close.empty()) {
- size_t func_close_pos = value_suffix.find(result.markers.func_close);
- if (func_close_pos != std::string::npos) {
- value_suffix = value_suffix.substr(0, func_close_pos);
- }
- } else if (!result.markers.per_call_end.empty() || !result.markers.tool_section_end.empty()) {
- std::string end_marker =
- !result.markers.per_call_end.empty() ? result.markers.per_call_end : result.markers.tool_section_end;
- size_t end_marker_pos = value_suffix.find(end_marker);
- if (end_marker_pos != std::string::npos) {
- value_suffix = value_suffix.substr(0, end_marker_pos);
- }
- }
- value_suffix = trim_leading_whitespace(value_suffix);
- if (!value_suffix.empty()) {
- result.markers.arg_value_suffix = value_suffix;
- }
-
- LOG_DBG("A2: arg_value_prefix='%s', arg_value_suffix='%s'\n", result.markers.arg_value_prefix.c_str(),
- result.markers.arg_value_suffix.c_str());
- }
+ return result;
}
void differential_analyzer::collect_preserved_tokens(diff_analysis_result & result) {
@@ -1623,23 +1530,24 @@ void differential_analyzer::collect_preserved_tokens(diff_analysis_result & resu
}
};
- add_token(result.markers.reasoning_start);
- add_token(result.markers.reasoning_end);
- add_token(result.markers.content_start);
- add_token(result.markers.content_end);
- add_token(result.markers.tool_section_start);
- add_token(result.markers.tool_section_end);
- add_token(result.markers.per_call_start);
- add_token(result.markers.per_call_end);
- add_token(result.markers.func_name_prefix);
- add_token(result.markers.func_name_suffix);
- add_token(result.markers.func_close);
- add_token(result.markers.arg_name_prefix);
- add_token(result.markers.arg_name_suffix);
- add_token(result.markers.arg_separator);
- add_token(result.markers.arg_value_prefix);
- add_token(result.markers.arg_value_suffix);
- add_token(result.markers.call_id_prefix);
- add_token(result.markers.call_id_suffix);
- add_token(result.markers.code_block_marker);
+ add_token(result.reasoning.start);
+ add_token(result.reasoning.end);
+ add_token(result.content.start);
+ add_token(result.content.end);
+ add_token(result.tools.format.section_start);
+ add_token(result.tools.format.section_end);
+ add_token(result.tools.format.per_call_start);
+ add_token(result.tools.format.per_call_end);
+ add_token(result.tools.function.name_prefix);
+ add_token(result.tools.function.name_suffix);
+ add_token(result.tools.function.close);
+ add_token(result.tools.arguments.start);
+ add_token(result.tools.arguments.end);
+ add_token(result.tools.arguments.name_prefix);
+ add_token(result.tools.arguments.name_suffix);
+ add_token(result.tools.arguments.separator);
+ add_token(result.tools.arguments.value_prefix);
+ add_token(result.tools.arguments.value_suffix);
+ add_token(result.tools.call_id.prefix);
+ add_token(result.tools.call_id.suffix);
}
diff --git a/common/chat-diff-analyzer.h b/common/chat-diff-analyzer.h
index ce729df0e6..c035203923 100644
--- a/common/chat-diff-analyzer.h
+++ b/common/chat-diff-analyzer.h
@@ -1,6 +1,7 @@
#pragma once
#include "chat.h"
+#include "jinja/caps.h"
#include "nlohmann/json.hpp"
#include
@@ -15,11 +16,11 @@ using json = nlohmann::ordered_json;
// Parameters for template application
// ============================================================================
struct template_params {
- json messages;
- json tools;
- bool add_generation_prompt = false;
- bool enable_thinking = true;
- std::optional extra_context = std::nullopt;
+ json messages;
+ json tools;
+ bool add_generation_prompt = false;
+ bool enable_thinking = true;
+ std::optional extra_context = std::nullopt;
};
struct diff_split {
@@ -35,9 +36,9 @@ struct diff_split {
// Result of compare_variants containing diff and original outputs
struct compare_variants_result {
- diff_split diff;
- std::string output_A;
- std::string output_B;
+ diff_split diff;
+ std::string output_A;
+ std::string output_B;
};
// ============================================================================
@@ -77,29 +78,23 @@ struct marker_registry {
std::string arg_separator; // e.g., "", "\n", ","
// === Call ID markers (for non-JSON formats with tool call IDs) ===
- std::string call_id_prefix; // e.g., "[CALL_ID]" (marker before call ID value)
- std::string call_id_suffix; // e.g., "" (marker after call ID value, before next section)
-
- // === Special markers ===
- std::string code_block_marker; // e.g., "Action:" (for markdown code block format)
- std::string code_block_language; // e.g., "json"
- std::string function_namespace; // e.g., "functions." (for prefixed-indexed format)
+ std::string call_id_prefix; // e.g., "[CALL_ID]" (marker before call ID value)
+ std::string call_id_suffix; // e.g., "" (marker after call ID value, before next section)
};
-
// ============================================================================
// Analysis Result Enums
// ============================================================================
// Reasoning handling mode (derived from R1-R3 comparisons)
enum class reasoning_mode {
- NONE, // No reasoning markers detected
- TAG_BASED, // Standard tag-based: ...
- DELIMITER, // Delimiter-based: [BEGIN FINAL RESPONSE] (reasoning ends at delimiter)
- FORCED_OPEN, // Template ends with open reasoning tag (empty start, non-empty end)
- FORCED_CLOSED,// Template ends with open reasoning tag on enabled thinking but
- // with both opened and closed tag for disabled thinking
- TOOLS_ONLY // Only reason on tool calls, not on normal content
+ NONE, // No reasoning markers detected
+ TAG_BASED, // Standard tag-based: ...
+ DELIMITER, // Delimiter-based: [BEGIN FINAL RESPONSE] (reasoning ends at delimiter)
+ FORCED_OPEN, // Template ends with open reasoning tag (empty start, non-empty end)
+ FORCED_CLOSED, // Template ends with open reasoning tag on enabled thinking but
+ // with both opened and closed tag for disabled thinking
+ TOOLS_ONLY // Only reason on tool calls, not on normal content
};
inline std::ostream & operator<<(std::ostream & os, const reasoning_mode & mode) {
@@ -143,10 +138,10 @@ inline std::ostream & operator<<(std::ostream & os, const content_mode & mode) {
// Call ID position in tool calls (for non-JSON formats)
enum class call_id_position {
- NONE, // No call ID support detected
- PRE_FUNC_NAME, // Call ID before function name: [CALL_ID]id[FUNC]name{args}
- BETWEEN_FUNC_AND_ARGS, // Call ID between function and args: [FUNC]name[CALL_ID]id{args}
- POST_ARGS, // Call ID after arguments: [FUNC]name{args}[CALL_ID]id
+ NONE, // No call ID support detected
+ PRE_FUNC_NAME, // Call ID before function name: [CALL_ID]id[FUNC]name{args}
+ BETWEEN_FUNC_AND_ARGS, // Call ID between function and args: [FUNC]name[CALL_ID]id{args}
+ POST_ARGS, // Call ID after arguments: [FUNC]name{args}[CALL_ID]id
};
inline std::ostream & operator<<(std::ostream & os, const call_id_position & pos) {
@@ -166,10 +161,10 @@ inline std::ostream & operator<<(std::ostream & os, const call_id_position & pos
// Tool call format classification (derived from T1-T5, A1-A3 comparisons)
enum class tool_format {
- NONE, // No tool support detected
- JSON_NATIVE, // Pure JSON: {"name": "X", "arguments": {...}}
- TAG_WITH_JSON, // Tag-based with JSON args: {...}
- TAG_WITH_TAGGED, // Tag-based with tagged args: value
+ NONE, // No tool support detected
+ JSON_NATIVE, // Pure JSON: {"name": "X", "arguments": {...}}
+ TAG_WITH_JSON, // Tag-based with JSON args: {...}
+ TAG_WITH_TAGGED, // Tag-based with tagged args: value
};
inline std::ostream & operator<<(std::ostream & os, const tool_format & format) {
@@ -187,33 +182,77 @@ inline std::ostream & operator<<(std::ostream & os, const tool_format & format)
}
}
+struct reasoning_analysis {
+ reasoning_mode mode = reasoning_mode::NONE;
+
+ std::string start; // e.g., "", "[THINK]", "<|START_THINKING|>", ""
+ std::string end; // e.g., "", "[BEGIN FINAL RESPONSE]", "<|END_THINKING|>"
+};
+
+struct content_analysis {
+ content_mode mode = content_mode::PLAIN;
+
+ std::string start; // e.g., "", ">>>all\n", ""
+ std::string end; // e.g., "", ""
+
+ bool requires_nonnull_content = false;
+};
+
+struct tool_format_analysis {
+ tool_format mode = tool_format::NONE;
+
+ std::string section_start; // e.g., "", "[TOOL_CALLS]", ""
+ std::string section_end; // e.g., "", ""
+ std::string per_call_start; // e.g., "<|tool_call_begin|>", "" (for multi-call templates)
+ std::string per_call_end; // e.g., "<|tool_call_end|>", ""
+
+ bool fun_name_is_key = false; // In JSON format function name is JSON key, i.e. { "": { ... arguments ... } }
+ bool tools_array_wrapped = false; // Tool calls wrapped in JSON array [...]
+
+ std::string function_field = "function";
+ std::string name_field = "name";
+ std::string args_field = "arguments";
+ std::string id_field;
+ std::string gen_id_field;
+ std::vector parameter_order;
+};
+
+struct tool_function_analysis {
+ std::string name_prefix; // e.g., "", "\"", ":0"
+ std::string close; // e.g., "", "" (for tag-based)
+};
+
+struct tool_arguments_analysis {
+ std::string start; // e.g., "<|tool_call_argument_begin|>", ""
+ std::string end; // e.g., "<|tool_call_argument_end|>", ""
+ std::string name_prefix; // e.g., "", "\""
+ std::string name_suffix; // e.g., ">", "", "\":"
+ std::string value_prefix; // e.g., "", "", ""
+ std::string value_suffix; // e.g., "", "", ""
+ std::string separator; // e.g., "", "\n", ","
+};
+
+struct tool_id_analysis {
+ call_id_position pos = call_id_position::NONE;
+
+ std::string prefix; // e.g., "[CALL_ID]" (marker before call ID value)
+ std::string suffix; // e.g., "" (marker after call ID value, before next section)
+};
+
+struct tool_analysis {
+ tool_format_analysis format;
+ tool_function_analysis function;
+ tool_arguments_analysis arguments;
+ tool_id_analysis call_id;
+};
+
// Complete result of differential analysis
struct diff_analysis_result {
- // Classification results
- reasoning_mode reasoning = reasoning_mode::NONE;
- content_mode content = content_mode::PLAIN;
- tool_format tools = tool_format::NONE;
-
- // All extracted markers
- marker_registry markers;
-
- // JSON field names (for JSON-based formats)
- bool fun_name_is_key = false;
- std::string function_field = "function";
- std::string name_field = "name";
- std::string args_field = "arguments";
- std::string id_field;
- std::string gen_id_field;
- std::vector parameter_order;
-
- // Call ID position (for non-JSON formats)
- call_id_position call_id_pos = call_id_position::NONE;
-
- // Flags
- bool supports_tools = false;
- bool supports_parallel_calls = false;
- bool requires_nonnull_content = false;
- bool tools_array_wrapped = false; // Tool calls wrapped in JSON array [...]
+ jinja::caps jinja_caps;
+ reasoning_analysis reasoning;
+ content_analysis content;
+ tool_analysis tools;
// Preserved tokens for tokenizer (union of all non-empty markers)
std::vector preserved_tokens;
@@ -227,94 +266,102 @@ class differential_analyzer {
static diff_analysis_result analyze(const common_chat_template & tmpl);
// Phase-specific analysis (can be called individually for testing)
- static void analyze_reasoning(const common_chat_template & tmpl, diff_analysis_result & result);
- static void analyze_content(const common_chat_template & tmpl, diff_analysis_result & result);
- static void analyze_tools(const common_chat_template & tmpl, diff_analysis_result & result);
- static void analyze_arguments(const common_chat_template & tmpl, diff_analysis_result & result);
+ static reasoning_analysis analyze_reasoning(const common_chat_template & tmpl, bool supports_tools);
+ static content_analysis analyze_content(const common_chat_template & tmpl, const reasoning_analysis & reasoning);
+ static tool_analysis analyze_tools(const common_chat_template & tmpl,
+ const jinja::caps & caps,
+ const reasoning_analysis & reasoning);
// Factorized differential comparison function (public for testing)
// Takes base params and a single modifier lambda to create variant B
// Returns compare_variants_result containing diff and both outputs, or std::nullopt on failure
static std::optional compare_variants(
- const common_chat_template & tmpl,
- const template_params & params_A,
- const std::function & params_modifier);
+ const common_chat_template & tmpl,
+ const template_params & params_A,
+ const std::function & params_modifier);
private:
// Comparison helpers (implement the comparison matrix from the plan)
- // R1: Extract reasoning markers by comparing with/without reasoning_content
- static void compare_reasoning_presence(const common_chat_template & tmpl, diff_analysis_result & result);
+ // 1. Reasoning analysis:
+ // Look for reasoning markers in rendered content
+ static void compare_reasoning_presence(const common_chat_template & tmpl, reasoning_analysis & reasoning);
- // R2: Detect forced-open reasoning by comparing enable_thinking=false vs true
- static void compare_thinking_enabled(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Compare generation prompt with enable_thinking=true vs false
+ static void compare_thinking_enabled(const common_chat_template & tmpl, reasoning_analysis & reasoning);
- // R3: Detect reasoning scope (content-only vs with tools)
- static void compare_reasoning_scope(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Check if reasoning is always possible or only in tool calls
+ static void compare_reasoning_scope(const common_chat_template & tmpl, reasoning_analysis & reasoning);
- // C1: Extract content markers by comparing different content values
- static void compare_content_values(const common_chat_template & tmpl, diff_analysis_result & result);
+ // 2. Content (fully inside analyze_content mentioned above)
- // T1: Analyze the tool calls
- static void analyze_tool_calls(const common_chat_template & tmpl, diff_analysis_result & result);
+ // 3. Tool calls
+ // a. format
+ // Extract tool calling 'haystack' for further analysis and delegate further analysis based on format
+ static tool_format_analysis analyze_tool_calls(const common_chat_template & tmpl,
+ const reasoning_analysis & reasoning);
- // Analyzes a tool call section to determine the format used (pure JSON, function name markers, or full markers)
- static void analyze_tool_call_format(const std::string & haystack,
- const std::string & fun_name_needle,
- const std::string & arg_name_needle,
- diff_analysis_result & result);
+ // Analyze format based on position of function and argument name in needle
+ static tool_format_analysis analyze_tool_call_format(const std::string & haystack,
+ const std::string & fun_name_needle,
+ const std::string & arg_name_needle,
+ const reasoning_analysis & reasoning);
- // Helper functions to handle the two branches of analyze_tool_call_format
+ // Analyze specifics of JSON native format (entire tool call is a JSON object)
static void analyze_tool_call_format_json_native(const std::string & clean_haystack,
const std::string & fun_name_needle,
const std::string & arg_name_needle,
- diff_analysis_result & result);
+ tool_format_analysis & format);
+ // Analyze specifics of non-JSON native format (tags for function name or for function name and arguments)
static void analyze_tool_call_format_non_json(const std::string & clean_haystack,
const std::string & fun_name_needle,
- diff_analysis_result & result);
+ tool_format_analysis & format);
- // T2: Check if markers are per call or per section
- static void check_per_call_markers(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Check for and extract specific per-call markers for non-native-JSON templates with parallel call support
+ static void check_per_call_markers(const common_chat_template & tmpl, tool_format_analysis & result);
- // T3: Extract call separator; also outputs second_call_content for per-call detection
- static void extract_call_separator(const common_chat_template & tmpl, diff_analysis_result & result,
- std::string & second_call_content);
+ // Logic below is only for non-JSON-native tool calling formats
+ // 3. b. function name
+ // Extract function name markers
+ static tool_function_analysis extract_function_markers(const common_chat_template & tmpl,
+ const tool_format_analysis & analysis);
- // T4: Analyze function name format and extract markers
- static void extract_function_markers(const common_chat_template & tmpl,
- diff_analysis_result & result);
+ // 4. c. function arguments
+ // Delegates to separate functions for: separator analysis, argument name analysis, argument value analysis
+ static tool_arguments_analysis analyze_arguments(const common_chat_template & tmpl,
+ const tool_analysis & analysis);
- // T5: Extract argument separator
- static void extract_argument_separator(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Extract argument name markers
+ static void extract_argument_name_markers(const common_chat_template & tmpl,
+ tool_arguments_analysis & args_analysis);
- // T6: Extract args container markers
- static void extract_args_markers(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Extract argument value markers
+ static void extract_argument_value_markers(const common_chat_template & tmpl,
+ const tool_analysis & analysis,
+ tool_arguments_analysis & args_analysis);
- // A1: Extract argument name markers
- static void extract_argument_name_markers(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Extract argument separator, if specified (eg. ......)
+ static void extract_argument_separator(const common_chat_template & tmpl,
+ tool_arguments_analysis & args_analysis);
- // A2: Extract argument value markers
- static void extract_argument_value_markers(const common_chat_template & tmpl, diff_analysis_result & result);
+ // Extract argument wrapper markers, if present (eg. '......')
+ static void extract_args_markers(const common_chat_template & tmpl,
+ const tool_analysis & analysis,
+ tool_arguments_analysis & args_analysis);
- // T7: Extract call ID markers (for non-JSON formats)
- static void extract_call_id_markers(const common_chat_template & tmpl, diff_analysis_result & result);
+ // 4. d. function call id
+ // Extract call ID markers, if present
+ static tool_id_analysis extract_call_id_markers(const common_chat_template & tmpl,
+ tool_format_analysis & analysis);
- // Classify tool format based on extracted markers
- static void classify_tool_format(diff_analysis_result & result);
-
- // Classification helpers
+ // Collect tokens from entire analysis to preserve
static void collect_preserved_tokens(diff_analysis_result & result);
- // Utility: Apply template with given parameters
- static std::string apply_template(const common_chat_template & tmpl,
- const template_params & params);
+ static std::string apply_template(const common_chat_template & tmpl, const template_params & params);
};
-enum segment_type {
- TEXT,
- MARKER
-};
+enum segment_type { TEXT, MARKER };
inline std::ostream & operator<<(std::ostream & os, const segment_type & type) {
switch (type) {
@@ -329,7 +376,7 @@ inline std::ostream & operator<<(std::ostream & os, const segment_type & type) {
struct segment {
segment_type type;
- std::string value;
+ std::string value;
segment(segment_type type, std::string value) : type(type), value(std::move(value)) {}
};
diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp
index cb38fb160f..6e58dc6761 100644
--- a/common/chat-peg-parser.cpp
+++ b/common/chat-peg-parser.cpp
@@ -167,13 +167,13 @@ void common_chat_peg_mapper::map(const common_peg_ast_node & node) {
bool is_content = node.tag == common_chat_peg_builder::CONTENT;
if (is_reasoning) { // GPT OSS can have more than 1 reasoning block, so concatenate here
- result.reasoning_content += std::string(trim_trailing_space(node.text));
+ result.reasoning_content += std::string(node.text);
}
if (is_content) {
// Concatenate content from multiple content nodes (e.g., when reasoning markers
// are preserved before content markers in reasoning_format=NONE mode)
- result.content += std::string(trim_trailing_space(node.text));
+ result.content += std::string(node.text);
}
}
diff --git a/common/chat.cpp b/common/chat.cpp
index 3fb17b4e9d..be4e19aebc 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -240,7 +240,7 @@ bool common_chat_templates_support_enable_thinking(const common_chat_templates *
? *chat_templates->template_tool_use
: *chat_templates->template_default;
diff_analysis_result result = differential_analyzer::analyze(tmpl);
- detect |= result.reasoning != reasoning_mode::NONE;
+ detect |= result.reasoning.mode != reasoning_mode::NONE;
return detect;
}
diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp
index abd4cd2d9f..aecad6efa6 100644
--- a/common/jinja/caps.cpp
+++ b/common/jinja/caps.cpp
@@ -42,7 +42,7 @@ static void caps_try_execute(jinja::program & prog,
jinja::runtime runtime(ctx);
auto results = runtime.execute(prog);
auto parts = jinja::runtime::gather_string_parts(results);
- std::string result = parts->as_string().str();
+ result = parts->as_string().str();
success = true;
} catch (const std::exception & e) {
JJ_DEBUG("Exception during execution: %s", e.what());
@@ -95,6 +95,8 @@ caps caps_get(jinja::program & prog) {
return v->stats.ops.find(op_name) != v->stats.ops.end();
};
+ JJ_DEBUG("%s\n", ">>> Running capability check: typed content");
+
// case: typed content support
caps_try_execute(
prog,
@@ -125,6 +127,7 @@ caps caps_get(jinja::program & prog) {
}
);
+ JJ_DEBUG("%s\n", ">>> Running capability check: system prompt");
// case: system prompt support
caps_try_execute(
@@ -155,6 +158,8 @@ caps caps_get(jinja::program & prog) {
}
);
+ JJ_DEBUG("%s\n", ">>> Running capability check: tool support");
+
// case: tools support
caps_try_execute(
prog,
@@ -167,7 +172,7 @@ caps caps_get(jinja::program & prog) {
},
{
{"role", "assistant"},
- {"content", "Assistant message"},
+ {"content", ""}, // Some templates expect content to be empty with tool calls
{"tool_calls", json::array({
{
{"id", "call00001"},
@@ -260,6 +265,8 @@ caps caps_get(jinja::program & prog) {
}
);
+ JJ_DEBUG("%s\n", ">>> Running capability check: preserve reasoning");
+
// case: preserve reasoning content in chat history
caps_try_execute(
prog,
diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp
index cc012c892f..b7e71115ed 100644
--- a/common/jinja/runtime.cpp
+++ b/common/jinja/runtime.cpp
@@ -114,8 +114,10 @@ value binary_expression::execute_impl(context & ctx) {
// Logical operators
if (op.value == "and") {
+ JJ_DEBUG("Executing logical test: %s AND %s", left->type().c_str(), right->type().c_str());
return left_val->as_bool() ? right->execute(ctx) : std::move(left_val);
} else if (op.value == "or") {
+ JJ_DEBUG("Executing logical test: %s OR %s", left->type().c_str(), right->type().c_str());
return left_val->as_bool() ? std::move(left_val) : right->execute(ctx);
}
@@ -835,7 +837,7 @@ value call_expression::execute_impl(context & ctx) {
for (auto & arg_stmt : this->args) {
auto arg_val = arg_stmt->execute(ctx);
JJ_DEBUG(" Argument type: %s", arg_val->type().c_str());
- args.push_back(std::move(arg_val));
+ args.push_back(arg_val);
}
// execute callee
value callee_val = callee->execute(ctx);
diff --git a/models/templates/deepseek-ai-DeepSeek-V3.1.jinja b/models/templates/deepseek-ai-DeepSeek-V3.1.jinja
index 6ef7fb123c..2fd1c415b8 100644
--- a/models/templates/deepseek-ai-DeepSeek-V3.1.jinja
+++ b/models/templates/deepseek-ai-DeepSeek-V3.1.jinja
@@ -27,7 +27,7 @@
{%- set ns.is_first = false -%}
{%- set ns.is_last_user = true -%}{{'<|User|>' + message['content']}}
{%- endif -%}
- {%- if message['role'] == 'assistant' and message['tool_calls'] is defined and message['tool_calls'] is not none -%}
+ {%- if message['role'] == 'assistant' and message['tool_calls'] -%}
{%- if ns.is_last_user -%}{{'<|Assistant|>'}}
{%- endif -%}
{%- set ns.is_last_user = false -%}
@@ -35,7 +35,7 @@
{%- set ns.is_tool = false -%}
{%- for tool in message['tool_calls'] -%}
{%- if not ns.is_first -%}
- {%- if message['content'] is none -%}{{'<|tool▁calls▁begin|><|tool▁call▁begin|>'+ tool['function']['name'] + '<|tool▁sep|>' + tool['function']['arguments'] + '<|tool▁call▁end|>'}}
+ {%- if not message['content'] -%}{{'<|tool▁calls▁begin|><|tool▁call▁begin|>'+ tool['function']['name'] + '<|tool▁sep|>' + tool['function']['arguments'] + '<|tool▁call▁end|>'}}
{%- else -%}{{message['content'] + '<|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['function']['name'] + '<|tool▁sep|>' + tool['function']['arguments'] + '<|tool▁call▁end|>'}}
{%- endif -%}
{%- set ns.is_first = true -%}
@@ -43,7 +43,7 @@
{%- endif -%}
{%- endfor -%}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}
{%- endif -%}
- {%- if message['role'] == 'assistant' and (message['tool_calls'] is not defined or message['tool_calls'] is none) -%}
+ {%- if message['role'] == 'assistant' and not message['tool_calls'] -%}
{%- if ns.is_last_user -%}{{'<|Assistant|>'}}
{%- if message['prefix'] is defined and message['prefix'] and thinking -%}{{''}}
{%- else -%}{{''}}
diff --git a/tests/test-chat-auto-parser.cpp b/tests/test-chat-auto-parser.cpp
index 90edaba32d..b2bdab15e9 100644
--- a/tests/test-chat-auto-parser.cpp
+++ b/tests/test-chat-auto-parser.cpp
@@ -57,7 +57,6 @@ static void test_nemotron_tool_format(testing & t);
// CohereForAI template analysis tests
static void test_cohere_reasoning_detection(testing & t);
-static void test_cohere_tool_format(testing & t);
static void test_cohere_analysis(testing & t);
// Marker separation
@@ -1283,18 +1282,18 @@ static void test_nemotron_reasoning_detection(testing & t) {
auto analysis = differential_analyzer::analyze(tmpl);
// Check reasoning markers
- t.assert_equal("reasoning_start should be ''", "", analysis.markers.reasoning_start);
- t.assert_equal("reasoning_end should be ''", "", analysis.markers.reasoning_end);
+ t.assert_equal("reasoning_start should be ''", "", analysis.reasoning.start);
+ t.assert_equal("reasoning_end should be ''", "", analysis.reasoning.end);
// Check reasoning mode detection
// Nemotron uses forced closed reasoning with add_generation_prompt
- t.assert_equal("reasoning should be FORCED_CLOSED", reasoning_mode::FORCED_CLOSED, analysis.reasoning);
+ t.assert_equal("reasoning should be FORCED_CLOSED", reasoning_mode::FORCED_CLOSED, analysis.reasoning.mode);
// Make sure reasoning markers don't spill over to content markers
- t.assert_equal("content start should be empty", "", analysis.markers.content_start);
- t.assert_equal("content end should be empty", "", analysis.markers.content_end);
+ t.assert_equal("content start should be empty", "", analysis.content.start);
+ t.assert_equal("content end should be empty", "", analysis.content.end);
- t.assert_equal("content should be PLAIN", content_mode::PLAIN, analysis.content);
+ t.assert_equal("content should be PLAIN", content_mode::PLAIN, analysis.content.mode);
}
static void test_nemotron_tool_format(testing & t) {
@@ -1304,27 +1303,27 @@ static void test_nemotron_tool_format(testing & t) {
auto analysis = differential_analyzer::analyze(tmpl);
// Check tool markers - Nemotron uses per-call wrapping (each call individually wrapped)
- t.assert_equal("tool_section_start should be empty (per-call format)", "", analysis.markers.tool_section_start);
- t.assert_equal("tool_section_end should be empty (per-call format)", "", analysis.markers.tool_section_end);
- t.assert_equal("per_call_start should be '\\n'", "\n", analysis.markers.per_call_start);
- t.assert_equal("per_call_end should be ''", "", analysis.markers.per_call_end);
- t.assert_true("should support parallel calls", analysis.supports_parallel_calls);
+ t.assert_equal("tool_section_start should be empty (per-call format)", "", analysis.tools.format.section_start);
+ t.assert_equal("tool_section_end should be empty (per-call format)", "", analysis.tools.format.section_end);
+ t.assert_equal("per_call_start should be '\\n'", "\n", analysis.tools.format.per_call_start);
+ t.assert_equal("per_call_end should be ''", "", analysis.tools.format.per_call_end);
+ t.assert_true("should support parallel calls", analysis.jinja_caps.supports_parallel_tool_calls);
// Check function markers
- t.assert_equal("func_name_prefix should be '\\n'", ">\n", analysis.markers.func_name_suffix);
- t.assert_equal("func_close should be '\\n'", "\n", analysis.markers.func_close);
+ t.assert_equal("func_name_prefix should be '\\n'", ">\n", analysis.tools.function.name_suffix);
+ t.assert_equal("func_close should be '\\n'", "\n", analysis.tools.function.close);
// Check argument markers (note: markers retain trailing newlines for proper parsing)
- t.assert_equal("arg_name_prefix should be '\\n'", ">\n", analysis.markers.arg_name_suffix);
- t.assert_equal("arg_value_suffix should be '\\n'", "\n", analysis.markers.arg_value_suffix);
+ t.assert_equal("arg_name_prefix should be '\\n'", ">\n", analysis.tools.arguments.name_suffix);
+ t.assert_equal("arg_value_suffix should be '\\n'", "\n", analysis.tools.arguments.value_suffix);
// Check format classification
- t.assert_true("tool format should be TAG_WITH_TAGGED", analysis.tools == tool_format::TAG_WITH_TAGGED);
+ t.assert_true("tool format should be TAG_WITH_TAGGED", analysis.tools.format.mode == tool_format::TAG_WITH_TAGGED);
// Verify tool support
- t.assert_true("should support tools", analysis.supports_tools);
+ t.assert_true("should support tools", analysis.jinja_caps.supports_tools);
}
static common_chat_template load_cohere_template(testing & t) {
@@ -1333,7 +1332,6 @@ static common_chat_template load_cohere_template(testing & t) {
static void test_cohere_analysis(testing & t) {
t.test("Cohere reasoning detection", test_cohere_reasoning_detection);
- t.test("Cohere tool format", test_cohere_tool_format);
}
static void test_cohere_reasoning_detection(testing & t) {
@@ -1343,64 +1341,64 @@ static void test_cohere_reasoning_detection(testing & t) {
auto analysis = differential_analyzer::analyze(tmpl);
// Check reasoning markers - Cohere uses special token format
- t.assert_equal("reasoning_start should be '<|START_THINKING|>'", "<|START_THINKING|>", analysis.markers.reasoning_start);
- t.assert_equal("reasoning_end should be '<|END_THINKING|>'", "<|END_THINKING|>", analysis.markers.reasoning_end);
+ t.assert_equal("reasoning_start should be '<|START_THINKING|>'", "<|START_THINKING|>", analysis.reasoning.start);
+ t.assert_equal("reasoning_end should be '<|END_THINKING|>'", "<|END_THINKING|>", analysis.reasoning.end);
// Check reasoning mode - Cohere only shows reasoning with tool calls (TOOLS_ONLY)
- t.assert_equal("reasoning should be TOOLS_ONLY", reasoning_mode::TOOLS_ONLY, analysis.reasoning);
+ t.assert_equal("reasoning should be TOOLS_ONLY", reasoning_mode::TOOLS_ONLY, analysis.reasoning.mode);
// Check content markers - Cohere wraps all content with START/END_RESPONSE
- t.assert_equal("content_start should be '<|START_RESPONSE|>'", "<|START_RESPONSE|>", analysis.markers.content_start);
- t.assert_equal("content_end should be '<|END_RESPONSE|>'", "<|END_RESPONSE|>", analysis.markers.content_end);
+ t.assert_equal("content_start should be '<|START_RESPONSE|>'", "<|START_RESPONSE|>", analysis.content.start);
+ t.assert_equal("content_end should be '<|END_RESPONSE|>'", "<|END_RESPONSE|>", analysis.content.end);
// Content is always wrapped (both with and without tools)
- t.assert_equal("content should be ALWAYS_WRAPPED", content_mode::ALWAYS_WRAPPED, analysis.content);
+ t.assert_equal("content should be ALWAYS_WRAPPED", content_mode::ALWAYS_WRAPPED, analysis.content.mode);
}
-static void test_cohere_tool_format(testing & t) {
+static void test_tool_format_cohere(testing & t) {
common_chat_template tmpl = load_cohere_template(t);
// Run differential analysis
auto analysis = differential_analyzer::analyze(tmpl);
// Check tool section markers - Cohere uses ACTION markers
- t.assert_equal("tool_section_start should be '<|START_ACTION|>'", "<|START_ACTION|>", analysis.markers.tool_section_start);
- t.assert_equal("tool_section_end should be '<|END_ACTION|>'", "<|END_ACTION|>", analysis.markers.tool_section_end);
+ t.assert_equal("tool_section_start should be '<|START_ACTION|>'", "<|START_ACTION|>", analysis.tools.format.section_start);
+ t.assert_equal("tool_section_end should be '<|END_ACTION|>'", "<|END_ACTION|>", analysis.tools.format.section_end);
// JSON_NATIVE format has no per-call markers
- t.assert_equal("per_call_start should be empty", "", analysis.markers.per_call_start);
- t.assert_equal("per_call_end should be empty", "", analysis.markers.per_call_end);
+ t.assert_equal("per_call_start should be empty", "", analysis.tools.format.per_call_start);
+ t.assert_equal("per_call_end should be empty", "", analysis.tools.format.per_call_end);
// JSON_NATIVE format has empty function markers (no XML-style markers)
- t.assert_equal("func_name_prefix should be empty", "", analysis.markers.func_name_prefix);
- t.assert_equal("func_name_suffix should be empty", "", analysis.markers.func_name_suffix);
- t.assert_equal("func_close should be empty", "", analysis.markers.func_close);
+ t.assert_equal("func_name_prefix should be empty", "", analysis.tools.function.name_prefix);
+ t.assert_equal("func_name_suffix should be empty", "", analysis.tools.function.name_suffix);
+ t.assert_equal("func_close should be empty", "", analysis.tools.function.close);
// JSON_NATIVE format has empty args markers
- t.assert_equal("args_start should be empty", "", analysis.markers.args_start);
- t.assert_equal("args_end should be empty", "", analysis.markers.args_end);
+ t.assert_equal("args_start should be empty", "", analysis.tools.arguments.start);
+ t.assert_equal("args_end should be empty", "", analysis.tools.arguments.end);
// JSON_NATIVE format has empty argument markers
- t.assert_equal("arg_name_prefix should be empty", "", analysis.markers.arg_name_prefix);
- t.assert_equal("arg_name_suffix should be empty", "", analysis.markers.arg_name_suffix);
- t.assert_equal("arg_value_prefix should be empty", "", analysis.markers.arg_value_prefix);
- t.assert_equal("arg_value_suffix should be empty", "", analysis.markers.arg_value_suffix);
- t.assert_equal("arg_separator should be empty", "", analysis.markers.arg_separator);
+ t.assert_equal("arg_name_prefix should be empty", "", analysis.tools.arguments.name_prefix);
+ t.assert_equal("arg_name_suffix should be empty", "", analysis.tools.arguments.name_suffix);
+ t.assert_equal("arg_value_prefix should be empty", "", analysis.tools.arguments.value_prefix);
+ t.assert_equal("arg_value_suffix should be empty", "", analysis.tools.arguments.value_suffix);
+ t.assert_equal("arg_separator should be empty", "", analysis.tools.arguments.separator);
// Check JSON field names - Cohere uses non-standard names
- t.assert_equal("name_field should be 'tool_name'", "tool_name", analysis.name_field);
- t.assert_equal("args_field should be 'parameters'", "parameters", analysis.args_field);
+ t.assert_equal("name_field should be 'tool_name'", "tool_name", analysis.tools.format.name_field);
+ t.assert_equal("args_field should be 'parameters'", "parameters", analysis.tools.format.args_field);
// This isn't a real tool call id field, i.e. with the OpenAI tool call ID format
- t.assert_equal("id_field should be 'tool_call_id'", "", analysis.id_field);
+ t.assert_equal("id_field should be 'tool_call_id'", "", analysis.tools.format.id_field);
// Check format classification
- t.assert_equal("tool format should be JSON_NATIVE", tool_format::JSON_NATIVE, analysis.tools);
+ t.assert_equal("tool format should be JSON_NATIVE", tool_format::JSON_NATIVE, analysis.tools.format.mode);
// Check flags
- t.assert_true("should support tools", analysis.supports_tools);
- t.assert_true("should support parallel calls", analysis.supports_parallel_calls);
- t.assert_true("should not require nonnull content", !analysis.requires_nonnull_content);
- t.assert_true("tools_array_wrapped should be true", analysis.tools_array_wrapped);
+ t.assert_true("should support tools", analysis.jinja_caps.supports_tools);
+ t.assert_true("should support parallel calls", analysis.jinja_caps.supports_parallel_tool_calls);
+ t.assert_true("should not require nonnull content", !analysis.content.requires_nonnull_content);
+ t.assert_true("tools_array_wrapped should be true", analysis.tools.format.tools_array_wrapped);
}
// ============================================================================
diff --git a/tests/test-chat-peg-parser.cpp b/tests/test-chat-peg-parser.cpp
index ae82966699..d59880e3dc 100644
--- a/tests/test-chat-peg-parser.cpp
+++ b/tests/test-chat-peg-parser.cpp
@@ -530,7 +530,7 @@ static void test_example_qwen3_non_coder(testing & t) {
auto mapper = common_chat_peg_unified_mapper(msg);
mapper.from_ast(ctx.ast, result);
- t.assert_equal("content", "I need to get the weather.", msg.content);
+ t.assert_equal("content", "I need to get the weather.\n", msg.content);
t.assert_equal("reasoning", "", msg.reasoning_content);
t.assert_equal("tool calls count", 1u, msg.tool_calls.size());
if (!msg.tool_calls.empty()) {
diff --git a/tests/test-chat-template.cpp b/tests/test-chat-template.cpp
index 8b331129b1..91ff83a729 100644
--- a/tests/test-chat-template.cpp
+++ b/tests/test-chat-template.cpp
@@ -32,6 +32,7 @@ static std::string HELP = R"(
Usage: test-chat-template [OPTIONS] PATH_TO_TEMPLATE
Options:
-h, --help Show this help message and exit.
+ --with-tools Add a tool and a tool call to the default JSON input
--json Path to the JSON input file.
--stop-on-first-fail Stop testing on the first failure (default: false).
--no-common Use direct Jinja engine instead of common chat templates (default: use common).
@@ -57,12 +58,65 @@ static std::string DEFAULT_JSON = R"({
"add_generation_prompt": true
})";
+static std::string DEFAULT_JSON_WITH_TOOLS = R"({
+ "messages": [
+ {
+ "role": "user",
+ "content": "Hello, how are you?"
+ },
+ {
+ "role": "assistant",
+ "content": "I am fine, thank you!"
+ },
+ {
+ "role": "user",
+ "content": "Call a tool!"
+ },
+ {
+ "role": "assistant",
+ "tool_calls": [
+ {
+ "id": "call00001",
+ "type": "function",
+ "function": {
+ "name": "test",
+ "arguments": { "arg": "hello" }
+ }
+ }
+ ]
+ }
+ ],
+ "tools": [
+ {
+ "type": "function",
+ "function": {
+ "name": "test",
+ "description": "Test",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "arg": {
+ "type": "string"
+ }
+ }
+ },
+ "required": ["arg"]
+ }
+ }
+ ],
+ "bos_token": "",
+ "eos_token": "",
+ "add_generation_prompt": true
+})";
+
+
int main(int argc, char ** argv) {
std::vector args(argv, argv + argc);
std::string tmpl_path;
std::string json_path;
std::string output_path;
+ std::string & json_to_use = DEFAULT_JSON;
bool stop_on_first_fail = false;
bool use_common = true;
@@ -74,6 +128,8 @@ int main(int argc, char ** argv) {
if (args[i] == "--json" && i + 1 < args.size()) {
json_path = args[i + 1];
i++;
+ } else if (args[i] == "--with-tools") {
+ json_to_use = DEFAULT_JSON_WITH_TOOLS;
} else if (args[i] == "--stop-on-first-fail") {
stop_on_first_fail = true;
} else if (args[i] == "--output" && i + 1 < args.size()) {
@@ -106,7 +162,7 @@ int main(int argc, char ** argv) {
std::istreambuf_iterator());
input_json = json::parse(content);
} else {
- input_json = json::parse(DEFAULT_JSON);
+ input_json = json::parse(json_to_use);
}
std::filesystem::path p(tmpl_path);
diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp
index c0e29c548a..c87f3c8e35 100644
--- a/tools/parser/debug-template-parser.cpp
+++ b/tools/parser/debug-template-parser.cpp
@@ -419,33 +419,33 @@ int main(int argc, char ** argv) {
LOG_ERR("\n=== Differential Analysis Results ===\n");
LOG_ERR("\n--- Reasoning & Content Structure ---\n");
- LOG_ERR("reasoning_mode: %s\n", mode_to_str(analysis.reasoning).c_str());
- LOG_ERR("reasoning_start: '%s'\n", analysis.markers.reasoning_start.c_str());
- LOG_ERR("reasoning_end: '%s'\n", analysis.markers.reasoning_end.c_str());
- LOG_ERR("content_mode: %s\n", mode_to_str(analysis.content).c_str());
- LOG_ERR("content_start: '%s'\n", analysis.markers.content_start.c_str());
- LOG_ERR("content_end: '%s'\n", analysis.markers.content_end.c_str());
+ LOG_ERR("reasoning_mode: %s\n", mode_to_str(analysis.reasoning.mode).c_str());
+ LOG_ERR("reasoning_start: '%s'\n", analysis.reasoning.start.c_str());
+ LOG_ERR("reasoning_end: '%s'\n", analysis.reasoning.end.c_str());
+ LOG_ERR("content_mode: %s\n", mode_to_str(analysis.content.mode).c_str());
+ LOG_ERR("content_start: '%s'\n", analysis.content.start.c_str());
+ LOG_ERR("content_end: '%s'\n", analysis.content.end.c_str());
LOG_ERR("\n--- Tool Call Structure ---\n");
- LOG_ERR("tool_mode: %s\n", mode_to_str(analysis.tools).c_str());
- LOG_ERR("supports_tools: %s\n", analysis.supports_tools ? "true" : "false");
- LOG_ERR("supports_parallel_calls: %s\n", analysis.supports_parallel_calls ? "true" : "false");
- LOG_ERR("tool_section_start: '%s'\n", analysis.markers.tool_section_start.c_str());
- LOG_ERR("tool_section_end: '%s'\n", analysis.markers.tool_section_end.c_str());
- LOG_ERR("per_call_start: '%s'\n", analysis.markers.per_call_start.c_str());
- LOG_ERR("per_call_end: '%s'\n", analysis.markers.per_call_end.c_str());
- LOG_ERR("func_name_prefix: '%s'\n", analysis.markers.func_name_prefix.c_str());
- LOG_ERR("func_name_suffix: '%s'\n", analysis.markers.func_name_suffix.c_str());
- LOG_ERR("func_close: '%s'\n", analysis.markers.func_close.c_str());
- LOG_ERR("arg_name_prefix: '%s'\n", analysis.markers.arg_name_prefix.c_str());
- LOG_ERR("arg_name_suffix: '%s'\n", analysis.markers.arg_name_suffix.c_str());
- LOG_ERR("arg_value_prefix: '%s'\n", analysis.markers.arg_value_prefix.c_str());
- LOG_ERR("arg_value_suffix: '%s'\n", analysis.markers.arg_value_suffix.c_str());
- LOG_ERR("name_field: '%s'\n", analysis.name_field.c_str());
- LOG_ERR("args_field: '%s'\n", analysis.args_field.c_str());
- LOG_ERR("id_field: '%s'\n", analysis.id_field.c_str());
- LOG_ERR("gen_id_field: '%s'\n", analysis.gen_id_field.c_str());
- LOG_ERR("parameter_order: '%s'\n", std::accumulate(analysis.parameter_order.begin(), analysis.parameter_order.end(),
+ LOG_ERR("tool_mode: %s\n", mode_to_str(analysis.tools.format.mode).c_str());
+ LOG_ERR("supports_tools: %s\n", analysis.jinja_caps.supports_tools ? "true" : "false");
+ LOG_ERR("supports_parallel_calls: %s\n", analysis.jinja_caps.supports_parallel_tool_calls ? "true" : "false");
+ LOG_ERR("tool_section_start: '%s'\n", analysis.tools.format.section_start.c_str());
+ LOG_ERR("tool_section_end: '%s'\n", analysis.tools.format.section_end.c_str());
+ LOG_ERR("per_call_start: '%s'\n", analysis.tools.format.per_call_start.c_str());
+ LOG_ERR("per_call_end: '%s'\n", analysis.tools.format.per_call_end.c_str());
+ LOG_ERR("func_name_prefix: '%s'\n", analysis.tools.function.name_prefix.c_str());
+ LOG_ERR("func_name_suffix: '%s'\n", analysis.tools.function.name_suffix.c_str());
+ LOG_ERR("func_close: '%s'\n", analysis.tools.function.close.c_str());
+ LOG_ERR("arg_name_prefix: '%s'\n", analysis.tools.arguments.name_prefix.c_str());
+ LOG_ERR("arg_name_suffix: '%s'\n", analysis.tools.arguments.name_suffix.c_str());
+ LOG_ERR("arg_value_prefix: '%s'\n", analysis.tools.arguments.value_prefix.c_str());
+ LOG_ERR("arg_value_suffix: '%s'\n", analysis.tools.arguments.value_suffix.c_str());
+ LOG_ERR("name_field: '%s'\n", analysis.tools.format.name_field.c_str());
+ LOG_ERR("args_field: '%s'\n", analysis.tools.format.args_field.c_str());
+ LOG_ERR("id_field: '%s'\n", analysis.tools.format.id_field.c_str());
+ LOG_ERR("gen_id_field: '%s'\n", analysis.tools.format.gen_id_field.c_str());
+ LOG_ERR("parameter_order: '%s'\n", std::accumulate(analysis.tools.format.parameter_order.begin(), analysis.tools.format.parameter_order.end(),
std::string(""), [] (const std::string & a, const std::string & b) { return a.empty() ? b : a + ", " + b; }
).c_str());
@@ -470,11 +470,13 @@ int main(int argc, char ** argv) {
LOG_ERR(" '%s'\n", token.c_str());
}
- LOG_ERR("\n=== Verifying created grammar ===\n");
- auto * grammar = llama_grammar_init_impl(nullptr, parser_data.grammar.c_str(), "root",
- parser_data.grammar_lazy, nullptr, 0, nullptr, 0);
- if (grammar != nullptr) {
- LOG_ERR("\n=== Grammar successfully created ===\n");
+ if (!parser_data.grammar.empty()) {
+ LOG_ERR("\n=== Verifying created grammar ===\n");
+ auto * grammar = llama_grammar_init_impl(nullptr, parser_data.grammar.c_str(), "root",
+ parser_data.grammar_lazy, nullptr, 0, nullptr, 0);
+ if (grammar != nullptr) {
+ LOG_ERR("\n=== Grammar successfully created ===\n");
+ }
}
}
} catch (const std::exception & e) {