Brunobkr/llama.cpp_AlgMor24_github
ΩFFFΣLLIa • llama.cpp • AlgMor24 ██████╗ ███████╗███████╗███████╗██╗ ██╗ ██╗ █████╗ ██╔═══██╗██╔════╝██╔════╝██╔════╝██║ ██║ ██║██╔══██╗ ██║ ██║█████╗ █████╗ █████╗ ██║ ██║ ██║███████║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██║ ██║ ██║██╔══██║ ╚██████╔╝██║ ██║ ███████╗███████╗███████╗██║██║ ██║ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚═╝╚═╝ ╚═╝ High-Performance LLM / VLM Inference & Autonomous Agentic Ecosystem… See the full description on the dataset page: https://huggingface.co/datasets/Brunobkr/llama.cpp_AlgMor24_github.
03k
1#include "chat-auto-parser.h"2#include "chat-auto-parser-helpers.h"3#include "chat-peg-parser.h"4#include "chat.h"5#include "common.h"6#include "log.h"7#include "nlohmann/json.hpp"8#include "peg-parser.h"9 10#include <algorithm>11#include <cctype>12#include <ostream>13#include <sstream>14 15#define ANSI_RESET "\033[0m"16#define ANSI_PURPLE "\033[1m\x1b[38;5;126m"17#define ANSI_ORANGE "\033[1m\x1b[38;5;214m"18#define ANSI_RED "\033[1m\x1b[38;5;196m"19 20using json = nlohmann::ordered_json;21 22namespace autoparser {23 24static const std::string FUN_FIRST = "FFF_FIRST_FUN_F";25static const std::string FUN_SECOND = "SSS_SECOND_FUN_S";26static const std::string ARG_FIRST = "AA_ARG_FST_AA";27static const std::string ARG_SECOND = "BB_ARG_SND_BB";28static const std::string USER_MSG = "U_USER_MSG Hello END_U";29static const std::string USER_MSG_TWO = "V_USER_MSG Hello END_V";30static const std::string ASSISTANT_MSG = "A_ASST_MSG I can help END_A";31static const std::string THINKING_CONTENT = "REASON_PART I am thinking END_R";32static const std::string CALL_ID_001 = "call00001";33static const std::string CALL_ID_002 = "call00002";34static const std::string CALL_ID_999 = "call99999";35 36static std::vector<std::function<void(const common_chat_template & tmpl, autoparser &)>> workarounds(37 { // Old reasoning Qwen templates - they don't really display reasoning content, but we still want to38 // support reasoning on them39 [](const common_chat_template & tmpl, autoparser & analysis) -> void {40 if (tmpl.src.find("content.split('</think>')") != std::string::npos &&41 tmpl.src.find("reasoning_content") == std::string::npos &&42 tmpl.src.find("<SPECIAL_12>") == std::string::npos &&43 analysis.reasoning.mode == reasoning_mode::NONE) {44 analysis.reasoning.mode = reasoning_mode::TAG_BASED;45 analysis.reasoning.start = "<think>";46 analysis.reasoning.end = "</think>";47 analysis.preserved_tokens.push_back("<think>");48 analysis.preserved_tokens.push_back("</think>");49 LOG_DBG(ANSI_ORANGE "[Patch: old Qwen/Deepseek thinking template]\n" ANSI_RESET);50 }51 },52 // Granite 3.3, with separate reasoning and content markers53 [](const common_chat_template & tmpl, autoparser & analysis) -> void {54 if (tmpl.src.find("Write your thoughts between <think></think> and write your response between "55 "<response></response>") != std::string::npos) {56 analysis.reasoning.mode = reasoning_mode::TAG_BASED;57 analysis.reasoning.start = "<think>";58 analysis.reasoning.end = "</think>";59 analysis.preserved_tokens.push_back("<think>");60 analysis.preserved_tokens.push_back("</think>");61 analysis.content.mode = content_mode::WRAPPED_WITH_REASONING;62 analysis.content.start = "<response>";63 analysis.content.end = "</response>";64 analysis.preserved_tokens.push_back("<response>");65 analysis.preserved_tokens.push_back("</response>");66 LOG_DBG(ANSI_ORANGE "[Patch: Granite 3.3]\n" ANSI_RESET);67 }68 },69 // Cohere Command R+ - content wrapped in <|CHATBOT_TOKEN|>...<|END_OF_TURN_TOKEN|>70 [](const common_chat_template & tmpl, autoparser & analysis) -> void {71 if (tmpl.src.find("<|CHATBOT_TOKEN|>") != std::string::npos &&72 tmpl.src.find("<|END_OF_TURN_TOKEN|>") != std::string::npos && analysis.content.start.empty()) {73 analysis.content.mode = content_mode::ALWAYS_WRAPPED;74 analysis.content.start = "<|CHATBOT_TOKEN|>";75 analysis.content.end = "<|END_OF_TURN_TOKEN|>";76 analysis.preserved_tokens.push_back("<|CHATBOT_TOKEN|>");77 analysis.preserved_tokens.push_back("<|END_OF_TURN_TOKEN|>");78 analysis.user_start = "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>";79 LOG_DBG(ANSI_ORANGE "[Patch: Cohere Command R+]\n" ANSI_RESET);80 }81 },82 // Functionary - no tool call section delimiter83 [](const common_chat_template & tmpl, autoparser & analysis) -> void {84 if (tmpl.src.find("set has_code_interpreter = tools | selectattr(\"type\", \"equalto\", "85 "\"code_interpreter\") | list | length > 0") != std::string::npos) {86 analysis.content.mode = content_mode::PLAIN;87 analysis.content.end = "";88 analysis.tools.function.name_prefix = "";89 analysis.tools.format.section_start = "";90 analysis.tools.format.section_end = "";91 analysis.tools.format.per_call_start = "<function=";92 analysis.tools.format.per_call_end = "</function>";93 analysis.tools.function.close = "";94 analysis.preserved_tokens.clear();95 analysis.preserved_tokens.push_back("<|eot_id|>");96 analysis.preserved_tokens.push_back("<|eom_id|>");97 analysis.preserved_tokens.push_back("<function=");98 analysis.preserved_tokens.push_back(">");99 analysis.preserved_tokens.push_back("</function>");100 LOG_DBG(ANSI_ORANGE "[Patch: Functionary 3.1]\n" ANSI_RESET);101 }102 },103 // DeepSeek-R1-Distill-Qwen104 [](const common_chat_template & tmpl, autoparser & analysis) -> void {105 if (tmpl.src.find(106 "{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>'") !=107 std::string::npos) {108 analysis.tools.format.section_start = "<|tool▁calls▁begin|>";109 analysis.tools.format.section_end = "<|tool▁calls▁end|>";110 analysis.tools.format.per_call_start = "<|tool▁call▁begin|>function";111 analysis.tools.function.name_prefix = "<|tool▁sep|>";112 analysis.tools.format.per_call_end = "<|tool▁call▁end|>";113 analysis.tools.function.close = "```";114 LOG_DBG(ANSI_ORANGE "[Patch: DeepSeek-R1-Distill-Qwen]\n" ANSI_RESET);115 }116 },117 // Nemotron Nano v2118 [](const common_chat_template & tmpl, autoparser & analysis) -> void {119 if (tmpl.src.find("<SPECIAL_10>") != std::string::npos && tmpl.src.find("<SPECIAL_11>") != std::string::npos &&120 tmpl.src.find("<SPECIAL_12>") != std::string::npos && tmpl.src.find("<TOOL_RESPONSE>") != std::string::npos) {121 122 analysis.tools.format.mode = tool_format::JSON_NATIVE;123 analysis.tools.format.section_start = "";124 analysis.tools.format.section_end = "";125 analysis.tools.format.per_call_start = "<TOOLCALL>";126 analysis.tools.format.per_call_end = "</TOOLCALL>";127 analysis.tools.format.tools_array_wrapped = true;128 analysis.content.mode = content_mode::PLAIN;129 analysis.content.start = "";130 analysis.content.end = "";131 analysis.reasoning.mode = reasoning_mode::TAG_BASED;132 analysis.reasoning.start = "<think>\n";133 analysis.reasoning.end = "</think>";134 analysis.assistant_start = "<SPECIAL_11>Assistant";135 analysis.user_start = "<SPECIAL_11>User";136 analysis.preserved_tokens.clear();137 analysis.preserved_tokens.push_back("<SPECIAL_11>");138 analysis.preserved_tokens.push_back("</think>");139 analysis.preserved_tokens.push_back("<TOOLCALL>");140 analysis.preserved_tokens.push_back("</TOOLCALL>");141 LOG_DBG(ANSI_ORANGE "[Patch: Nemotron Nano v2]\n" ANSI_RESET);142 }143 },144 // Fireworks145 [](const common_chat_template & tmpl, autoparser & analysis) -> void {146 if (tmpl.src.find("{%- set system_prompt = '<|start_header_id|>' + 'system' + '<|end_header_id|>\\n\\n'"147 " + message['content'] | trim + '\\n' + system_prompt_suffix + '<|eot_id|>' -%}") != std::string::npos) {148 analysis.assistant_start = "<|start_header_id|>assistant<|end_header_id|>";149 analysis.user_start = "<|start_header_id|>user<|end_header_id|>";150 LOG_DBG(ANSI_ORANGE "[Patch: Fireworks v2]\n" ANSI_RESET);151 }152 },153 // Solar Open154 [](const common_chat_template & tmpl, autoparser & analysis) -> void {155 if (tmpl.src.find("<|begin|>assistant<|think|><|end|>") != std::string::npos) {156 analysis.assistant_start = "<|begin|>assistant";157 LOG_DBG(ANSI_ORANGE "[Patch: Solar Open]\n" ANSI_RESET);158 }159 },160 // Apriel 1.6161 [](const common_chat_template & tmpl, autoparser & analysis) -> void {162 if (tmpl.src.find("if not loop.last and '[BEGIN FINAL RESPONSE]' in asst_text") != std::string::npos) {163 analysis.user_start = "<|begin_user|>";164 analysis.assistant_start = "<|begin_assistant|>";165 LOG_DBG(ANSI_ORANGE "[Patch: Apriel 1.6]\n" ANSI_RESET);166 }167 },168 // template uses the JSON {name, parameters} tool instruction, emits the OpenAI function wrapper169 [](const common_chat_template & tmpl, autoparser & analysis) -> void {170 if (tmpl.src.find("Respond in the format {\"name\": function name") != std::string::npos &&171 tmpl.src.find("Do not use variables.") != std::string::npos) {172 analysis.tools.format.openai_wrapper_trigger = true;173 LOG_DBG(ANSI_ORANGE "[Patch: JSON name/parameters tool instruction]\n" ANSI_RESET);174 }175 },176 // Laguna (poolside) - the v4 chat template renders reasoning and tool-arg177 // delimiters with formatting whitespace ("<think>\n", "</arg_value>\n") that178 // the model does not emit, so the inferred delimiters carry a spurious179 // newline and never match the model output. Trim to the bare tag. (v8180 // renders without the whitespace, so this is a no-op there.)181 [](const common_chat_template & tmpl, autoparser & analysis) -> void {182 if (tmpl.src.find("laguna_glm_thinking") != std::string::npos) {183 analysis.reasoning.start = trim_whitespace(analysis.reasoning.start);184 analysis.reasoning.end = trim_whitespace(analysis.reasoning.end);185 analysis.tools.arguments.value_prefix = trim_whitespace(analysis.tools.arguments.value_prefix);186 analysis.tools.arguments.value_suffix = trim_whitespace(analysis.tools.arguments.value_suffix);187 analysis.tools.arguments.separator = trim_whitespace(analysis.tools.arguments.separator);188 analysis.tools.arguments.tolerate_intertag_whitespace = true;189 // The CONTROL/eot </assistant> token only halts generation when emitted as the190 // single token; after tool calls the model can spell it out as text tokens.191 // A literal stop string catches it either way.192 analysis.additional_stops.push_back("</assistant>");193 LOG_DBG(ANSI_ORANGE "[Patch: Laguna]\n" ANSI_RESET);194 }195 },196 197 });198 199// Common JSON structures200static json params_schema = {201 { "type", "object" },202 { "properties",203 { { ARG_FIRST, { { "type", "string" }, { "description", "First argument" } } },204 { ARG_SECOND, { { "type", "string" }, { "description", "Second argument" } } } } },205 { "required", json::array({}) }206};207 208static json tools = json::array({209 { { "type", "function" },210 { "function",211 json{ { "name", FUN_FIRST }, { "description", "Test function foo" }, { "parameters", params_schema } } } },212 { { "type", "function" },213 { "function",214 json{ { "name", FUN_SECOND }, { "description", "Test function bar" }, { "parameters", params_schema } } } }215});216 217static json user_msg = json{218 { "role", "user" },219 { "content", USER_MSG }220};221 222static json build_tool_call(const std::string & name, const json & args, const std::string & id = CALL_ID_001) {223 return json{224 { "id", id },225 { "type", "function" },226 { "function", json{ { "name", name }, { "arguments", args } } }227 };228}229 230static json first_tool_call_zero_args = build_tool_call(FUN_FIRST, json::object(), CALL_ID_001);231static json first_tool_call_one_arg = build_tool_call(FUN_FIRST, {{ ARG_FIRST, "XXXX" }}, CALL_ID_001);232static json first_tool_call_one_arg_other_val = build_tool_call(FUN_FIRST, {{ ARG_FIRST, "YYYY" }}, CALL_ID_001);233static json first_tool_call_other_arg = build_tool_call(FUN_FIRST, {{ ARG_SECOND, "YYYY" }}, CALL_ID_001);234 235static json first_tool_call =236 build_tool_call(FUN_FIRST, json{{ ARG_FIRST, "XXXX" }, { ARG_SECOND, "YYYY" }}, CALL_ID_001);237static json second_tool_call =238 build_tool_call(FUN_SECOND, json{ { ARG_FIRST, "XXXX" }, { ARG_SECOND, "YYYY" }}, CALL_ID_002);239static json first_tool_call_alt_id =240 build_tool_call(FUN_FIRST, json{{ ARG_FIRST, "XXXX" }, { ARG_SECOND, "YYYY" }}, CALL_ID_999);241 242template <typename T>243static std::string mode_to_str(T mode) {244 std::ostringstream os;245 os << mode;246 return os.str();247}248 249void autoparser::analyze_template(const common_chat_template & tmpl) {250 jinja_caps = tmpl.original_caps();251 reasoning = analyze_reasoning(tmpl, jinja_caps.supports_tool_calls);252 content = analyze_content(tmpl, reasoning);253 tools = analyze_tools(jinja_caps.supports_tool_calls ? analyze_tools(tmpl, jinja_caps, reasoning) : analyze_tools());254 assistant_start = detect_assistant_start_marker(tmpl);255 user_start = detect_user_start_marker(tmpl);256 collect_preserved_tokens();257 258 for (auto & workaround : workarounds) {259 workaround(tmpl, *this);260 }261 262 LOG_DBG("\n--- Reasoning & Content Structure ---\n");263 LOG_DBG("user_msg_start: %s\n", user_start.c_str());264 LOG_DBG("assistant_msg_start: %s\n", assistant_start.c_str());265 LOG_DBG("reasoning_mode: %s\n", mode_to_str(reasoning.mode).c_str());266 LOG_DBG("reasoning_start: '%s'\n", reasoning.start.c_str());267 LOG_DBG("reasoning_end: '%s'\n", reasoning.end.c_str());268 LOG_DBG("content_mode: %s\n", mode_to_str(content.mode).c_str());269 LOG_DBG("content_start: '%s'\n", content.start.c_str());270 LOG_DBG("content_end: '%s'\n", content.end.c_str());271 272 LOG_DBG("\n--- Tool Call Structure ---\n");273 LOG_DBG("tool_mode: %s\n", mode_to_str(tools.format.mode).c_str());274 LOG_DBG("supports_tools: %s\n", jinja_caps.supports_tools ? "true" : "false");275 LOG_DBG("supports_parallel_calls: %s\n", jinja_caps.supports_parallel_tool_calls ? "true" : "false");276 LOG_DBG("tool_section_start: '%s'\n", tools.format.section_start.c_str());277 LOG_DBG("tool_section_end: '%s'\n", tools.format.section_end.c_str());278 LOG_DBG("per_call_start: '%s'\n", tools.format.per_call_start.c_str());279 LOG_DBG("per_call_end: '%s'\n", tools.format.per_call_end.c_str());280 LOG_DBG("func_name_prefix: '%s'\n", tools.function.name_prefix.c_str());281 LOG_DBG("func_name_suffix: '%s'\n", tools.function.name_suffix.c_str());282 LOG_DBG("func_args_separator: '%s'\n", tools.function.args_separator.c_str());283 LOG_DBG("func_close: '%s'\n", tools.function.close.c_str());284 LOG_DBG("call_id_prefix: '%s'\n", tools.call_id.prefix.c_str());285 LOG_DBG("call_id_suffix: '%s'\n", tools.call_id.suffix.c_str());286 LOG_DBG("call_id_pos: '%s'\n", mode_to_str(tools.call_id.pos).c_str());287 LOG_DBG("args_start: '%s'\n", tools.arguments.start.c_str());288 LOG_DBG("args_end: '%s'\n", tools.arguments.end.c_str());289 LOG_DBG("arg_name_prefix: '%s'\n", tools.arguments.name_prefix.c_str());290 LOG_DBG("arg_name_suffix: '%s'\n", tools.arguments.name_suffix.c_str());291 LOG_DBG("arg_value_prefix: '%s'\n", tools.arguments.value_prefix.c_str());292 LOG_DBG("arg_value_suffix: '%s'\n", tools.arguments.value_suffix.c_str());293 LOG_DBG("name_field: '%s'\n", tools.format.name_field.c_str());294 LOG_DBG("args_field: '%s'\n", tools.format.args_field.c_str());295 LOG_DBG("id_field: '%s'\n", tools.format.id_field.c_str());296 LOG_DBG("gen_id_field: '%s'\n", tools.format.gen_id_field.c_str());297 LOG_DBG("parameter_order: '%s'\n", std::accumulate(tools.format.parameter_order.begin(), tools.format.parameter_order.end(),298 std::string(""), [] (const std::string & a, const std::string & b) { return a.empty() ? b : a + ", " + b; }299 ).c_str());300 301 LOG_DBG(ANSI_PURPLE "=== Differential analysis complete ===\n" ANSI_RESET);302 analysis_complete = true;303}304 305void autoparser::collect_preserved_tokens() {306 auto add_token = [this](const std::string & org_token) {307 std::string token = trim_whitespace(org_token);308 if (!token.empty()) {309 // Avoid duplicates310 if (std::find(preserved_tokens.begin(), preserved_tokens.end(), token) == preserved_tokens.end()) {311 preserved_tokens.push_back(token);312 }313 }314 };315 316 add_token(reasoning.start);317 add_token(reasoning.end);318 add_token(content.start);319 add_token(content.end);320 add_token(tools.format.section_start);321 add_token(tools.format.section_end);322 add_token(tools.format.per_call_start);323 add_token(tools.format.per_call_end);324 add_token(tools.function.name_prefix);325 add_token(tools.function.name_suffix);326 add_token(tools.function.args_separator);327 add_token(tools.function.close);328 add_token(tools.arguments.start);329 add_token(tools.arguments.end);330 add_token(tools.arguments.name_prefix);331 add_token(tools.arguments.name_suffix);332 add_token(tools.arguments.separator);333 add_token(tools.arguments.value_prefix);334 add_token(tools.arguments.value_suffix);335 add_token(tools.call_id.prefix);336 add_token(tools.call_id.suffix);337}338 339std::string autoparser::detect_assistant_start_marker(const common_chat_template & tmpl) {340 json user_msg = json{341 { "role", "user" },342 { "content", USER_MSG }343 };344 345 json assistant_no_reasoning = json{346 { "role", "assistant" },347 { "content", ASSISTANT_MSG }348 };349 350 template_params params;351 params.messages = json::array({ user_msg });352 params.add_generation_prompt = false;353 params.enable_thinking = true;354 355 auto comparison = compare_variants(356 tmpl, params, [&](template_params & p) {357 p.messages = json::array({ user_msg, assistant_no_reasoning });358 }359 );360 361 if (!comparison) {362 LOG_DBG(ANSI_ORANGE "%s: Template application failed, skipping assistant start detection\n" ANSI_RESET, __func__);363 return "";364 }365 366 auto usermsg = comparison->diff.right;367 if (usermsg.find(ASSISTANT_MSG) == std::string::npos) {368 LOG_DBG(ANSI_ORANGE "%s: Did not find assistant message in assistant message block, skipping detection\n" ANSI_RESET, __func__);369 }370 371 auto ast_prefix = usermsg.substr(0, usermsg.find(ASSISTANT_MSG));372 if (!reasoning.start.empty() && ast_prefix.find(trim_whitespace(reasoning.start)) != std::string::npos) {373 ast_prefix = ast_prefix.substr(0, ast_prefix.find(trim_whitespace(reasoning.start)));374 }375 if (!reasoning.end.empty() && ast_prefix.find(trim_whitespace(reasoning.end)) != std::string::npos) {376 ast_prefix = ast_prefix.substr(0, ast_prefix.find(trim_whitespace(reasoning.end)));377 }378 return trim_whitespace(ast_prefix);379}380 381std::string autoparser::detect_user_start_marker(const common_chat_template & tmpl) {382 json user_msg = json{383 { "role", "user" },384 { "content", USER_MSG }385 };386 387 json assistant = json{388 { "role", "assistant" },389 { "content", ASSISTANT_MSG }390 };391 392 json user_msg_two = json{393 { "role", "user" },394 { "content", USER_MSG_TWO }395 };396 397 template_params params;398 params.messages = json::array({});399 params.add_generation_prompt = false;400 params.enable_thinking = true;401 402 auto comparison = compare_variants(403 tmpl, params, [&](template_params & p) {404 p.messages = json::array({ user_msg });405 }406 );407 408 if (!comparison) {409 LOG_DBG(ANSI_ORANGE "%s: Template application failed, unsupported empty messages? trying complex variant\n" ANSI_RESET, __func__);410 params.messages = json::array({ user_msg_two, assistant });411 comparison = compare_variants(412 tmpl, params, [&](template_params & p) {413 p.messages = json::array({ user_msg_two, assistant, user_msg });414 }415 );416 if (!comparison) {417 LOG_DBG(ANSI_ORANGE "%s: Template application failed for reserve variant, aborting\n" ANSI_RESET, __func__);418 return "";419 }420 }421 422 auto usermsg = comparison->diff.right;423 if (usermsg.find(USER_MSG) == std::string::npos) {424 LOG_DBG(ANSI_ORANGE "%s: Did not find user message in user message block, aborting detection\n" ANSI_RESET, __func__);425 }426 427 if (usermsg.find(ASSISTANT_MSG) != std::string::npos) {428 usermsg = usermsg.substr(usermsg.find(ASSISTANT_MSG) + ASSISTANT_MSG.size());429 }430 431 auto candidate = usermsg.substr(0, usermsg.find(USER_MSG));432 auto candidate_split = segmentize_markers(candidate);433 std::stringstream result;434 bool encountered_marker = false;435 for (const auto & mrk : candidate_split) {436 std::string lower_mrk = std::string(mrk.value);437 std::transform(lower_mrk.begin(), lower_mrk.end(), lower_mrk.begin(),438 [](unsigned char c) { return std::tolower(c); });439 // heuristic to weed out potential end markers, but only at the start440 if (mrk.type == segment_type::MARKER && !encountered_marker &&441 (lower_mrk.find("end") != std::string::npos || lower_mrk.find("close") != std::string::npos)) {442 continue;443 }444 if (mrk.type == segment_type::TEXT && !encountered_marker && trim_whitespace(mrk.value).empty()) {445 continue;446 }447 encountered_marker |= mrk.type == segment_type::MARKER;448 result << mrk.value;449 }450 return trim_whitespace(result.str());451}452 453analyze_reasoning::analyze_reasoning(const common_chat_template & tmpl, bool supports_tools)454 : analyze_base(tmpl) {455 LOG_DBG(ANSI_PURPLE "=== Starting differential analysis ===\n" ANSI_RESET);456 LOG_DBG(ANSI_ORANGE "Phase 1: Reasoning analysis\n" ANSI_RESET);457 458 compare_reasoning_presence();459 compare_thinking_enabled();460 if (supports_tools) {461 compare_reasoning_scope();462 }463}464 465void analyze_reasoning::compare_reasoning_presence() {466 json user_msg = json{467 { "role", "user" },468 { "content", USER_MSG }469 };470 471 json assistant_no_reasoning = json{472 { "role", "assistant" },473 { "content", ASSISTANT_MSG }474 };475 476 json assistant_with_reasoning = json{477 { "role", "assistant" },478 { "content", ASSISTANT_MSG },479 { "reasoning_content", THINKING_CONTENT }480 };481 482 template_params params;483 params.messages = json::array({ user_msg, assistant_no_reasoning });484 params.add_generation_prompt = false;485 params.enable_thinking = true;486 487 auto comparison = compare_variants(488 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_reasoning }); });489 490 if (!comparison) {491 LOG_DBG(ANSI_ORANGE "%s: Template application failed, skipping reasoning detection\n" ANSI_RESET, __func__);492 return;493 }494 495 const auto & diff = comparison->diff;496 497 const std::string reasoning_content = THINKING_CONTENT;498 499 if (!diff.right.empty() && diff.right.find(reasoning_content) != std::string::npos) {500 auto parser_delimiter = build_tagged_peg_parser([&](common_peg_parser_builder &p) {501 return p.literal(reasoning_content) + p.space() + p.optional(p.tag("post", (p.marker() + p.space())) + p.rest());502 });503 auto parser_wrapped = build_tagged_peg_parser([&](common_peg_parser_builder &p) {504 return p.tag("pre", p.marker() + p.space()) + p.literal(reasoning_content) + p.tag("post", (p.space() + p.marker() + p.space())) + p.rest();505 });506 // try the more aggressive parse first, if it fails, fall back to the delimiter one507 auto result = parser_wrapped.parse_anywhere_and_extract(comparison->output_B);508 if (!result.result.success()) {509 result = parser_delimiter.parse_anywhere_and_extract(comparison->output_B);510 }511 if (result.result.success()) {512 if (!result.tags["pre"].empty() && !result.tags["post"].empty()) {513 mode = reasoning_mode::TAG_BASED;514 start = result.tags["pre"];515 end = result.tags["post"];516 } else if (!result.tags["post"].empty()) {517 mode = reasoning_mode::TAG_BASED;518 end = result.tags["post"];519 }520 }521 }522}523 524void analyze_reasoning::compare_thinking_enabled() {525 json user_msg = json{526 { "role", "user" },527 { "content", USER_MSG }528 };529 530 template_params params;531 params.messages = json::array({ user_msg });532 params.add_generation_prompt = true;533 params.enable_thinking = false;534 535 auto comparison = compare_variants(*tmpl, params, [&](template_params & p) { p.enable_thinking = true; });536 537 if (!comparison) {538 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET , __func__);539 return;540 }541 542 const auto & diff = comparison->diff;543 544 std::string left_trimmed = trim_whitespace(diff.left);545 std::string right_trimmed = trim_whitespace(diff.right);546 547 if (left_trimmed.empty() && !diff.right.empty()) {548 if (!right_trimmed.empty() && string_ends_with(comparison->output_B, right_trimmed)) {549 if (start.empty()) {550 start = diff.right;551 mode = reasoning_mode::TAG_BASED;552 }553 }554 } else if (right_trimmed.empty() && !diff.left.empty()) {555 if (!left_trimmed.empty() && string_ends_with(comparison->output_A, left_trimmed)) {556 if (end.empty()) {557 auto seg = prune_whitespace_segments(segmentize_markers(comparison->output_A));558 if (seg.size() >= 2 && seg[seg.size() - 1].value == left_trimmed && seg[seg.size() - 2].type == segment_type::MARKER) {559 start = seg[seg.size() - 2].value;560 }561 end = diff.left;562 mode = reasoning_mode::TAG_BASED;563 }564 }565 } else if (!left_trimmed.empty() && !right_trimmed.empty()) {566 // Full-output diff is noisy (e.g., SmolLM3 changes the system message when enable_thinking flips).567 // Try to find reasoning markers by tail-anchoring:568 // one output's generation prompt tail may appear in the other with extra reasoning markers appended.569 const auto & output_A = comparison->output_A;570 const auto & output_B = comparison->output_B;571 const size_t anchor_len = 64;572 573 for (int dir = 0; dir < 2; dir++) {574 const auto & base = dir == 0 ? output_B : output_A;575 const auto & extended = dir == 0 ? output_A : output_B;576 577 size_t len = std::min(base.size(), anchor_len);578 std::string anchor = base.substr(base.size() - len);579 auto pos = extended.rfind(anchor);580 if (pos == std::string::npos || pos + len >= extended.size()) {581 continue;582 }583 584 std::string extra = trim_whitespace(extended.substr(pos + len));585 if (extra.empty()) {586 continue;587 }588 589 auto seg = prune_whitespace_segments(segmentize_markers(extra));590 if (seg.size() == 2 && seg[0].type == segment_type::MARKER && seg[1].type == segment_type::MARKER) {591 if (start.empty()) {592 start = seg[0].value;593 }594 if (end.empty()) {595 end = seg[1].value;596 }597 mode = reasoning_mode::TAG_BASED;598 break;599 }600 }601 }602 603 if (mode == reasoning_mode::NONE && start.empty() && !end.empty()) {604 mode = reasoning_mode::TAG_BASED;605 }606}607 608void analyze_reasoning::compare_reasoning_scope() {609 json assistant_reasoning_content = json{610 { "role", "assistant" },611 { "content", ASSISTANT_MSG },612 { "reasoning_content", THINKING_CONTENT }613 };614 615 json assistant_reasoning_tools = json{616 { "role", "assistant" },617 { "content", nullptr },618 { "reasoning_content", THINKING_CONTENT },619 { "tool_calls",620 json::array({ build_tool_call(FUN_FIRST, json{ { ARG_FIRST, "VVVV" }, { ARG_SECOND, "XXXX" } }) }) }621 };622 623 template_params params;624 params.messages = json::array({ user_msg, assistant_reasoning_content });625 params.tools = tools;626 params.add_generation_prompt = false;627 params.enable_thinking = true;628 629 auto comparison = compare_variants(630 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_reasoning_tools }); });631 632 if (!comparison) {633 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);634 return;635 }636 637 std::string reasoning_content = THINKING_CONTENT;638 639 // Check if reasoning only appears in variant B (with tools)640 bool reasoning_in_A = comparison->output_A.find(reasoning_content) != std::string::npos;641 bool reasoning_in_B = comparison->output_B.find(reasoning_content) != std::string::npos;642 643 if (!reasoning_in_A && reasoning_in_B) {644 mode = reasoning_mode::TOOLS_ONLY;645 LOG_DBG(ANSI_ORANGE "%s: Detected TOOLS_ONLY reasoning mode\n" ANSI_RESET, __func__);646 647 auto parser_wrapped = build_tagged_peg_parser([&](common_peg_parser_builder &p) {648 return p.tag("pre", p.marker() + p.space()) + p.literal(reasoning_content) + p.space() + p.tag("post", (p.marker() + p.space()));649 });650 auto result = parser_wrapped.parse_anywhere_and_extract(comparison->output_B);651 if (result.result.success()) {652 start = result.tags["pre"];653 end = result.tags["post"];654 } else {655 auto parser_delimiter = build_tagged_peg_parser([&](common_peg_parser_builder &p) {656 return p.literal(reasoning_content) + p.space() + p.optional(p.tag("post", (p.marker() + p.space())));657 });658 result = parser_delimiter.parse_anywhere_and_extract(comparison->output_B);659 if (result.result.success()) {660 end = result.tags["post"];661 } else {662 LOG_DBG(ANSI_ORANGE "%s: Unable to extract reasoning markers, falling back to reasoning = NONE\n" ANSI_RESET, __func__);663 mode = reasoning_mode::NONE;664 }665 }666 }667}668 669analyze_content::analyze_content(const common_chat_template & tmpl, const analyze_reasoning & reasoning)670 : analyze_base(tmpl) {671 LOG_DBG(ANSI_ORANGE "Phase 2: Content analysis\n" ANSI_RESET);672 673 json assistant_content_only = json{674 { "role", "assistant" },675 { "content", ASSISTANT_MSG }676 };677 678 json assistant_with_tools = json{679 { "role", "assistant" },680 { "content", "" },681 { "tool_calls", json::array({ build_tool_call("test_func", json{ { "arg1", "value1" } }) }) }682 };683 684 json assistant_with_reasoning = json{685 { "role", "assistant" },686 { "content", "" },687 { "reasoning_content", THINKING_CONTENT }688 };689 690 template_params params_content_only;691 params_content_only.messages = json::array({ user_msg, assistant_content_only });692 params_content_only.add_generation_prompt = false;693 params_content_only.enable_thinking = true;694 params_content_only.tools = tools;695 696 auto comparison_with_tools = compare_variants(tmpl, params_content_only, [&](template_params & p) {697 p.messages = json::array({ user_msg, assistant_with_tools });698 });699 700 auto comparison_with_reasoning = compare_variants(tmpl, params_content_only, [&](template_params & p) {701 p.messages = json::array({ user_msg, assistant_with_reasoning });702 });703 704 if (!comparison_with_tools || !comparison_with_reasoning) {705 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);706 return;707 }708 709 const auto & diff_tools = comparison_with_tools->diff;710 const auto & diff_reasoning = comparison_with_reasoning->diff;711 712 std::string response = ASSISTANT_MSG;713 714 bool found_plain_content = false;715 if (trim_whitespace(diff_tools.left) == response) {716 auto parser = build_tagged_peg_parser([&](common_peg_parser_builder & p) {717 return p.space() + diff_reasoning.left + p.space() + p.optional(p.marker()) + p.space() + p.end();718 });719 if (parser.parse_and_extract(diff_reasoning.left).result.success()) {720 // We only have the content text in the diff (possibly with a stray EOG marker), so no markers721 mode = content_mode::PLAIN;722 found_plain_content = true;723 } else if (reasoning.mode != reasoning_mode::NONE && !reasoning.end.empty()) {724 auto post_reasoning_parser = build_tagged_peg_parser([&](common_peg_parser_builder & p) {725 return p.literal(reasoning.end) + p.space() + p.literal(response);726 });727 if (post_reasoning_parser.parse_anywhere_and_extract(diff_reasoning.left).result.success()) {728 mode = content_mode::PLAIN;729 found_plain_content = true;730 }731 }732 }733 if (!found_plain_content) {734 std::string rdiff = diff_reasoning.left;735 if (!reasoning.end.empty() && rdiff.find(reasoning.end) != std::string::npos) {736 rdiff = rdiff.substr(rdiff.find(reasoning.end) + reasoning.end.length());737 }738 // Take the more promising diff739 std::string pure_content = rdiff.length() > diff_tools.left.length() ? rdiff : diff_tools.left;740 auto parser_wrapped = build_tagged_peg_parser([&](common_peg_parser_builder &p) {741 return p.tag("pre", p.marker() + p.space()) + p.literal(response) + p.space() + p.tag("post", (p.marker() + p.space())) + p.rest();742 });743 auto result = parser_wrapped.parse_anywhere_and_extract(pure_content);744 start = result.tags["pre"];745 end = result.tags["post"];746 // TODO: WRAPPED_WITH_REASONING747 }748 749 // Determine content mode750 if (!start.empty() || !end.empty()) {751 mode = content_mode::ALWAYS_WRAPPED;752 // TODO: END_DELIMITED content mode - delimited at end but not at start?753 }754}755 756bool analyze_content::is_always_wrapped() const {757 return mode == content_mode::ALWAYS_WRAPPED && !start.empty() && !end.empty();758}759 760analyze_tools::analyze_tools(const common_chat_template & tmpl,761 const jinja::caps & caps,762 const analyze_reasoning & reasoning)763 : analyze_base(tmpl) {764 LOG_DBG(ANSI_ORANGE "Phase 3: Tool call analysis\n" ANSI_RESET);765 766 analyze_tool_calls(reasoning, caps.supports_parallel_tool_calls);767 768 if (format.mode != tool_format::NONE && format.mode != tool_format::JSON_NATIVE) {769 if (caps.supports_parallel_tool_calls) {770 check_per_call_markers();771 }772 LOG_DBG(ANSI_ORANGE "Phase 3a: Function call analysis\n" ANSI_RESET);773 extract_function_markers();774 LOG_DBG(ANSI_ORANGE "Phase 3b: Argument analysis\n" ANSI_RESET);775 if (format.mode == tool_format::TAG_WITH_TAGGED) {776 analyze_arguments();777 }778 extract_argument_separator();779 extract_args_markers();780 LOG_DBG(ANSI_ORANGE "Phase 3c: Call id analysis\n" ANSI_RESET);781 extract_call_id_markers();782 }783}784 785void analyze_tools::analyze_tool_calls(const analyze_reasoning & reasoning, bool supports_parallel_tool_calls) {786 json assistant_no_tools = json{787 { "role", "assistant" },788 { "content", ASSISTANT_MSG }789 };790 791 json assistant_with_tools = json{792 { "role", "assistant" },793 { "content", "" },794 { "tool_calls", json::array({ first_tool_call }) }795 };796 797 template_params params;798 params.messages = json::array({ user_msg, assistant_no_tools });799 params.tools = tools;800 params.add_generation_prompt = false;801 params.enable_thinking = true;802 803 auto comparison = compare_variants(804 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_with_tools }); });805 806 if (!comparison) {807 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);808 return;809 }810 811 const auto & diff = comparison->diff;812 813 std::string tool_section = diff.right;814 815 if (tool_section.empty()) {816 return;817 }818 819 analyze_tool_call_format(tool_section, FUN_FIRST, ARG_FIRST, reasoning, supports_parallel_tool_calls);820}821 822void analyze_tools::analyze_tool_call_format(const std::string & haystack,823 const std::string & fun_name_needle,824 const std::string & arg_name_needle,825 const analyze_reasoning & reasoning,826 bool supports_parallel_tool_calls) {827 if (fun_name_needle.empty() || arg_name_needle.empty() || haystack.empty()) {828 return;829 }830 831 auto in_json_haystack = [&haystack](const std::string & needle) -> bool {832 auto parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {833 return p.choice({ p.literal("{"), p.literal(":") }) << p.choice({834 p.tag("dq", p.literal("\"") + p.literal(needle) + p.literal("\"")) });835 });836 auto result = parser.parse_anywhere_and_extract(haystack);837 return result.result.success();838 };839 840 auto fun_quote = in_json_haystack(fun_name_needle);841 auto arg_quote = in_json_haystack(arg_name_needle);842 843 if (fun_quote) {844 // no need to check further, we're in JSON land845 format.mode = tool_format::JSON_NATIVE;846 } else if (arg_quote) {847 format.mode = tool_format::TAG_WITH_JSON;848 } else {849 format.mode = tool_format::TAG_WITH_TAGGED;850 }851 852 // first, remove any reasoning markers853 std::string clean_haystack = haystack;854 if (!reasoning.start.empty()) {855 auto pos = haystack.find(reasoning.start);856 if (pos != std::string::npos) {857 clean_haystack = haystack.substr(0, pos) + haystack.substr(pos + reasoning.start.length());858 }859 }860 if (!reasoning.end.empty()) {861 auto pos = clean_haystack.find(reasoning.end);862 if (pos != std::string::npos) {863 clean_haystack = clean_haystack.substr(0, pos) + clean_haystack.substr(pos + reasoning.end.length());864 }865 }866 867 if (format.mode == tool_format::JSON_NATIVE) {868 analyze_tool_call_format_json_native(clean_haystack, fun_name_needle, arg_name_needle);869 if (supports_parallel_tool_calls) {870 analyze_json_native_parallel_calls();871 }872 } else {873 analyze_tool_call_format_non_json(clean_haystack, fun_name_needle);874 }875 // always relax whitespace requirements on ending markers since they don't influence content876 format.section_end = trim_whitespace(format.section_end);877 format.per_call_end = trim_whitespace(format.per_call_end);878}879 880void analyze_tools::analyze_json_native_parallel_calls() {881 json assistant_one_tool = json{882 { "role", "assistant" },883 { "content", "" },884 { "tool_calls", json::array({ first_tool_call }) }885 };886 887 json assistant_two_tools = json{888 { "role", "assistant" },889 { "content", "" },890 { "tool_calls", json::array({ first_tool_call, second_tool_call }) }891 };892 893 template_params params;894 params.messages = json::array({ user_msg, assistant_one_tool });895 params.tools = tools;896 params.add_generation_prompt = false;897 params.enable_thinking = true;898 899 auto comparison = compare_variants(900 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_two_tools }); });901 902 if (!comparison) {903 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);904 return;905 }906 907 std::string & second_call = comparison->diff.right;908 if (!format.section_start.empty() && second_call.find(format.section_start) != std::string::npos) {909 format.per_call_start = format.section_start;910 format.per_call_end = format.section_end;911 format.section_start.clear();912 format.section_end.clear();913 }914}915 916void analyze_tools::analyze_tool_call_format_json_native(const std::string & clean_haystack,917 const std::string & fun_name_needle,918 const std::string & arg_name_needle) {919 // we might not have the typical OpenAI tool calling structure920 int json_start = clean_haystack.find_first_of('{');921 int json_end = clean_haystack.find_last_of('}');922 std::string cut = clean_haystack.substr(json_start, json_end - json_start + 1);923 json call_struct = json::parse(cut);924 auto register_field = [&](const std::string & prefix, const nlohmann::detail::iteration_proxy_value<json::iterator> & subel) {925 if (subel.value().is_string() && std::string(subel.value()).find("call0000") != std::string::npos) {926 format.id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();927 } else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) {928 format.name_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();929 } else if (subel.value().dump().find(arg_name_needle) !=930 std::string::npos) { // handle both string and JSON obj variants931 format.args_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();932 } else if (subel.key().find("id") != std::string::npos) {933 // heuristics for generated id field934 format.gen_id_field = !prefix.empty() ? prefix + "." + subel.key() : subel.key();935 }936 };937 for (const auto & el : call_struct.items()) {938 if (el.key() == fun_name_needle) {939 format.fun_name_is_key = true;940 // When function name is the key, there's no name field and args are direct941 format.name_field.clear();942 format.args_field.clear();943 // Don't register this element - the function name IS the key, not a field944 } else {945 if (el.value().is_object() &&946 el.value().dump().find(arg_name_needle) == std::string::npos) { // not the args object947 format.function_field = el.key();948 for (const auto & subel : el.value().items()) {949 register_field(el.key(), subel);950 }951 }952 // Register this element as a potential field953 register_field("", el);954 }955 }956 auto array_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {957 return p.tag("pre", p.literal("[") + p.space()) + p.literal(cut) + p.tag("post", p.space() + p.literal("]"));958 });959 960 auto ar_parse_res = array_parser.parse_anywhere_and_extract(clean_haystack);961 if (ar_parse_res.result.success()) {962 format.tools_array_wrapped = true;963 json_start -= ar_parse_res.tags["pre"].length();964 json_end += ar_parse_res.tags["post"].length();965 }966 json_end++; // we want to move past the closing char for end marker extraction967 968 std::vector<std::pair<size_t, std::string>> located_params;969 if (!format.name_field.empty()) {970 located_params.push_back({ clean_haystack.find(format.name_field), format.name_field });971 }972 if (!format.args_field.empty()) {973 located_params.push_back({ clean_haystack.find(format.args_field), format.args_field });974 }975 if (!format.id_field.empty()) {976 located_params.push_back({ clean_haystack.find(format.id_field), format.id_field });977 }978 if (!format.gen_id_field.empty()) {979 located_params.push_back({ clean_haystack.find(format.gen_id_field), format.gen_id_field });980 }981 std::sort(located_params.begin(), located_params.end());982 for (auto & pair : located_params) {983 format.parameter_order.push_back(pair.second);984 }985 // we can immediately extract tool calling markers too986 format.section_start = trim_leading_whitespace(clean_haystack.substr(0, json_start));987 format.section_end = trim_whitespace(clean_haystack.substr(json_end));988 // When tools_array_wrapped is true, the closing bracket is part of the array structure,989 // not a separate section end marker. Clear tool_section_end to avoid duplicate brackets.990 if (format.tools_array_wrapped && format.section_end == "]") {991 format.section_end.clear();992 }993}994 995void analyze_tools::analyze_tool_call_format_non_json(const std::string & clean_haystack,996 const std::string & fun_name_needle) {997 // first, let's find out if the function is inside a tag or standalone998 auto fun_marker_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {999 return p.tag("fun_marker", p.choice({1000 p.tag("fun_pre", p.literal("<") + p.until_one_of({ ">", fun_name_needle })) + p.literal(fun_name_needle) +1001 p.tag("fun_post", p.negate(p.space() + p.literal("<")) + p.until(">") + p.literal(">")) + p.space(),1002 p.tag("fun_pre", p.literal("[") + p.until_one_of({ "]", fun_name_needle })) + p.literal(fun_name_needle) +1003 p.tag("fun_post", p.negate(p.space() + p.literal("[") + p.until("]") + p.literal("]")) + p.space()) }));1004 });1005 auto fun_res = fun_marker_parser.parse_anywhere_and_extract(clean_haystack);1006 std::string fun_marker = fun_name_needle;1007 if (fun_res.result.success()) {1008 fun_marker = fun_res.tags["fun_marker"];1009 }1010 // now, consume up to two markers, then treat everything up to the function marker as function name prefix1011 auto per_tool_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1012 return p.tag("sec_start", p.marker() + p.space()) + p.tag("call_start", p.marker() + p.space()) +1013 p.tag("fun_pre", p.until(fun_marker)) + fun_marker + p.tag("rest", p.rest());1014 });1015 auto section_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1016 return p.tag("sec_start", p.marker() + p.space()) + fun_marker + p.tag("rest", p.rest());1017 });1018 auto result = per_tool_parser.parse_anywhere_and_extract(clean_haystack);1019 tagged_parse_result result_end;1020 if (result.result.success()) {1021 auto double_closer_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1022 return p.tag("call_end", p.marker() + p.space()) + p.tag("sec_end", p.marker() + p.space()) + p.end();1023 });1024 result_end = double_closer_parser.parse_anywhere_and_extract(result.tags["rest"]);1025 function.name_prefix = fun_res.tags["fun_pre"] + function.name_prefix;1026 } else {1027 result = section_parser.parse_anywhere_and_extract(clean_haystack);1028 auto single_closer_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1029 return p.tag("sec_end", p.marker() + p.space()) + p.end();1030 });1031 result_end = single_closer_parser.parse_anywhere_and_extract(result.tags["rest"]);1032 }1033 format.per_call_start = result.tags["call_start"];1034 format.per_call_end = result_end.tags["call_end"];1035 format.section_start = result.tags["sec_start"];1036 format.section_end = result_end.tags["sec_end"];1037}1038 1039void analyze_tools::check_per_call_markers() {1040 json assistant_one_tool = json{1041 { "role", "assistant" },1042 { "content", "" },1043 { "tool_calls", json::array({ first_tool_call }) }1044 };1045 1046 json assistant_two_tools = json{1047 { "role", "assistant" },1048 { "content", "" },1049 { "tool_calls", json::array({ first_tool_call, second_tool_call }) }1050 };1051 1052 template_params params;1053 params.messages = json::array({ user_msg, assistant_one_tool });1054 params.tools = tools;1055 params.add_generation_prompt = false;1056 params.enable_thinking = true;1057 1058 auto one_vs_two = compare_variants(1059 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_two_tools }); });1060 1061 if (!one_vs_two) {1062 LOG_DBG(ANSI_ORANGE "%s: Generating double tool call comparison failed\n" ANSI_RESET, __func__);1063 return;1064 }1065 1066 diff_split filter_common_call_part = calculate_diff_split(one_vs_two->diff.suffix, one_vs_two->diff.right);1067 1068 std::string second_tool_content = trim_leading_whitespace(filter_common_call_part.right);1069 if (!format.section_start.empty() &&1070 second_tool_content.find(format.section_start) == 0) {1071 format.per_call_start = format.section_start;1072 format.per_call_end = format.section_end;1073 format.section_start.clear();1074 format.section_end.clear();1075 }1076 1077 if (!format.per_call_end.empty()) {1078 auto count_occurrences = [](const std::string & haystack, const std::string & needle) {1079 size_t count = 0;1080 for (size_t pos = haystack.find(needle); pos != std::string::npos;1081 pos = haystack.find(needle, pos + needle.size())) {1082 count++;1083 }1084 return count;1085 };1086 size_t calls_one = count_occurrences(one_vs_two->output_A, format.per_call_end);1087 size_t calls_two = count_occurrences(one_vs_two->output_B, format.per_call_end);1088 if (calls_one > 0 && calls_one == calls_two) {1089 format.section_end = format.per_call_end;1090 format.per_call_end.clear();1091 }1092 }1093}1094 1095void analyze_tools::extract_function_markers() {1096 json assistant_nocall = json{1097 { "role", "assistant" },1098 { "content", ASSISTANT_MSG },1099 };1100 1101 json assistant_foofoo = json{1102 { "role", "assistant" },1103 { "content", "" },1104 { "tool_calls", json::array({ first_tool_call }) }1105 };1106 1107 json assistant_barbar = json{1108 { "role", "assistant" },1109 { "content", "" },1110 { "tool_calls", json::array({ second_tool_call }) }1111 };1112 1113 template_params params;1114 params.messages = json::array({ user_msg, assistant_foofoo });1115 params.tools = tools;1116 params.add_generation_prompt = false;1117 params.enable_thinking = true;1118 1119 auto comparison = compare_variants(1120 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_barbar }); });1121 1122 if (!comparison) {1123 LOG_DBG(ANSI_ORANGE "%s: Template application failed\n" ANSI_RESET, __func__);1124 return;1125 }1126 1127 const auto & diff = comparison->diff;1128 1129 if (diff.left.find(FUN_FIRST) != std::string::npos && diff.right.find(FUN_SECOND) != std::string::npos) {1130 std::string prefix_marker;1131 if (!format.per_call_start.empty()) {1132 prefix_marker = format.per_call_start;1133 } else {1134 prefix_marker = format.section_start;1135 }1136 if (!prefix_marker.empty() && diff.prefix.rfind(prefix_marker) != std::string::npos) {1137 function.name_prefix =1138 diff.prefix.substr(diff.prefix.rfind(prefix_marker) + prefix_marker.size());1139 }1140 1141 // Extract name prefix/suffix from diff.left (stop at the next marker boundary)1142 auto name_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1143 return p.tag("pre", p.until(FUN_FIRST)) + p.literal(FUN_FIRST) +1144 p.tag("post", p.zero_or_more(p.negate(p.marker()) + p.any()));1145 });1146 auto name_result = name_parser.parse_and_extract(diff.left);1147 if (name_result.result.success()) {1148 function.name_prefix += name_result.tags["pre"];1149 function.name_suffix = name_result.tags["post"];1150 }1151 1152 // Extend name_suffix with content from diff.suffix before args begin1153 if (format.mode == tool_format::TAG_WITH_JSON) {1154 // For JSON: name_suffix extends to the first non-marker { or [, including any1155 // markers along the way. Only applies if there's at least one marker after1156 // the JSON content (matching the original "stop < seg_suf.size() - 1" guard).1157 auto suffix_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1158 auto non_json = p.marker() | (p.negate(p.literal("{")) + p.negate(p.literal("[")) + p.any());1159 auto after_json = p.zero_or_more(p.negate(p.marker()) + p.any()) + p.marker();1160 return p.tag("ext", p.zero_or_more(non_json)) + after_json;1161 });1162 auto suf_result = suffix_parser.parse_and_extract(diff.suffix);1163 if (suf_result.result.success()) {1164 function.name_suffix += suf_result.tags["ext"];1165 }1166 } else {1167 // For tagged: name_suffix extends to the first marker (arg marker)1168 auto suffix_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1169 return p.tag("ext", p.zero_or_more(p.negate(p.marker()) + p.any()));1170 });1171 auto suf_result = suffix_parser.parse_and_extract(diff.suffix);1172 if (suf_result.result.success()) {1173 function.name_suffix += suf_result.tags["ext"];1174 1175 auto arg_start = [&](common_peg_parser_builder &p) {1176 return p.marker() + p.space() + p.choice({ p.literal(ARG_FIRST), p.literal(ARG_SECOND) });1177 };1178 auto sep_parser = build_tagged_peg_parser([&](common_peg_parser_builder &p) {1179 return p.tag("sep", p.zero_or_more(p.negate(arg_start(p)) + p.any())) + arg_start(p);1180 });1181 auto sep_result = sep_parser.parse_and_extract(diff.suffix.substr(suf_result.tags["ext"].size()));1182 if (sep_result.result.success()) {1183 function.args_separator = trim_whitespace(sep_result.tags["sep"]);1184 }1185 }1186 }1187 1188 // Extract the closer (between last arg and call/section end marker)1189 std::string suffix_marker;1190 if (!format.per_call_end.empty()) {1191 suffix_marker = format.per_call_end;1192 } else {1193 suffix_marker = format.section_end;1194 }1195 std::string closer_suffix;1196 if (suffix_marker.empty()) {1197 // we'll have to rely on an extra diff with no-calls version1198 auto notool_comp = compare_variants(1199 *tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_nocall }); });1200 if (notool_comp) {