From 0d4179c8aa1cf122032242d3f7f76dcf44192ce0 Mon Sep 17 00:00:00 2001 From: Piotr Wilkin Date: Wed, 21 Jan 2026 14:28:18 +0100 Subject: [PATCH] ANOTHER GIANT POST-FIXUP SQUISH --- 1 | 12 + CMakePresets.json | 421 +- common/CMakeLists.txt | 3 +- common/chat-auto-parser-analyzer.cpp | 1461 ---- common/chat-auto-parser-generator.cpp | 553 +- common/chat-auto-parser-helpers.cpp | 1647 +--- common/chat-auto-parser-helpers.h | 139 +- common/chat-auto-parser.h | 181 +- common/chat-diff-analyzer.cpp | 1670 ++++ common/chat-diff-analyzer.h | 347 + common/chat-peg-parser.cpp | 931 +- common/chat-peg-parser.h | 63 +- common/chat.cpp | 176 +- common/chat.h | 9 +- common/jinja/caps.cpp | 111 +- common/jinja/caps.h | 1 - common/json-schema-to-grammar.cpp | 148 +- common/peg-parser.cpp | 127 +- common/peg-parser.h | 28 + docs/autoparser.md | 521 +- ...AI-c4ai-command-r7b-12-2024-tool_use.jinja | 2 +- ...seek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja | 2 +- template.ans | 7774 +++++++++++++++++ tests/CMakeLists.txt | 2 + tests/peg-parser/test-python-dict-parser.cpp | 279 + tests/peg-parser/tests.h | 1 + tests/test-chat-auto-parser.cpp | 1845 ++++ tests/test-chat.cpp | 870 +- tests/test-peg-parser.cpp | 1 + tools/parser/CMakeLists.txt | 9 + tools/parser/debug-template-parser.cpp | 153 +- tools/parser/template-analysis.cpp | 610 ++ 32 files changed, 15068 insertions(+), 5029 deletions(-) create mode 100644 1 delete mode 100644 common/chat-auto-parser-analyzer.cpp create mode 100644 common/chat-diff-analyzer.cpp create mode 100644 common/chat-diff-analyzer.h create mode 100644 template.ans create mode 100644 tests/peg-parser/test-python-dict-parser.cpp create mode 100644 tests/test-chat-auto-parser.cpp create mode 100644 tools/parser/template-analysis.cpp diff --git a/1 b/1 new file mode 100644 index 0000000000..b77756c404 --- /dev/null +++ b/1 @@ -0,0 +1,12 @@ +Unknown option: 2 +Usage: llama-template-analysis [options] + +Options: + --template Analyze specific template from test suite (e.g., 'deepseek' or 'DeepSeek-V3.1') + --template-file Analyze custom template file + --all Analyze all templates from test suite + +Examples: + llama-template-analysis --all + llama-template-analysis --template deepseek + llama-template-analysis --template-file my-template.jinja diff --git a/CMakePresets.json b/CMakePresets.json index b5afeb3c0f..accdd72d18 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,95 +1,332 @@ { - "version": 4, - "configurePresets": [ - { - "name": "base", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/build-${presetName}", - "cacheVariables": { - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_INSTALL_RPATH": "$ORIGIN;$ORIGIN/.." + "version": 4, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_INSTALL_RPATH": "$ORIGIN;$ORIGIN/.." + } + }, + { + "name": "sycl-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_COMPILER": "icx", + "CMAKE_C_COMPILER": "cl", + "GGML_SYCL": "ON", + "CMAKE_INSTALL_RPATH": "$ORIGIN;$ORIGIN/.." + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "reldbg", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "static", + "hidden": true, + "cacheVariables": { + "GGML_STATIC": "ON" + } + }, + { + "name": "sycl_f16", + "hidden": true, + "cacheVariables": { + "GGML_SYCL_F16": "ON" + } + }, + { + "name": "vulkan", + "hidden": true, + "cacheVariables": { + "GGML_VULKAN": "ON" + } + }, + { + "name": "x64-windows-llvm", + "hidden": true, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/x64-windows-llvm.cmake" + } + }, + { + "name": "arm64-windows-llvm", + "hidden": true, + "architecture": { + "value": "arm64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm64-windows-llvm.cmake" + } + }, + { + "name": "arm64-apple-clang", + "hidden": true, + "architecture": { + "value": "arm64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm64-apple-clang.cmake" + } + }, + { + "name": "x64-linux-gcc", + "hidden": true, + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "x64-linux-gcc-debug", + "inherits": [ + "base", + "x64-linux-gcc", + "debug" + ] + }, + { + "name": "x64-linux-gcc-release", + "inherits": [ + "base", + "x64-linux-gcc", + "release" + ] + }, + { + "name": "x64-linux-gcc-reldbg", + "inherits": [ + "base", + "x64-linux-gcc", + "reldbg" + ] + }, + { + "name": "x64-linux-gcc+static-release", + "inherits": [ + "base", + "x64-linux-gcc", + "release", + "static" + ] + }, + { + "name": "arm64-windows-llvm-debug", + "inherits": [ + "base", + "arm64-windows-llvm", + "debug" + ] + }, + { + "name": "arm64-windows-llvm-release", + "inherits": [ + "base", + "arm64-windows-llvm", + "reldbg" + ] + }, + { + "name": "arm64-windows-llvm+static-release", + "inherits": [ + "base", + "arm64-windows-llvm", + "reldbg", + "static" + ] + }, + { + "name": "arm64-apple-clang-debug", + "inherits": [ + "base", + "arm64-apple-clang", + "debug" + ] + }, + { + "name": "arm64-apple-clang-release", + "inherits": [ + "base", + "arm64-apple-clang", + "reldbg" + ] + }, + { + "name": "arm64-apple-clang+static-release", + "inherits": [ + "base", + "arm64-apple-clang", + "reldbg", + "static" + ] + }, + { + "name": "x64-windows-llvm-debug", + "inherits": [ + "base", + "x64-windows-llvm", + "debug" + ] + }, + { + "name": "x64-windows-llvm-release", + "inherits": [ + "base", + "x64-windows-llvm", + "release" + ] + }, + { + "name": "x64-windows-llvm-reldbg", + "inherits": [ + "base", + "x64-windows-llvm", + "reldbg" + ] + }, + { + "name": "x64-windows-llvm+static-release", + "inherits": [ + "base", + "x64-windows-llvm", + "reldbg", + "static" + ] + }, + { + "name": "x64-windows-msvc-debug", + "inherits": [ + "base", + "debug" + ] + }, + { + "name": "x64-windows-msvc-release", + "inherits": [ + "base", + "reldbg" + ] + }, + { + "name": "x64-windows-msvc+static-release", + "inherits": [ + "base", + "reldbg", + "static" + ] + }, + { + "name": "x64-windows-sycl-debug", + "inherits": [ + "sycl-base", + "debug" + ] + }, + { + "name": "x64-windows-sycl-debug-f16", + "inherits": [ + "sycl-base", + "debug", + "sycl_f16" + ] + }, + { + "name": "x64-windows-sycl-release", + "inherits": [ + "sycl-base", + "release" + ] + }, + { + "name": "x64-windows-sycl-release-f16", + "inherits": [ + "sycl-base", + "release", + "sycl_f16" + ] + }, + { + "name": "x64-windows-vulkan-debug", + "inherits": [ + "base", + "vulkan", + "debug" + ] + }, + { + "name": "x64-windows-vulkan-release", + "inherits": [ + "base", + "vulkan", + "release" + ] + }, + { + "name": "ilintar-release", + "hidden": false, + "description": "Release build", + "displayName": "Release build", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "GGML_CUDA": "ON", + "GGML_CUDA_FORCE_CUBLAS": "OFF", + "GGML_CUDA_FORCE_MMQ": "OFF", + "GGML_CUDA_FA_ALL_QUANTS": "1", + "CMAKE_CUDA_ARCHITECTURES": "86;120", + "GGML_BLAS": "ON", + "GGML_BLAS_VENDOR": "OpenBLAS", + "GGML_CPU_ALL_VARIANTS": "ON", + "GGML_BACKEND_DL": "ON", + "CMAKE_CUDA_COMPILER": "nvcc" + }, + "inherits": [ + "base", + "release", + "x64-linux-gcc-release" + ] } - }, - { - "name": "sycl-base", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/build-${presetName}", - "cacheVariables": { - "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", - "CMAKE_CXX_COMPILER": "icx", - "CMAKE_C_COMPILER": "cl", - "GGML_SYCL": "ON", - "CMAKE_INSTALL_RPATH": "$ORIGIN;$ORIGIN/.." + ], + "buildPresets": [ + { + "name": "parallel", + "description": "Parallel build", + "displayName": "Parallel build", + "configurePreset": "ilintar-release", + "jobs": 8 } - }, - { "name": "debug", "hidden": true, "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, - { "name": "release", "hidden": true, "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } }, - { "name": "reldbg", "hidden": true, "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } }, - { "name": "static", "hidden": true, "cacheVariables": { "GGML_STATIC": "ON" } }, - { "name": "sycl_f16", "hidden": true, "cacheVariables": { "GGML_SYCL_F16": "ON" } }, - { "name": "vulkan", "hidden": true, "cacheVariables": { "GGML_VULKAN": "ON" } }, - - { - "name": "x64-windows-llvm", "hidden": true, - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/x64-windows-llvm.cmake" - } - }, - - { - "name": "arm64-windows-llvm", "hidden": true, - "architecture": { "value": "arm64", "strategy": "external" }, - "toolset": { "value": "host=x64", "strategy": "external" }, - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm64-windows-llvm.cmake" - } - }, - - { - "name": "arm64-apple-clang", "hidden": true, - "architecture": { "value": "arm64", "strategy": "external" }, - "toolset": { "value": "host=x64", "strategy": "external" }, - "cacheVariables": { - "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm64-apple-clang.cmake" - } - }, - { - "name": "x64-linux-gcc", "hidden": true, - "cacheVariables": { - "CMAKE_C_COMPILER": "gcc", - "CMAKE_CXX_COMPILER": "g++" - } - }, - { "name": "x64-linux-gcc-debug", "inherits": [ "base", "x64-linux-gcc", "debug" ] }, - { "name": "x64-linux-gcc-release", "inherits": [ "base", "x64-linux-gcc", "release" ] }, - { "name": "x64-linux-gcc-reldbg", "inherits": [ "base", "x64-linux-gcc", "reldbg" ] }, - { "name": "x64-linux-gcc+static-release", "inherits": [ "base", "x64-linux-gcc", "release", "static" ] }, - - { "name": "arm64-windows-llvm-debug", "inherits": [ "base", "arm64-windows-llvm", "debug" ] }, - { "name": "arm64-windows-llvm-release", "inherits": [ "base", "arm64-windows-llvm", "reldbg" ] }, - { "name": "arm64-windows-llvm+static-release", "inherits": [ "base", "arm64-windows-llvm", "reldbg", "static" ] }, - - { "name": "arm64-apple-clang-debug", "inherits": [ "base", "arm64-apple-clang", "debug" ] }, - { "name": "arm64-apple-clang-release", "inherits": [ "base", "arm64-apple-clang", "reldbg" ] }, - { "name": "arm64-apple-clang+static-release", "inherits": [ "base", "arm64-apple-clang", "reldbg", "static" ] }, - - { "name": "x64-windows-llvm-debug", "inherits": [ "base", "x64-windows-llvm", "debug" ] }, - { "name": "x64-windows-llvm-release", "inherits": [ "base", "x64-windows-llvm", "release" ] }, - { "name": "x64-windows-llvm-reldbg", "inherits": [ "base", "x64-windows-llvm", "reldbg" ] }, - { "name": "x64-windows-llvm+static-release", "inherits": [ "base", "x64-windows-llvm", "reldbg", "static" ] }, - - { "name": "x64-windows-msvc-debug", "inherits": [ "base", "debug" ] }, - { "name": "x64-windows-msvc-release", "inherits": [ "base", "reldbg" ] }, - { "name": "x64-windows-msvc+static-release", "inherits": [ "base", "reldbg", "static" ] }, - - { "name": "x64-windows-sycl-debug", "inherits": [ "sycl-base", "debug" ] }, - { "name": "x64-windows-sycl-debug-f16", "inherits": [ "sycl-base", "debug", "sycl_f16" ] }, - { "name": "x64-windows-sycl-release", "inherits": [ "sycl-base", "release" ] }, - { "name": "x64-windows-sycl-release-f16", "inherits": [ "sycl-base", "release", "sycl_f16" ] }, - - { "name": "x64-windows-vulkan-debug", "inherits": [ "base", "vulkan", "debug" ] }, - { "name": "x64-windows-vulkan-release", "inherits": [ "base", "vulkan", "release" ] } - ] -} + ] +} \ No newline at end of file diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 689fd367da..41069a04ef 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -48,10 +48,11 @@ add_library(${TARGET} STATIC arg.cpp arg.h base64.hpp - chat-auto-parser-analyzer.cpp chat-auto-parser-generator.cpp chat-auto-parser-helpers.cpp chat-auto-parser.h + chat-diff-analyzer.cpp + chat-diff-analyzer.h chat-peg-parser.cpp chat-peg-parser.h chat.cpp diff --git a/common/chat-auto-parser-analyzer.cpp b/common/chat-auto-parser-analyzer.cpp deleted file mode 100644 index db6aa2c547..0000000000 --- a/common/chat-auto-parser-analyzer.cpp +++ /dev/null @@ -1,1461 +0,0 @@ -#include "chat-auto-parser-helpers.h" -#include "chat-auto-parser.h" -#include "chat.h" -#include "log.h" -#include "nlohmann/json.hpp" - -using json = nlohmann::ordered_json; - -template_analysis_result template_analyzer::analyze_template(const common_chat_template & tmpl) { - LOG_DBG("=== STARTING UNIFIED TEMPLATE ANALYSIS ===\n"); - - template_analysis_result result; - - // Phase 1: Analyze content and reasoning structure (no tools involved) - result.content = analyze_content_structure(tmpl); - - // Phase 2: Analyze tool call structure (layered on Phase 1) - result.tools = analyze_tool_structure(tmpl, result.content); - - // Post-processing: Extract reasoning markers from tool_section_start if Phase 1 didn't detect them - // Some templates (like Command-R7B) include reasoning markers in tool outputs but not in prompts - if (result.content.reasoning_start.empty() && !result.tools.tool_section_start.empty()) { - // Known reasoning end marker patterns that might be embedded in tool_section_start - std::vector> reasoning_patterns = { - { "<|START_THINKING|>", "<|END_THINKING|>" }, - { "<|START_THOUGHT|>", "<|END_THOUGHT|>" }, - { "<|START_REASON|>", "<|END_REASON|>" }, - { "", "" }, - { "", "" }, - }; - - for (const auto & [start_marker, end_marker] : reasoning_patterns) { - size_t end_pos = result.tools.tool_section_start.find(end_marker); - if (end_pos != std::string::npos) { - // Found reasoning end marker in tool_section_start - // Extract it and clean up tool_section_start - result.content.reasoning_start = start_marker; - result.content.reasoning_end = end_marker; - result.content.reasoning_mode = content_structure::REASONING_OPTIONAL; - - // Clean up tool_section_start: remove everything before and including the end marker - size_t after_end = end_pos + end_marker.length(); - if (after_end < result.tools.tool_section_start.length()) { - result.tools.tool_section_start = result.tools.tool_section_start.substr(after_end); - // Trim leading whitespace - size_t first_non_ws = result.tools.tool_section_start.find_first_not_of(" \t\n\r"); - if (first_non_ws != std::string::npos && first_non_ws > 0) { - result.tools.tool_section_start = result.tools.tool_section_start.substr(first_non_ws); - } - } - - LOG_DBG("Post-processing: Extracted reasoning markers from tool_section_start\n"); - LOG_DBG(" reasoning_start: '%s', reasoning_end: '%s'\n", result.content.reasoning_start.c_str(), - result.content.reasoning_end.c_str()); - LOG_DBG(" cleaned tool_section_start: '%s'\n", result.tools.tool_section_start.c_str()); - break; - } - } - } - - // Post-processing: Detect content markers for recipient-based format - // For recipient-based format, content is prefixed with tool_call_start_marker + recipient_name + \n - // (e.g., ">>>all\n"). We need to detect and extract this as the content_start marker. - if (result.tools.function_format == tool_call_structure::FUNC_RECIPIENT_BASED && - result.content.content_start.empty() && !result.tools.tool_section_start.empty()) { - // Render template with content only (no tools) to detect the content marker - templates_params inputs; - inputs.messages = { - { { "role", "user" }, { "content", "Hello" } }, - { { "role", "assistant" }, { "content", "ACTUAL_CONTENT_HERE" } } - }; - inputs.add_generation_prompt = true; - - std::string output; - try { - output = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - output = ""; - } - - if (!output.empty()) { - // Find where the actual content starts - size_t content_pos = output.find("ACTUAL_CONTENT_HERE"); - - if (content_pos != std::string::npos) { - // For recipient-based format, find the last occurrence of tool_call_start_marker - // before the content. The marker is from that position to the content (including the newline). - size_t marker_pos = output.rfind(result.tools.tool_section_start, content_pos); - - if (marker_pos != std::string::npos && marker_pos < content_pos) { - // Find the newline after the marker - size_t newline_pos = output.find('\n', marker_pos); - - if (newline_pos != std::string::npos && newline_pos < content_pos) { - // Extract everything up to and including the newline after the marker - std::string detected_marker = output.substr(marker_pos, newline_pos - marker_pos + 1); - - // Verify the marker starts with tool_call_start_marker - if (detected_marker.find(result.tools.tool_section_start) == 0) { - result.content.content_start = detected_marker; - result.content.content_mode = content_structure::CONTENT_ALWAYS_WRAPPED; - LOG_DBG("Post-processing: Detected recipient-based content marker: '%s'\n", - result.content.content_start.c_str()); - } - } - } - } - } - } - - // Collect preserved tokens from both phases - collect_preserved_tokens(result); - - LOG_DBG("=== UNIFIED TEMPLATE ANALYSIS COMPLETE ===\n"); - LOG_DBG("Content structure:\n"); - LOG_DBG(" reasoning_mode: %d\n", static_cast(result.content.reasoning_mode)); - LOG_DBG(" reasoning_start: '%s'\n", result.content.reasoning_start.c_str()); - LOG_DBG(" reasoning_end: '%s'\n", result.content.reasoning_end.c_str()); - LOG_DBG(" content_mode: %d\n", static_cast(result.content.content_mode)); - LOG_DBG(" content_start: '%s'\n", result.content.content_start.c_str()); - LOG_DBG(" content_end: '%s'\n", result.content.content_end.c_str()); - LOG_DBG("Tool structure:\n"); - LOG_DBG(" supports_tools: %s\n", result.tools.supports_tools ? "true" : "false"); - LOG_DBG(" function_format: %d\n", static_cast(result.tools.function_format)); - LOG_DBG(" argument_format: %d\n", static_cast(result.tools.argument_format)); - LOG_DBG(" tool_section_start: '%s'\n", result.tools.tool_section_start.c_str()); - LOG_DBG(" tool_section_end: '%s'\n", result.tools.tool_section_end.c_str()); - - return result; -} - -content_structure template_analyzer::analyze_content_structure(const common_chat_template & tmpl) { - LOG_DBG("=== PHASE 1: ANALYZING CONTENT STRUCTURE ===\n"); - - content_structure cs; - - // Step 1: Detect reasoning markers by toggling enable_thinking - detect_reasoning_markers(tmpl, cs); - - // Step 2: Detect content wrapping markers - detect_content_markers(tmpl, cs); - - // Step 3: Determine reasoning mode (NONE, OPTIONAL, FORCED_OPEN) - templates_params inputs; - inputs.messages = { - { { "role", "user" }, { "content", "Hello" } } - }; - inputs.add_generation_prompt = true; - inputs.enable_thinking = true; - - std::string prompt; - try { - prompt = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - LOG_DBG("Failed to render template for reasoning mode detection\n"); - return cs; - } - - cs.reasoning_mode = detect_reasoning_mode(cs, prompt); - - LOG_DBG("Phase 1 complete: reasoning_mode=%d, content_mode=%d\n", static_cast(cs.reasoning_mode), - static_cast(cs.content_mode)); - - return cs; -} - -void template_analyzer::detect_reasoning_markers(const common_chat_template & tmpl, content_structure & cs) { - LOG_DBG("=== DETECTING REASONING MARKERS ===\n"); - - // Method 1: Compare outputs with reasoning_content field present vs absent - json reasoning_msg = { - { "role", "assistant" }, - { "content", "CONTENT_MARKER" }, - { "reasoning_content", "THOUGHT_MARKER" } - }; - - json base_msg = { - { "role", "assistant" }, - { "content", "CONTENT_MARKER" } - }; - - templates_params inputs; - - inputs.messages = { reasoning_msg }; - std::string reasoning_output; - try { - reasoning_output = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - LOG_DBG("Failed to render template with reasoning_content\n"); - reasoning_output = ""; - } - - inputs.messages = { base_msg }; - std::string base_output; - try { - base_output = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - LOG_DBG("Failed to render base template\n"); - base_output = ""; - } - - // If outputs differ and we can find THOUGHT_MARKER, extract the reasoning markers - if (!reasoning_output.empty() && reasoning_output != base_output) { - size_t thought_pos = reasoning_output.find("THOUGHT_MARKER"); - size_t content_pos = reasoning_output.find("CONTENT_MARKER"); - - if (thought_pos != std::string::npos && content_pos != std::string::npos && content_pos > thought_pos) { - // Extract what's between THOUGHT_MARKER and CONTENT_MARKER as the end marker - size_t thought_end = thought_pos + strlen("THOUGHT_MARKER"); - cs.reasoning_end = reasoning_output.substr(thought_end, content_pos - thought_end); - - // Find what's before THOUGHT_MARKER by comparing with base_output - size_t diff_start = 0; - while (diff_start < base_output.length() && diff_start < reasoning_output.length() && - base_output[diff_start] == reasoning_output[diff_start]) { - diff_start++; - } - - // If diff_start is in the middle of a tag (previous char is '<'), back up to include it - // This handles cases like base="" vs reasoning="" where both share '<' - if (diff_start > 0 && diff_start < reasoning_output.length() && - reasoning_output[diff_start - 1] == '<') { - diff_start--; - } - - if (diff_start < thought_pos) { - cs.reasoning_start = reasoning_output.substr(diff_start, thought_pos - diff_start); - } - - trim_whitespace(cs.reasoning_start); - trim_whitespace(cs.reasoning_end); - - // If we found reasoning_end but not reasoning_start, try to derive it from reasoning_end - // For example: -> , -> <|START_THINKING|> - if (cs.reasoning_start.empty() && !cs.reasoning_end.empty()) { - // First, try to derive directly from the closing tag format - if (cs.reasoning_end.length() > 3 && cs.reasoning_end[0] == '<' && cs.reasoning_end[1] == '/') { - // Standard XML closing tag like -> - size_t tag_end_pos = cs.reasoning_end.find('>'); - if (tag_end_pos != std::string::npos) { - std::string tag_name = cs.reasoning_end.substr(2, tag_end_pos - 2); - cs.reasoning_start = "<" + tag_name + ">"; - LOG_DBG("Method 1: Derived reasoning_start from closing tag format\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - } - } else if (cs.reasoning_end.find("<|END_") == 0 || cs.reasoning_end.find("<|/") == 0) { - // Special token format like <|END_THINKING|> -> <|START_THINKING|> - // or <|/think|> -> <|think|> - if (cs.reasoning_end.find("<|END_") == 0) { - std::string core = cs.reasoning_end.substr(6); // Remove "<|END_" - cs.reasoning_start = "<|START_" + core; - } else { - std::string core = cs.reasoning_end.substr(3); // Remove "<|/" - cs.reasoning_start = "<|" + core; - } - LOG_DBG("Method 1: Derived reasoning_start from special token format\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - } - } - - if (!cs.reasoning_start.empty()) { - LOG_DBG("Method 1: Found reasoning markers via reasoning_content field\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - } - } - } - - // Method 2: Compare prompts with enable_thinking true vs false - if (cs.reasoning_start.empty()) { - LOG_DBG("Method 1 failed, trying Method 2 (enable_thinking toggle)\n"); - - json user_msg = { - { "role", "user" }, - { "content", "Hello" } - }; - - templates_params inputs_prompt; - inputs_prompt.messages = { user_msg }; - inputs_prompt.add_generation_prompt = true; - inputs_prompt.enable_thinking = false; - std::string prompt_no_think; - try { - prompt_no_think = common_chat_template_direct_apply(tmpl, inputs_prompt); - } catch (...) { - prompt_no_think = ""; - } - - inputs_prompt.enable_thinking = true; - std::string prompt_think; - try { - prompt_think = common_chat_template_direct_apply(tmpl, inputs_prompt); - } catch (...) { - prompt_think = ""; - } - - if (!prompt_think.empty() && prompt_think != prompt_no_think) { - // Find the difference - this should be the reasoning start marker - size_t diff_pos = 0; - while (diff_pos < prompt_no_think.length() && diff_pos < prompt_think.length() && - prompt_no_think[diff_pos] == prompt_think[diff_pos]) { - diff_pos++; - } - - // Check which direction has extra content - if (prompt_think.length() > prompt_no_think.length()) { - // Normal case: enable_thinking=true adds content (e.g., at the end) - std::string diff = prompt_think.substr(diff_pos); - - // Only use if it looks like a tag - if (diff.find('<') != std::string::npos || diff.find('[') != std::string::npos) { - cs.reasoning_start = diff; - cs.reasoning_end = create_closing_tag(diff); - trim_whitespace(cs.reasoning_start); - trim_whitespace(cs.reasoning_end); - - LOG_DBG("Method 2: Found reasoning markers via enable_thinking toggle\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - } - } else { - // Reverse case: enable_thinking=false adds content (e.g., GLM-4.6 adds ) - // This means the template adds an empty thinking block when thinking is disabled - std::string diff = prompt_no_think.substr(diff_pos); - - // Look for adjacent opening and closing tags like - size_t open_start = diff.find('<'); - if (open_start != std::string::npos) { - size_t open_end = diff.find('>', open_start); - if (open_end != std::string::npos) { - std::string opening_tag = diff.substr(open_start, open_end - open_start + 1); - // Skip if it looks like a role marker - if (opening_tag.find("assistant") == std::string::npos && - opening_tag.find("user") == std::string::npos && - opening_tag.find("system") == std::string::npos) { - std::string expected_close = create_closing_tag(opening_tag); - // Check if the closing tag follows immediately (empty thinking block) - size_t close_pos = diff.find(expected_close, open_end + 1); - if (close_pos != std::string::npos) { - // Verify only whitespace between tags - std::string between = diff.substr(open_end + 1, close_pos - open_end - 1); - bool only_ws = true; - for (char c : between) { - if (!std::isspace(static_cast(c))) { - only_ws = false; - break; - } - } - if (only_ws) { - cs.reasoning_start = opening_tag; - cs.reasoning_end = expected_close; - trim_whitespace(cs.reasoning_start); - trim_whitespace(cs.reasoning_end); - - LOG_DBG("Method 2: Found reasoning markers via enable_thinking toggle (reverse)\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), - cs.reasoning_end.c_str()); - } - } - } - } - } - } - } - } - - // Method 3: Check if the prompt ends with an unclosed reasoning tag - if (cs.reasoning_start.empty()) { - LOG_DBG("Method 2 failed, trying Method 3 (prompt ending with open tag)\n"); - - json user_msg = { - { "role", "user" }, - { "content", "Hello" } - }; - - templates_params inputs_prompt; - inputs_prompt.messages = { user_msg }; - inputs_prompt.add_generation_prompt = true; - inputs_prompt.enable_thinking = true; - - std::string prompt; - try { - prompt = common_chat_template_direct_apply(tmpl, inputs_prompt); - } catch (...) { - prompt = ""; - } - - if (!prompt.empty()) { - // Save trailing whitespace before trimming - std::string trailing_ws; - size_t end_pos = prompt.length(); - while (end_pos > 0 && (prompt[end_pos - 1] == '\n' || prompt[end_pos - 1] == '\r')) { - trailing_ws = prompt[end_pos - 1] + trailing_ws; - end_pos--; - } - - trim_trailing_newlines(prompt); - - // Find the last tag in the prompt - size_t last_open_angle = prompt.rfind('<'); - size_t last_close_angle = prompt.rfind('>'); - - // Check for closed tags at the end - if (last_open_angle != std::string::npos && last_close_angle != std::string::npos && - last_close_angle == prompt.length() - 1 && last_close_angle > last_open_angle) { - std::string tag = prompt.substr(last_open_angle); - - // Check if this looks like a reasoning tag (not a role marker) - std::vector blacklisted_tags = { - "<|CHATBOT_TOKEN|>", "<|SYSTEM_TOKEN|>", "<|USER_TOKEN|>", "<|ASSISTANT_TOKEN|>", "<|im_start|>", - "<|im_end|>", "<|start_of_role|>", "<|end_of_role|>", "<|end_of_text|>", "<|end|>", - "<|assistant|>", "<|user|>", "<|system|>", "", "", - "" - }; - - bool is_blacklisted = false; - for (const auto & blacklisted : blacklisted_tags) { - if (tag == blacklisted) { - is_blacklisted = true; - break; - } - } - - // Check if it looks like a thinking/reasoning tag - std::string lower_tag = tag; - std::transform(lower_tag.begin(), lower_tag.end(), lower_tag.begin(), ::tolower); - bool looks_like_reasoning = lower_tag.find("think") != std::string::npos || - lower_tag.find("reason") != std::string::npos || - lower_tag.find("thought") != std::string::npos; - - if (!is_blacklisted && looks_like_reasoning) { - // Check if the detected tag is a close tag (starts with when thinking is disabled - bool is_close_tag = (tag.size() > 2 && tag[0] == '<' && tag[1] == '/'); - - if (is_close_tag) { - // The tag is a close tag (e.g., ) - // Derive the open tag by removing the '/' - std::string tag_name = extract_tag_name(tag); // Returns "/think" for - if (!tag_name.empty() && tag_name[0] == '/') { - tag_name = tag_name.substr(1); // Remove leading '/' - } - cs.reasoning_start = "<" + tag_name + ">"; - cs.reasoning_end = tag; - trim_whitespace(cs.reasoning_start); - trim_whitespace(cs.reasoning_end); - - LOG_DBG("Method 3: Found reasoning markers via prompt ending with CLOSE tag\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - - // Note: The prompt ends with the close tag, meaning thinking is disabled. - // The reasoning_mode will be set in detect_reasoning_mode() which will - // correctly identify this as NOT forced open since the prompt ends with - // the end marker, not the start marker. - } else { - // Standard case: open tag at the end (e.g., ) - cs.reasoning_start = tag + trailing_ws; - cs.reasoning_end = create_closing_tag(tag) + trailing_ws; - trim_whitespace(cs.reasoning_start); - trim_whitespace(cs.reasoning_end); - - LOG_DBG("Method 3: Found reasoning markers via prompt ending with tag\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - } - } - } - } - } - - // Method 4: Look for adjacent opening/closing tag pairs with common content in prompt - // This detects patterns like , <|START_THINKING|><|END_THINKING|>, [think][/think] - if (cs.reasoning_start.empty()) { - LOG_DBG("Method 3 failed, trying Method 4 (adjacent tag pairs with common content)\n"); - - json user_msg = { - { "role", "user" }, - { "content", "Hello" } - }; - - templates_params inputs_prompt; - inputs_prompt.messages = { user_msg }; - inputs_prompt.add_generation_prompt = true; - // Try with thinking disabled - templates may output empty thinking blocks - inputs_prompt.enable_thinking = false; - - std::string prompt; - try { - prompt = common_chat_template_direct_apply(tmpl, inputs_prompt); - } catch (...) { - prompt = ""; - } - - if (!prompt.empty()) { - // Look for patterns like or ... where tag1 and tag2 share a common word - // Common patterns: - // - // <|START_THINKING|><|END_THINKING|> - // [think][/think] - - // Find potential tag pairs by looking for closing tags that immediately follow opening tags - // Pattern: opening tag followed by closing tag with same keyword - std::vector> tag_patterns = { - // (opening pattern, closing pattern, keyword to match) - { "<|START_", "<|END_", "THINKING" }, - { "<|START_", "<|END_", "THOUGHT" }, - { "<|START_", "<|END_", "REASON" }, - { "", "", "" }, - { "", "", "" }, - { "", "", "" }, - { "[think]", "[/think]", "" }, - { "[THINK]", "[/THINK]", "" }, - { "", "", "" }, - { "", "", "" }, - { "<|think|>", "<|/think|>", "" }, - }; - - for (const auto & [open_prefix, close_prefix, keyword] : tag_patterns) { - size_t open_pos = prompt.find(open_prefix); - if (open_pos == std::string::npos) { - continue; - } - - std::string start_tag; - std::string end_tag; - - if (!keyword.empty()) { - // Pattern like <|START_THINKING|><|END_THINKING|> - std::string full_open = open_prefix + keyword; - size_t full_open_pos = prompt.find(full_open); - if (full_open_pos == std::string::npos) { - continue; - } - - // Find the end of this tag (look for |> or >) - size_t tag_end = prompt.find("|>", full_open_pos + full_open.length()); - if (tag_end == std::string::npos) { - tag_end = prompt.find('>', full_open_pos + full_open.length()); - } - if (tag_end == std::string::npos) { - continue; - } - - start_tag = - prompt.substr(full_open_pos, tag_end - full_open_pos + (prompt[tag_end] == '|' ? 2 : 1)); - - // Look for the corresponding end tag - std::string expected_close = close_prefix + keyword; - size_t close_pos = prompt.find(expected_close, tag_end); - if (close_pos == std::string::npos) { - continue; - } - - // Find end of close tag - size_t close_end = prompt.find("|>", close_pos + expected_close.length()); - if (close_end == std::string::npos) { - close_end = prompt.find('>', close_pos + expected_close.length()); - } - if (close_end == std::string::npos) { - continue; - } - - end_tag = prompt.substr(close_pos, close_end - close_pos + (prompt[close_end] == '|' ? 2 : 1)); - } else { - // Simple pattern like - start_tag = open_prefix; - size_t close_pos = prompt.find(close_prefix, open_pos + start_tag.length()); - if (close_pos == std::string::npos) { - continue; - } - end_tag = close_prefix; - } - - // Verify the tags are adjacent or nearly adjacent (only whitespace between) - size_t start_end_pos = prompt.find(start_tag) + start_tag.length(); - size_t end_start_pos = prompt.find(end_tag, start_end_pos); - if (end_start_pos != std::string::npos) { - std::string between = prompt.substr(start_end_pos, end_start_pos - start_end_pos); - // Allow only whitespace between the tags (empty thinking block) - bool only_whitespace = true; - for (char c : between) { - if (!std::isspace(static_cast(c))) { - only_whitespace = false; - break; - } - } - - if (only_whitespace) { - cs.reasoning_start = start_tag; - cs.reasoning_end = end_tag; - LOG_DBG("Method 4: Found reasoning markers via adjacent tag pairs\n"); - LOG_DBG(" start: '%s', end: '%s'\n", cs.reasoning_start.c_str(), cs.reasoning_end.c_str()); - break; - } - } - } - } - } - - if (cs.reasoning_start.empty()) { - LOG_DBG("No reasoning markers detected\n"); - } -} - -void template_analyzer::detect_content_markers(const common_chat_template & tmpl, content_structure & cs) { - LOG_DBG("=== DETECTING CONTENT MARKERS ===\n"); - - // Render template with a unique content marker - json user_msg = { - { "role", "user" }, - { "content", "Hello" } - }; - json assistant_msg = { - { "role", "assistant" }, - { "content", "UNIQUE_CONTENT_12345" } - }; - - templates_params inputs; - inputs.messages = { user_msg, assistant_msg }; - // Try with thinking enabled first (some templates only wrap content when reasoning is present) - inputs.extra_context["thinking"] = true; - inputs.enable_thinking = true; - - std::string output_with_thinking; - try { - output_with_thinking = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - output_with_thinking = ""; - } - - // Also render without thinking - inputs.extra_context["thinking"] = false; - inputs.enable_thinking = false; - - std::string output_no_thinking; - try { - output_no_thinking = common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - output_no_thinking = ""; - } - - // Check both outputs for content markers - auto find_content_markers = [&](const std::string & output) -> std::pair { - size_t marker_pos = output.find("UNIQUE_CONTENT_12345"); - if (marker_pos == std::string::npos) { - return { "", "" }; - } - - // Known content marker patterns - std::vector> patterns = { - { "<|START_RESPONSE|>", "<|END_RESPONSE|>" }, - { "<|response|>", "<|/response|>" }, - { "", "" }, - { "", "" }, - { "", "" }, - { "<|CHATBOT_TOKEN|>", "<|END_OF_TURN_TOKEN|>" }, - }; - - for (const auto & [start_pattern, end_pattern] : patterns) { - size_t start_pos = output.rfind(start_pattern, marker_pos); - if (start_pos != std::string::npos) { - // Check that there's only whitespace between the start pattern and our marker - std::string between = - output.substr(start_pos + start_pattern.length(), marker_pos - start_pos - start_pattern.length()); - size_t first_non_ws = between.find_first_not_of(" \t\n\r"); - if (first_non_ws == std::string::npos) { - // Found valid start marker, look for end marker - size_t marker_end = marker_pos + strlen("UNIQUE_CONTENT_12345"); - size_t end_pos = output.find(end_pattern, marker_end); - if (end_pos != std::string::npos) { - std::string after = output.substr(marker_end, end_pos - marker_end); - size_t first_non_ws_after = after.find_first_not_of(" \t\n\r"); - if (first_non_ws_after == std::string::npos) { - return { start_pattern, end_pattern }; - } - } - } - } - } - - return { "", "" }; - }; - - auto [start_with_thinking, end_with_thinking] = find_content_markers(output_with_thinking); - auto [start_no_thinking, end_no_thinking] = find_content_markers(output_no_thinking); - - if (!start_with_thinking.empty() && !start_no_thinking.empty()) { - // Content is always wrapped - cs.content_mode = content_structure::CONTENT_ALWAYS_WRAPPED; - cs.content_start = start_with_thinking; - cs.content_end = end_with_thinking; - LOG_DBG("Content markers found in both thinking modes (ALWAYS_WRAPPED)\n"); - } else if (!start_with_thinking.empty() && start_no_thinking.empty()) { - // Content is wrapped only when reasoning is present - cs.content_mode = content_structure::CONTENT_WRAPPED_WITH_REASONING; - cs.content_start = start_with_thinking; - cs.content_end = end_with_thinking; - LOG_DBG("Content markers found only with thinking enabled (WRAPPED_WITH_REASONING)\n"); - } else if (!start_no_thinking.empty()) { - // Unusual: content wrapped without thinking but not with? Use what we found - cs.content_mode = content_structure::CONTENT_ALWAYS_WRAPPED; - cs.content_start = start_no_thinking; - cs.content_end = end_no_thinking; - LOG_DBG("Content markers found only without thinking (treating as ALWAYS_WRAPPED)\n"); - } else { - cs.content_mode = content_structure::CONTENT_PLAIN; - LOG_DBG("No content markers detected (PLAIN)\n"); - } - - LOG_DBG("Content markers: start='%s', end='%s'\n", cs.content_start.c_str(), cs.content_end.c_str()); -} - -content_structure::reasoning_mode_type template_analyzer::detect_reasoning_mode(const content_structure & cs, - const std::string & prompt) { - LOG_DBG("=== DETECTING REASONING MODE ===\n"); - - // If both markers are empty, mode is NONE - if (cs.reasoning_start.empty() && cs.reasoning_end.empty()) { - LOG_DBG("No reasoning markers, mode=REASONING_NONE\n"); - return content_structure::REASONING_NONE; - } - - // Handle case with end marker but no start marker (implicit start) - if (cs.reasoning_start.empty() && !cs.reasoning_end.empty()) { - LOG_DBG("Reasoning end marker present but no start marker, mode=REASONING_FORCED_OPEN\n"); - return content_structure::REASONING_FORCED_OPEN; - } - - // Check if the prompt ends with the reasoning start marker (forced open) - std::string trimmed_prompt = prompt; - trim_trailing_newlines(trimmed_prompt); - - std::string trimmed_marker = cs.reasoning_start; - trim_whitespace(trimmed_marker); - - if (string_ends_with(trimmed_prompt, trimmed_marker)) { - LOG_DBG("Prompt ends with reasoning start marker, mode=REASONING_FORCED_OPEN\n"); - return content_structure::REASONING_FORCED_OPEN; - } - - // Otherwise, reasoning is optional - LOG_DBG("Reasoning markers present but not forced, mode=REASONING_OPTIONAL\n"); - return content_structure::REASONING_OPTIONAL; -} - -tool_call_structure template_analyzer::analyze_tool_structure(const common_chat_template & tmpl, - const content_structure & content) { - (void) content; // May be used in future for better tool detection - - LOG_DBG("=== PHASE 2: ANALYZING TOOL STRUCTURE ===\n"); - - tool_call_structure ts; - - // Use differential analysis to detect tool patterns - // This now includes a robust test that renders two payloads: - // 1. Tool definitions + content only - // 2. Tool definitions + content + tool calls - // If outputs are identical, the template doesn't support tool calls - auto discovered = analyze_by_differential(tmpl); - auto format = determine_format_from_patterns(discovered); - - // Strip EOS tokens from discovered patterns (handles both standard <|eos|> and fullwidth <|end▁of▁sentence|>) - if (!discovered.tool_call_closer.empty()) { - LOG_DBG("Before stripping: tool_call_closer='%s' (len=%zu)\n", discovered.tool_call_closer.c_str(), - discovered.tool_call_closer.length()); - discovered.tool_call_closer = strip_eos_token(discovered.tool_call_closer); - LOG_DBG("After stripping: tool_call_closer='%s'\n", discovered.tool_call_closer.c_str()); - } - if (!discovered.tool_call_end_marker.empty()) { - discovered.tool_call_end_marker = strip_eos_token(discovered.tool_call_end_marker); - } - - if (format == FORMAT_UNKNOWN) { - LOG_DBG("Template does not support tool calls (differential analysis returned no patterns)\n"); - ts.supports_tools = false; - return ts; - } - - // Propagate requires_nonnull_content flag from differential analysis - ts.requires_nonnull_content = discovered.requires_nonnull_content; - if (ts.requires_nonnull_content) { - LOG_DBG("Template requires non-null content (renders null as 'None')\n"); - } - - // Check if minja reports tool call support (for informational purposes) - auto caps = tmpl.original_caps(); - if (!caps.supports_tool_calls) { - LOG_DBG("Note: minja caps indicate no tool support, but differential analysis found patterns\n"); - } - - if (format == FORMAT_JSON_NATIVE) { - analyze_json_format(ts, discovered); - } else if (format == FORMAT_XML_CONSTRUCTED) { - analyze_xml_format(ts, discovered); - } else if (format == FORMAT_BRACKET_TAG) { - analyze_bracket_tag_format(ts, discovered); - } else if (format == FORMAT_RECIPIENT_BASED) { - analyze_recipient_based_format(ts, discovered); - } else if (format == FORMAT_MARKDOWN_CODE_BLOCK) { - analyze_markdown_code_block_format(ts, discovered); - } - - return ts; -} - -void template_analyzer::collect_preserved_tokens(template_analysis_result & result) { - LOG_DBG("=== COLLECTING PRESERVED TOKENS ===\n"); - - std::vector tokens; - - // Add reasoning markers - if (!result.content.reasoning_start.empty()) { - tokens.push_back(result.content.reasoning_start); - } - if (!result.content.reasoning_end.empty()) { - tokens.push_back(result.content.reasoning_end); - } - - // Add content markers - if (!result.content.content_start.empty()) { - tokens.push_back(result.content.content_start); - } - if (!result.content.content_end.empty()) { - tokens.push_back(result.content.content_end); - } - - // Add tool section markers - if (!result.tools.tool_section_start.empty()) { - tokens.push_back(result.tools.tool_section_start); - } - if (!result.tools.tool_section_end.empty()) { - tokens.push_back(result.tools.tool_section_end); - } - - // Add function markers for tag-based formats - if (result.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME) { - if (!result.tools.function_prefix.empty()) { - tokens.push_back(result.tools.function_prefix); - } - if (!result.tools.function_close.empty()) { - tokens.push_back(result.tools.function_close); - } - } - - // Add markers for prefixed-indexed formats (e.g., Kimi-K2) - if (result.tools.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED) { - if (!result.tools.per_call_start.empty()) { - tokens.push_back(result.tools.per_call_start); - } - if (!result.tools.args_marker.empty()) { - tokens.push_back(result.tools.args_marker); - } - if (!result.tools.per_call_end.empty()) { - tokens.push_back(result.tools.per_call_end); - } - } - - // Add argument markers for tagged formats - if (result.tools.argument_format == tool_call_structure::ARGS_TAGGED) { - if (!result.tools.arg_prefix.empty()) { - tokens.push_back(result.tools.arg_prefix); - } - if (!result.tools.arg_close.empty()) { - tokens.push_back(result.tools.arg_close); - } - } - - // Add markers for markdown code block format (Cohere Command-R Plus) - if (result.tools.function_format == tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK) { - if (!result.tools.code_block_marker.empty()) { - tokens.push_back(result.tools.code_block_marker); - } - if (!result.tools.tool_section_end.empty()) { - tokens.push_back(result.tools.tool_section_end); // Closing code fence ``` - } - } - - result.preserved_tokens = tokens; - LOG_DBG("Collected %zu preserved tokens\n", tokens.size()); -} - -void template_analyzer::analyze_json_format(tool_call_structure & ts, const internal_discovered_pattern & discovered) { - ts.supports_tools = true; - ts.function_format = tool_call_structure::FUNC_JSON_OBJECT; - ts.argument_format = tool_call_structure::ARGS_JSON; - ts.tool_section_start = discovered.tool_call_start_marker; - ts.tool_section_end = discovered.tool_call_end_marker; - ts.name_field = discovered.tool_name_field; - ts.args_field = discovered.tool_args_field; - ts.id_field = discovered.tool_id_field; - - // Check for FUNC_NAME_AS_KEY format (e.g. Apertus: {"function_name": args}) - // This is characterized by the opener ending in {" and no explicit name field found yet - if (!discovered.tool_call_opener.empty() && discovered.tool_call_opener.length() >= 2 && - discovered.tool_call_opener.substr(discovered.tool_call_opener.length() - 2) == "{\"") { - LOG_DBG("Detected FUNC_NAME_AS_KEY format from tool_call_opener ending in '{\"' \n"); - ts.function_format = tool_call_structure::FUNC_NAME_AS_KEY; - } - - // For JSON_NATIVE format, clean up tool_section_end to only include the closing tag - // The differential analysis may include JSON closing braces (e.g., "}}\n") - // but the parser handles JSON separately, so we only need the tag marker - if (!ts.tool_section_end.empty()) { - size_t tag_start = ts.tool_section_end.find("', tag_start); - if (tag_end != std::string::npos) { - // Check if there is a closing bracket ']' before the tag - size_t bracket_pos = ts.tool_section_end.rfind(']', tag_start); - if (bracket_pos != std::string::npos) { - // Include the bracket - ts.tool_section_end = ts.tool_section_end.substr(bracket_pos, tag_end - bracket_pos + 1); - } else { - ts.tool_section_end = ts.tool_section_end.substr(tag_start, tag_end - tag_start + 1); - } - } - } else { - // Try other closing patterns like ]<|END_ACTION|> - tag_start = ts.tool_section_end.find("<|"); - if (tag_start != std::string::npos) { - size_t tag_end = ts.tool_section_end.find("|>", tag_start); - if (tag_end != std::string::npos) { - // Include the opening bracket if present - size_t bracket_pos = ts.tool_section_end.rfind(']', tag_start); - if (bracket_pos != std::string::npos && bracket_pos + 1 == tag_start) { - ts.tool_section_end = ts.tool_section_end.substr(bracket_pos, tag_end - bracket_pos + 2); - } else { - ts.tool_section_end = ts.tool_section_end.substr(tag_start, tag_end - tag_start + 2); - } - } - } - } - } -} - -void template_analyzer::analyze_xml_format(tool_call_structure & ts, const internal_discovered_pattern & discovered) { - ts.supports_tools = true; - ts.function_format = tool_call_structure::FUNC_TAG_WITH_NAME; - ts.tool_section_start = discovered.tool_call_start_marker; - ts.tool_section_end = discovered.tool_call_end_marker; - - // Extract function tag patterns - if (!discovered.function_opener.empty()) { - char first = discovered.function_opener[0]; - if (first != '<' && first != '{' && first != '[') { - // Non-XML/JSON prefix format (e.g., ">>>", "##", etc.) - // Function name follows prefix directly, ends with newline - ts.function_prefix = discovered.function_opener; - ts.function_suffix = "\n"; // Function name typically ends with newline - ts.function_close = ""; // No closing tag for prefix formats - } else { - size_t eq_pos = discovered.function_opener.find('='); - if (eq_pos != std::string::npos) { - // Check if there's a quote after the equals sign - if (eq_pos + 1 < discovered.function_opener.length() && - (discovered.function_opener[eq_pos + 1] == '"' || discovered.function_opener[eq_pos + 1] == '\'')) { - ts.function_prefix = discovered.function_opener.substr(0, eq_pos + 2); - } else { - ts.function_prefix = discovered.function_opener.substr(0, eq_pos + 1); - } - ts.function_suffix = discovered.function_name_suffix; - - // For formats like {args}, where function_prefix - // IS the section start (no separate wrapper), tool_section_end is the function close. - // But for nested formats like ..., - // the function_close is separate from tool_section_end. - // We detect the non-nested case when tool_section_start matches function_prefix - // (or tool_section_start was already cleared because it matched). - bool section_start_matches_prefix = ts.tool_section_start.empty() || - ts.tool_section_start.find(ts.function_prefix) == 0 || - ts.function_prefix.find(ts.tool_section_start) == 0; - if (section_start_matches_prefix && ts.function_prefix.find('<') == 0 && !ts.tool_section_end.empty() && - ts.tool_section_end.find("functions.name:0<|tool_call_argument_begin|> - size_t namespace_dot = discovered.function_opener.rfind('.'); - bool has_namespace = - (namespace_dot != std::string::npos && namespace_dot == discovered.function_opener.length() - 1); - - bool has_index = - (!discovered.function_name_suffix.empty() && discovered.function_name_suffix[0] == ':' && - discovered.function_name_suffix.length() > 1 && - std::isdigit(static_cast(discovered.function_name_suffix[1]))); - - if (has_namespace && has_index) { - LOG_DBG("Detected FUNC_PREFIXED_INDEXED format: namespace ends with '.', suffix has ':N' index\n"); - ts.function_format = tool_call_structure::FUNC_PREFIXED_INDEXED; - - // Split function_opener into per_call_start and function_namespace - // e.g., "<|tool_call_begin|>functions." -> "<|tool_call_begin|>" + "functions." - // Find where the namespace starts (after the last '>' before the '.') - size_t namespace_start = discovered.function_opener.rfind('>'); - if (namespace_start != std::string::npos && namespace_start < namespace_dot) { - ts.per_call_start = discovered.function_opener.substr(0, namespace_start + 1); - ts.function_namespace = discovered.function_opener.substr(namespace_start + 1); - } else { - // Fallback: namespace is just the part ending with '.' - ts.per_call_start = discovered.function_opener.substr(0, namespace_dot); - ts.function_namespace = "."; - } - - // Extract args_marker from function_name_suffix - // Format: ":0<|some_marker|>" -> index is ":0", args_marker is "<|some_marker|>" - size_t args_marker_start = discovered.function_name_suffix.find('<'); - if (args_marker_start != std::string::npos) { - size_t args_marker_end = discovered.function_name_suffix.find('>', args_marker_start); - if (args_marker_end != std::string::npos) { - ts.args_marker = discovered.function_name_suffix.substr( - args_marker_start, args_marker_end - args_marker_start + 1); - } - } - - // Derive per_call_end from tool_call_closer by finding corresponding end marker - // tool_call_closer contains per_call_end + tool_section_end - // We find per_call_end by looking for a marker that structurally matches per_call_start - if (!discovered.tool_call_closer.empty() && !ts.per_call_start.empty()) { - // Extract structural pattern from per_call_start - // e.g., "<|tool_call_begin|>" -> look for "<|tool_call_...|>" in closer - size_t start_marker_begin = ts.per_call_start.find("<|"); - size_t start_marker_end = ts.per_call_start.rfind("|>"); - if (start_marker_begin != std::string::npos && start_marker_end != std::string::npos) { - // Find the base pattern (e.g., "<|tool_call" from "<|tool_call_begin|>") - std::string start_content = ts.per_call_start.substr( - start_marker_begin + 2, start_marker_end - start_marker_begin - 2); - // Find a related marker in the closer - size_t closer_pos = discovered.tool_call_closer.find("<|"); - while (closer_pos != std::string::npos) { - size_t closer_end = discovered.tool_call_closer.find("|>", closer_pos); - if (closer_end != std::string::npos) { - std::string candidate = - discovered.tool_call_closer.substr(closer_pos, closer_end - closer_pos + 2); - // Check if this marker shares a common prefix with per_call_start - // (ignoring _begin vs _end suffix differences) - std::string candidate_content = candidate.substr(2, candidate.length() - 4); - // Find common prefix between start_content and candidate_content - size_t common_len = 0; - while (common_len < start_content.length() && - common_len < candidate_content.length() && - start_content[common_len] == candidate_content[common_len]) { - common_len++; - } - // If substantial overlap (>50%), this is likely the per_call_end - if (common_len > start_content.length() / 2 && - candidate_content.find("end") != std::string::npos) { - ts.per_call_end = candidate; - break; - } - } - closer_pos = discovered.tool_call_closer.find("<|", closer_pos + 1); - } - } - } - - // Derive tool_section_end from tool_section_start by finding matching end marker - // For FUNC_PREFIXED_INDEXED, we always derive this to get the correct marker - // (the default discovered.tool_call_end_marker may contain extra content) - if (!ts.tool_section_start.empty()) { - size_t start_marker_begin = ts.tool_section_start.find("<|"); - size_t start_marker_end = ts.tool_section_start.rfind("|>"); - if (start_marker_begin != std::string::npos && start_marker_end != std::string::npos) { - std::string start_content = ts.tool_section_start.substr( - start_marker_begin + 2, start_marker_end - start_marker_begin - 2); - size_t closer_pos = discovered.tool_call_closer.find("<|"); - while (closer_pos != std::string::npos) { - size_t closer_end = discovered.tool_call_closer.find("|>", closer_pos); - if (closer_end != std::string::npos) { - std::string candidate = - discovered.tool_call_closer.substr(closer_pos, closer_end - closer_pos + 2); - std::string candidate_content = candidate.substr(2, candidate.length() - 4); - size_t common_len = 0; - while (common_len < start_content.length() && - common_len < candidate_content.length() && - start_content[common_len] == candidate_content[common_len]) { - common_len++; - } - if (common_len > start_content.length() / 2 && - candidate_content.find("end") != std::string::npos) { - ts.tool_section_end = candidate; - break; - } - } - closer_pos = discovered.tool_call_closer.find("<|", closer_pos + 1); - } - } - } - - LOG_DBG( - "FUNC_PREFIXED_INDEXED: per_call_start='%s', namespace='%s', args_marker='%s', " - "per_call_end='%s'\n", - ts.per_call_start.c_str(), ts.function_namespace.c_str(), ts.args_marker.c_str(), - ts.per_call_end.c_str()); - } else { - // Other formats like <|tool_call_begin|>name (non-indexed) - // Use function_opener as default, but try to use full tool_call_opener if it contains more - ts.function_prefix = discovered.function_opener; - LOG_DBG("Initial function_prefix: '%s', tool_call_opener: '%s', tool_section_start: '%s'\n", - ts.function_prefix.c_str(), discovered.tool_call_opener.c_str(), - ts.tool_section_start.c_str()); - if (!ts.tool_section_start.empty() && - discovered.tool_call_opener.find(ts.tool_section_start) == 0) { - std::string remainder = discovered.tool_call_opener.substr(ts.tool_section_start.length()); - LOG_DBG("Derived remainder: '%s'\n", remainder.c_str()); - if (remainder.length() > ts.function_prefix.length()) { - ts.function_prefix = remainder; - } - } - ts.function_suffix = discovered.function_name_suffix; - ts.function_close = discovered.function_closer; - } - } - } - } - - // Fix for templates where tool_section_start matches function_prefix (double wrapping) - // e.g. Functionary: tool_section_start="<|tool▁call▁begin|>function - // We need to derive tool_section_end from the outer marker pattern - if (ts.function_suffix.find("```") != std::string::npos && !ts.tool_section_start.empty()) { - // Check if tool_section_start contains nested markers (both outer and per-call) - // Pattern: ... - // We look for "calls" pattern which indicates an outer container - size_t calls_pos = ts.tool_section_start.find("calls"); - if (calls_pos != std::string::npos && calls_pos < ts.tool_section_start.length()) { - // Find where the outer marker ends (after the first >) - size_t first_close = ts.tool_section_start.find('>', calls_pos); - if (first_close != std::string::npos && first_close < ts.tool_section_start.length() - 1) { - // Extract the outer marker (e.g., "<|tool▁calls▁begin|>") - std::string outer_start = ts.tool_section_start.substr(0, first_close + 1); - // Derive the outer end marker by replacing "begin" with "end" - size_t begin_pos = outer_start.find("begin"); - if (begin_pos != std::string::npos) { - std::string outer_end = - outer_start.substr(0, begin_pos) + "end" + outer_start.substr(begin_pos + 5); - ts.tool_section_end = outer_end; - - // Strip outer marker from function_prefix and function_opener if they were combined - if (ts.tool_section_start.find(outer_start) == 0) { - std::string remainder = ts.tool_section_start.substr(outer_start.length()); - // Trim leading whitespace from remainder - size_t first_non_ws = remainder.find_first_not_of(" \t\n\r"); - if (first_non_ws != std::string::npos && first_non_ws > 0) { - remainder = remainder.substr(first_non_ws); - } - - // Concatenate with existing function_prefix (e.g. separator tag) - // but avoid double-concatenation if already present - if (!remainder.empty() && ts.function_prefix.find(remainder) == std::string::npos) { - ts.function_prefix = remainder + ts.function_prefix; - } - } - - // Update tool_section_start to be just the outer marker - ts.tool_section_start = outer_start; - - // Check if there's a fence in tool_call_closer that should be in function_close - // (DeepSeek R1 wraps JSON in markdown blocks within the custom tags) - if (discovered.tool_call_closer.find("```") != std::string::npos) { - size_t fence_pos = discovered.tool_call_closer.find("```"); - // Include leading newlines if present before the fence - while (fence_pos > 0 && (discovered.tool_call_closer[fence_pos - 1] == '\n' || - discovered.tool_call_closer[fence_pos - 1] == '\r')) { - fence_pos--; - } - ts.function_close = discovered.tool_call_closer.substr(fence_pos); - - // Clip function_close to not include tool_section_end (if they were combined in differential analysis) - if (!ts.tool_section_end.empty()) { - size_t end_pos = ts.function_close.find(ts.tool_section_end); - if (end_pos != std::string::npos) { - ts.function_close = ts.function_close.substr(0, end_pos); - } - } - - // Further trim any trailing EOS or prompt garbage - ts.function_close = strip_eos_token(ts.function_close); - size_t prompt_garbage = ts.function_close.find("<|"); - if (prompt_garbage != std::string::npos && prompt_garbage > 0 && - ts.function_close.substr(prompt_garbage).find("Assistant") != std::string::npos) { - ts.function_close = ts.function_close.substr(0, prompt_garbage); - } - } - } - } - } - } - - // General cleanup for tool_section_end when tool_section_start uses token markers (<|...|> or <|...|>) - // If tool_section_start contains a token marker with "begin" and tool_section_end is messy (contains } - // or multiple markers), derive tool_section_end by finding matching end marker in tool_call_closer - if (!ts.tool_section_start.empty() && !discovered.tool_call_closer.empty()) { - // Check if tool_section_start contains a token marker - size_t start_opener_pos = find_token_opener(ts.tool_section_start, 0); - size_t start_closer_pos = find_token_closer(ts.tool_section_start, start_opener_pos); - if (start_opener_pos != std::string::npos && start_closer_pos != std::string::npos) { - size_t opener_len = get_token_opener_length(ts.tool_section_start, start_opener_pos); - // Extract the token content (between opener and closer) - std::string start_content = ts.tool_section_start.substr(start_opener_pos + opener_len, - start_closer_pos - start_opener_pos - opener_len); - - // Check if tool_section_end needs cleanup (starts with } or contains multiple markers) - bool needs_cleanup = false; - if (!ts.tool_section_end.empty() && ts.tool_section_end[0] == '}') { - needs_cleanup = true; - } - // Count tokens in tool_section_end - size_t token_count = 0; - size_t pos = 0; - while ((pos = find_token_opener(ts.tool_section_end, pos)) != std::string::npos) { - token_count++; - pos += get_token_opener_length(ts.tool_section_end, pos); - } - if (token_count > 1) { - needs_cleanup = true; - } - - if (needs_cleanup) { - // Find matching end marker in tool_call_closer - // Look for a token that has similar content but with "end" instead of "begin" - pos = 0; - while ((pos = find_token_opener(discovered.tool_call_closer, pos)) != std::string::npos) { - size_t end_closer_pos = find_token_closer(discovered.tool_call_closer, pos); - if (end_closer_pos != std::string::npos) { - size_t op_len = get_token_opener_length(discovered.tool_call_closer, pos); - size_t cl_len = get_token_closer_length(discovered.tool_call_closer, end_closer_pos); - std::string candidate = discovered.tool_call_closer.substr(pos, end_closer_pos + cl_len - pos); - std::string candidate_content = - discovered.tool_call_closer.substr(pos + op_len, end_closer_pos - pos - op_len); - - // Check if this candidate matches our start marker structure - // Start content might be "tool▁calls▁begin" and candidate might be "tool▁calls▁end" - size_t begin_in_start = start_content.find("begin"); - size_t end_in_candidate = candidate_content.find("end"); - if (begin_in_start != std::string::npos && end_in_candidate != std::string::npos) { - // Check if they share a common prefix (e.g., "tool▁calls▁") - std::string start_base = start_content.substr(0, begin_in_start); - std::string cand_base = candidate_content.substr(0, end_in_candidate); - if (start_base == cand_base) { - ts.tool_section_end = candidate; - LOG_DBG( - "Derived tool_section_end='%s' from tool_section_start='%s' using token matching\n", - ts.tool_section_end.c_str(), ts.tool_section_start.c_str()); - break; - } - } - } - pos += get_token_opener_length(discovered.tool_call_closer, pos); - } - } - } - } - - // Determine argument format - if (!discovered.parameter_key_prefix.empty() && discovered.parameter_key_prefix.find('<') != std::string::npos) { - ts.argument_format = tool_call_structure::ARGS_TAGGED; - ts.arg_prefix = discovered.parameter_key_prefix; - ts.arg_suffix = discovered.parameter_key_suffix; - ts.arg_close = discovered.parameter_closer; - ts.arg_separator = discovered.argument_separator; - - // Check for specific GLM-4 style key-value tags - // Format: key\nvalue - // Analyzer detects suffix as: \n - if (ts.arg_suffix.find("") != std::string::npos) { - ts.argument_format = tool_call_structure::ARGS_KEY_VALUE_TAGS; - - // Clean up suffix to be just the key closer - size_t val_opener = ts.arg_suffix.find(""); - if (val_opener != std::string::npos) { - // Extract just the part (trimming whitespace/newlines before ) - std::string key_closer = ts.arg_suffix.substr(0, val_opener); - // Trim trailing whitespace/newlines - while (!key_closer.empty() && - (key_closer.back() == '\n' || key_closer.back() == '\r' || key_closer.back() == ' ')) { - key_closer.pop_back(); - } - ts.arg_suffix = key_closer; - } - } - } else { - ts.argument_format = tool_call_structure::ARGS_JSON; - } - - LOG_DBG("%s: final markers: section_start='%s', section_end='%s', prefix='%s', close='%s'\n", __func__, - ts.tool_section_start.c_str(), ts.tool_section_end.c_str(), ts.function_prefix.c_str(), - ts.function_close.c_str()); -} - -void template_analyzer::analyze_bracket_tag_format(tool_call_structure & ts, - const internal_discovered_pattern & discovered) { - // Bracket-tag format: [TOOL_CALLS]name[CALL_ID]id[ARGS]{...} (Mistral Small 3.2) - ts.supports_tools = true; - ts.function_format = tool_call_structure::FUNC_BRACKET_TAG; - ts.argument_format = tool_call_structure::ARGS_JSON; - - // The function_opener contains the bracket tag before the function name (e.g., "[TOOL_CALLS]") - // Each tool call starts with this tag, so it's the per_call_start, not a section wrapper - // tool_section_start/end should be empty since there's no overall section wrapper - ts.tool_section_start = ""; - ts.tool_section_end = ""; - ts.per_call_start = discovered.function_opener; - - // Extract markers from function_name_suffix (e.g., "[CALL_ID]call_0001[ARGS]" or just "[ARGS]") - // Pattern: [ID_MARKER]...[ARGS_MARKER] or just [ARGS_MARKER] - if (!discovered.function_name_suffix.empty()) { - // Find all bracket tags in the suffix - std::vector tags; - size_t pos = 0; - while ((pos = discovered.function_name_suffix.find('[', pos)) != std::string::npos) { - size_t end = discovered.function_name_suffix.find(']', pos); - if (end != std::string::npos) { - tags.push_back(discovered.function_name_suffix.substr(pos, end - pos + 1)); - pos = end + 1; - } else { - break; - } - } - - // Classify tags: args marker contains "ARG", id marker contains "ID" or "CALL" - for (const auto & tag : tags) { - std::string upper_tag = tag; - for (auto & c : upper_tag) { - c = static_cast(std::toupper(static_cast(c))); - } - if (upper_tag.find("ARG") != std::string::npos) { - ts.args_marker = tag; - } else if (upper_tag.find("ID") != std::string::npos || upper_tag.find("CALL") != std::string::npos) { - ts.id_marker = tag; - } - } - } - - LOG_DBG("FUNC_BRACKET_TAG: per_call_start='%s', id_marker='%s', args_marker='%s'\n", ts.per_call_start.c_str(), - ts.id_marker.c_str(), ts.args_marker.c_str()); -} - -void template_analyzer::analyze_recipient_based_format(tool_call_structure & ts, - const internal_discovered_pattern & discovered) { - // Recipient-based format (Functionary v3.2): >>>recipient\n{content} - // where recipient is either "all" (for content) or a function name (for tools) - ts.supports_tools = true; - ts.function_format = tool_call_structure::FUNC_RECIPIENT_BASED; - ts.argument_format = tool_call_structure::ARGS_JSON; // Python dict format, parse as JSON - - // The tool_call_start_marker is used as the recipient delimiter - ts.tool_section_start = discovered.tool_call_start_marker; - ts.tool_section_end = ""; - - // For recipient-based format, content is wrapped in tool_call_start_marker + "all\n" - // This needs to be detected and stripped. We detect this by checking if the - // content_start marker (from phase 1 analysis) starts with tool_call_start_marker - // If not already detected, infer it from the pattern. - // Note: This is set on the ContentStructure result, not ToolCallStructure - // The caller (analyze_template) will have the ContentStructure to modify - - LOG_DBG("FUNC_RECIPIENT_BASED: delimiter='%s'\n", ts.tool_section_start.c_str()); -} - -void template_analyzer::analyze_markdown_code_block_format(tool_call_structure & ts, - const internal_discovered_pattern & discovered) { - // Markdown code block format (Cohere Command-R Plus): - // Action: - // ```json - // [ - // { - // "tool_name": "...", - // "parameters": {...} - // } - // ] - // ``` - ts.supports_tools = true; - ts.function_format = tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK; - ts.argument_format = tool_call_structure::ARGS_JSON; - - // Extract the code block marker (e.g., "Action:") - // The tool_call_start_marker should contain "Action:" followed by newline - if (!discovered.tool_call_start_marker.empty()) { - // Extract just the marker text (e.g., "Action:") - // The marker may be followed by whitespace/newline in the template - size_t marker_end = discovered.tool_call_start_marker.find_first_of(" \n\r\t"); - if (marker_end != std::string::npos) { - ts.code_block_marker = discovered.tool_call_start_marker.substr(0, marker_end); - } else { - ts.code_block_marker = discovered.tool_call_start_marker; - } - } - - // Extract the code block language (e.g., "json") - // For Command-R Plus format: Action:\n```json\n[...] - // The code fence is in tool_call_opener (before the function name), not function_name_suffix - if (!discovered.function_name_suffix.empty() && discovered.function_name_suffix.find("```") != std::string::npos) { - // Format: ```json or ```json\n - size_t code_fence_pos = discovered.function_name_suffix.find("```"); - size_t lang_start = code_fence_pos + 3; - // Find the end of the language identifier (newline, space, or end of string) - size_t lang_end = discovered.function_name_suffix.find_first_of(" \n\r\t", lang_start); - if (lang_end != std::string::npos && lang_end > lang_start) { - ts.code_block_language = discovered.function_name_suffix.substr(lang_start, lang_end - lang_start); - } else { - // No language identifier after ```, will use "json" as default - ts.code_block_language = "json"; - } - } else if (!discovered.tool_call_opener.empty() && discovered.tool_call_opener.find("```") != std::string::npos) { - // Code fence is in tool_call_opener (before the function name) - // Format: Action:\n```json\n[... - size_t code_fence_pos = discovered.tool_call_opener.find("```"); - size_t lang_start = code_fence_pos + 3; - // Find the end of the language identifier (newline, space, or end of string) - size_t lang_end = discovered.tool_call_opener.find_first_of(" \n\r\t", lang_start); - if (lang_end != std::string::npos && lang_end > lang_start) { - ts.code_block_language = discovered.tool_call_opener.substr(lang_start, lang_end - lang_start); - } else { - // No language identifier after ```, will use "json" as default - ts.code_block_language = "json"; - } - } else { - // Default to "json" if no code fence found - ts.code_block_language = "json"; - } - - // The tool_section_end should be the closing code fence: ``` - if (!discovered.tool_call_closer.empty() && discovered.tool_call_closer.find("```") != std::string::npos) { - // Extract just the closing code fence (may have trailing content) - size_t fence_pos = discovered.tool_call_closer.find("```"); - size_t fence_end = fence_pos + 3; - // Include any non-newline characters after ``` (like language identifier if present) - while (fence_end < discovered.tool_call_closer.length() && discovered.tool_call_closer[fence_end] != '\n' && - discovered.tool_call_closer[fence_end] != '\r') { - fence_end++; - } - ts.tool_section_end = discovered.tool_call_closer.substr(fence_pos, fence_end - fence_pos); - } else { - // Default closing code fence - ts.tool_section_end = "```"; - } - - // JSON array format for function calls - ts.name_field = discovered.tool_name_field; - ts.args_field = discovered.tool_args_field; - ts.id_field = discovered.tool_id_field; - - LOG_DBG("FUNC_MARKDOWN_CODE_BLOCK: marker='%s', language='%s', section_end='%s'\n", ts.code_block_marker.c_str(), - ts.code_block_language.c_str(), ts.tool_section_end.c_str()); -} diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index 0f4d153d06..a721a30f1c 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -1,250 +1,361 @@ -#include "chat-auto-parser-helpers.h" #include "chat-auto-parser.h" +#include "chat-diff-analyzer.h" #include "chat-peg-parser.h" #include "chat.h" #include "json-schema-to-grammar.h" -#include "log.h" #include "nlohmann/json.hpp" +#include -#include using json = nlohmann::ordered_json; -common_chat_params universal_peg_generator::generate_parser(const template_analysis_result & analysis, - const common_chat_template & tmpl, - const struct templates_params & inputs) { +// Helper to iterate over tools/functions +static void foreach_function(const json & tools, const std::function & fn) { + for (const auto & tool : tools) { + if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { + continue; + } + fn(tool); + } +} + +common_chat_params universal_peg_generator::generate_parser(const common_chat_template & tmpl, + const struct templates_params & inputs) { + // Run differential analysis to extract template structure + auto analysis = differential_analyzer::analyze(tmpl); + return generate_parser(tmpl, inputs, analysis); +} + +common_chat_params universal_peg_generator::generate_parser(const common_chat_template & tmpl, + 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); + + // Build the parser using the analysis results + auto parser = build_parser(analysis, inputs, thinking_forced_open, thinking_forced_closed); + + // Create the result structure common_chat_params data; + data.prompt = common_chat_template_direct_apply(tmpl, inputs); + data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; + data.preserved_tokens = analysis.preserved_tokens; + data.parser = parser.save(); - try { - LOG_DBG("%s\n", __func__); + // Build grammar if tools are present + bool has_tools = inputs.tools.is_array() && !inputs.tools.empty(); + bool include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE; - // Patch messages if template requires non-null content - // Some templates (e.g., iquest) render null as "None" when concatenating strings - std::optional messages_override; - if (analysis.tools.requires_nonnull_content && !inputs.messages.empty()) { - LOG_DBG("Patching null content to empty string (template requires non-null content)\n"); - json patched_messages = inputs.messages; - for (auto & msg : patched_messages) { - if (msg.contains("content") && msg["content"].is_null()) { - msg["content"] = ""; - } - } - messages_override = patched_messages; - } + if (include_grammar) { + data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; - if (inputs.messages.empty()) { - // Some templates don't handle empty messages well - always leave something in - json message = { - { { "role", "user" }, { "content", "Hello" } } - }; - messages_override.emplace(message); - } - - // Calculate prompt first to detect forced thinking - data.prompt = common_chat_template_direct_apply(tmpl, inputs, messages_override); - - // Determine if thinking is forced open based on prompt ending - bool thinking_forced_open = false; - if (analysis.content.reasoning_mode == content_structure::REASONING_FORCED_OPEN) { - if (inputs.enable_thinking) { - thinking_forced_open = true; - LOG_DBG("Thinking forced open based on template analysis\n"); - } else { - // Template ends with reasoning start marker but thinking is disabled - // Append the end marker to close it - data.prompt += analysis.content.reasoning_end; - LOG_DBG("Appended reasoning end marker since thinking is disabled\n"); - } - } - data.thinking_forced_open = thinking_forced_open; - - // Build the unified parser - auto arena = build_parser(analysis, tmpl, inputs, thinking_forced_open); - data.parser = arena.save(); - - // Determine format - bool has_tools = - inputs.tools.is_array() && !inputs.tools.empty() && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE; - - if (has_tools && analysis.tools.supports_tools) { - // Unified format that handles both JSON and tagged tool calls - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser with tool support (format: PEG_NATIVE)\n"); - } else if (analysis.content.reasoning_mode != content_structure::REASONING_NONE) { - // Reasoning markers detected - use PEG parser to handle thinking blocks - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for reasoning handling (format: PEG_NATIVE)\n"); - } else if (analysis.content.content_mode != content_structure::CONTENT_PLAIN) { - // Content markers detected - use PEG parser to strip them even without tools - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for content marker stripping (format: PEG_NATIVE)\n"); - } else if (analysis.tools.function_format == tool_call_structure::FUNC_RECIPIENT_BASED) { - // Recipient-based format (e.g., Functionary v3.2): >>>recipient\n{content} - // Need PEG parser to handle recipient delimiter parsing - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for recipient-based format (format: PEG_NATIVE)\n"); - } else if (analysis.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME) { - // Tag-with-name format (e.g., func_name\n{args} for Functionary) - // Need PEG parser to handle function name parsing - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for tag-with-name format (format: PEG_NATIVE)\n"); - } else if (analysis.tools.function_format == tool_call_structure::FUNC_BRACKET_TAG) { - // Bracket-tag format (e.g., [TOOL_CALLS]name[CALL_ID]id[ARGS]{...} for Mistral Small 3.2) - // Need PEG parser to handle bracket tag parsing - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for bracket-tag format (format: PEG_NATIVE)\n"); - } else if (analysis.tools.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED) { - // Prefixed-indexed format (e.g., Kimi-K2) - // Need PEG parser to handle namespace and indexed format - data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; - LOG_DBG("Generated unified parser for prefixed-indexed format (format: PEG_NATIVE)\n"); - } else { - data.format = COMMON_CHAT_FORMAT_CONTENT_ONLY; - LOG_DBG("Generated unified parser without tools or content markers (format: CONTENT_ONLY)\n"); - } - - // Determine trigger word for lazy grammar - std::string trigger_word; - if (!analysis.tools.tool_section_start.empty() || - analysis.tools.function_format == tool_call_structure::FUNC_RECIPIENT_BASED) { - trigger_word = analysis.tools.tool_section_start; - } else if (analysis.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME) { - trigger_word = analysis.tools.function_prefix; - } else if (analysis.tools.function_format == tool_call_structure::FUNC_BRACKET_TAG || - analysis.tools.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED) { - // For formats with per-call markers, use per_call_start as trigger - trigger_word = analysis.tools.per_call_start; - } - - // Build grammar for tool calls - data.grammar_lazy = analysis.tools.supports_tools && has_tools; - - // For FUNC_TAG_WITH_NAME with empty prefix (Functionary), disable lazy grammar - // since there's no clear trigger word - constrain from the start - if (analysis.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME && - analysis.tools.function_prefix.empty()) { - data.grammar_lazy = false; - } - - if (data.grammar_lazy) { - if (!trigger_word.empty()) { - data.grammar_triggers.push_back({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_word }); - } - } - - // Build grammar data.grammar = build_grammar([&](const common_grammar_builder & builder) { - if (inputs.tools.is_array()) { - for (const auto & tool : inputs.tools) { - if (!tool.contains("type") || tool.at("type") != "function" || !tool.contains("function")) { - continue; - } - const auto & function = tool.at("function"); - if (function.contains("parameters")) { - auto params = function.at("parameters"); - builder.resolve_refs(params); - } - } - } - arena.build_grammar(builder, data.grammar_lazy); + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + auto schema = function.at("parameters"); + builder.resolve_refs(schema); + }); + parser.build_grammar(builder, data.grammar_lazy); }); - // Set preserved tokens from analysis - data.preserved_tokens = analysis.preserved_tokens; - - LOG_DBG("=== UNIFIED PEG PARSER GENERATION COMPLETED ===\n"); - - } catch (const std::exception & e) { - LOG_DBG("Unified parser generation failed: %s\n", e.what()); - throw; + // 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; + if (!trigger_marker.empty()) { + data.grammar_triggers = { + { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_marker } + }; + } } return data; } -common_peg_arena universal_peg_generator::build_parser(const template_analysis_result & analysis, - const common_chat_template & tmpl, - const struct templates_params & inputs, - bool thinking_forced_open) { - GGML_UNUSED(tmpl); +common_peg_arena universal_peg_generator::build_parser(const diff_analysis_result & analysis, + const struct templates_params & inputs, + bool thinking_forced_open, + 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; - auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { - // Build reasoning block using ContentStructure - auto reasoning = p.build_reasoning_block(analysis.content, inputs.reasoning_format, thinking_forced_open); + common_peg_parser reasoning = p.eps(); + bool extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE; + bool enable_thinking = inputs.enable_thinking; - // Build content block using ContentStructure - // Note: we don't pass tool_section_start here because content-before-tools handling - // is done inline in each branch below with p.content(p.until(marker)) - auto content = p.build_content_block(analysis.content, inputs.reasoning_format); - - // Build tool section using ToolCallStructure (if applicable) - bool has_tools = - inputs.tools.is_array() && !inputs.tools.empty() && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE; - - if (has_tools && analysis.tools.supports_tools) { - bool force_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; - auto tool_section = - p.build_tool_section(analysis.tools, inputs.tools, inputs.parallel_tool_calls, force_calls); - - // Compose: reasoning -> content before tools -> tool_section -> trailing content - // When thinking is forced open, the reasoning block expects . - // For tool-only messages (no thinking content), the model may output tools directly - // without the tag, so we need to make reasoning optional in that case. - // But if reasoning_format is NONE, the reasoning block is already eps() - don't wrap it - // in optional() as that would generate invalid grammar. - auto reasoning_for_tools = - (thinking_forced_open && inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE) ? - p.optional(reasoning) : - reasoning; - - if (!analysis.tools.tool_section_start.empty()) { - // With section markers: look for start marker to delimit content - auto content_before_tools = p.content(p.until(analysis.tools.tool_section_start)); - return p.sequence({ reasoning_for_tools, p.space(), content_before_tools, p.space(), tool_section, - p.space(), p.optional(p.content(p.rest())), p.end() }); - } - if (analysis.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME && - !analysis.tools.function_prefix.empty()) { - // Tag-with-name format (e.g., >>>func_name): content stops at function prefix - auto content_before_tools = p.content(p.until(analysis.tools.function_prefix)); - return p.sequence( - { reasoning_for_tools, p.space(), content_before_tools, p.space(), tool_section, p.end() }); - } - if (analysis.tools.function_format == tool_call_structure::FUNC_TAG_WITH_NAME) { - // Functionary-style format: tool call starts immediately (e.g., func_name\n{args}) - // No content before tools in this format - the entire output is the tool call - return p.sequence({ reasoning_for_tools, p.space(), tool_section, p.end() }); - } - if (analysis.tools.function_format == tool_call_structure::FUNC_BRACKET_TAG || - analysis.tools.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED) { - // Bracket-tag (Mistral Small 3.2) or prefixed-indexed (Kimi-K2) format: - // Tool calls start with per_call_start marker (e.g., [TOOL_CALLS], <|tool_call_begin|>) - if (!analysis.tools.per_call_start.empty()) { - auto content_before_tools = p.content(p.until(analysis.tools.per_call_start)); - return p.sequence( - { reasoning_for_tools, p.space(), content_before_tools, p.space(), tool_section, p.end() }); + if (extract_reasoning && enable_thinking && analysis.reasoning != 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) { + // 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); } - // Fallback: no content before tools - return p.sequence({ reasoning_for_tools, p.space(), tool_section, p.end() }); + } else if (analysis.reasoning == reasoning_mode::DELIMITER) { + reasoning = p.optional(p.reasoning(p.until(m.reasoning_end)) + m.reasoning_end); } - if (analysis.tools.function_format == tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK && - !analysis.tools.code_block_marker.empty()) { - // Markdown code block format (Cohere Command-R Plus): - // Content stops at the code_block_marker (e.g., "Action:") - auto content_before_tools = p.content(p.until(analysis.tools.code_block_marker)); - return p.sequence( - { reasoning_for_tools, p.space(), content_before_tools, p.space(), tool_section, p.end() }); - } - // No section markers (raw JSON format): content must stop at JSON object start - // Tool calls start with "{", so use that as a delimiter - auto content_before_tools = p.content(p.until("{")); - return p.sequence( - { reasoning_for_tools, p.space(), content_before_tools, p.space(), tool_section, p.end() }); } - // No tools - just reasoning (if any) followed by content - return p.sequence({ reasoning, p.space(), content, p.end() }); - }); + bool has_tools = inputs.tools.is_array() && !inputs.tools.empty(); + bool has_response_format = inputs.json_schema.is_object() && !inputs.json_schema.empty(); - return parser; + if (has_response_format) { + 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) { + return build_tool_parser(p, analysis, inputs, reasoning); + } + + if (analysis.content == content_mode::ALWAYS_WRAPPED && + !m.content_start.empty() && !m.content_end.empty()) { + + bool extracting_reasoning = extract_reasoning && enable_thinking && analysis.reasoning != reasoning_mode::NONE; + + if (extracting_reasoning) { + return reasoning + m.content_start + p.content(p.until(m.content_end)) + m.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 reasoning + p.content(p.rest()) + p.end(); + }); } + +common_peg_parser universal_peg_generator::build_tool_parser( + common_chat_peg_unified_builder & p, + const diff_analysis_result & analysis, + const templates_params & inputs, + const common_peg_parser & reasoning) { + + const auto & m = analysis.markers; + + // Build tool choice parser based on format + common_peg_parser tool_choice = p.choice(); + + if (analysis.tools == tool_format::JSON_NATIVE) { + // Pure JSON format: use standard_json_tools helper + // 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; + + if (!analysis.function_field.empty() && + analysis.function_field != "function" && + name_field.find('.') == std::string::npos) { + name_field = analysis.function_field + "." + name_field; + args_field = analysis.function_field + "." + args_field; + } + + auto tools_parser = p.standard_json_tools( + m.tool_section_start, + m.tool_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 + ); + + // 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); + 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); + return reasoning + p.optional(p.content(content_before_tools)) + tools_parser + p.end(); + } + + if (analysis.tools == tool_format::TAG_WITH_JSON) { + // Tag-based with JSON args: {args} + // With optional call_id: [CALL_ID]id[ARGS]{args} + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + std::string name = function.at("name"); + const auto & schema = function.at("parameters"); + + // 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()) { + // Optional call_id followed by required call_id_suffix (which is also args_start) + // Format: optional([CALL_ID] + call_id_value) + [ARGS] + call_id_section = p.optional(m.call_id_prefix + p.tool_id(p.until(m.call_id_suffix))) + m.call_id_suffix; + } + + auto func_parser = p.tool_open(m.func_name_prefix + p.tool_name(p.literal(name)) + m.func_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; + } + + tool_choice |= p.rule("tool-" + name, func_parser); + }); + + auto require_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; + + common_peg_parser tool_calls = p.eps(); + + if (!m.per_call_start.empty()) { + // Per-call wrapping: each call individually wrapped + auto wrapped_call = m.per_call_start + tool_choice + m.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))); + } + } else { + std::string separator = m.call_separator; + if (separator.empty()) { + 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); + } else { + tool_calls = p.trigger_rule("tool-call", + m.tool_section_start + tool_choice + m.tool_section_end); + } + } + + if (!require_calls) { + tool_calls = p.optional(tool_calls); + } + + std::string trigger_marker = !m.tool_section_start.empty() ? m.tool_section_start : m.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(); + } + + if (analysis.tools == tool_format::TAG_WITH_TAGGED) { + // Tag-based with tagged args: value + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + std::string name = function.at("name"); + const auto & params = function.at("parameters"); + + if (!params.contains("properties") || !params.at("properties").is_object()) { + return; + } + + const auto & properties = params.at("properties"); + std::set required; + if (params.contains("required") && params.at("required").is_array()) { + params.at("required").get_to(required); + } + + // Build parser for each argument + std::vector arg_parsers; + for (const auto & [param_name, param_schema] : properties.items()) { + bool is_required = required.find(param_name) != required.end(); + 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 + + (type == "string" ? + p.tool_arg_string_value(p.schema(p.until(m.arg_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)) + ); + + if (is_required) { + arg_parsers.push_back(p.rule("tool-" + name + "-arg-" + param_name, arg)); + } else { + arg_parsers.push_back(p.optional(p.rule("tool-" + name + "-arg-" + param_name, arg))); + } + } + + // Build arg sequence with space() between consecutive args + common_peg_parser args_seq = p.eps(); + for (size_t i = 0; i < arg_parsers.size(); i++) { + if (i > 0) { + args_seq = args_seq + p.space(); + } + args_seq = args_seq + arg_parsers[i]; + } + + // 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()) { + // Optional call_id followed by required call_id_suffix + call_id_section = p.optional(m.call_id_prefix + p.tool_id(p.until(m.call_id_suffix))) + m.call_id_suffix; + } + + auto func_parser = p.tool_open(m.func_name_prefix + p.tool_name(p.literal(name)) + m.func_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 { + func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper + } + + tool_choice |= p.rule("tool-" + name, func_parser); + }); + + auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; + + common_peg_parser tool_calls = p.eps(); + + if (!m.per_call_start.empty()) { + // Per-call wrapping: each call individually wrapped (e.g., ...) + auto wrapped_call = m.per_call_start + p.space() + tool_choice + p.space() + m.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))); + } + } else { + std::string separator = m.call_separator; + if (separator.empty()) { + 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); + } else { + tool_calls = p.trigger_rule("tool-call", + m.tool_section_start + p.space() + tool_choice + p.space() + m.tool_section_end); + } + } + + if (!require_tools) { + tool_calls = p.optional(tool_calls); + } + + std::string trigger_marker = !m.tool_section_start.empty() ? m.tool_section_start : m.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(); + } + + GGML_ABORT("Unable to create tool parser"); +} \ No newline at end of file diff --git a/common/chat-auto-parser-helpers.cpp b/common/chat-auto-parser-helpers.cpp index c63012c2a8..9c345d6f6e 100644 --- a/common/chat-auto-parser-helpers.cpp +++ b/common/chat-auto-parser-helpers.cpp @@ -1,1419 +1,376 @@ #include "chat-auto-parser-helpers.h" -#include "chat-auto-parser.h" -#include "chat.h" -#include "log.h" - +#include "chat-diff-analyzer.h" #include "nlohmann/json.hpp" +#include + using json = nlohmann::ordered_json; -bool string_ends_with(const std::string & str, const std::string & suffix) { - return str.size() >= suffix.size() && str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; +std::string trim_whitespace(const std::string & str) { + size_t start = 0; + while (start < str.length() && std::isspace(static_cast(str[start]))) { + start++; + } + + if (start == str.length()) { + return ""; + } + + size_t end = str.length() - 1; + while (end > start && std::isspace(static_cast(str[end]))) { + end--; + } + + return str.substr(start, end - start + 1); } -void trim_whitespace(std::string & str) { +std::string trim_leading_whitespace(const std::string & str) { + size_t start = 0; + while (start < str.length() && std::isspace(static_cast(str[start]))) { + start++; + } + + return str.substr(start); +} + +std::string trim_trailing_whitespace(const std::string & str) { if (str.empty()) { - return; - } - size_t first = str.find_first_not_of(" \n\t\r"); - if (first == std::string::npos) { - str.clear(); - return; - } - size_t last = str.find_last_not_of(" \n\t\r"); - str = str.substr(first, (last - first + 1)); -} - -void trim_trailing_newlines(std::string & str) { - while (!str.empty() && (str.back() == '\n' || str.back() == '\r')) { - str.pop_back(); - } -} - -size_t count_non_whitespace(const std::string & str) { - size_t count = 0; - for (char c : str) { - if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { - count++; - } - } - return count; -} - -size_t find_last_of_any(const std::string & str, const std::string & chars, size_t start_pos) { - size_t last_pos = std::string::npos; - for (char c : chars) { - size_t pos = str.rfind(c, start_pos); - if (pos != std::string::npos && (last_pos == std::string::npos || pos > last_pos)) { - last_pos = pos; - } - } - return last_pos; -} - -std::string extract_tag_name(const std::string & tag) { - if (tag.empty() || tag[0] != '<') { return ""; } - std::string tag_name = tag.substr(1); - size_t end_bracket = tag_name.find_first_of(" >"); - if (end_bracket != std::string::npos) { - tag_name = tag_name.substr(0, end_bracket); + + size_t end = str.length() - 1; + while (end > 0 && std::isspace(static_cast(str[end]))) { + end--; } - return tag_name; -} - -std::string create_closing_tag(const std::string & opening_tag) { - if (opening_tag.empty()) { + + // If first char is also whitespace, return empty string + if (end == 0 && std::isspace(static_cast(str[0]))) { return ""; } - if (opening_tag[0] == '<') { - std::string name = extract_tag_name(opening_tag); - return ""; - } - if (opening_tag.front() == '[' && opening_tag.back() == ']') { - std::string name = opening_tag.substr(1, opening_tag.length() - 2); - return "[/" + name + "]"; - } - return ""; + + return str.substr(0, end + 1); } -std::string find_common_prefix(const std::vector & strings) { - if (strings.empty()) { - return ""; - } - if (strings.size() == 1) { - return strings[0]; +std::string trim_trailing_newlines(const std::string & str) { + size_t end = str.length(); + while (end > 0 && str[end - 1] == '\n') { + end--; } - std::string common = strings[0]; - for (size_t i = 1; i < strings.size(); ++i) { - const std::string & current = strings[i]; - std::string temp_common; - for (size_t j = 0; j < common.length() && j < current.length(); ++j) { - if (common[j] == current[j]) { - temp_common += common[j]; - } else { - break; + return str.substr(0, end); +} + +// Helper to find unmatched bracket/tag in a string +// Finds an unmatched bracket in a string. +// search_backwards=true: finds unclosed opening bracket at end (returns bracket position) +// search_backwards=false: finds unopened closing bracket at start (returns position after bracket) +static size_t find_unmatched_bracket(const std::string & str, bool search_backwards) { + if (str.empty()) { + return std::string::npos; + } + + // Compute iteration bounds and bracket types based on direction + const char * primary_brackets = search_backwards ? "<[" : ">]"; + + for (size_t i = 0; i < str.length(); ++i) { + // Map iteration index to actual position based on direction + size_t pos = search_backwards ? (str.length() - 1 - i) : i; + char c = str[pos]; + + // Check if this is a primary bracket we're looking for + if (c == primary_brackets[0] || c == primary_brackets[1]) { + // Get the matching bracket: < matches >, [ matches ], and vice versa + char match_bracket = (c == '<' || c == '>') ? (c == '<' ? '>' : '<') : (c == '[' ? ']' : '['); + + // Search for matching bracket in the appropriate range + size_t inner_start = search_backwards ? (pos + 1) : 0; + size_t inner_end = search_backwards ? str.length() : pos; + bool found_match = false; + + for (size_t j = inner_start; j < inner_end; ++j) { + if (str[j] == match_bracket) { + found_match = true; + break; + } + } + + if (!found_match) { + return search_backwards ? pos : (pos + 1); } } - common = temp_common; } - return common; + + return std::string::npos; } -std::string find_common_suffix_generic(const std::vector & strings) { - if (strings.empty()) { - return ""; - } - if (strings.size() == 1) { - return strings[0]; +static size_t find_unclosed_bracket_at_end(const std::string & str) { + return find_unmatched_bracket(str, true); +} + +static size_t find_unopened_bracket_at_start(const std::string & str) { + return find_unmatched_bracket(str, false); +} + +// Returns true if `s` contains an unmatched bracket. +// search_backwards=true: looks for opening bracket without matching closing after it +// search_backwards=false: looks for closing bracket without matching opening before it +static bool contains_unmatched_bracket(const std::string & s, char opening, char closing, bool search_backwards) { + if (s.empty()) { + return false; } - std::string common = strings[0]; - for (size_t i = 1; i < strings.size(); ++i) { - const std::string & current = strings[i]; - std::string temp_common; - size_t min_len = std::min(common.length(), current.length()); - for (size_t j = 0; j < min_len; ++j) { - size_t pos_common = common.length() - j - 1; - size_t pos_current = current.length() - j - 1; - if (common[pos_common] == current[pos_current]) { - temp_common = common[pos_common] + temp_common; - } else { - break; + char primary = search_backwards ? opening : closing; + + for (size_t i = 0; i < s.length(); ++i) { + // Map iteration index to actual position based on direction + size_t pos = search_backwards ? (s.length() - 1 - i) : i; + + if (s[pos] == primary) { + // Search for matching bracket in the appropriate range + size_t inner_start = search_backwards ? (pos + 1) : 0; + size_t inner_end = search_backwards ? s.length() : pos; + char match_bracket = search_backwards ? closing : opening; + bool found_match = false; + + for (size_t j = inner_start; j < inner_end; ++j) { + if (s[j] == match_bracket) { + found_match = true; + break; + } + } + + if (!found_match) { + return true; } } - common = temp_common; } - return common; + return false; } -std::string find_common_substring_limited(const std::vector & strings, - size_t max_length, - const std::string & delimiters) { - std::string common = find_common_prefix(strings); - if (common.length() > max_length) { - size_t pos = find_last_of_any(common, delimiters, common.length() - 1); - if (pos != std::string::npos && pos > 0) { - return common.substr(0, pos + 1); +static bool contains_unopened_closing(const std::string & s, char opening, char closing) { + return contains_unmatched_bracket(s, opening, closing, false); +} + +static bool contains_unclosed_opening(const std::string & s, char opening, char closing) { + return contains_unmatched_bracket(s, opening, closing, true); +} + +// Moves incomplete tags from prefix/suffix into left/right parts +// Only moves tags when we detect the split pattern in BOTH left and right +static diff_split fix_tag_boundaries(diff_split result) { + // Check if prefix ends with an unclosed bracket/tag + // No fixed window: search the entire neighboring strings for matching brackets + size_t unclosed_pos = find_unclosed_bracket_at_end(result.prefix); + if (unclosed_pos != std::string::npos) { + char opening_bracket = result.prefix[unclosed_pos]; + char closing_bracket = (opening_bracket == '<') ? '>' : ']'; + + // Look for the specific closing bracket that matches our opening bracket + bool left_has_pattern = contains_unopened_closing(result.left, opening_bracket, closing_bracket); + bool right_has_pattern = contains_unopened_closing(result.right, opening_bracket, closing_bracket); + bool suffix_has_pattern = contains_unopened_closing(result.suffix, opening_bracket, closing_bracket); + + // Move the tag if both sides satisfy: has pattern OR is empty (and other has pattern) + // This handles cases like: left="" right="_begin|>..." or left="stuff>" right="stuff>" + bool left_satisfies = left_has_pattern || (result.left.empty() && suffix_has_pattern); + bool right_satisfies = right_has_pattern || (result.right.empty() && suffix_has_pattern); + + if (left_satisfies && right_satisfies) { + // Move the unclosed tag from prefix to left/right + std::string tag_part = result.prefix.substr(unclosed_pos); + result.prefix = result.prefix.substr(0, unclosed_pos); + result.left = tag_part + result.left; + result.right = tag_part + result.right; } - return common.substr(0, max_length); - } - return common; -} - -std::string apply_template(common_chat_template & tmpl, - const struct templates_params & inputs, - const std::optional & messages_override, - const std::optional & tools_override, - const std::optional & additional_context) { - struct templates_params final_inputs(inputs); - final_inputs.messages = messages_override ? *messages_override : inputs.messages; - if (tools_override) { - final_inputs.tools = *tools_override; - } else { - final_inputs.tools = inputs.tools.empty() ? json() : inputs.tools; - } - final_inputs.add_generation_prompt = inputs.add_generation_prompt; - final_inputs.extra_context = inputs.extra_context; - final_inputs.extra_context["enable_thinking"] = inputs.enable_thinking; - if (additional_context) { - final_inputs.extra_context.merge_patch(*additional_context); } - try { - return common_chat_template_direct_apply(tmpl, inputs); - } catch (const std::exception & e) { - LOG_ERR("Template application failed: %s\n", e.what()); - return ""; - } -} + // Check if suffix starts with an unopened bracket/tag + size_t unopened_end = find_unopened_bracket_at_start(result.suffix); + if (unopened_end != std::string::npos) { + char closing_bracket = + result.suffix[unopened_end - 1]; // -1 because unopened_end is position after the bracket + char opening_bracket = (closing_bracket == '>') ? '<' : '['; -std::string adjust_to_token_boundary(const std::string & str) { - if (str.empty()) { - return str; - } + // Check if BOTH left and right have the pattern of unclosed opening bracket at the end + bool left_has_pattern = contains_unclosed_opening(result.left, opening_bracket, closing_bracket); + bool right_has_pattern = contains_unclosed_opening(result.right, opening_bracket, closing_bracket); + bool prefix_has_pattern = contains_unclosed_opening(result.prefix, opening_bracket, closing_bracket); - // Check if the string ends in the middle of a <|...|> token - // Look for unmatched <| at the end + // Move the tag if both sides satisfy: has pattern OR is empty (and other has pattern) + bool left_satisfies = left_has_pattern || (result.left.empty() && prefix_has_pattern); + bool right_satisfies = right_has_pattern || (result.right.empty() && prefix_has_pattern); - // Find the last <| in the string - size_t last_open = str.rfind("<|"); - if (last_open == std::string::npos) { - return str; // No special tokens - } - - // Find if there's a |> after the last <| - size_t matching_close = str.find("|>", last_open + 2); - if (matching_close != std::string::npos) { - // The token is complete, return as-is - return str; - } - - // The string is truncated mid-token - // Truncate to just before the incomplete token - std::string result = str.substr(0, last_open); - - // Trim any trailing whitespace - while (!result.empty() && (result.back() == ' ' || result.back() == '\t' || result.back() == '\n')) { - result.pop_back(); + if (left_satisfies && right_satisfies) { + // Move the unopened tag from suffix to left/right + std::string tag_part = result.suffix.substr(0, unopened_end); + result.suffix = result.suffix.substr(unopened_end); + result.left = result.left + tag_part; + result.right = result.right + tag_part; + } } return result; } -// Fullwidth vertical bar: | (U+FF5C) is 3 bytes in UTF-8: 0xEF 0xBD 0x9C -static const std::string FULLWIDTH_PIPE = "\xef\xbd\x9c"; // | -static const std::string TOKEN_OPENER_STD = "<|"; -static const std::string TOKEN_OPENER_FW = "<" + FULLWIDTH_PIPE; // <| -static const std::string TOKEN_CLOSER_STD = "|>"; -static const std::string TOKEN_CLOSER_FW = FULLWIDTH_PIPE + ">"; // |> +diff_split calculate_diff_split(const std::string & left, const std::string & right) { + diff_split result; -size_t find_token_opener(const std::string & str, size_t start_pos) { - size_t pos_std = str.find(TOKEN_OPENER_STD, start_pos); - size_t pos_fw = str.find(TOKEN_OPENER_FW, start_pos); - - if (pos_std == std::string::npos) { - return pos_fw; + // Find longest common prefix + size_t prefix_len = 0; + size_t min_len = std::min(left.length(), right.length()); + while (prefix_len < min_len && left[prefix_len] == right[prefix_len]) { + prefix_len++; } - if (pos_fw == std::string::npos) { - return pos_std; - } - return std::min(pos_std, pos_fw); -} + result.prefix = left.substr(0, prefix_len); -size_t find_token_closer(const std::string & str, size_t start_pos) { - size_t pos_std = str.find(TOKEN_CLOSER_STD, start_pos); - size_t pos_fw = str.find(TOKEN_CLOSER_FW, start_pos); + // Find longest common suffix, ending no later than the end of the longest common prefix + size_t suffix_len = 0; + while (suffix_len < min_len - prefix_len) { + size_t left_pos = left.length() - 1 - suffix_len; + size_t right_pos = right.length() - 1 - suffix_len; - if (pos_std == std::string::npos) { - return pos_fw; - } - if (pos_fw == std::string::npos) { - return pos_std; - } - return std::min(pos_std, pos_fw); -} - -size_t get_token_opener_length(const std::string & str, size_t pos) { - if (pos >= str.length()) { - return 0; - } - if (str.compare(pos, TOKEN_OPENER_FW.length(), TOKEN_OPENER_FW) == 0) { - return TOKEN_OPENER_FW.length(); // 4 bytes for <| - } - if (str.compare(pos, TOKEN_OPENER_STD.length(), TOKEN_OPENER_STD) == 0) { - return TOKEN_OPENER_STD.length(); // 2 bytes for <| - } - return 0; -} - -size_t get_token_closer_length(const std::string & str, size_t pos) { - if (pos >= str.length()) { - return 0; - } - if (str.compare(pos, TOKEN_CLOSER_FW.length(), TOKEN_CLOSER_FW) == 0) { - return TOKEN_CLOSER_FW.length(); // 4 bytes for |> - } - if (str.compare(pos, TOKEN_CLOSER_STD.length(), TOKEN_CLOSER_STD) == 0) { - return TOKEN_CLOSER_STD.length(); // 2 bytes for |> - } - return 0; -} - -std::string strip_eos_token(const std::string & str) { - if (str.empty()) { - return str; - } - - // Find the last token in the string - // We need to find a token that looks like an EOS marker - // Common patterns: - // - <|eot_id|>, <|eos|>, <|end|>, <|endoftext|> - // - <|end▁of▁sentence|> (DeepSeek fullwidth) - - size_t last_closer = std::string::npos; - size_t search_pos = str.length(); - - // Search backwards for the last token closer - while (search_pos > 0) { - // Check for fullwidth closer first (it's longer) - if (search_pos >= TOKEN_CLOSER_FW.length()) { - size_t check_pos = search_pos - TOKEN_CLOSER_FW.length(); - if (str.compare(check_pos, TOKEN_CLOSER_FW.length(), TOKEN_CLOSER_FW) == 0) { - last_closer = check_pos; - break; - } - } - // Check for standard closer - if (search_pos >= TOKEN_CLOSER_STD.length()) { - size_t check_pos = search_pos - TOKEN_CLOSER_STD.length(); - if (str.compare(check_pos, TOKEN_CLOSER_STD.length(), TOKEN_CLOSER_STD) == 0) { - last_closer = check_pos; - break; - } - } - search_pos--; - } - - if (last_closer == std::string::npos) { - return str; // No token closer found - } - - // Find the corresponding opener - size_t opener_search_start = (last_closer > 100) ? last_closer - 100 : 0; - size_t last_opener = std::string::npos; - size_t opener_len = 0; - - for (size_t pos = opener_search_start; pos < last_closer; pos++) { - size_t len = get_token_opener_length(str, pos); - if (len > 0) { - last_opener = pos; - opener_len = len; - } - } - - if (last_opener == std::string::npos) { - return str; // No matching opener found - } - - // Extract the token content to check if it's an EOS marker - size_t closer_len = get_token_closer_length(str, last_closer); - size_t content_start = last_opener + opener_len; - size_t content_length = last_closer - content_start; - - if (content_length == 0 || content_length > 50) { - return str; // Invalid or too long token content - } - - std::string token_content = str.substr(content_start, content_length); - - // Convert to lowercase for comparison (ASCII only, sufficient for EOS markers) - std::string lower_content; - for (char c : token_content) { - lower_content += (c >= 'A' && c <= 'Z') ? (c + 32) : c; - } - - // Check if this looks like an EOS token - // True EOS tokens: - // - <|eos|>, <|eot_id|>, <|end_of_text|>, <|endoftext|> - // - <|end▁of▁sentence|> (DeepSeek fullwidth) - // NOT EOS tokens (structural markers): - // - <|END_ACTION|>, <|TOOL_CALL_END|>, <|end_thinking|>, etc. - - bool is_eos = false; - - // Check for specific EOS patterns - if (lower_content == "eos" || lower_content == "eot_id" || lower_content == "eot" || - lower_content == "end_of_text" || lower_content == "endoftext") { - is_eos = true; - } - // DeepSeek's end_of_sentence uses fullwidth underscore (▁) which is preserved in lower_content - // The token content would be "end▁of▁sentence" (with ▁ = U+2581) - else if (token_content.find("sentence") != std::string::npos || - token_content.find("\xe2\x96\x81of\xe2\x96\x81sentence") != std::string::npos) { - is_eos = true; - } - - if (!is_eos) { - return str; // Not an EOS token - } - - // Strip the EOS token - std::string result = str.substr(0, last_opener); - - LOG_DBG("Stripped EOS token '%s' from string\n", - str.substr(last_opener, last_closer + closer_len - last_opener).c_str()); - - return result; -} - -std::string find_string_difference(const std::string & base, const std::string & extended) { - size_t common_prefix = 0; - while (common_prefix < base.length() && common_prefix < extended.length() && - base[common_prefix] == extended[common_prefix]) { - common_prefix++; - } - return extended.substr(common_prefix); -} - -std::string extract_json_field_name(const std::string & opener, - const std::string & default_name, - const std::vector & candidates) { - for (const auto & candidate : candidates) { - std::string pattern = "\"" + candidate + "\""; - if (opener.find(pattern) != std::string::npos) { - LOG_DBG("Found JSON field name '%s' in opener\n", candidate.c_str()); - return candidate; - } - } - return default_name; -} - -std::string find_closing_pattern(const std::string & diff, size_t func_pos) { - std::vector closers = { "", " " }; - - std::string best_pattern; - size_t best_pos = std::string::npos; - - for (const auto & pattern : closers) { - size_t pos = diff.find(pattern, func_pos); - if (pos != std::string::npos) { - if (pos < best_pos) { - if (pattern == "', pos); - if (end_pos != std::string::npos) { - best_pattern = diff.substr(pos, end_pos - pos + 1); - best_pos = pos; - } - } else { - best_pattern = pattern; - best_pos = pos; - } - } - } - } - return best_pattern; -} - -std::string find_tool_call_start(const std::string & diff) { - std::vector start_patterns = { "<", "[", "{", "call", "func", "tool", "TOOL" }; - for (const auto & pattern : start_patterns) { - size_t pos = diff.find(pattern); - if (pos < 5) { - if (pattern == "<") { - size_t end_pos = diff.find('>', pos); - if (end_pos != std::string::npos) { - return diff.substr(pos, end_pos - pos + 1); - } - } - if (pattern == "[" || pattern == "{") { - size_t chunk_len = std::min(diff.length() - pos, (size_t) 60); - return diff.substr(pos, chunk_len); - } - - size_t end_pos = diff.find_first_of(">]} \n", pos); - if (end_pos != std::string::npos) { - if (diff[end_pos] == '>' || diff[end_pos] == ']' || diff[end_pos] == '}') { - return diff.substr(pos, end_pos - pos + 1); - } - return diff.substr(pos, end_pos - pos); - } - return diff.substr(pos, pattern.length()); - } - } - return ""; -} - -std::string find_tool_call_end(const std::string & diff, size_t func_pos) { - char opener_char = 0; - std::string start_tag_name; - - std::string openers = "[{<"; - size_t last_opener_pos = std::string::npos; - for (char c : openers) { - size_t p = diff.rfind(c, func_pos); - if (p != std::string::npos) { - if (last_opener_pos == std::string::npos || p > last_opener_pos) { - last_opener_pos = p; - opener_char = c; - } - } - } - - size_t unclosed_bracket = diff.rfind('[', func_pos); - if (unclosed_bracket != std::string::npos) { - size_t closer = diff.find(']', unclosed_bracket); - if (closer == std::string::npos || closer > func_pos) { - opener_char = '['; - } - } - - if (opener_char == '<') { - size_t tag_start = diff.find('<', last_opener_pos); - if (tag_start != std::string::npos) { - // Include '=' in search to handle style tags - // where the closing tag is , not - size_t tag_end = diff.find_first_of(" >=\n", tag_start); - if (tag_end != std::string::npos) { - start_tag_name = diff.substr(tag_start + 1, tag_end - (tag_start + 1)); - } - } - } - - if (!start_tag_name.empty()) { - std::string expected_closer = ""; - size_t pos = diff.find(expected_closer, func_pos); - if (pos != std::string::npos) { - if (opener_char == '[') { - size_t bracket_pos = diff.rfind(']', pos); - if (bracket_pos != std::string::npos && bracket_pos > func_pos) { - return diff.substr(bracket_pos, (pos + expected_closer.length()) - bracket_pos); - } - } - return expected_closer; - } - } - - std::vector end_patterns = { "", "```", "\n", " " }; - std::string best_pattern; - size_t best_pos = std::string::npos; - - auto is_structural = [](const std::string & s) { - if (s.empty()) { - return false; - } - return s[0] == ']' || s[0] == '}' || s[0] == '>' || (s.size() >= 2 && s.substr(0, 2) == "= 3 && s.substr(0, 3) == "```"); - }; - - for (const auto & pattern : end_patterns) { - size_t pos = diff.find(pattern, func_pos); - if (pos == std::string::npos) { - continue; + // Ensure we're not going into the prefix region + if (left_pos < prefix_len || right_pos < prefix_len) { + break; } - bool current_is_struct = is_structural(pattern); - bool best_is_struct = is_structural(best_pattern); - - bool better = false; - if (best_pattern.empty()) { - better = true; - } else if (pos < best_pos) { - better = !(best_is_struct && !current_is_struct) && - !(opener_char == '[' && best_pattern[0] == ']' && pattern[0] == '}'); + if (left[left_pos] == right[right_pos]) { + suffix_len++; } else { - if (!best_is_struct && current_is_struct && pos < best_pos + 400) { - better = true; - } else if (best_is_struct && current_is_struct && opener_char == '[' && pattern[0] == ']' && - best_pattern[0] == '}') { - if (pos < best_pos + 100) { - better = true; - } - } - } - - if (better) { - best_pattern = pattern; - best_pos = pos; - - if (current_is_struct && (pattern == "]" || pattern == "}" || pattern == "```")) { - size_t tag_start = diff.find('<', best_pos + pattern.length()); - if (tag_start != std::string::npos && tag_start < best_pos + pattern.length() + 5) { - size_t tag_end = diff.find('>', tag_start); - if (tag_end != std::string::npos) { - best_pattern = diff.substr(best_pos, tag_end - best_pos + 1); - } - } - } + break; } } + result.suffix = left.substr(left.length() - suffix_len); - return best_pattern; -} + // Extract the remainders (the parts between prefix and suffix) + result.left = left.substr(prefix_len, left.length() - prefix_len - suffix_len); + result.right = right.substr(prefix_len, right.length() - prefix_len - suffix_len); -std::string infer_tool_call_opener(const std::string & diff1, const std::string & diff2, const std::string & diff3) { - std::vector differences = { diff1, diff2, diff3 }; - return find_common_prefix(differences); -} + // Fix tag boundaries by moving incomplete tags to left/right + // We iterate because: + // 1. fix_tag_boundaries may move content from prefix/suffix to left/right + // 2. After that, we find common suffix in left/right to extract + // 3. The extracted suffix might contain tag parts that need fixing + // We apply fix AFTER suffix extraction to ensure incomplete tags aren't left in suffix + diff_split prev_result; + do { + prev_result = result; -std::string infer_tool_call_closer(const std::string & diff1, const std::string & diff2, const std::string & diff3) { - std::vector differences = { diff1, diff2, diff3 }; - return find_common_suffix_generic(differences); -} - -internal_discovered_pattern extract_patterns_from_differences(const std::string & tool1_diff, - const std::string & tool2_diff, - const std::string & tool3_diff, - const std::string & tool1_full) { - LOG_DBG("%s\n", __func__); - - internal_discovered_pattern patterns; - - size_t func1_pos = tool1_diff.rfind("test_function_name"); - size_t func2_pos = tool2_diff.rfind("test_function_name"); - - if (func1_pos != std::string::npos && func2_pos != std::string::npos) { - patterns.tool_call_opener = tool1_diff.substr(0, func1_pos); - - if (tool1_full.length() >= tool1_diff.length()) { - size_t diff_start = tool1_full.length() - tool1_diff.length(); - - if (diff_start > 0 && tool1_full[diff_start - 1] == '<' && !patterns.tool_call_opener.empty() && - patterns.tool_call_opener[0] != '<') { - patterns.tool_call_opener = "<" + patterns.tool_call_opener; - } - } - - if (func1_pos == 0 && !tool1_full.empty()) { - size_t func_in_full = tool1_full.rfind("test_function_name"); - if (func_in_full != std::string::npos && func_in_full > 0) { - // Look backwards from function name to find prefix pattern - // Find where the prefix ends (skip whitespace immediately before function name) - size_t prefix_end = func_in_full; - while (prefix_end > 0 && (tool1_full[prefix_end - 1] == ' ' || tool1_full[prefix_end - 1] == '\t')) { - prefix_end--; - } - - // Find where the prefix starts by looking for newline or alphanumeric boundary - size_t prefix_start = prefix_end; - while (prefix_start > 0) { - char c = tool1_full[prefix_start - 1]; - // Stop at newline - if (c == '\n' || c == '\r') { - break; - } - // Stop if we hit alphanumeric (probably content, not a prefix delimiter) - if (std::isalnum(static_cast(c)) || c == '_') { - prefix_start = prefix_end; // Reset - no valid prefix found - break; - } - prefix_start--; - } - - // Extract the prefix if we found something meaningful - if (prefix_start < prefix_end) { - std::string prefix = tool1_full.substr(prefix_start, prefix_end - prefix_start); - // Validate: prefix should contain non-whitespace and be reasonable length - bool has_content = false; - for (char c : prefix) { - if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { - has_content = true; - break; - } - } - if (has_content && prefix.length() >= 2 && prefix.length() <= 20) { - LOG_DBG("Found prefix pattern in full output: '%s'\n", prefix.c_str()); - patterns.function_opener = prefix; - patterns.tool_call_start_marker = prefix; - } - } - } - } - - patterns.tool_name_field = extract_json_field_name(patterns.tool_call_opener, "name", - { "tool_name", "name", "function_name", "function" }); - - patterns.tool_args_field = - extract_json_field_name(patterns.tool_call_opener + tool1_diff.substr(func1_pos), "arguments", - { "parameters", "arguments", "args", "params", "input" }); - - patterns.tool_id_field = - extract_json_field_name(tool1_diff, "", { "tool_call_id", "tool_id", "id", "call_id" }); - - size_t param1_pos = tool2_diff.find("\"param1\""); - bool param_has_quotes = (param1_pos != std::string::npos); - size_t param2_pos = tool2_diff.find("\"param2\""); - size_t value1_pos = tool2_diff.find("\"value1\""); - - if (param1_pos == std::string::npos) { - param1_pos = tool2_diff.find("param1"); - } - if (param_has_quotes && param1_pos != std::string::npos) { - param1_pos++; - } - if (param2_pos == std::string::npos) { - param2_pos = tool2_diff.find("param2"); - } - if (param_has_quotes && param2_pos != std::string::npos) { - param2_pos++; - } - if (value1_pos == std::string::npos) { - value1_pos = tool2_diff.find("value1"); - } - // Only skip quote if value was actually found quoted - bool value_has_quotes = (value1_pos != std::string::npos && tool2_diff[value1_pos] == '"'); - if (value_has_quotes) { - value1_pos++; - } - - if (param1_pos != std::string::npos && value1_pos != std::string::npos) { - size_t search_start = (param1_pos > 20) ? param1_pos - 20 : 0; - std::string pre_param = tool2_diff.substr(search_start, param1_pos - search_start); - - size_t delim_pos = pre_param.find_last_of('\n'); - if (delim_pos == std::string::npos) { - delim_pos = pre_param.find_last_of('>'); - } - - if (delim_pos != std::string::npos) { - patterns.parameter_key_prefix = pre_param.substr(delim_pos + 1); - - // If prefix is empty after '>', check for GLM-style key-value tags - // Pattern: param1value1 - // In this case, the '>' ends the opening tag, and we should include the whole tag - if (patterns.parameter_key_prefix.empty() && delim_pos > 0) { - // Look for matching '<' before the '>' - size_t open_bracket = pre_param.rfind('<', delim_pos); - if (open_bracket != std::string::npos) { - // Extract the whole tag as the prefix - patterns.parameter_key_prefix = pre_param.substr(open_bracket); - } - } + // First, find and extract any common suffix from left/right + size_t suffix_len = 0; + size_t min_len = std::min(result.left.length(), result.right.length()); + while (suffix_len < min_len) { + size_t left_pos = result.left.length() - 1 - suffix_len; + size_t right_pos = result.right.length() - 1 - suffix_len; + if (result.left[left_pos] == result.right[right_pos]) { + suffix_len++; } else { - size_t start_marker = pre_param.find_last_of("<{[ \""); - if (start_marker != std::string::npos) { - patterns.parameter_key_prefix = pre_param.substr(start_marker); - } else { - patterns.parameter_key_prefix = pre_param; - } - } - - trim_whitespace(patterns.parameter_key_prefix); - - size_t key_end = param1_pos + std::string("param1").length(); - if (value1_pos > key_end) { - patterns.parameter_key_suffix = tool2_diff.substr(key_end, value1_pos - key_end); - } - - size_t value1_end = value1_pos + std::string("value1").length(); - if (value1_end < tool2_diff.length()) { - // Try to find XML-style closing tag like - size_t close_start = tool2_diff.find("', close_start); - if (close_end != std::string::npos) { - patterns.parameter_closer = tool2_diff.substr(close_start, close_end - close_start + 1); - } - } + break; } } - const std::string & func_context = tool1_diff; - size_t open_pos = func_context.rfind('<', func1_pos); - if (open_pos != std::string::npos && open_pos < func1_pos) { - size_t close_pos = func_context.find('>', open_pos); - if (close_pos != std::string::npos && close_pos < func1_pos) { - bool is_adjacent = true; - for (size_t k = close_pos + 1; k < func1_pos; ++k) { - char c = func_context[k]; - if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { - is_adjacent = false; - break; - } - } - if (is_adjacent) { - patterns.function_opener = func_context.substr(open_pos, close_pos - open_pos + 1); - } - } else { - patterns.function_opener = func_context.substr(open_pos, func1_pos - open_pos); - } + if (suffix_len > 0) { + std::string common_suffix = result.left.substr(result.left.length() - suffix_len); + result.suffix = common_suffix + result.suffix; + result.left = result.left.substr(0, result.left.length() - suffix_len); + result.right = result.right.substr(0, result.right.length() - suffix_len); } - if (func1_pos > 0 && patterns.function_opener.empty()) { - size_t prefix_end = func1_pos; - // Skip whitespace immediately before function name - while (prefix_end > 0 && (func_context[prefix_end - 1] == ' ' || func_context[prefix_end - 1] == '\t')) { - prefix_end--; - } + // Then apply fix_tag_boundaries to move incomplete tags from prefix/suffix to left/right + result = fix_tag_boundaries(result); - // Find prefix start - look for newline or alphanumeric boundary - size_t prefix_start = prefix_end; - while (prefix_start > 0) { - char c = func_context[prefix_start - 1]; - if (c == '\n' || c == '\r') { - break; - } - if (std::isalnum(static_cast(c)) || c == '_') { - prefix_start = prefix_end; // Reset - no valid prefix - break; - } - prefix_start--; - } + } while (!(result == prev_result) && result.left != left && result.right != right); - if (prefix_start < prefix_end) { - // ... - } - } - - // Fallback: look for standard delimiters - if (patterns.function_opener.empty()) { - for (int i = (int) func1_pos - 1; i >= 0; i--) { - if (func_context[i] == '{' || func_context[i] == '[' || func_context[i] == '(' || - func_context[i] == '<') { - patterns.function_opener = func_context.substr(i, func1_pos - i); - break; - } - } - } - - size_t func_name_end = func1_pos + std::string("test_function_name").length(); - if (func_name_end < func_context.length()) { - char next_char = func_context[func_name_end]; - if (next_char == '>' || next_char == ']' || next_char == '}') { - patterns.function_name_suffix = std::string(1, next_char); - } else if (next_char == '"') { - if (func_name_end + 1 < func_context.length() && func_context[func_name_end + 1] == '>') { - patterns.function_name_suffix = "\">"; - } else { - patterns.function_name_suffix = "\""; - } - } else if (next_char == '<') { - // Check if it's an XML-like tag suffix (e.g. <|tool_call_argument_begin|>) - // But NOT if it's a closing tag (e.g., ) - that should be function_closer - if (func_name_end + 1 < func_context.length() && func_context[func_name_end + 1] == '/') { - // This is a closing tag like , not a suffix - // Leave function_name_suffix empty; function_closer will capture this - } else { - size_t tag_close = func_context.find('>', func_name_end); - if (tag_close != std::string::npos) { - // It seems to be a tag, use it as suffix - patterns.function_name_suffix = func_context.substr(func_name_end, tag_close - func_name_end + 1); - } - } - } else if (next_char == '[') { - // Bracket-tag format: [CALL_ID]id[ARGS] (Mistral Small 3.2 style) - // Find where the JSON arguments start (at '{') - size_t json_start = func_context.find('{', func_name_end); - if (json_start != std::string::npos) { - patterns.function_name_suffix = func_context.substr(func_name_end, json_start - func_name_end); - LOG_DBG("Found bracket-tag suffix: '%s'\n", patterns.function_name_suffix.c_str()); - } - } else if (next_char == ':') { - // Indexed format: function_name:0<|marker|> or function_name:0{args} - // Find where the suffix ends - either at a tag marker or at the JSON args start - size_t suffix_end = func_name_end + 1; - // Skip the index digits - while (suffix_end < func_context.length() && - std::isdigit(static_cast(func_context[suffix_end]))) { - suffix_end++; - } - if (suffix_end < func_context.length()) { - char after_index = func_context[suffix_end]; - if (after_index == '<') { - // There's a marker after the index (e.g., :0<|tool_call_argument_begin|>) - size_t tag_close = func_context.find('>', suffix_end); - if (tag_close != std::string::npos) { - patterns.function_name_suffix = - func_context.substr(func_name_end, tag_close - func_name_end + 1); - } else { - patterns.function_name_suffix = - func_context.substr(func_name_end, suffix_end - func_name_end); - } - } else { - // Just the index part (e.g., :0) - patterns.function_name_suffix = func_context.substr(func_name_end, suffix_end - func_name_end); - } - } - } else if (next_char == '\n' || next_char == '\r') { - // Check for markdown code block pattern (e.g., DeepSeek R1): \n```json\n{...}\n``` - size_t code_block_start = func_context.find("```", func_name_end); - if (code_block_start != std::string::npos && code_block_start < func_name_end + 10) { - // Found code block start after function name - // Skip the optional language tag (e.g., "json") - size_t newline_after_lang = func_context.find('\n', code_block_start + 3); - if (newline_after_lang != std::string::npos) { - // function_name_suffix should include everything up to (and including) the newline after language tag - patterns.function_name_suffix = - func_context.substr(func_name_end, newline_after_lang - func_name_end + 1); - LOG_DBG("Found markdown code block suffix: '%s'\n", patterns.function_name_suffix.c_str()); - } - } - } - } - - // Function closer - size_t search_start = func_name_end; - if (!patterns.function_name_suffix.empty()) { - search_start += patterns.function_name_suffix.length(); - } - patterns.function_closer = find_closing_pattern(func_context, search_start); - - // Fix for XML-style tag formats where function_closer was detected as "}" (JSON closing) - // but should be the actual tag closer (e.g., <|tool_call_end|> or <|tool▁call▁end|>) - if (patterns.function_closer == "}" && !patterns.function_opener.empty() && - patterns.function_opener[0] == '<') { - // This is an XML-style tag format, so the closer should be a tag, not just "}" - // Find the next tag marker after the search position - size_t next_tag = func_context.find('<', search_start); - if (next_tag != std::string::npos) { - // Handle both standard <|...|> and fullwidth <|...|> formats - size_t closer_pos = find_token_closer(func_context, next_tag); - if (closer_pos != std::string::npos) { - size_t closer_len = get_token_closer_length(func_context, closer_pos); - patterns.function_closer = func_context.substr(next_tag, closer_pos - next_tag + closer_len); - LOG_DBG("Adjusted function_closer from '}' to tag '%s' for XML-style format\n", - patterns.function_closer.c_str()); - } - } - } - - if (patterns.function_closer == "}" && !patterns.function_name_suffix.empty() && - patterns.function_name_suffix.find("```") != std::string::npos) { - // function_name_suffix contains a code block opener, look for the closing code block - size_t code_block_end = func_context.find("```", search_start); - if (code_block_end != std::string::npos) { - // Found closing code block, extract everything from ``` to end of tool call - // The closer should be \n``` (everything from ``` to the end marker) - size_t after_block = code_block_end + 3; - // Find the next tag marker (e.g., <|tool_call_end|>) - size_t next_tag = func_context.find('<', after_block); - if (next_tag != std::string::npos) { - size_t tag_end = func_context.find('>', next_tag); - if (tag_end != std::string::npos) { - // Don't include leading newline - the JSON args parser consumes trailing whitespace - // So start exactly at the ``` (code_block_end) - patterns.function_closer = func_context.substr(code_block_end, tag_end - code_block_end + 1); - LOG_DBG("Detected markdown code block args, adjusted function_closer to: '%s'\n", - patterns.function_closer.c_str()); - } - } - } - } - - // Tool call start marker - if (patterns.function_opener.length() > 0 && - patterns.tool_call_opener.length() > patterns.function_opener.length()) { - size_t opener_start = patterns.tool_call_opener.length() - patterns.function_opener.length(); - if (opener_start > 0) { - std::string before_func = patterns.tool_call_opener.substr(0, opener_start); - size_t last_bracket = before_func.find_last_of('['); - size_t tool_obj_brace = std::string::npos; - if (last_bracket != std::string::npos && last_bracket + 1 < before_func.length()) { - tool_obj_brace = before_func.find('{', last_bracket + 1); - } - - if (tool_obj_brace != std::string::npos) { - patterns.tool_call_start_marker = before_func.substr(0, tool_obj_brace); - } else if (last_bracket != std::string::npos) { - patterns.tool_call_start_marker = before_func.substr(0, last_bracket + 1); - } else { - patterns.tool_call_start_marker = before_func; - } - } - } else if (patterns.tool_call_start_marker.empty()) { - // Only search if not already set (e.g., by >>> prefix detection) - patterns.tool_call_start_marker = find_tool_call_start(tool1_diff); - } - - if (patterns.tool_call_opener.empty()) { - patterns.tool_call_opener = infer_tool_call_opener(tool1_diff, tool2_diff, tool3_diff); - if (func1_pos != std::string::npos && patterns.tool_call_opener.length() > func1_pos) { - patterns.tool_call_opener = patterns.tool_call_opener.substr(0, func1_pos); - } - } - if (patterns.tool_call_closer.empty()) { - patterns.tool_call_closer = infer_tool_call_closer(tool1_diff, tool2_diff, tool3_diff); - } - - patterns.tool_call_end_marker = find_tool_call_end(func_context, func1_pos); - - if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.length() > 1) { - size_t eos_pos = patterns.tool_call_end_marker.find("<|"); - if (eos_pos == 1) { - // Check if there's a bracket/brace before the token - char first_char = patterns.tool_call_end_marker[0]; - if (first_char == ']' || first_char == '}') { - // Check if this is an actual EOS token (contains "eot_id" or "eos") - std::string token_content = patterns.tool_call_end_marker.substr(eos_pos); - if (token_content.find("eot_id") != std::string::npos || - token_content.find("eos") != std::string::npos) { - // This is an EOS token, strip it - patterns.tool_call_end_marker = patterns.tool_call_end_marker.substr(0, 1); - } - } - } - } - - // Trim whitespace - if (!patterns.tool_call_end_marker.empty()) { - size_t first = patterns.tool_call_end_marker.find_first_not_of(" \n\t"); - size_t last = patterns.tool_call_end_marker.find_last_not_of(" \n\t"); - if (first != std::string::npos && last != std::string::npos) { - patterns.tool_call_end_marker = patterns.tool_call_end_marker.substr(first, (last - first + 1)); - } - } - - // If tool_call_end_marker matches function_closer, it found the wrong tag. - // Use tool_call_closer instead which is derived from common suffix of diffs. - if (!patterns.function_closer.empty() && patterns.tool_call_end_marker == patterns.function_closer) { - if (!patterns.tool_call_closer.empty()) { - // Try to extract a proper closing tag from tool_call_closer - // Use rfind to get the LAST closing tag (e.g., not ) - size_t close_start = patterns.tool_call_closer.rfind("', close_start); - if (close_end != std::string::npos) { - patterns.tool_call_end_marker = - patterns.tool_call_closer.substr(close_start, close_end - close_start + 1); - } - } - } - } else if (patterns.tool_call_end_marker == ">" && !patterns.tool_call_closer.empty() && - patterns.tool_call_closer.length() > 3) { - // If the specific end marker is just ">", but the common suffix (tool_call_closer) is substantial (e.g. <|tool_calls_section_end|>) - // then prefer the common suffix, as finding ">" might just be hitting the end of the last function call - if (patterns.tool_call_closer.find(patterns.tool_call_end_marker) != std::string::npos) { - patterns.tool_call_end_marker = patterns.tool_call_closer; - } - } - - if (patterns.tool_call_start_marker.empty()) { - std::vector diffs = { tool1_diff, tool2_diff, tool3_diff }; - patterns.tool_call_start_marker = find_common_substring_limited(diffs, 20, " \n\t<[{"); - } - - // Truncate if needed, but skip if func_pos is 0 (marker found via full output) - if (func1_pos != std::string::npos && func1_pos > 0 && patterns.tool_call_start_marker.length() > func1_pos) { - std::string candidate = patterns.tool_call_start_marker.substr(0, func1_pos); - size_t last_opener = candidate.find_last_of("{["); - if (last_opener != std::string::npos) { - patterns.tool_call_start_marker = candidate.substr(0, last_opener); - } else { - patterns.tool_call_start_marker = candidate; - } - } - - // Ensure we don't truncate in the middle of <|...|> tokens - patterns.tool_call_start_marker = adjust_to_token_boundary(patterns.tool_call_start_marker); - patterns.tool_call_end_marker = adjust_to_token_boundary(patterns.tool_call_end_marker); - - // Final trim - if (!patterns.tool_call_start_marker.empty()) { - size_t first = patterns.tool_call_start_marker.find_first_not_of(" \n\t\r"); - size_t last = patterns.tool_call_start_marker.find_last_not_of(" \n\t\r"); - if (first != std::string::npos && last != std::string::npos) { - patterns.tool_call_start_marker = patterns.tool_call_start_marker.substr(first, (last - first + 1)); - } - } - } - - return patterns; + return result; } -internal_tool_format determine_format_from_patterns(const internal_discovered_pattern & patterns) { - LOG_DBG("%s\n", __func__); - - if (patterns.tool_call_opener.empty() && patterns.tool_call_closer.empty() && patterns.function_opener.empty() && - patterns.function_closer.empty() && patterns.parameter_opener.empty() && patterns.parameter_closer.empty() && - patterns.argument_separator.empty() && patterns.tool_call_start_marker.empty() && - patterns.tool_call_end_marker.empty()) { - LOG_DBG("All patterns are empty - template doesn't support tool calls\n"); - return FORMAT_UNKNOWN; +// Returns the prefix of `full` up until the first occurrence of the common prefix of `left` and `right` +std::string until_common_prefix(const std::string & full, const std::string & left, const std::string & right) { + // Find the common prefix of left and right + size_t common_prefix_len = 0; + size_t min_len = std::min(left.length(), right.length()); + while (common_prefix_len < min_len && left[common_prefix_len] == right[common_prefix_len]) { + common_prefix_len++; } - // Check for markdown code block format (Cohere Command-R Plus) - // STRUCTURAL PATTERN: Action:\n```json\n[...]\n``` - // Key indicators: - // 1. tool_call_start_marker contains "Action:" or similar plain text marker - // 2. function_name_suffix or tool_call_closer contains "```" (markdown code fence) - // 3. tool_call_opener starts with "[" indicating JSON array - bool has_code_fence = false; - if (!patterns.function_name_suffix.empty() && patterns.function_name_suffix.find("```") != std::string::npos) { - has_code_fence = true; - } - if (!patterns.tool_call_closer.empty() && patterns.tool_call_closer.find("```") != std::string::npos) { - has_code_fence = true; - } - bool has_action_marker = false; - if (!patterns.tool_call_start_marker.empty()) { - std::string marker_lower = patterns.tool_call_start_marker; - std::transform(marker_lower.begin(), marker_lower.end(), marker_lower.begin(), ::tolower); - if (marker_lower.find("action") != std::string::npos) { - has_action_marker = true; - } - } - if (has_code_fence && has_action_marker) { - LOG_DBG("Detected MARKDOWN_CODE_BLOCK format (Action: + ```json code fence)\n"); - return FORMAT_MARKDOWN_CODE_BLOCK; + // If there's no common prefix, return empty string + if (common_prefix_len == 0) { + return ""; } - // Check for recipient-based routing format (e.g., Functionary v3.2) - // STRUCTURAL PATTERN: The same marker is used for both content routing and tool routing - // Key indicators: - // 1. tool_call_start_marker == function_opener (same marker used for both) - // 2. No parameter markers (arguments are plain dict/JSON, not wrapped in tags) - // 3. No XML-style tags (differentiates from FUNC_TAG_WITH_NAME) - // 4. function_opener doesn't start with structural chars like {, [, < (differentiates from other formats) - if (!patterns.tool_call_start_marker.empty() && !patterns.function_opener.empty() && - patterns.tool_call_start_marker == patterns.function_opener) { - // Check this isn't an XML-tagged format (opener would start with '<') - if (patterns.function_opener[0] != '<' && patterns.function_opener[0] != '{' && - patterns.function_opener[0] != '[') { - // Check there are no parameter markers - if (patterns.parameter_opener.empty() && patterns.parameter_closer.empty()) { - LOG_DBG("Detected RECIPIENT_BASED format (tool_call_start_marker == function_opener = '%s')\n", - patterns.tool_call_start_marker.c_str()); - return FORMAT_RECIPIENT_BASED; - } - } + // Find the common prefix in the full string + std::string common_prefix = left.substr(0, common_prefix_len); + size_t pos = full.find(common_prefix); + + // If not found, return empty string + if (pos == std::string::npos) { + return ""; } - if (!patterns.tool_call_opener.empty()) { - if (patterns.tool_call_opener.find("{\"name\":") != std::string::npos || - patterns.tool_call_opener.find("{"name":") != std::string::npos) { - LOG_DBG("Detected JSON_NATIVE format from tool_call_opener JSON structure\n"); - return FORMAT_JSON_NATIVE; - } - } - - if (!patterns.function_opener.empty() && patterns.function_opener.find('<') == 0) { - bool has_substantial_param_markers = false; - if (!patterns.parameter_opener.empty()) { - has_substantial_param_markers = (count_non_whitespace(patterns.parameter_opener) > 1); - } - if (!has_substantial_param_markers && !patterns.parameter_closer.empty()) { - has_substantial_param_markers = (count_non_whitespace(patterns.parameter_closer) > 1); - } - - if (!has_substantial_param_markers) { - if ((!patterns.tool_call_opener.empty() && (patterns.tool_call_opener.find('[') != std::string::npos || - patterns.tool_call_opener.find('{') != std::string::npos)) || - (!patterns.tool_call_start_marker.empty() && - (patterns.tool_call_start_marker.find('[') != std::string::npos || - patterns.tool_call_start_marker.find('{') != std::string::npos))) { - LOG_DBG("Detected JSON_NATIVE format (XML markers but JSON structure)\n"); - return FORMAT_JSON_NATIVE; - } - } - - LOG_DBG("Detected XML_CONSTRUCTED format from function_opener\n"); - return FORMAT_XML_CONSTRUCTED; - } - - if (!patterns.function_opener.empty() && patterns.function_opener.find('{') == 0) { - LOG_DBG("Detected JSON_NATIVE format from function_opener\n"); - return FORMAT_JSON_NATIVE; - } - - // Check for bracket-tag format: [TOOL_CALLS]name[CALL_ID]id[ARGS]{...} - // Detected when function_name_suffix contains bracket tags like [CALL_ID]...[ARGS] - if (!patterns.function_name_suffix.empty() && patterns.function_name_suffix.find('[') != std::string::npos && - patterns.function_name_suffix.find(']') != std::string::npos) { - LOG_DBG("Detected BRACKET_TAG format from function_name_suffix containing bracket tags\n"); - return FORMAT_BRACKET_TAG; - } - - if (!patterns.tool_call_start_marker.empty() && - (patterns.tool_call_start_marker.find('<') == 0 || patterns.tool_call_start_marker.find('[') == 0)) { - bool is_prefix_marker = - patterns.tool_call_start_marker.find("<|") == 0 || patterns.tool_call_start_marker.find("[|") == 0; - // Check for bracket-tag format: [TAG] style without | (e.g., [TOOL_CALLS]) - bool is_bracket_tag = patterns.tool_call_start_marker.find('[') == 0 && - patterns.tool_call_start_marker.find("[|") != 0 && - patterns.tool_call_start_marker.find(']') != std::string::npos; - if (is_bracket_tag) { - LOG_DBG("Detected BRACKET_TAG format from tool_call_start_marker\n"); - return FORMAT_BRACKET_TAG; - } - if (is_prefix_marker) { - LOG_DBG("Detected JSON_NATIVE format from tool_call_start_marker (instruction-based)\n"); - return FORMAT_JSON_NATIVE; - } - - LOG_DBG("Detected XML_CONSTRUCTED format from tool_call_start_marker\n"); - return FORMAT_XML_CONSTRUCTED; - } - - if (!patterns.tool_call_start_marker.empty() && patterns.tool_call_start_marker.find('{') == 0) { - LOG_DBG("Detected JSON_NATIVE format from tool_call_start_marker\n"); - return FORMAT_JSON_NATIVE; - } - - if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.find('>') == 0) { - LOG_DBG("Detected XML_CONSTRUCTED format from tool_call_end_marker\n"); - return FORMAT_XML_CONSTRUCTED; - } - - if (!patterns.tool_call_end_marker.empty() && patterns.tool_call_end_marker.find('}') == 0) { - LOG_DBG("Detected JSON_NATIVE format from tool_call_end_marker\n"); - return FORMAT_JSON_NATIVE; - } - - LOG_DBG("Format could not be determined from patterns\n"); - return FORMAT_UNKNOWN; + // Return everything before the common prefix + return full.substr(0, pos); } -internal_discovered_pattern analyze_by_differential(const common_chat_template & tmpl) { - internal_discovered_pattern patterns; - - try { - LOG_DBG("%s\n", __func__); - - auto caps = tmpl.original_caps(); - bool minja_supports_tool_calls = caps.supports_tool_calls; - if (!minja_supports_tool_calls) { - LOG_DBG("Template doesn't support standard tool calls (per minja caps detection)\n"); - } - - // Define tools for testing - json tools = { - { { "type", "function" }, - { "function", - { { "name", "test_function_name" }, - { "description", "A test function" }, - { "parameters", - { { "type", "object" }, - { "properties", - { { "param1", { { "type", "string" }, { "description", "First parameter" } } }, - { "param2", { { "type", "string" }, { "description", "Second parameter" } } } } }, - { "required", json::array({ "param1", "param2" }) } } } } } }, - { { "type", "function" }, - { "function", - { { "name", "another_test_function" }, - { "description", "Another test function" }, - { "parameters", - { { "type", "object" }, - { "properties", - { { "param1", { { "type", "string" }, { "description", "First parameter" } } } } }, - { "required", json::array({ "param1" }) } } } } } } - }; - - // Test payload 1: Tool definitions + user + assistant with content only (no tool calls) - json user_msg = { - { "role", "user" }, - { "content", "Please help me with a task." } - }; - - json assistant_content_only = { - { "role", "assistant" }, - { "content", "I'll help you with that task right away." } - }; - - // Test payload 2: Tool definitions + user + assistant with content + tool calls - json assistant_content_with_tool = { - { "role", "assistant" }, - { "content", "I'll help you with that task right away." }, - { "tool_calls", - json::array( - { { { "id", "call_0001" }, - { "type", "function" }, - { "function", - { { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } - }; - - // Also test with content = null + tool calls (some templates check for this) - json assistant_null_content_with_tool = { - { "role", "assistant" }, - { "content", nullptr }, - { "tool_calls", - json::array( - { { { "id", "call_0001" }, - { "type", "function" }, - { "function", - { { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } - }; - - struct templates_params inputs; - inputs.tools = tools; - inputs.add_generation_prompt = false; - - // Helper function to safely render template, handling null content issues - auto safe_render = [&](const json & messages) -> std::string { - try { - // First try with the original messages - inputs.messages = messages; - return common_chat_template_direct_apply(tmpl, inputs); - } catch (const std::exception & e) { - // If it fails, try replacing null content with empty string - json fixed_messages = messages; - for (auto & msg : fixed_messages) { - if (msg.contains("content") && msg["content"].is_null()) { - msg["content"] = ""; - } - } - inputs.messages = fixed_messages; - try { - return common_chat_template_direct_apply(tmpl, inputs); - } catch (...) { - return ""; - } - } - }; - - // Render payload 1: content only - std::string output_content_only = safe_render({ user_msg, assistant_content_only }); - - // Render payload 2: content + tool calls - std::string output_content_with_tool = safe_render({ user_msg, assistant_content_with_tool }); - - // Render payload 3: null content + tool calls - std::string output_null_content_with_tool = safe_render({ user_msg, assistant_null_content_with_tool }); - - LOG_DBG("Output 1 (content only): %s\n", output_content_only.c_str()); - LOG_DBG("Output 2 (content + tools): %s\n", output_content_with_tool.c_str()); - LOG_DBG("Output 3 (null + tools): %s\n", output_null_content_with_tool.c_str()); - - // Check if the template renders tool calls in any scenario - // Test 1: content vs content+tool_calls (for templates that render both) - // Test 2: content vs null+tool_calls (for templates that only render tools when content is null) - bool renders_tool_calls_with_content = (output_content_only != output_content_with_tool); - bool renders_tool_calls_without_content = (output_content_only != output_null_content_with_tool); - - if (!renders_tool_calls_with_content && !renders_tool_calls_without_content) { - LOG_DBG("Template does NOT render tool calls in any scenario\n"); - // Return empty patterns to indicate no tool support - return patterns; - } - - LOG_DBG("Template renders tool calls, proceeding with differential analysis\n"); - - // If we get here, the template does support tool calls - // Use the original differential analysis approach but now we know it's valid - json base_msg = { - { "role", "assistant" }, - { "content", "MARKER" } - }; - - // Use nullptr for content to trigger tool_calls branch in templates that check "content is none" - // Include "id" field as some templates (e.g., Mistral Nemo) require it - json tool_msg1 = { - { "role", "assistant" }, - { "content", nullptr }, - { "tool_calls", - json::array( - { { { "id", "call_0001" }, - { "type", "function" }, - { "function", { { "name", "test_function_name" }, { "arguments", json::object() } } } } }) } - }; - - json tool_msg2 = { - { "role", "assistant" }, - { "content", nullptr }, - { "tool_calls", - json::array( - { { { "id", "call_0001" }, - { "type", "function" }, - { "function", - { { "name", "test_function_name" }, - { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } - }; - - json tool_msg3 = { - { "role", "assistant" }, - { "content", nullptr }, - { "tool_calls", - json::array( - { { { "id", "call_0001" }, - { "type", "function" }, - { "function", { { "name", "test_function_name" }, { "arguments", json::object() } } } }, - { { "id", "call_0002" }, - { "type", "function" }, - { "function", { { "name", "another_test_function" }, { "arguments", json::object() } } } } }) } - }; - - inputs.messages = { user_msg, base_msg }; - auto base_output = safe_render({ user_msg, base_msg }); - - inputs.messages = { user_msg, tool_msg1 }; - auto tool1_output = safe_render({ user_msg, tool_msg1 }); - - // Detect if template renders null content as "None" (Python/Jinja string representation) - // This happens when templates concatenate content without null checks, e.g.: - // {{ '<|im_start|>' + message.role + '\n' + content }} - // Check if "None" appears in the tool output where it shouldn't - if (tool1_output.find("None") != std::string::npos) { - // Verify this is actually from null content by checking if it goes away with empty string - json tool_msg1_empty_content = tool_msg1; - tool_msg1_empty_content["content"] = ""; - auto tool1_output_empty = safe_render({ user_msg, tool_msg1_empty_content }); - if (tool1_output_empty.find("None") == std::string::npos) { - LOG_DBG("Template renders null content as 'None', switching to empty string\n"); - patterns.requires_nonnull_content = true; - tool1_output = tool1_output_empty; - - // Update tool messages to use empty string instead of null - tool_msg1["content"] = ""; - tool_msg2["content"] = ""; - tool_msg3["content"] = ""; - } - } - - inputs.messages = { user_msg, tool_msg2 }; - auto tool2_output = safe_render({ user_msg, tool_msg2 }); - - inputs.messages = { user_msg, tool_msg3 }; - auto tool3_output = safe_render({ user_msg, tool_msg3 }); - - std::string tool1_diff = find_string_difference(base_output, tool1_output); - std::string tool2_diff = find_string_difference(base_output, tool2_output); - std::string tool3_diff = find_string_difference(base_output, tool3_output); - - LOG_DBG("Tool1 diff length: %zu\n", tool1_diff.length()); - LOG_DBG("Tool2 diff length: %zu\n", tool2_diff.length()); - LOG_DBG("Tool3 diff length: %zu\n", tool3_diff.length()); - - if (tool1_diff.empty() && tool2_diff.empty() && tool3_diff.empty()) { - LOG_DBG("All diffs are empty - trying without add_generation_prompt\n"); - // Try with add_generation_prompt variations - json alternative_base_msg = { - { "role", "assistant" }, - { "content", "MARKER" } - }; - - templates_params alt_inputs; - alt_inputs.tools = tools; - alt_inputs.messages = { user_msg, alternative_base_msg }; - alt_inputs.add_generation_prompt = false; - auto alt_base = common_chat_template_direct_apply(tmpl, alt_inputs); - - alt_inputs.messages = { user_msg, tool_msg1 }; - auto alt_tool1 = common_chat_template_direct_apply(tmpl, alt_inputs); - - tool1_diff = find_string_difference(alt_base, alt_tool1); - if (!tool1_diff.empty()) { - // If we found a diff using the alternative approach, we must use the corresponding - // full output for pattern extraction (otherwise diff indices will be invalid) - tool1_output = alt_tool1; - - alt_inputs.messages = { user_msg, tool_msg2 }; - tool2_diff = find_string_difference(alt_base, common_chat_template_direct_apply(tmpl, inputs)); - alt_inputs.messages = { user_msg, tool_msg3 }; - tool3_diff = find_string_difference(alt_base, common_chat_template_direct_apply(tmpl, inputs)); - } - } - - patterns = extract_patterns_from_differences(tool1_diff, tool2_diff, tool3_diff, tool1_output); - - LOG_DBG("=== ENDING TEMPLATE DIFFERENTIAL ANALYSIS ===\n"); - - } catch (const std::exception & e) { - LOG_DBG("Template differential analysis failed: %s\n", e.what()); +// Returns the suffix of `full` after the last occurrence of the common suffix of `left` and `right` +std::string after_common_suffix(const std::string & full, const std::string & left, const std::string & right) { + // Find the common suffix of left and right (compare from the end) + size_t common_suffix_len = 0; + size_t min_len = std::min(left.length(), right.length()); + while (common_suffix_len < min_len && + left[left.length() - 1 - common_suffix_len] == right[right.length() - 1 - common_suffix_len]) { + common_suffix_len++; } - return patterns; + // If there's no common suffix, return empty string + if (common_suffix_len == 0) { + return ""; + } + + // Extract the common suffix + std::string common_suffix = left.substr(left.length() - common_suffix_len); + + // Find the last occurrence of the common suffix in the full string + size_t pos = full.rfind(common_suffix); + + // If not found, return empty string + if (pos == std::string::npos) { + return ""; + } + + // Return everything after the common suffix + return full.substr(pos + common_suffix_len); } + +std::vector segmentize_markers(const std::string & text) { + std::vector retval; + bool in_marker = false; + char marker_opener = '\0'; + + auto is_marker_opener = [](char c) -> bool { return c == '<' || c == '['; }; + auto is_marker_closer = [](char op, char c) -> bool { return (op == '<' && c == '>') || (op == '[' && c == ']'); }; + + size_t last_border = 0; + + for (size_t cur_pos = 0; cur_pos < text.length(); cur_pos++) { + if (!in_marker && is_marker_opener(text[cur_pos])) { + if (last_border < cur_pos) { + retval.push_back(segment(segment_type::TEXT, text.substr(last_border, cur_pos - last_border))); + } + last_border = cur_pos; + in_marker = true; + marker_opener = text[cur_pos]; + } else if (in_marker && is_marker_closer(marker_opener, text[cur_pos])) { + // no need to check because last_border will always be smaller + retval.push_back(segment(segment_type::MARKER, text.substr(last_border, cur_pos - last_border + 1))); + last_border = cur_pos + 1; + in_marker = false; + marker_opener = '\0'; + } + } + if (last_border < text.length()) { + retval.push_back(segment(segment_type::TEXT, text.substr(last_border))); + } + return retval; +} + diff --git a/common/chat-auto-parser-helpers.h b/common/chat-auto-parser-helpers.h index 5162b09fbe..e9534d6715 100644 --- a/common/chat-auto-parser-helpers.h +++ b/common/chat-auto-parser-helpers.h @@ -1,133 +1,22 @@ #pragma once -#include +#include "chat-diff-analyzer.h" #include -#include -#include "chat.h" -#include "nlohmann/json.hpp" +std::string trim_whitespace(const std::string & str); +std::string trim_leading_whitespace(const std::string & str); +std::string trim_trailing_whitespace(const std::string & str); +std::string trim_trailing_newlines(const std::string & str); -using json = nlohmann::ordered_json; +// calculate a diff split (longest common prefix, longest common suffix excluding prefix, +// mismatched part on the left, mismatched part on the right) between two strings +diff_split calculate_diff_split(const std::string & left, const std::string & right); -namespace minja { -class chat_template; -} +// Returns the prefix of `full` up until the first occurrence of the common prefix of `left` and `right` +std::string until_common_prefix(const std::string & full, const std::string & left, const std::string & right); -void trim_whitespace(std::string & str); -void trim_trailing_newlines(std::string & str); -size_t count_non_whitespace(const std::string & str); -size_t find_last_of_any(const std::string & str, const std::string & chars, size_t start_pos); +// Returns the suffix of `full` after the last occurrence of the common suffix of `left` and `right` +std::string after_common_suffix(const std::string & full, const std::string & left, const std::string & right); -std::string extract_tag_name(const std::string & tag); -std::string create_closing_tag(const std::string & opening_tag); - -std::string find_common_prefix(const std::vector & strings); -std::string find_common_suffix_generic(const std::vector & strings); -std::string find_common_substring_limited(const std::vector & strings, - size_t max_length, - const std::string & delimiters); - -bool string_ends_with(const std::string & str, const std::string & suffix); -std::string apply_template(common_chat_template & tmpl, - const struct templates_params & inputs, - const std::optional & messages_override = std::nullopt, - const std::optional & tools_override = std::nullopt, - const std::optional & additional_context = std::nullopt); - -// Adjust a marker string to ensure it ends at a complete <|...|> token boundary -// This prevents truncation mid-token -std::string adjust_to_token_boundary(const std::string & str); - -// Find the position of a token opener (<| or <|) in a string -// Returns std::string::npos if not found -size_t find_token_opener(const std::string & str, size_t start_pos = 0); - -// Find the position of a token closer (|> or |>) in a string -// Returns std::string::npos if not found -size_t find_token_closer(const std::string & str, size_t start_pos = 0); - -// Get the length of the token opener at the given position (2 for <| or 4 for <|) -// Returns 0 if no valid opener at position -size_t get_token_opener_length(const std::string & str, size_t pos); - -// Get the length of the token closer at the given position (2 for |> or 4 for |>) -// Returns 0 if no valid closer at position -size_t get_token_closer_length(const std::string & str, size_t pos); - -// Strip EOS/end-of-sentence tokens from the end of a string -// Handles both standard (<|eos|>, <|eot_id|>) and fullwidth (<|end▁of▁sentence|>) formats -std::string strip_eos_token(const std::string & str); - -// Internal structure for differential analysis (used during pattern extraction) -struct internal_discovered_pattern { - std::string tool_call_opener; - std::string tool_call_closer; - std::string function_opener; - std::string function_closer; - std::string function_name_suffix; - std::string parameter_opener; - std::string parameter_closer; - std::string argument_separator; - std::string parameter_key_prefix; - std::string parameter_key_suffix; - std::string tool_call_start_marker; - std::string tool_call_end_marker; - std::string reasoning_start_marker; - std::string reasoning_end_marker; - std::string content_start_marker; - std::string content_end_marker; - std::string tool_name_field = "name"; - std::string tool_args_field = "arguments"; - std::string tool_id_field; - // For markdown code block format (Cohere Command-R Plus) - std::string code_block_marker; // e.g., "Action:" - std::string code_block_language; // e.g., "json" - // Flag: template renders null content as "None" string, requires empty string instead - bool requires_nonnull_content = false; -}; - -// Internal enum for format classification -enum internal_tool_format { - FORMAT_JSON_NATIVE, - FORMAT_XML_CONSTRUCTED, - FORMAT_BRACKET_TAG, // [TOOL_CALLS]name[CALL_ID]id[ARGS]{...} (Mistral Small 3.2) - FORMAT_RECIPIENT_BASED, // >>>recipient\n{content} (Functionary v3.2) - FORMAT_MARKDOWN_CODE_BLOCK, // Action:\n```json\n[...]\n``` (Cohere Command-R Plus) - FORMAT_CONTENT_ONLY, - FORMAT_UNKNOWN -}; - -// Find the suffix that differentiates an extended string from a base string -std::string find_string_difference(const std::string & base, const std::string & extended); - -// Extract JSON field name from an opener string -std::string extract_json_field_name(const std::string & opener, - const std::string & default_name, - const std::vector & candidates); - -// Find a closing pattern in a string starting from a given position -std::string find_closing_pattern(const std::string & diff, size_t func_pos); - -// Find the tool call start marker in a difference string -std::string find_tool_call_start(const std::string & diff); - -// Find the tool call end marker in a difference string -std::string find_tool_call_end(const std::string & diff, size_t func_pos); - -// Infer the tool call opener from multiple difference strings -std::string infer_tool_call_opener(const std::string & diff1, const std::string & diff2, const std::string & diff3); - -// Infer the tool call closer from multiple difference strings -std::string infer_tool_call_closer(const std::string & diff1, const std::string & diff2, const std::string & diff3); - -// Extract patterns from differences between tool calls -internal_discovered_pattern extract_patterns_from_differences(const std::string & tool1_diff, - const std::string & tool2_diff, - const std::string & tool3_diff, - const std::string & tool1_full = ""); - -// Determine the format classification from discovered patterns -internal_tool_format determine_format_from_patterns(const internal_discovered_pattern & patterns); - -// Analyze template using differential analysis (internal use) -internal_discovered_pattern analyze_by_differential(const common_chat_template & tmpl); +// Segmentize text into markers and non-marker fragments +std::vector segmentize_markers(const std::string & text); \ No newline at end of file diff --git a/common/chat-auto-parser.h b/common/chat-auto-parser.h index 6062f4d37a..c6587667d1 100644 --- a/common/chat-auto-parser.h +++ b/common/chat-auto-parser.h @@ -1,183 +1,54 @@ #pragma once +#include "chat-diff-analyzer.h" #include "chat.h" +#include "chat-peg-parser.h" #include "common.h" -#include "jinja/runtime.h" #include #include -#include using json = nlohmann::ordered_json; -// Phase 1 result: Content and reasoning structure (analyzed without tools) -struct content_structure { - // Reasoning handling mode - enum reasoning_mode_type { - REASONING_NONE, // No reasoning markers detected - REASONING_OPTIONAL, // ... may appear before content - REASONING_FORCED_OPEN, // Template ends with open reasoning tag (thinking_forced_open) - }; - - reasoning_mode_type reasoning_mode = REASONING_NONE; - std::string reasoning_start; // e.g., "", "<|START_THINKING|>" - std::string reasoning_end; // e.g., "", "<|END_THINKING|>" - - // Content wrapping mode - enum content_mode_type { - CONTENT_PLAIN, // No content markers - CONTENT_ALWAYS_WRAPPED, // ... always present - CONTENT_WRAPPED_WITH_REASONING, // Content wrapped only when reasoning present - }; - - content_mode_type content_mode = CONTENT_PLAIN; - std::string content_start; // e.g., "", "<|START_RESPONSE|>" - std::string content_end; // e.g., "", "<|END_RESPONSE|>" -}; - -// Phase 2 result: Tool call structure (layered on Phase 1) -struct tool_call_structure { - bool supports_tools = false; - - // Container markers (what wraps all tool calls) - std::string tool_section_start; // e.g., "", "[TOOL_CALLS]", "", "" - std::string tool_section_end; // e.g., "", "]", "", "" - - // Function format (how individual functions are structured) - enum function_format { - FUNC_JSON_OBJECT, // {"name": "X", "arguments": {...}} - FUNC_TAG_WITH_NAME, // {...} - FUNC_TAG_NAME_ONLY, // ... where X is function name (rare) - FUNC_PREFIXED_INDEXED, // <|tool_call_begin|>functions.X:0<|tool_call_argument_begin|>{...}<|tool_call_end|> - FUNC_NAME_AS_KEY, // [{"function_name": {...arguments...}}] (Apertus-style) - FUNC_BRACKET_TAG, // [TOOL_CALLS]X[CALL_ID]id[ARGS]{...} (Mistral Small 3.2 style) - FUNC_RECIPIENT_BASED, // >>>recipient\n{content} where recipient is "all" (content) or function name (tools) - FUNC_MARKDOWN_CODE_BLOCK, // Action:\n```json\n[...]\n``` (Cohere Command-R Plus style) - }; - - function_format function_format = FUNC_JSON_OBJECT; - - // For FUNC_JSON_OBJECT format - field names (may vary between templates) - std::string name_field = "name"; // Could be "tool_name", "function" - std::string args_field = "arguments"; // Could be "parameters", "params", "input" - std::string id_field; // Optional: "id", "tool_call_id", "" - - // For FUNC_TAG_WITH_NAME format - std::string function_prefix; // e.g., "" - std::string function_close; // e.g., "" - - // For FUNC_PREFIXED_INDEXED format (e.g., Kimi-K2) - std::string per_call_start; // e.g., "<|tool_call_begin|>" - std::string function_namespace; // e.g., "functions." (prefix before function name) - std::string args_marker; // e.g., "<|tool_call_argument_begin|>" - std::string per_call_end; // e.g., "<|tool_call_end|>" - - // For FUNC_BRACKET_TAG format (e.g., Mistral Small 3.2) - std::string id_marker; // e.g., "[CALL_ID]" - marker before tool call ID - - // For FUNC_MARKDOWN_CODE_BLOCK format (e.g., Cohere Command-R Plus) - std::string code_block_marker; // e.g., "Action:" - text marker before code block - std::string code_block_language; // e.g., "json" - language identifier in code fence - - // Argument format (how arguments are structured within a function) - enum argument_format { - ARGS_JSON, // Standard JSON object: {"key": "value", ...} - ARGS_TAGGED, // XML-style: value - ARGS_KEY_VALUE_TAGS, // keyvalue (GLM-4.6) - }; - - argument_format argument_format = ARGS_JSON; - - // For ARGS_TAGGED format - std::string arg_prefix; // e.g., "" - std::string arg_close; // e.g., "", "" - std::string arg_separator; // e.g., "", "\n" - - // Flag: template renders null content as "None" string, requires empty string instead - bool requires_nonnull_content = false; -}; - -// Combined result of unified template analysis -struct template_analysis_result { - content_structure content; - tool_call_structure tools; - - // Preserved tokens for tokenizer (union of all markers) - std::vector preserved_tokens; -}; - -// Template analyzer that uses two-phase differential analysis -class template_analyzer { - public: - // Main entry point: Unified two-phase analysis - static template_analysis_result analyze_template(const common_chat_template & tmpl); - - // Phase 1 - Analyze content and reasoning structure (no tools) - static content_structure analyze_content_structure(const common_chat_template & tmpl); - - // Phase 2 - Analyze tool call structure (layered on Phase 1) - static tool_call_structure analyze_tool_structure(const common_chat_template & tmpl, - const content_structure & content); - - private: - // Phase 1 detection helpers - static void detect_reasoning_markers(const common_chat_template & tmpl, content_structure & cs); - static void detect_content_markers(const common_chat_template & tmpl, content_structure & cs); - static content_structure::reasoning_mode_type detect_reasoning_mode(const content_structure & cs, - const std::string & prompt); - - // Phase 2 detection helpers - static void detect_tool_markers(const common_chat_template & tmpl, tool_call_structure & ts); - static void detect_function_format(const common_chat_template & tmpl, tool_call_structure & ts); - static void detect_argument_format(const common_chat_template & tmpl, tool_call_structure & ts); - - // Phase 2 helper methods - static void analyze_json_format(tool_call_structure & ts, const struct internal_discovered_pattern & discovered); - static void analyze_xml_format(tool_call_structure & ts, const struct internal_discovered_pattern & discovered); - static void analyze_bracket_tag_format(tool_call_structure & ts, - const struct internal_discovered_pattern & discovered); - static void analyze_recipient_based_format(tool_call_structure & ts, - const struct internal_discovered_pattern & discovered); - static void analyze_markdown_code_block_format(tool_call_structure & ts, - const struct internal_discovered_pattern & discovered); - - // Helper to collect preserved tokens from analysis result - static void collect_preserved_tokens(template_analysis_result & result); -}; - struct templates_params { json messages; json tools; common_chat_tool_choice tool_choice = COMMON_CHAT_TOOL_CHOICE_AUTO; json json_schema; bool parallel_tool_calls = true; - common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_AUTO; - bool stream = true; + common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_AUTO; + bool stream = true; std::string grammar; bool add_generation_prompt = false; - bool enable_thinking = true; - std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + bool enable_thinking = true; + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); json extra_context; - bool add_bos = false; - bool add_eos = false; - bool is_inference = true; + bool add_bos = false; + bool add_eos = false; + bool is_inference = true; bool add_inference = false; - bool mark_input = true; // whether to mark input strings in the jinja context + bool mark_input = true; // whether to mark input strings in the jinja context }; class universal_peg_generator { public: - // Generate parser from analysis result - static common_chat_params generate_parser(const template_analysis_result & analysis, - const common_chat_template & tmpl, - const struct templates_params & inputs); + static common_chat_params generate_parser(const common_chat_template & tmpl, + const struct templates_params & inputs); + + static common_chat_params generate_parser(const common_chat_template & tmpl, + const struct templates_params & inputs, + const diff_analysis_result & analysis); private: // Build unified parser (single code path for all formats) - static common_peg_arena build_parser(const template_analysis_result & analysis, - const common_chat_template & tmpl, - const struct templates_params & inputs, - bool thinking_forced_open); + static common_peg_arena build_parser(const diff_analysis_result & analysis, + const struct templates_params & inputs, + bool thinking_forced_open, + bool thinking_forced_closed = false); + + // Build tool calling parser based on detected format + static common_peg_parser build_tool_parser(common_chat_peg_unified_builder & p, + const diff_analysis_result & analysis, + const templates_params & inputs, + const common_peg_parser & reasoning); }; diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp new file mode 100644 index 0000000000..6afb9342c2 --- /dev/null +++ b/common/chat-diff-analyzer.cpp @@ -0,0 +1,1670 @@ +#include "chat-diff-analyzer.h" + +#include "chat-auto-parser-helpers.h" +#include "chat-auto-parser.h" +#include "chat.h" +#include "log.h" +#include "nlohmann/json.hpp" + +#include +#include + +#define ANSI_RESET "\033[0m" +#define ANSI_PURPLE "\033[1m\x1b[38;5;126m" +#define ANSI_ORANGE "\033[1m\x1b[38;5;214m" +#define ANSI_RED "\033[1m\x1b[38;5;196m" + +using json = nlohmann::ordered_json; + +static std::vector> workarounds( + { // Old reasoning Qwen templates - they don't really display reasoning content, but we still want to + // support reasoning on them + [](const common_chat_template & tmpl, diff_analysis_result & analysis) -> 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); + } + }, + // 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); + } + }, + // 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); + } + }, + // 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); + } + }, + // 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|>'") != + 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 = "```"; + } + } }); + +// Common JSON structures +static json params_schema = { + { "type", "object" }, + { "properties", + { { "first", { { "type", "string" }, { "description", "First argument" } } }, + { "second", { { "type", "string" }, { "description", "Second argument" } } } } }, + { "required", json::array({}) } +}; + +static json tools = json::array({ + { { "type", "function" }, + { "function", + json{ { "name", "foofoo" }, { "description", "Test function foo" }, { "parameters", params_schema } } } }, + { { "type", "function" }, + { "function", + json{ { "name", "barbar" }, { "description", "Test function bar" }, { "parameters", params_schema } } } } +}); + +static json user_msg = json{ + { "role", "user" }, + { "content", "Hello" } +}; + +static json build_tool_call(const std::string & name, const json & args, const std::string & id = "call00001") { + return json{ + { "id", id }, + { "type", "function" }, + { "function", json{ { "name", name }, { "arguments", args } } } + }; +} + +static json first_tool_call_zero_args = build_tool_call("foofoo", json::object(), "call00001"); +static json first_tool_call_one_arg = build_tool_call("foofoo", + json{ + { "first", "XXXX" } +}, + "call00001"); +static json first_tool_call_one_arg_other_val = build_tool_call("foofoo", + json{ + { "first", "YYYY" } +}, + "call00001"); +static json first_tool_call_other_arg = build_tool_call("foofoo", + json{ + { "second", "YYYY" } +}, + "call00001"); +static json first_tool_call = build_tool_call("foofoo", + json{ + { "first", "XXXX" }, + { "second", "YYYY" } +}, + "call00001"); +static json second_tool_call = build_tool_call("barbar", + json{ + { "first", "XXXX" }, + { "second", "YYYY" } +}, + "call00002"); +// Tool call variants with different IDs for call_id detection +static json first_tool_call_alt_id = build_tool_call("foofoo", + json{ + { "first", "XXXX" }, + { "second", "YYYY" } +}, + "call99999"); + +std::string differential_analyzer::apply_template(const common_chat_template & tmpl, const template_params & params) { + templates_params tmpl_params; + tmpl_params.messages = params.messages; + tmpl_params.tools = params.tools; + tmpl_params.add_generation_prompt = params.add_generation_prompt; + tmpl_params.enable_thinking = params.enable_thinking; + + if (params.extra_context) { + tmpl_params.extra_context = *params.extra_context; + } + tmpl_params.extra_context["enable_thinking"] = params.enable_thinking; + + try { + return common_chat_template_direct_apply(tmpl, tmpl_params); + } catch (const std::exception & e) { + LOG_DBG("Template application failed: %s\n", e.what()); + return ""; + } +} + +std::optional differential_analyzer::compare_variants( + const common_chat_template & tmpl, + const template_params & params_A, + const std::function & params_modifier) { + // Create variant B by copying A + template_params params_B = params_A; + + // Apply modifier to create variant B + if (params_modifier) { + params_modifier(params_B); + } + + // Apply template to both variants + std::string output_A = apply_template(tmpl, params_A); + std::string output_B = apply_template(tmpl, params_B); + + // Check for template application failures + if (output_A.empty() || output_B.empty()) { + return std::nullopt; + } + + // Calculate diff and return result with both outputs + compare_variants_result result; + result.diff = calculate_diff_split(output_A, output_B); + result.output_A = output_A; + result.output_B = output_B; + + return result; +} + +diff_analysis_result differential_analyzer::analyze(const common_chat_template & tmpl) { + diff_analysis_result result; + + 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; + + analyze_reasoning(tmpl, result); + analyze_content(tmpl, result); + if (result.supports_tools) { + analyze_tools(tmpl, result); + } + collect_preserved_tokens(result); + + for (auto & workaround : workarounds) { + workaround(tmpl, result); + } + + LOG_DBG(ANSI_PURPLE "=== Differential analysis complete ===\n" ANSI_RESET); + + return result; +} + +void differential_analyzer::analyze_reasoning(const common_chat_template & tmpl, diff_analysis_result & result) { + LOG_DBG(ANSI_ORANGE "Phase 1: Reasoning analysis\n" ANSI_RESET); + + compare_reasoning_presence(tmpl, result); + compare_thinking_enabled(tmpl, result); + if (result.supports_tools) { + compare_reasoning_scope(tmpl, result); + } +} + +void differential_analyzer::compare_reasoning_presence(const common_chat_template & tmpl, + diff_analysis_result & result) { + json user_msg = json{ + { "role", "user" }, + { "content", "Hello" } + }; + + json assistant_no_reasoning = json{ + { "role", "assistant" }, + { "content", "I can help." } + }; + + json assistant_with_reasoning = json{ + { "role", "assistant" }, + { "content", "I can help." }, + { "reasoning_content", "Let me think about this." } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_no_reasoning }); + 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_reasoning }); }); + + if (!comparison) { + LOG_DBG(ANSI_ORANGE "R1: Template application failed, skipping reasoning detection\n" ANSI_RESET); + 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 = 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); + for (size_t i = 3; i < seg.size(); i++) { + result.markers.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); + } 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); + for (size_t i = 2; i < seg.size(); i++) { + result.markers.reasoning_end += seg[i].value; + } + result.markers.reasoning_end = trim_whitespace(result.markers.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 + // right: reasoning_content + // suffix: content + // prefix: ... + auto suf_seg = segmentize_markers(diff.suffix); + if (trim_whitespace(diff.left).empty() && suf_seg.size() >= 2 && suf_seg[0].type == segment_type::MARKER && + trim_whitespace(suf_seg[1].value).substr(0, 11) == "I can help.") { + auto pre_seg = segmentize_markers(diff.prefix); + if (pre_seg[pre_seg.size() - 1].type == segment_type::MARKER || + (pre_seg.size() > 1 && trim_whitespace(pre_seg[pre_seg.size() - 1].value).empty() && + pre_seg[pre_seg.size() - 2].type == segment_type::MARKER)) { + auto marker_seg = pre_seg[pre_seg.size() - 1]; + 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); + } + } + } + } +} + +void differential_analyzer::compare_thinking_enabled(const common_chat_template & tmpl, diff_analysis_result & result) { + json user_msg = json{ + { "role", "user" }, + { "content", "Hello" } + }; + + template_params params; + params.messages = json::array({ user_msg }); + params.add_generation_prompt = true; + params.enable_thinking = false; + + auto comparison = compare_variants(tmpl, params, [&](template_params & p) { p.enable_thinking = true; }); + + if (!comparison) { + LOG_DBG("R2: Template application failed\n"); + 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); + + if (left_trimmed.empty() && !diff.right.empty()) { + std::string right_trimmed = diff.right; + 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 (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()); + } + + // Check for FORCED_CLOSED: when enable_thinking=false produces both start and end markers, + // but enable_thinking=true produces only the start marker + if (!comparison->output_A.empty() && !comparison->output_B.empty()) { + std::string output_A = comparison->output_A; // enable_thinking=false + std::string output_B = comparison->output_B; // enable_thinking=true + + // 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()) { + // 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; + + // 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; + + // 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"); + } + } else if (!result.markers.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) { + 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; + } + } + } + } + + // 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 (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; + } + } + } +} + +void differential_analyzer::compare_reasoning_scope(const common_chat_template & tmpl, diff_analysis_result & result) { + json assistant_reasoning_content = json{ + { "role", "assistant" }, + { "content", "Here is my response." }, + { "reasoning_content", "Let me think." } + }; + + json assistant_reasoning_tools = json{ + { "role", "assistant" }, + { "content", nullptr }, + { "reasoning_content", "Let me think." }, + { "tool_calls", + json::array({ build_tool_call("foofoo", json{ { "first", "VVVV" }, { "second", "XXXX" } }) }) } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_reasoning_content }); + 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_reasoning_tools }); }); + + if (!comparison) { + LOG_DBG("R3: Template application failed\n"); + 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; + LOG_DBG("R3: Detected TOOLS_ONLY reasoning mode\n"); + + // Extract reasoning markers from output_B + // The reasoning_content is "Let me think." + size_t reasoning_pos = comparison->output_B.find(reasoning_content); + if (reasoning_pos != std::string::npos) { + // Find start marker before reasoning_content + std::string before_reasoning = comparison->output_B.substr(0, reasoning_pos); + before_reasoning = trim_trailing_whitespace(before_reasoning); + auto segments_before = segmentize_markers(before_reasoning); + std::reverse(segments_before.begin(), segments_before.end()); + + 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()); + break; + } + } + + // Find end marker after reasoning_content + size_t reasoning_end = reasoning_pos + reasoning_content.length(); + std::string after_reasoning = comparison->output_B.substr(reasoning_end); + after_reasoning = trim_leading_whitespace(after_reasoning); + + if (!after_reasoning.empty()) { + // Try to find matching end marker + if (!result.markers.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; + 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) { + 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) { + json assistant_content_only = json{ + { "role", "assistant" }, + { "content", "Response text" } + }; + + json assistant_with_tools = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ build_tool_call("test_func", json{ { "arg1", "value1" } }) }) } + }; + + json assistant_with_reasoning = json{ + { "role", "assistant" }, + { "content", "" }, + { "reasoning_content", "Need to think" } + }; + + template_params params_content_only; + params_content_only.messages = json::array({ user_msg, assistant_content_only }); + params_content_only.add_generation_prompt = false; + params_content_only.enable_thinking = true; + params_content_only.tools = tools; + + auto comparison_with_tools = compare_variants(tmpl, params_content_only, [&](template_params & p) { + p.messages = json::array({ user_msg, assistant_with_tools }); + }); + + auto comparison_with_reasoning = compare_variants(tmpl, params_content_only, [&](template_params & p) { + p.messages = json::array({ user_msg, assistant_with_reasoning }); + }); + + if (!comparison_with_tools || !comparison_with_reasoning) { + LOG_DBG("C1: Template application failed\n"); + return; + } + + const auto & diff_tools = comparison_with_tools->diff; + const auto & diff_reasoning = comparison_with_reasoning->diff; + + std::string response = "Response text"; + + bool found_plain_content = false; + if (trim_whitespace(diff_tools.left) == response) { + auto segments = segmentize_markers(diff_reasoning.left); + 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; + found_plain_content = true; + } else if (result.reasoning == reasoning_mode::FORCED_CLOSED && + diff_reasoning.left.find(result.markers.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()); + 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; + 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()); + } + // 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; + } + 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" + // 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"); + // TODO: END_DELIMITED content mode - delimited at end but not at start? + } +} + +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) { + if (fun_name_needle.empty() || arg_name_needle.empty() || haystack.empty()) { + return; + } + + auto in_json_haystack = [&haystack](const std::string & needle) -> bool { + // Find the needle in the haystack + size_t needle_pos = haystack.find(needle); + if (needle_pos == std::string::npos) { + return false; + } + if (needle_pos < 2) { + return false; // not enough space for a JSON structure + } + if (haystack[needle_pos - 1] == '\'' || haystack[needle_pos - 1] == '"') { + int cur = needle_pos - 2; + for (; cur >= 0 && std::isspace(haystack[cur]); cur--) { + } + if (haystack[cur] == ':' || haystack[cur] == '{') { + return true; + } + } + return false; + }; + + if (in_json_haystack(fun_name_needle)) { + // no need to check further, we're in JSON land + result.tools = tool_format::JSON_NATIVE; + } else if (in_json_haystack(arg_name_needle)) { + result.tools = tool_format::TAG_WITH_JSON; + } else { + result.tools = 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 (pos != std::string::npos) { + clean_haystack = haystack.substr(0, pos) + haystack.substr(pos + result.markers.reasoning_start.length()); + } + } + if (!result.markers.reasoning_end.empty()) { + auto pos = clean_haystack.find(result.markers.reasoning_end); + if (pos != std::string::npos) { + clean_haystack = + clean_haystack.substr(0, pos) + clean_haystack.substr(pos + result.markers.reasoning_end.length()); + } + } + + if (result.tools == 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); +} + +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) { + // 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('}'); + json call_struct = json::parse(clean_haystack.substr(json_start, json_end - json_start + 1)); + 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(); + } else if (subel.value().is_string() && std::string(subel.value()) == fun_name_needle) { + result.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(); + } 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(); + } + }; + for (const auto & el : call_struct.items()) { + if (el.key() == fun_name_needle) { + result.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(); + // 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(); + for (const auto & subel : el.value().items()) { + register_field(el.key(), subel); + } + } + // Register this element as a potential field + register_field("", el); + } + } + // TODO: support for generated (not provided) tool call IDs + auto space_or_bracket = [](bool opening, char c) -> bool { + return std::isspace(c) || (opening ? c == '[' : c == ']'); + }; + // now let's check if we're in an array construction, mark it if so and get out of it + 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; + break; + } + } + if (!result.tools_array_wrapped) { + json_start++; // we ate into the last pre-json character + } + } + if (json_end < (int) clean_haystack.length() - 1 && space_or_bracket(false, clean_haystack[json_end + 1])) { + for (++json_end; + space_or_bracket(false, clean_haystack[json_end]) && json_end < (int) clean_haystack.length() - 1; + json_end++) { + } + } + + std::vector> located_params; + if (!result.name_field.empty()) { + located_params.push_back({ clean_haystack.find(result.name_field), result.name_field }); + } + if (!result.args_field.empty()) { + located_params.push_back({ clean_haystack.find(result.args_field), result.args_field }); + } + if (!result.id_field.empty()) { + located_params.push_back({ clean_haystack.find(result.id_field), result.id_field }); + } + if (!result.gen_id_field.empty()) { + located_params.push_back({ clean_haystack.find(result.gen_id_field), result.gen_id_field }); + } + std::sort(located_params.begin(), located_params.end()); + for (auto & pair : located_params) { + result.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)); + // 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(); + } +} + +void differential_analyzer::analyze_tool_call_format_non_json(const std::string & clean_haystack, + const std::string & fun_name_needle, + diff_analysis_result & result) { + // we need to split by markers... + auto haystack_split = segmentize_markers(trim_leading_whitespace(clean_haystack)); + int where_is_nemo = 0; + int i = 0; + for (auto & segment : haystack_split) { + if (segment.value.find(fun_name_needle) != std::string::npos) { + where_is_nemo = i; + break; + } + i++; + } + + // basically the rule here is: + // - we append everything adjacent to a marker to the marker (treat it as part of the marker) + // - we assume symmetry (as many opening as closing markers) + // - we count the number of opening markers and then try to move backwards from the end until we've + // eaten as many closing markers as there were opening markers + if (where_is_nemo > 1) { // we might have more than one marker set here + std::vector preceding_markers; + for (int seg = where_is_nemo - 1; seg >= 0; seg--) { + if (haystack_split[seg].type == MARKER) { + preceding_markers.push_back(haystack_split[seg]); + } + } + size_t how_many_markers = preceding_markers.size(); + if (how_many_markers > 1) { + bool had_marker = false; + for (int seg = where_is_nemo - 1; seg >= 0; seg--) { + 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; + } else { + result.markers.tool_section_start = + haystack_split[seg].value + result.markers.tool_section_start; + } + } else { + if (had_marker) { + result.markers.tool_section_start = + haystack_split[seg].value + result.markers.tool_section_start; + } else { + result.markers.per_call_start = haystack_split[seg].value + result.markers.per_call_start; + } + } + } + had_marker = false; + size_t backtracked_so_far = 0; + for (size_t seg = haystack_split.size() - 1; seg > (size_t) where_is_nemo; seg--) { + if (haystack_split[seg].type == MARKER) { + backtracked_so_far++; + if (!had_marker) { + had_marker = true; + result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end; + } else { + result.markers.per_call_end = haystack_split[seg].value + result.markers.per_call_end; + } + } else { + if (had_marker) { + result.markers.per_call_end = haystack_split[seg].value + result.markers.per_call_end; + } else { + result.markers.tool_section_end = haystack_split[seg].value + result.markers.tool_section_end; + } + } + if (backtracked_so_far >= how_many_markers) { + break; + } + } + } else { + for (int seg = 0; seg < where_is_nemo; seg++) { + result.markers.tool_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; + if (haystack_split[seg].type == segment_type::MARKER) { + break; + } + } + } + } else { + result.markers.tool_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; + if (haystack_split[seg].type == segment_type::MARKER) { + break; + } + } + } +} + +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) { + json assistant_one_tool = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call }) } + }; + + json assistant_two_tools = 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_tool }); + params.tools = tools; + params.add_generation_prompt = false; + params.enable_thinking = true; + + auto one_vs_two = compare_variants( + 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"); + 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(); + } +} + +void differential_analyzer::analyze_tool_calls(const common_chat_template & tmpl, diff_analysis_result & result) { + 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("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" }, + }; + + json assistant_foofoo = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call }) } + }; + + json assistant_barbar = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ second_tool_call }) } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_foofoo }); + 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_barbar }); }); + + if (!comparison) { + LOG_DBG("T3: Template application failed\n"); + return; + } + + const auto & diff = comparison->diff; + LOG_DBG("T3 diff - suffix: '%s'\n", diff.suffix.c_str()); + LOG_DBG("T3 diff - left: '%s', right: '%s'\n", diff.left.c_str(), diff.right.c_str()); + + 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; + } else { + prefix_marker = result.markers.tool_section_start; + } + if (!prefix_marker.empty() && diff.prefix.rfind(prefix_marker) != std::string::npos) { + result.markers.func_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; + } 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; + break; + } + } + + auto seg_suf = segmentize_markers(diff.suffix); + size_t stop = 0; + size_t stop_internal_pos = 0; + for (const auto & ss : seg_suf) { + bool has_needle = false; + if (result.tools == 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("{["); + break; + } + } else { + has_needle = ss.value.find("first") != std::string::npos; + if (has_needle) { + stop_internal_pos = ss.value.find("first"); + break; + } + } + stop++; + } + if (stop < seg_suf.size() - 1) { + if (result.tools == tool_format::TAG_WITH_TAGGED) { + size_t how_far = 0; + if (stop > 0) { + if (seg_suf[stop].type == segment_type::MARKER) { + how_far = stop; + } else { + how_far = stop - 1; + } + for (size_t i = 0; i < how_far; i++) { + result.markers.func_name_suffix += seg_suf[i].value; + } + } + } else { + for (size_t i = 0; i < stop; i++) { + result.markers.func_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); + } + } + + // now just to find the closer + std::string suffix_marker; + if (!result.markers.per_call_end.empty()) { + suffix_marker = result.markers.per_call_end; + } else { + suffix_marker = result.markers.tool_section_end; + } + std::string closer_suffix; + if (suffix_marker.empty()) { + // we'll have to rely on an extra diff with no-calls version + auto notool_comp = compare_variants( + tmpl, params, [&](template_params & p) { p.messages = json::array({ user_msg, assistant_nocall }); }); + auto nt_diff = notool_comp->diff; + closer_suffix = nt_diff.left.substr(nt_diff.left.find("YYYY") + 4); + } else { + closer_suffix = diff.suffix.substr(0, diff.suffix.find(suffix_marker)); + } + if (!closer_suffix.empty()) { + auto closer_seg = segmentize_markers(closer_suffix); + bool need_to_eat_arg_marker = (result.tools == 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) { + 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)); + } + } + for (size_t i = last_arg_seg + 1; i < closer_seg.size(); i++) { + if (closer_seg[i].type == segment_type::MARKER) { + if (need_to_eat_arg_marker) { + need_to_eat_arg_marker = false; + } else { + result.markers.func_close += closer_seg[i].value; + } + } else if (!need_to_eat_arg_marker) { + result.markers.func_close += closer_seg[i].value; + } + } + } + result.markers.func_close = trim_leading_whitespace(result.markers.func_close); + + 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()); + } +} + +void differential_analyzer::extract_argument_separator(const common_chat_template & tmpl, + diff_analysis_result & result) { + json assistant_one_arg = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call_one_arg }) } + }; + + json assistant_two_args = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call }) } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_one_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_two_args }); }); + + if (!comparison) { + LOG_DBG("T4: Template application failed\n"); + 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()); + } +} + +void differential_analyzer::extract_args_markers(const common_chat_template & tmpl, diff_analysis_result & result) { + json assistant_no_args = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call_zero_args }) } + }; + + json assistant_with_args = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call_one_arg }) } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_no_args }); + 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_args }); }); + + if (!comparison) { + LOG_DBG("T5: Template application failed\n"); + 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; + // 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); + if (prefix_pos == std::string::npos) { + prefix_pos = 0; + } + if (suffix_pos == std::string::npos) { + suffix_pos = diff.suffix.size(); + } + std::string prefix_cut = diff.prefix.substr(prefix_pos + prefix_marker.size()); + std::string suffix_cut = diff.suffix.substr(0, suffix_pos); + std::string args_start = until_common_prefix(prefix_cut, "{}", "{\"first\":"); + 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()); + } + } +} + +void differential_analyzer::extract_call_id_markers(const common_chat_template & tmpl, diff_analysis_result & result) { + json assistant_id1 = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call }) } + }; + + json assistant_id2 = json{ + { "role", "assistant" }, + { "content", "" }, + { "tool_calls", json::array({ first_tool_call_alt_id }) } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_id1 }); + 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_id2 }); }); + + if (!comparison) { + LOG_DBG("T6: Template application failed for call_id detection\n"); + return; + } + + const auto & diff = comparison->diff; + LOG_DBG("T6 diff (call_id) - prefix: '%s', suffix: '%s'\n", diff.prefix.c_str(), diff.suffix.c_str()); + 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; + } + + std::string id_value_1 = "call00001"; + std::string id_value_2 = "call99999"; + + size_t common_id_prefix_len = 0; + for (size_t i = 0; i < std::min(id_value_1.length(), id_value_2.length()); i++) { + if (id_value_1[i] == id_value_2[i]) { + common_id_prefix_len++; + } else { + break; + } + } + std::string common_id_part = id_value_1.substr(0, common_id_prefix_len); + + // Check if the function name is in the prefix (normal case: BETWEEN_FUNC_AND_ARGS or POST_ARGS) + // or in the suffix (call_id is PRE_FUNC_NAME) + std::string func_name = "foofoo"; + size_t func_name_in_prefix = diff.prefix.rfind(func_name); + size_t func_name_in_suffix = diff.suffix.find(func_name); + + if (func_name_in_prefix != std::string::npos && func_name_in_suffix == std::string::npos) { + // Function name is only in prefix - call_id is BETWEEN_FUNC_AND_ARGS or POST_ARGS + // Check if args indicator "{" is in prefix or suffix + size_t args_in_prefix = diff.prefix.find('{', func_name_in_prefix); + size_t args_in_suffix = diff.suffix.find('{'); + + 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"); + + // The prefix ends with: ... + // Segmentize to find the call_id_prefix marker + std::string after_func = diff.prefix.substr(func_name_in_prefix + func_name.length()); + auto segments = segmentize_markers(after_func); + + std::string marker_before_id; + for (size_t i = 0; i < segments.size(); i++) { + if (segments[i].type == segment_type::MARKER) { + // Check if the next segment (if any) contains the common_id_part + if (i + 1 < segments.size() && segments[i + 1].value.find(common_id_part) != std::string::npos) { + marker_before_id = segments[i].value; + break; + } + // Or if this is the last marker and the text after contains common_id_part + if (i == segments.size() - 1 || + (i + 1 < segments.size() && segments[i + 1].type == segment_type::TEXT && + segments[i + 1].value.find(common_id_part) != std::string::npos)) { + marker_before_id = segments[i].value; + } + } + } + + 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()); + } 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()); + break; + } + } + } + + // Extract call_id_suffix: the first marker in the suffix before args + 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()); + break; + } + // Stop if we hit the args + if (suffix_segments[i].value.find('{') != std::string::npos) { + break; + } + } + } 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"); + + // Extract markers from between args and the ID + std::string after_args = diff.prefix.substr(args_in_prefix); + size_t closing_brace = after_args.rfind('}'); + if (closing_brace != std::string::npos) { + std::string between_args_and_id = after_args.substr(closing_brace + 1); + 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()); + break; + } + } + } + + // call_id_suffix would be in the suffix (first marker) + 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()); + 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"); + + // 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()); + break; + } + } + + // Extract call_id_suffix from suffix (first marker before func_name) + std::string before_func = diff.suffix.substr(0, func_name_in_suffix); + 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()); + 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; + } + + 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()); + } +} + +void differential_analyzer::collect_preserved_tokens(diff_analysis_result & result) { + auto & tokens = result.preserved_tokens; + + auto add_token = [&tokens](const std::string & org_token) { + std::string token = trim_whitespace(org_token); + if (!token.empty()) { + // Avoid duplicates + if (std::find(tokens.begin(), tokens.end(), token) == tokens.end()) { + tokens.push_back(token); + } + } + }; + + 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); +} diff --git a/common/chat-diff-analyzer.h b/common/chat-diff-analyzer.h new file mode 100644 index 0000000000..b1c601181e --- /dev/null +++ b/common/chat-diff-analyzer.h @@ -0,0 +1,347 @@ +#pragma once + +#include "chat.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include + +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; +}; + +struct diff_split { + std::string prefix; + std::string suffix; + std::string left; + std::string right; + + bool operator==(struct diff_split & other) const { + return prefix == other.prefix && suffix == other.suffix && left == other.left && right == other.right; + } +}; + +// Result of compare_variants containing diff and original outputs +struct compare_variants_result { + diff_split diff; + std::string output_A; + std::string output_B; +}; + +// ============================================================================ +// Marker Registry: All markers extracted via differential analysis +// ============================================================================ + +// Markers extracted from differential analysis of template outputs +// Each marker is derived from a specific comparison in the analysis matrix +struct marker_registry { + // === Reasoning markers (from Phase 1: R1-R3) === + std::string reasoning_start; // e.g., "", "[THINK]", "<|START_THINKING|>", "" + std::string reasoning_end; // e.g., "", "[BEGIN FINAL RESPONSE]", "<|END_THINKING|>" + + // === Content markers (from Phase 2: C1-C2) === + std::string content_start; // e.g., "", ">>>all\n", "" + std::string content_end; // e.g., "", "" + + // === Tool section markers (from Phase 3: T1-T2) === + std::string tool_section_start; // e.g., "", "[TOOL_CALLS]", "" + std::string tool_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|>", "" + std::string call_separator; // e.g., ",", "\n", "" (between multiple calls) + + // === Function markers (from Phase 3: T3-T5) === + std::string func_name_prefix; // e.g., "", "\"", ":0" + std::string func_close; // e.g., "", "" (for tag-based) + std::string args_start; // e.g., "{", "<|tool_call_argument_begin|>" + std::string args_end; // e.g., "}", "" + + // === Argument markers (from Phase 4: A1-A3, for tagged args format) === + std::string arg_name_prefix; // e.g., "", "\"" + std::string arg_name_suffix; // e.g., ">", "", "\":" + std::string arg_value_prefix; // e.g., "", "", "" + std::string arg_value_suffix; // e.g., "", "", "" + 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) +}; + + +// ============================================================================ +// 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 +}; + +inline std::ostream & operator<<(std::ostream & os, const reasoning_mode & mode) { + switch (mode) { + case reasoning_mode::NONE: + return os << "NONE"; + case reasoning_mode::TAG_BASED: + return os << "TAG_BASED"; + case reasoning_mode::DELIMITER: + return os << "DELIMITER"; + case reasoning_mode::FORCED_OPEN: + return os << "FORCED_OPEN"; + case reasoning_mode::FORCED_CLOSED: + return os << "FORCED_CLOSED"; + case reasoning_mode::TOOLS_ONLY: + return os << "TOOLS_ONLY"; + default: + return os << "UNKNOWN"; + } +} + +// Content wrapping mode (derived from C1 comparison) +enum class content_mode { + PLAIN, // No content markers + ALWAYS_WRAPPED, // Content always wrapped with markers + WRAPPED_WITH_REASONING, // Content wrapped only when reasoning present +}; + +inline std::ostream & operator<<(std::ostream & os, const content_mode & mode) { + switch (mode) { + case content_mode::PLAIN: + return os << "PLAIN"; + case content_mode::ALWAYS_WRAPPED: + return os << "ALWAYS_WRAPPED"; + case content_mode::WRAPPED_WITH_REASONING: + return os << "WRAPPED_WITH_REASONING"; + default: + return os << "UNKNOWN"; + } +} + +// 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 +}; + +inline std::ostream & operator<<(std::ostream & os, const call_id_position & pos) { + switch (pos) { + case call_id_position::NONE: + return os << "NONE"; + case call_id_position::PRE_FUNC_NAME: + return os << "PRE_FUNC_NAME"; + case call_id_position::BETWEEN_FUNC_AND_ARGS: + return os << "BETWEEN_FUNC_AND_ARGS"; + case call_id_position::POST_ARGS: + return os << "POST_ARGS"; + default: + return os << "UNKNOWN"; + } +} + +// 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: {...} + BRACKET_TAG, // Bracket-tag: [TOOL_CALLS]name[CALL_ID]id[ARGS]{...} + PREFIXED_INDEXED, // Prefixed-indexed: functions.X:0{...} + RECIPIENT_BASED, // Recipient routing: >>>func_name\n{...} + TAG_WITH_TAGGED, // Tag-based with tagged args: value + MARKDOWN_BLOCK, // Markdown code block: Action:\n```json\n[...]\n``` +}; + +inline std::ostream & operator<<(std::ostream & os, const tool_format & format) { + switch (format) { + case tool_format::NONE: + return os << "NONE"; + case tool_format::JSON_NATIVE: + return os << "JSON_NATIVE"; + case tool_format::TAG_WITH_JSON: + return os << "TAG_WITH_JSON"; + case tool_format::BRACKET_TAG: + return os << "BRACKET_TAG"; + case tool_format::PREFIXED_INDEXED: + return os << "PREFIXED_INDEXED"; + case tool_format::RECIPIENT_BASED: + return os << "RECIPIENT_BASED"; + case tool_format::TAG_WITH_TAGGED: + return os << "TAG_WITH_TAGGED"; + case tool_format::MARKDOWN_BLOCK: + return os << "MARKDOWN_BLOCK"; + default: + return os << "UNKNOWN"; + } +} + +// 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 [...] + + // Preserved tokens for tokenizer (union of all non-empty markers) + std::vector preserved_tokens; +}; + +// Performs systematic differential analysis on chat templates +// Uses comparison matrix to extract markers without heuristics +class differential_analyzer { + public: + // Main entry point: Run full differential analysis on a template + 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); + + // 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); + + 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); + + // 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); + + // R3: Detect reasoning scope (content-only vs with tools) + static void compare_reasoning_scope(const common_chat_template & tmpl, diff_analysis_result & result); + + // C1: Extract content markers by comparing different content values + static void compare_content_values(const common_chat_template & tmpl, diff_analysis_result & result); + + // T1: Analyze the tool calls + static void analyze_tool_calls(const common_chat_template & tmpl, diff_analysis_result & result); + + // 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); + + // Helper functions to handle the two branches of analyze_tool_call_format + 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); + + static void analyze_tool_call_format_non_json(const std::string & clean_haystack, + const std::string & fun_name_needle, + diff_analysis_result & result); + + // 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); + + // 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); + + // T4: Analyze function name format and extract markers + static void extract_function_markers(const common_chat_template & tmpl, + diff_analysis_result & result); + + // T5: Extract argument separator + static void extract_argument_separator(const common_chat_template & tmpl, diff_analysis_result & result); + + // T6: Extract args container markers + static void extract_args_markers(const common_chat_template & tmpl, diff_analysis_result & result); + + // A1: Extract argument name markers + static void extract_argument_name_markers(const common_chat_template & tmpl, diff_analysis_result & result); + + // A2: Extract argument value markers + static void extract_argument_value_markers(const common_chat_template & tmpl, diff_analysis_result & result); + + // T7: Extract call ID markers (for non-JSON formats) + static void extract_call_id_markers(const common_chat_template & tmpl, diff_analysis_result & result); + + // Classify tool format based on extracted markers + static void classify_tool_format(diff_analysis_result & result); + + // Classification helpers + 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); +}; + +enum segment_type { + TEXT, + MARKER +}; + +inline std::ostream & operator<<(std::ostream & os, const segment_type & type) { + switch (type) { + case segment_type::TEXT: + return os << "TEXT"; + case segment_type::MARKER: + return os << "MARKER"; + default: + return os << "UNKNOWN"; + } +} + +struct segment { + segment_type type; + std::string value; + + segment(segment_type type, std::string value) : type(type), value(std::move(value)) {} +}; \ No newline at end of file diff --git a/common/chat-peg-parser.cpp b/common/chat-peg-parser.cpp index ba49ecf29b..f72bece7b0 100644 --- a/common/chat-peg-parser.cpp +++ b/common/chat-peg-parser.cpp @@ -148,585 +148,6 @@ common_peg_parser common_chat_peg_builder::tag_with_safe_content(const std::stri return zero_or_more(choice({ p, content_chunk })); } -common_peg_parser common_chat_peg_unified_builder::build_reasoning_block(const content_structure & cs, - common_reasoning_format reasoning_format, - bool thinking_forced_open) { - // If reasoning is explicitly disabled, return empty - if (reasoning_format == COMMON_REASONING_FORMAT_NONE) { - return eps(); - } - - // Get reasoning markers - use from content_structure or fallback for DEEPSEEK format - std::string reason_start = cs.reasoning_start; - std::string reason_end = cs.reasoning_end; - - // If DEEPSEEK format is specified but markers weren't detected, use fallback markers - if ((reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK || - reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY) && - (reason_start.empty() || reason_end.empty())) { - // Try standard DeepSeek markers - if (reason_start.empty()) { - reason_start = ""; - } - if (reason_end.empty()) { - reason_end = ""; - } - } - - // If still no markers, return empty - // But allow empty start marker if thinking is forced open (implicit start) - if ((reason_start.empty() && !thinking_forced_open) || reason_end.empty()) { - return eps(); - } - - if (thinking_forced_open) { - // Mandatory reasoning: parse from current position to end marker - auto parser = reasoning(until(reason_end)) + literal(reason_end); - return rule("reasoning", reasoning_block(parser)); - } - // Optional reasoning: may or may not appear - // Also try <|START_THINKING|> style markers if standard markers don't match - auto standard_reasoning = - reasoning_block(literal(reason_start) + reasoning(until(reason_end)) + literal(reason_end)); - - // For templates that use <|START_THINKING|> style markers - if (reason_start == "" && reason_end == "") { - auto alt_reasoning = reasoning_block(literal("<|START_THINKING|>") + reasoning(until("<|END_THINKING|>")) + - literal("<|END_THINKING|>")); - return optional(rule("reasoning", choice({ standard_reasoning, alt_reasoning }))); - } - - return optional(rule("reasoning", standard_reasoning)); -} - -common_peg_parser common_chat_peg_unified_builder::build_content_block(const content_structure & cs, - common_reasoning_format reasoning_format, - const std::string & tool_section_start) { - GGML_UNUSED(tool_section_start); // leaving for now just in case - std::string content_start = cs.content_start; - std::string content_end = cs.content_end; - - // Add fallback content markers for DEEPSEEK format if not detected - // Some templates use tags for content when reasoning is enabled - if ((reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK || - reasoning_format == COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY) && - (content_start.empty() || content_end.empty())) { - content_start = ""; - content_end = ""; - } - - // Handle content markers with both start and end - if (cs.content_mode != content_structure::CONTENT_PLAIN && !cs.content_start.empty() && !cs.content_end.empty()) { - // Content is wrapped in markers - if (reasoning_format == COMMON_REASONING_FORMAT_NONE) { - // When reasoning_format=NONE, preserve any content before the content start marker - // (this may include reasoning/thinking markers that the model generates). - // This applies even if reasoning markers weren't detected by the analyzer. - auto with_markers = content(until(cs.content_start)) + literal(cs.content_start) + - content(until(cs.content_end)) + literal(cs.content_end); - // Fallback: content wrapped in end marker only (start marker might be in prompt) - auto implicit_markers = content(until(cs.content_end)) + literal(cs.content_end); - auto without_markers = content(rest()); - return choice({ with_markers, implicit_markers, without_markers }); - } // When reasoning is parsed separately, content starts directly after reasoning block - auto with_markers = literal(cs.content_start) + content(until(cs.content_end)) + literal(cs.content_end); - auto implicit_markers = content(until(cs.content_end)) + literal(cs.content_end); - auto without_markers = content(rest()); - return choice({ with_markers, implicit_markers, without_markers }); - } - - // Handle content with only start marker (no end marker) - // This is for formats like recipient-based (Functionary v3.2) where content is prefixed with - // a marker but has no explicit closing marker - content ends at end of message or before tool calls - if (cs.content_mode != content_structure::CONTENT_PLAIN && !cs.content_start.empty() && cs.content_end.empty()) { - if (reasoning_format == COMMON_REASONING_FORMAT_NONE) { - // Preserve any content before the start marker, then consume the marker and capture rest - auto with_start_marker = content(until(cs.content_start)) + literal(cs.content_start) + content(rest()); - auto without_markers = content(rest()); - return choice({ with_start_marker, without_markers }); - } // Content starts directly after reasoning block - auto with_start_marker = literal(cs.content_start) + content(rest()); - auto without_markers = content(rest()); - return choice({ with_start_marker, without_markers }); - } - - // For DEEPSEEK format, try fallback content markers even if not detected - if (!content_start.empty() && !content_end.empty()) { - auto with_markers = literal(content_start) + content(until(content_end)) + literal(content_end); - auto without_markers = content(rest()); - return choice({ with_markers, without_markers }); - } - - // Plain content - capture rest - return content(rest()); -} - -common_peg_parser common_chat_peg_unified_builder::build_tool_section(const tool_call_structure & ts, - const nlohmann::json & tools, - bool parallel_tool_calls, - bool force_tool_calls) { - if (!ts.supports_tools || !tools.is_array() || tools.empty()) { - return eps(); - } - - // Build tool choices based on function format - auto tool_choices = choice(); - - for (const auto & tool_def : tools) { - if (!tool_def.contains("function")) { - continue; - } - const auto & function = tool_def.at("function"); - std::string name = function.at("name"); - nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); - - tool_choices |= rule("tool-" + name, build_function(ts, name, params)); - } - - // Build the section with or without markers - auto build_section = [&]() -> common_peg_parser { - // Markdown code block format (Cohere Command-R Plus): - // Action:\n```json\n[{...}]\n``` - if (ts.function_format == tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK) { - // Build the opening: "Action:\n```json" - std::string code_fence_open = "```"; - if (!ts.code_block_language.empty()) { - code_fence_open += ts.code_block_language; - } - - auto opening = literal(ts.code_block_marker) + literal("\n") + literal(code_fence_open) + literal("\n"); - auto closing = literal("\n") + literal(ts.tool_section_end); // "\n```" - - // Build the JSON array of tool calls - // Don't use trigger_rule here since we're nested inside a sequence - auto tools_array = literal("[") + space(); - if (parallel_tool_calls) { - tools_array = tools_array + tool_choices; - tools_array = tools_array + zero_or_more(space() + literal(",") + space() + tool_choices); - } else { - tools_array = tools_array + optional(tool_choices); - } - tools_array = tools_array + space() + literal("]"); - - // Full section: Action:\n```json\n[{...}]\n``` - return trigger_rule("tool-call", opening + tools_array + closing); - } - - // Recipient-based format (Functionary v3.2): >>>function_name\n{arguments} - // Uses tool_section_start as delimiter, but no array wrapper or section markers - if (ts.function_format == tool_call_structure::FUNC_RECIPIENT_BASED) { - auto tool_call = trigger_rule("tool-call", tool_choices); - if (parallel_tool_calls) { - // Multiple tool calls: each starts with >>> - return one_or_more(tool_call + space()); - } - return tool_call; - } - - if (!ts.tool_section_start.empty() && !ts.tool_section_end.empty()) { - // Check if this format has SEPARATE section markers and per-call markers. - // This happens when: - // - Section markers wrap the ENTIRE section (e.g., ...) - // - Function prefix contains its own per-call marker (e.g., ...) - // Example: DeepSeek R1 with section and call markers, Kimi-K2 with prefixed-indexed format - // We detect this by checking if function_prefix contains a per-call START marker - // (indicated by words like "call_begin", "call_start", or similar patterns) - bool has_separate_section_and_call_markers = false; - - // FUNC_PREFIXED_INDEXED and FUNC_BRACKET_TAG always have separate section and per-call markers - if (ts.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED || - ts.function_format == tool_call_structure::FUNC_BRACKET_TAG) { - has_separate_section_and_call_markers = true; - } else if (ts.function_format == tool_call_structure::FUNC_NAME_AS_KEY) { - // FUNC_NAME_AS_KEY uses comma-separated JSON objects in an array - // Format: [{"func1": args}, {"func2": args}] - // The brackets are included in section markers - auto tool_call = trigger_rule("tool-call", tool_choices); - auto tool_calls = tool_call; - if (parallel_tool_calls) { - tool_calls = tool_call + zero_or_more(space() + literal(",") + space() + tool_call); - } - return literal(ts.tool_section_start) + space() + tool_calls + space() + literal(ts.tool_section_end); - } else if (ts.function_format == tool_call_structure::FUNC_TAG_WITH_NAME && !ts.function_prefix.empty()) { - // Check if function_prefix contains a per-call marker like "" - // This differentiates DeepSeek R1 (where function_prefix has its own call marker) - // from Nemotron (where function_prefix is just " ... - auto tool_call = trigger_rule("tool-call", tool_choices); - auto tool_calls = parallel_tool_calls ? one_or_more(tool_call + space()) : tool_call; - return literal(ts.tool_section_start) + space() + tool_calls + space() + literal(ts.tool_section_end); - } // Each tool call has its own wrapper: tool - auto single_tool_section = - trigger_rule("tool-call", literal(ts.tool_section_start) + space() + tool_choices + space() + - literal(ts.tool_section_end)); - if (parallel_tool_calls) { - // Multiple wrapped tool calls - return one_or_more(single_tool_section + space()); - } - return single_tool_section; - } - if (!ts.tool_section_start.empty()) { - // Start marker only (no end marker) - e.g., <|tool_call|>[...] - // Wrap all tool calls in an array after the start marker - auto tools_array = literal("[") + space(); - if (parallel_tool_calls) { - tools_array = tools_array + tool_choices; - tools_array = tools_array + zero_or_more(space() + literal(",") + space() + tool_choices); - } else { - tools_array = tools_array + optional(tool_choices); - } - tools_array = tools_array + space() + literal("]"); - - return trigger_rule("tool-call", literal(ts.tool_section_start) + tools_array); - } // No section markers (raw JSON format, e.g., Llama 3.1) - // Use trigger rule since tool calls are identified by regex trigger on the grammar - if (parallel_tool_calls) { - return trigger_rule("tool-call", one_or_more(tool_choices + space())); - } - return trigger_rule("tool-call", tool_choices); - }; - - auto section = build_section(); - if (!force_tool_calls) { - section = optional(section); - } - - return section; -} - -common_peg_parser common_chat_peg_unified_builder::build_function(const tool_call_structure & ts, - const std::string & name, - const nlohmann::json & schema) { - auto args = build_arguments(ts, schema); - - switch (ts.function_format) { - case tool_call_structure::FUNC_JSON_OBJECT: - { - // Build JSON object parser that accepts id field in either position: - // - Before name: {"id": "...", "name": "X", "arguments": {...}} (R7B style) - // - After args: {"name": "X", "arguments": {...}, "id": "..."} (Mistral style) - auto tool_name_ = json_member(ts.name_field, "\"" + tool_name(literal(name)) + "\""); - auto tool_args_ = json_member(ts.args_field, tool_args(args)); - - // id can appear before name or after args - auto id_member = json_member(ts.id_field, tool_id(json_string())); - auto id_before = ts.id_field.empty() ? eps() : optional(id_member << space() << "," << space()); - auto id_after = ts.id_field.empty() ? eps() : optional(space() << "," << space() << id_member); - - return tool(tool_open(literal("{")) << space() << id_before // optional id before name (R7B style) - << tool_name_ << space() << "," << space() << tool_args_ - << id_after // optional id after args (Mistral style) - << zero_or_more(space() << "," << space() << json_string() - << space() << ":" << space() << json()) - << space() << "}"); - } - - case tool_call_structure::FUNC_TAG_WITH_NAME: - { - // Build tag parser: {...} - // Combine prefix + name + suffix into tool_open to ensure the tool is only created - // when the FULL opening tag is confirmed. This prevents partial name matches during - // incremental parsing (e.g., matching "special_function" when input is "special_function_") - auto opening = literal(ts.function_prefix) + tool_name(literal(name)) + literal(ts.function_suffix); - // Note: No space() before tool_close because function_close may start with newline - // (e.g., "\n```") and space() would consume it, preventing the literal match - return tool(tool_open(opening) + space() + tool_args(args) + tool_close(literal(ts.function_close))); - } - - case tool_call_structure::FUNC_TAG_NAME_ONLY: - { - // Build tag parser: ... - // Combine < + name + > into tool_open to prevent partial matches - auto opening = literal("<") + tool_name(literal(name)) + literal(">"); - return tool(tool_open(opening) + space() + tool_args(args) + space() + - tool_close(literal(""))); - } - - case tool_call_structure::FUNC_PREFIXED_INDEXED: - { - // Build prefixed-indexed parser (e.g., Kimi-K2): - // <|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{...}<|tool_call_end|> - // The index number after : is ignored (we use zero_or_more(digit) to skip it) - auto opening = literal(ts.per_call_start) + literal(ts.function_namespace) + tool_name(literal(name)) + - literal(":") + zero_or_more(chars("0-9", 1, 1)) + // Skip the index - literal(ts.args_marker); - return tool(tool_open(opening) + space() + tool_args(args) + space() + - tool_close(literal(ts.per_call_end))); - } - - case tool_call_structure::FUNC_NAME_AS_KEY: - { - // Build name-as-key parser (e.g., Apertus): - // {"function_name": {...arguments...}} - // The function name IS the JSON key, and arguments are the value directly - auto opening = literal("{\"") + tool_name(literal(name)) + literal("\":"); - return tool(tool_open(opening) + space() + tool_args(args) + space() + literal("}")); - } - - case tool_call_structure::FUNC_BRACKET_TAG: - { - // Build bracket-tag parser (e.g., Mistral Small 3.2): - // [TOOL_CALLS]function_name[CALL_ID]call_id[ARGS]{...} - // per_call_start = "[TOOL_CALLS]" - // id_marker = "[CALL_ID]" - // args_marker = "[ARGS]" - auto opening = literal(ts.per_call_start) + tool_name(literal(name)); - if (!ts.id_marker.empty()) { - // Add id_marker + id value (captured as tool_id) - opening = opening + literal(ts.id_marker) + tool_id(until(ts.args_marker)); - } - if (!ts.args_marker.empty()) { - opening = opening + literal(ts.args_marker); - } - // No explicit closer for this format (EOS terminates) - return tool(tool_open(opening) + space() + tool_args(args)); - } - - case tool_call_structure::FUNC_RECIPIENT_BASED: - { - // Build recipient-based parser (e.g., Functionary v3.2): - // >>>function_name - // {'param1': 'value1', 'param2': 'value2'} - // tool_section_start = ">>>" - // Function name directly follows ">>>" with newline, arguments are Python dict (parse as JSON) - auto opening = literal(ts.tool_section_start) + tool_name(literal(name)); - // No explicit closer (newline + arguments, then EOS or next >>>) - return tool(tool_open(opening) + space() + tool_args(args)); - } - - case tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK: - { - // Build markdown code block parser (e.g., Cohere Command-R Plus): - // Action: - // ```json - // [ - // { - // "tool_name": "function_name", - // "parameters": {...} - // } - // ] - // ``` - // The individual function is a JSON object within the array - auto tool_name_ = json_member(ts.name_field, "\"" + tool_name(literal(name)) + "\""); - auto tool_args_ = json_member(ts.args_field, tool_args(args)); - - // Build the JSON object: {"tool_name": "...", "parameters": {...}} - // Use same pattern as FUNC_JSON_OBJECT: tool_open with atomic wrapper - return tool(tool_open(literal("{")) << space() << tool_name_ << space() << "," << space() << tool_args_ - << zero_or_more(space() << "," << space() << json_string() - << space() << ":" << space() << json()) - << space() << "}"); - } - } - - return eps(); -} - -common_peg_parser common_chat_peg_unified_builder::build_arguments(const tool_call_structure & ts, - const nlohmann::json & params) { - switch (ts.argument_format) { - case tool_call_structure::ARGS_JSON: - { - // Standard JSON object arguments - if (params.is_object()) { - return schema(json(), "args", params); - } - return json(); - } - - case tool_call_structure::ARGS_TAGGED: - { - // Tagged arguments: value - if (!params.contains("properties") || params.at("properties").empty()) { - return eps(); - } - - auto arg_choice = choice(); - for (const auto & el : params.at("properties").items()) { - const std::string & prop_name = el.key(); - const auto & prop_schema = el.value(); - - // Check if the schema declares this as a string type - bool is_string_type = prop_schema.contains("type") && prop_schema.at("type") == "string"; - - auto arg_name_parser = choice( - { literal(prop_name), literal("\"" + prop_name + "\""), literal("'" + prop_name + "'") }); - - // Use tool_arg_string_value for string types to prevent treating "[..." as JSON array - auto value_parser = is_string_type ? tool_arg_string_value(until(ts.arg_close)) - : tool_arg_value(until(ts.arg_close)); - - auto arg_rule = tool_arg(tool_arg_open(literal(ts.arg_prefix)) + tool_arg_name(arg_name_parser) + - literal(ts.arg_suffix) + value_parser + - tool_arg_close(literal(ts.arg_close)) + - (ts.arg_separator.empty() ? eps() : optional(literal(ts.arg_separator)))); - arg_choice |= arg_rule; - } - return zero_or_more(arg_choice + space()); - } - - case tool_call_structure::ARGS_KEY_VALUE_TAGS: - { - // Key-value tag arguments (GLM-4.6 style): - // key - // value - if (!params.contains("properties") || params.at("properties").empty()) { - return eps(); - } - - auto arg_choice = choice(); - for (const auto & el : params.at("properties").items()) { - const std::string & prop_name = el.key(); - const auto & prop_schema = el.value(); - - // Check if the schema declares this as a string type - bool is_string_type = prop_schema.contains("type") && prop_schema.at("type") == "string"; - - // Parse: key\nvalue - // ts.arg_prefix = "", ts.arg_suffix = "", ts.arg_close = "" - // Use tool_arg_string_value for string types to prevent treating "[..." as JSON array - auto value_parser = is_string_type ? tool_arg_string_value(until(ts.arg_close)) - : tool_arg_value(until(ts.arg_close)); - - auto arg_rule = tool_arg(tool_arg_open(literal(ts.arg_prefix)) + tool_arg_name(literal(prop_name)) + - literal(ts.arg_suffix) + // - space() + literal("") + value_parser + - tool_arg_close(literal(ts.arg_close))); - arg_choice |= arg_rule; - } - return zero_or_more(arg_choice + space()); - } - } - - return eps(); -} - -common_peg_parser common_chat_peg_unified_builder::standard_json_tools(const std::string & section_start, - const std::string & section_end, - const nlohmann::json & tools, - bool parallel_tool_calls, - bool force_tool_calls) { - if (!tools.is_array() || tools.empty()) { - return eps(); - } - - // Build tool choices for JSON format - auto tool_choices = choice(); - - for (const auto & tool_def : tools) { - if (!tool_def.contains("function")) { - continue; - } - const auto & function = tool_def.at("function"); - std::string name = function.at("name"); - nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); - - // Build JSON object parser: {"name": "X", "arguments": {...}} - auto tool_name_ = json_member("name", "\"" + tool_name(literal(name)) + "\""); - auto tool_args_ = json_member("arguments", tool_args(schema(json(), "tool-" + name + "-schema", params))); - - auto tool_parser = - tool(tool_open(literal("{")) << space() << tool_name_ << space() << "," << space() << tool_args_ - << zero_or_more(space() << "," << space() << json_string() << space() << ":" - << space() << json()) - << space() << "}"); - - tool_choices |= rule("tool-" + name, tool_parser); - } - - // Build the section with markers - auto tool_calls = tool_choices; - if (parallel_tool_calls) { - tool_calls = tool_calls + zero_or_more(space() + literal(",") + space() + tool_choices); - } - - auto section = - trigger_rule("tool-call", literal(section_start) + space() + tool_calls + space() + literal(section_end)); - - return force_tool_calls ? section : optional(section); -} - -common_peg_parser common_chat_peg_unified_builder::standard_constructed_tools( - const std::map & markers, - const nlohmann::json & tools, - bool parallel_tool_calls, - bool force_tool_calls) { - if (!tools.is_array() || tools.empty()) { - return eps(); - } - - // Extract markers with defaults - auto get_marker = [&markers](const std::string & key, const std::string & default_val = "") -> std::string { - auto it = markers.find(key); - return it != markers.end() ? it->second : default_val; - }; - - std::string section_start = get_marker("tool_call_start_marker", ""); - std::string section_end = get_marker("tool_call_end_marker", ""); - std::string func_opener = get_marker("function_opener", ""); - std::string func_closer = get_marker("function_closer", ""); - std::string param_key_prefix = get_marker("parameter_key_prefix", ""); - std::string param_closer = get_marker("parameter_closer", ""); - - // Build tool choices for tagged format - auto tool_choices = choice(); - - for (const auto & tool_def : tools) { - if (!tool_def.contains("function")) { - continue; - } - const auto & function = tool_def.at("function"); - std::string name = function.at("name"); - nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); - - // Build argument parsers - auto args = eps(); - if (params.contains("properties") && !params["properties"].empty()) { - auto arg_choice = choice(); - for (const auto & el : params["properties"].items()) { - const std::string & prop_name = el.key(); - - auto arg_name_parser = - choice({ literal(prop_name), literal("\"" + prop_name + "\""), literal("'" + prop_name + "'") }); - - auto arg_rule = tool_arg(tool_arg_open(literal(param_key_prefix)) + tool_arg_name(arg_name_parser) + - literal(param_key_suffix) + tool_arg_value(until(param_closer)) + - tool_arg_close(literal(param_closer))); - arg_choice |= arg_rule; - } - args = zero_or_more(arg_choice + space()); - } - - // Build function parser: args - auto tool_parser = tool(tool_open(literal(func_opener) + tool_name(literal(name)) + literal(func_name_suffix)) + - space() + tool_args(args) + space() + tool_close(literal(func_closer))); - - tool_choices |= rule("tool-" + name, tool_parser); - } - - // Build the section with markers - auto section = - parallel_tool_calls ? - trigger_rule("tool-call", literal(section_start) + space() + one_or_more(tool_choices + space()) + - literal(section_end)) : - trigger_rule("tool-call", literal(section_start) + space() + tool_choices + space() + literal(section_end)); - - return force_tool_calls ? section : optional(section); -} - void common_chat_peg_unified_mapper::from_ast(const common_peg_ast_arena & arena, const common_peg_parse_result & parse_result_arg) { // Call base class to visit all nodes @@ -734,7 +155,7 @@ void common_chat_peg_unified_mapper::from_ast(const common_peg_ast_arena & ar // Flush any pending tool call that was started but never got a name // This happens during partial parsing when the tool call is incomplete - if (pending_tool_call.has_value()) { + if (pending_tool_call.has_value() && !pending_tool_call->name.empty()) { // Transfer any buffered arguments if (!args_buffer.empty()) { pending_tool_call->arguments = args_buffer; @@ -954,7 +375,6 @@ void common_chat_peg_unified_mapper::map(const common_peg_ast_node & node) { current_tool->arguments += "\""; needs_closing_quote = false; } - // Close the arguments object if using tagged format if (!current_tool->arguments.empty() && current_tool->arguments.back() != '}') { current_tool->arguments += "}"; } @@ -982,3 +402,352 @@ void common_chat_peg_unified_mapper::map(const common_peg_ast_node & node) { } } } + +common_peg_parser common_chat_peg_unified_builder::standard_constructed_tools( + const std::map & markers, + const nlohmann::json & tools, + bool parallel_tool_calls, + bool force_tool_calls) { + if (!tools.is_array() || tools.empty()) { + return eps(); + } + + // Extract markers with defaults + auto get_marker = [&markers](const std::string & key, const std::string & default_val = "") -> std::string { + auto it = markers.find(key); + return it != markers.end() ? it->second : default_val; + }; + + std::string section_start = get_marker("tool_call_start_marker", ""); + std::string section_end = get_marker("tool_call_end_marker", ""); + std::string func_opener = get_marker("function_opener", ""); + std::string func_closer = get_marker("function_closer", ""); + std::string param_key_prefix = get_marker("parameter_key_prefix", ""); + std::string param_closer = get_marker("parameter_closer", ""); + + // Build tool choices for tagged format + auto tool_choices = choice(); + + for (const auto & tool_def : tools) { + if (!tool_def.contains("function")) { + continue; + } + const auto & function = tool_def.at("function"); + std::string name = function.at("name"); + nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); + + // Build argument parsers + auto args = eps(); + if (params.contains("properties") && !params["properties"].empty()) { + auto arg_choice = choice(); + for (const auto & el : params["properties"].items()) { + const std::string & prop_name = el.key(); + + auto arg_name_parser = + choice({ literal(prop_name), literal("\"" + prop_name + "\""), literal("'" + prop_name + "'") }); + + auto arg_rule = tool_arg(tool_arg_open(literal(param_key_prefix)) + tool_arg_name(arg_name_parser) + + literal(param_key_suffix) + tool_arg_value(until(param_closer)) + + tool_arg_close(literal(param_closer))); + arg_choice |= arg_rule; + } + args = zero_or_more(arg_choice + space()); + } + + // Build function parser: args + auto tool_parser = tool(tool_open(literal(func_opener) + tool_name(literal(name)) + literal(func_name_suffix)) + + space() + tool_args(args) + space() + tool_close(literal(func_closer))); + + tool_choices |= rule("tool-" + name, tool_parser); + } + + // Build the section with markers + auto section = + parallel_tool_calls ? + trigger_rule("tool-call", literal(section_start) + space() + one_or_more(tool_choices + space()) + + literal(section_end)) : + trigger_rule("tool-call", literal(section_start) + space() + tool_choices + space() + literal(section_end)); + + return force_tool_calls ? section : optional(section); +} + +// Helper: Parse dot notation key into prefix and field name +static std::pair parse_key_spec(const std::string & key) { + auto dot_pos = key.find('.'); + if (dot_pos == std::string::npos) { + return {"", key}; // Top-level field + } + return {key.substr(0, dot_pos), key.substr(dot_pos + 1)}; +} + +common_peg_parser common_chat_peg_unified_builder::standard_json_tools( + const std::string & section_start, + const std::string & section_end, + const nlohmann::json & tools, + bool parallel_tool_calls, + bool force_tool_calls, + const std::string & name_key, + const std::string & args_key, + bool array_wrapped, + bool function_is_key, + const std::string & call_id_key, + const std::string & gen_call_id_key, + const std::vector & parameters_order) { + if (!tools.is_array() || tools.empty()) { + return eps(); + } + + // Build tool choices for JSON format + auto tool_choices = choice(); + // auto other_member = json_string() + space() + literal(":") + space() + json(); + + // Determine effective field names + std::string effective_name_key = name_key.empty() ? "name" : name_key; + std::string effective_args_key = args_key.empty() ? "arguments" : args_key; + + // Check if we have nested keys (dot notation) + auto name_spec = parse_key_spec(effective_name_key); + auto args_spec = parse_key_spec(effective_args_key); + bool has_nested_keys = !name_spec.first.empty() || !args_spec.first.empty(); + + // Mode 1: function_is_key - parse {"function_name": {...}} + if (function_is_key) { + for (const auto & tool_def : tools) { + if (!tool_def.contains("function")) { + continue; + } + const auto & function = tool_def.at("function"); + std::string name = function.at("name"); + nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); + + // Build inner object fields + std::vector inner_fields; + + // Add optional string ID field + if (!call_id_key.empty()) { + auto id_parser = atomic( + literal("\"" + call_id_key + "\"") + space() + literal(":") + space() + + literal("\"") + tool_id(json_string_content()) + literal("\"") + ); + inner_fields.push_back(optional(id_parser + space() + optional(literal(",") + space()))); + } + + // Add optional generated integer ID field + if (!gen_call_id_key.empty()) { + auto gen_id_parser = atomic( + literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() + + choice({ + literal("\"") + tool_id(json_string_content()) + literal("\""), + tool_id(json_number()) + }) + ); + inner_fields.push_back(optional(gen_id_parser + space() + optional(literal(",") + space()))); + } + + // Add arguments - either wrapped in args_key or parsed directly + common_peg_parser args_parser = eps(); + if (args_key.empty()) { + // Arguments are directly the inner object value: {"func_name": {"arg1": "val"}} + args_parser = tool_args(schema(json(), "tool-" + name + "-schema", params)); + } else { + // Arguments are wrapped in a key: {"func_name": {"arguments": {"arg1": "val"}}} + args_parser = literal("\"" + effective_args_key + "\"") + space() + literal(":") + space() + + tool_args(schema(json(), "tool-" + name + "-schema", params)); + } + inner_fields.push_back(args_parser); + + // Build inner object parser - no greedy other_member skipping to avoid consuming ID + common_peg_parser inner_object = eps(); + if (args_key.empty() && inner_fields.size() == 1) { + // Direct arguments: {"func_name": {"arg1": "val"}} + // The args_parser is already the full object schema + inner_object = inner_fields[0]; + } else { + // Wrapped arguments: {"func_name": {"arguments": {"arg1": "val"}}} + inner_object = literal("{") + space(); + for (size_t i = 0; i < inner_fields.size(); i++) { + inner_object = inner_object + inner_fields[i]; + if (i < inner_fields.size() - 1) { + inner_object = inner_object + space(); + } + } + inner_object = inner_object + space() + literal("}"); + } + + // Tool call format: { "function_name": { inner_object } } + auto tool_parser = tool( + tool_open(literal("{")) + space() + + literal("\"") + tool_name(literal(name)) + literal("\"") + + space() + literal(":") + space() + + inner_object + + space() + tool_close(literal("}")) + ); + + tool_choices |= rule("tool-" + name, tool_parser); + } + } + // Mode 2: Nested keys (dot notation like "function.name") + else if (has_nested_keys) { + // Group fields by prefix + std::string nested_prefix = !name_spec.first.empty() ? name_spec.first : args_spec.first; + std::string nested_name_field = !name_spec.first.empty() ? name_spec.second : effective_name_key; + std::string nested_args_field = !args_spec.first.empty() ? args_spec.second : effective_args_key; + + for (const auto & tool_def : tools) { + if (!tool_def.contains("function")) { + continue; + } + const auto & function = tool_def.at("function"); + std::string name = function.at("name"); + nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); + + // Build nested object with name and arguments + auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() + + literal("\"") + tool_name(literal(name)) + literal("\""); + auto nested_args = literal("\"" + nested_args_field + "\"") + space() + literal(":") + space() + + tool_args(schema(json(), "tool-" + name + "-schema", params)); + + auto nested_object = literal("{") + space() + + nested_name + space() + literal(",") + space() + + nested_args + + space() + literal("}"); + + // Build top-level parser - simpler structure without greedy other_member skipping + // Format: { id?, "function": {...} } + auto tool_parser_body = tool_open(literal("{")) + space(); + + // Add optional string ID field at top level + if (!call_id_key.empty()) { + auto id_spec = parse_key_spec(call_id_key); + if (id_spec.first.empty()) { // Top-level ID field + auto id_parser = atomic( + literal("\"" + call_id_key + "\"") + space() + literal(":") + space() + + literal("\"") + tool_id(json_string_content()) + literal("\"") + ); + tool_parser_body = tool_parser_body + optional(id_parser + space() + literal(",") + space()); + } + } + + // Add optional generated integer ID field at top level + if (!gen_call_id_key.empty()) { + auto gen_id_spec = parse_key_spec(gen_call_id_key); + if (gen_id_spec.first.empty()) { // Top-level gen ID field + auto gen_id_parser = atomic( + literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() + + choice({ + literal("\"") + tool_id(json_string_content()) + literal("\""), + tool_id(json_number()) + }) + ); + tool_parser_body = tool_parser_body + optional(gen_id_parser + space() + literal(",") + space()); + } + } + + // Add the nested object field + auto nested_field = literal("\"" + nested_prefix + "\"") + space() + literal(":") + space() + nested_object; + tool_parser_body = tool_parser_body + nested_field + space() + tool_close(literal("}")); + + tool_choices |= rule("tool-" + name, tool(tool_parser_body)); + } + } + // Mode 3: Flat keys (enhanced with ID fields and parameter ordering) + else { + auto name_key_parser = literal("\"" + name_key + "\""); + auto args_key_parser = literal("\"" + args_key + "\""); + + for (const auto & tool_def : tools) { + if (!tool_def.contains("function")) { + continue; + } + const auto & function = tool_def.at("function"); + std::string name = function.at("name"); + nlohmann::json params = function.contains("parameters") ? function.at("parameters") : nlohmann::json::object(); + + auto tool_name_ = name_key_parser + space() + literal(":") + space() + + literal("\"") + tool_name(literal(name)) + literal("\""); + auto tool_args_ = args_key_parser + space() + literal(":") + space() + + tool_args(schema(json(), "tool-" + name + "-schema", params)); + + // Build ID parsers if keys are provided + common_peg_parser id_parser = eps(); + if (!call_id_key.empty()) { + id_parser = atomic( + literal("\"" + call_id_key + "\"") + space() + literal(":") + space() + + choice({ + literal("\"") + tool_id(json_string_content()) + literal("\""), + tool_id(json_number()) + }) + ); + } + + common_peg_parser gen_id_parser = eps(); + if (!gen_call_id_key.empty()) { + gen_id_parser = atomic( + literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() + + choice({ + literal("\"") + tool_id(json_string_content()) + literal("\""), + tool_id(json_number()) + }) + ); + } + + common_peg_parser tool_parser = eps(); + + // Use parameter ordering if provided - parse fields in specified order without greedy skipping + if (!parameters_order.empty()) { + } + // Build parser using parameter ordering (works with or without explicit parameters_order) + // Create list of (parser, key) pairs for all fields + std::vector> parser_pairs; + parser_pairs.emplace_back(tool_name_, effective_name_key); + parser_pairs.emplace_back(tool_args_, effective_args_key); + if (!call_id_key.empty()) { + parser_pairs.emplace_back(optional(id_parser), call_id_key); + } + if (!gen_call_id_key.empty()) { + parser_pairs.emplace_back(optional(gen_id_parser), gen_call_id_key); + } + + // Sort by position in parameters_order (or at end if not present) + std::sort(parser_pairs.begin(), parser_pairs.end(), + [¶meters_order](const auto & a, const auto & b) { + auto pos_a = std::find(parameters_order.begin(), parameters_order.end(), a.second); + auto pos_b = std::find(parameters_order.begin(), parameters_order.end(), b.second); + size_t idx_a = (pos_a == parameters_order.end()) ? parameters_order.size() : std::distance(parameters_order.begin(), pos_a); + size_t idx_b = (pos_b == parameters_order.end()) ? parameters_order.size() : std::distance(parameters_order.begin(), pos_b); + return idx_a < idx_b; + }); + + // Build ordered parser + auto ordered_body = tool_open(literal("{")) + space(); + for (size_t i = 0; i < parser_pairs.size(); i++) { + ordered_body = ordered_body + parser_pairs[i].first; + if (i < parser_pairs.size() - 1) { + ordered_body = ordered_body + space() + literal(",") + space(); + } + } + ordered_body = ordered_body + space() + tool_close(literal("}")); + tool_parser = tool(ordered_body); + + tool_choices |= rule("tool-" + name, tool_parser); + } + } + + // Build the section with markers + auto tool_calls = tool_choices; + if (parallel_tool_calls) { + tool_calls = tool_calls + zero_or_more(space() + literal(",") + space() + tool_choices); + } + + // Optionally wrap in array brackets + if (array_wrapped) { + tool_calls = literal("[") + space() + tool_calls + space() + literal("]"); + } + + auto section = + trigger_rule("tool-call", literal(section_start) + space() + tool_calls + space() + literal(section_end)); + + return force_tool_calls ? section : optional(section); +} diff --git a/common/chat-peg-parser.h b/common/chat-peg-parser.h index 920d5cffd4..7304ca7e61 100644 --- a/common/chat-peg-parser.h +++ b/common/chat-peg-parser.h @@ -5,6 +5,7 @@ #include #include +#include class common_chat_peg_builder : public common_peg_parser_builder { public: @@ -63,65 +64,43 @@ class common_chat_peg_unified_builder : public common_chat_peg_builder { // Low-level tag methods common_peg_parser tool(const common_peg_parser & p) { return tag(TOOL, p); } - common_peg_parser tool_open(const common_peg_parser & p) { return atomic(tag(TOOL_OPEN, p)); } - common_peg_parser tool_close(const common_peg_parser & p) { return atomic(tag(TOOL_CLOSE, p)); } - common_peg_parser tool_id(const common_peg_parser & p) { return atomic(tag(TOOL_ID, p)); } - common_peg_parser tool_name(const common_peg_parser & p) { return atomic(tag(TOOL_NAME, p)); } - common_peg_parser tool_args(const common_peg_parser & p) { return tag(TOOL_ARGS, p); } - common_peg_parser tool_arg(const common_peg_parser & p) { return tag(TOOL_ARG, p); } - common_peg_parser tool_arg_open(const common_peg_parser & p) { return atomic(tag(TOOL_ARG_OPEN, p)); } - common_peg_parser tool_arg_close(const common_peg_parser & p) { return atomic(tag(TOOL_ARG_CLOSE, p)); } - common_peg_parser tool_arg_name(const common_peg_parser & p) { return atomic(tag(TOOL_ARG_NAME, p)); } - common_peg_parser tool_arg_value(const common_peg_parser & p) { return tag(TOOL_ARG_VALUE, p); } // Use for schema-declared string types - won't be treated as potential JSON container common_peg_parser tool_arg_string_value(const common_peg_parser & p) { return tag(TOOL_ARG_STRING_VALUE, p); } - common_peg_parser tool_arg_json_value(const common_peg_parser & p) { return tag(TOOL_ARG_VALUE, p); } - // High-level building methods - - // Build reasoning block based on ContentStructure - common_peg_parser build_reasoning_block(const content_structure & cs, - common_reasoning_format reasoning_format, - bool thinking_forced_open); - - // Build content block based on ContentStructure - common_peg_parser build_content_block(const content_structure & cs, - common_reasoning_format reasoning_format, - const std::string & tool_section_start = ""); - - // Build complete tool section based on ToolCallStructure - common_peg_parser build_tool_section(const tool_call_structure & ts, - const nlohmann::json & tools, - bool parallel_tool_calls, - bool force_tool_calls); - - // Build single function parser based on ToolCallStructure - common_peg_parser build_function(const tool_call_structure & ts, - const std::string & name, - const nlohmann::json & schema); - - // Build arguments parser based on ToolCallStructure - common_peg_parser build_arguments(const tool_call_structure & ts, const nlohmann::json & params); - // Legacy-compatible helper for building standard JSON tool calls // Used by tests and manual parsers - common_peg_parser standard_json_tools(const std::string & section_start, - const std::string & section_end, - const nlohmann::json & tools, - bool parallel_tool_calls, - bool force_tool_calls); + // name_key/args_key: JSON key names for function name and arguments + // Empty or "name"/"arguments" will accept both common variations + // Supports dot notation for nested objects (e.g., "function.name") + // array_wrapped: if true, tool calls are wrapped in JSON array [...] + // function_is_key: if true, function name is the JSON key (e.g., {"func_name": {...}}) + // call_id_key: JSON key for string call ID (e.g., "id") + // gen_call_id_key: JSON key for generated integer call ID (e.g., "tool_call_id") + // parameters_order: order in which JSON fields should be parsed + common_peg_parser standard_json_tools(const std::string & section_start, + const std::string & section_end, + const nlohmann::json & tools, + bool parallel_tool_calls, + bool force_tool_calls, + const std::string & name_key = "", + const std::string & args_key = "", + bool array_wrapped = false, + bool function_is_key = false, + const std::string & call_id_key = "", + const std::string & gen_call_id_key = "", + const std::vector & parameters_order = {}); // Legacy-compatible helper for building XML/tagged style tool calls // Used by tests and manual parsers diff --git a/common/chat.cpp b/common/chat.cpp index 1ab77ee518..bca66132c6 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1,15 +1,12 @@ #include "chat.h" -#include "chat-auto-parser-helpers.h" #include "chat-auto-parser.h" #include "chat-peg-parser.h" #include "common.h" #include "ggml.h" #include "json-schema-to-grammar.h" #include "log.h" -#include "regex-partial.h" -#include "jinja/parser.h" #include "jinja/value.h" #include "jinja/runtime.h" #include "jinja/caps.h" @@ -1026,6 +1023,114 @@ static common_chat_params common_chat_params_init_gpt_oss(const common_chat_temp return data; } +// Functionary v3.2 - uses recipient-based format: >>>recipient\n{content} +static common_chat_params common_chat_params_init_functionary_v3_2(const common_chat_template & tmpl, + const struct templates_params & inputs) { + common_chat_params data; + + data.prompt = common_chat_template_direct_apply(tmpl, inputs); + data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; + data.preserved_tokens = { + ">>>all", + }; + + auto has_tools = inputs.tools.is_array() && !inputs.tools.empty(); + auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE; + + auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { + // Functionary v3.2 format: + // - Normal content: >>>all\n{content} + // - Tool calls: >>>function_name\n{json_args} + // Generation prompt ends with ">>>" so model outputs recipient immediately + + // Build content parser for >>>all\n{content} + // When tools are present, content stops before the next ">>>" (tool call) + // When no tools, content goes until end + auto content_until_tool = p.literal(">>>all\n") + p.content(p.until(">>>")); + auto content_until_end = p.literal(">>>all\n") + p.content(p.rest()); + + // If no tools or tool_choice is NONE, just parse content + if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) { + // When no tools, just match the prefix and capture everything after + return content_until_end + p.end(); + } + + // Build tool call parsers for each available function + auto tool_choice = p.choice(); + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + std::string name = function.at("name"); + const auto & schema = function.at("parameters"); + + // Tool format: >>>function_name\n{json_args} + auto tool_parser = p.tool( + p.tool_open(p.literal(">>>") + p.tool_name(p.literal(name)) + p.literal("\n")) + + p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)) + ); + + tool_choice |= p.rule("tool-" + name, tool_parser); + }); + + // The model can output: + // 1. Just content: >>>all\n{content} + // 2. Just tool call(s): >>>function_name\n{json_args} + // 3. Both: >>>all\n{content}>>>function_name\n{json_args} + + // Option 1: Content only (no following tool call) + auto content_only = content_until_end; + + // Option 2: Content followed by tool call(s) + auto content_and_tools = content_until_tool + p.one_or_more(tool_choice); + + // Option 3: Just tool call(s) (no content) + auto tools_only = p.one_or_more(tool_choice); + + if (inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED) { + // Must have at least one tool call + if (inputs.parallel_tool_calls) { + // Multiple tool calls allowed + return p.choice({ content_and_tools, tools_only }) + p.end(); + } else { + // Single tool call only + return p.choice({ content_until_tool + tool_choice, tools_only }) + p.end(); + } + } else { + // Tool calls are optional (auto mode) + if (inputs.parallel_tool_calls) { + // Multiple tool calls allowed + return p.choice({ content_and_tools, content_only, tools_only }) + p.end(); + } else { + // Single tool call at most + auto content_and_tool = content_until_tool + tool_choice; + return p.choice({ content_and_tool, content_only, tool_choice }) + p.end(); + } + } + }); + + data.parser = parser.save(); + + if (include_grammar) { + data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO; + + data.grammar = build_grammar([&](const common_grammar_builder & builder) { + foreach_function(inputs.tools, [&](const json & tool) { + const auto & function = tool.at("function"); + auto schema = function.at("parameters"); + builder.resolve_refs(schema); + }); + parser.build_grammar(builder, data.grammar_lazy); + }); + + // Grammar trigger for when the model starts outputting a tool call + // (after the initial ">>>" in the generation prompt) + data.grammar_triggers = { + { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, ">>>" } + }; + } + + return data; +} + namespace workaround { // if first message is system and template does not support it, merge it with next message @@ -1074,6 +1179,8 @@ static void func_args_not_string(json & messages) { } } +} + static common_chat_params common_chat_templates_apply_jinja(const struct common_chat_templates * tmpls, const struct common_chat_templates_inputs & inputs) { templates_params params; @@ -1097,7 +1204,10 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ workaround::system_message_not_supported(params.messages); } - if (!tmpl.original_caps().requires_non_null_content) { + if (tmpl.original_caps().supports_tool_calls) { + // some templates will require the content field in tool call messages + // to still be non-null, this puts an empty string everywhere where the + // content field is null workaround::requires_non_null_content(params.messages); } @@ -1132,20 +1242,26 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ // Note: Mistral Small 3.2 uses [CALL_ID] which Ministral doesn't have, so we can distinguish them if (src.find("[SYSTEM_PROMPT]") != std::string::npos && src.find("[TOOL_CALLS]") != std::string::npos && src.find("[ARGS]") != std::string::npos && src.find("[CALL_ID]") == std::string::npos) { - LOG_INF("Using specialized template: Ministral/Magistral Large 3\n"); + LOG_DBG("Using specialized template: Ministral/Magistral Large 3\n"); return common_chat_params_init_ministral_3(tmpl, params); } // GPT-OSS - has unique channel-based structure that needs dedicated handler if (src.find("<|channel|>") != std::string::npos) { - LOG_INF("Using specialized template: GPT-OSS\n"); + LOG_DBG("Using specialized template: GPT-OSS\n"); return common_chat_params_init_gpt_oss(tmpl, params); } + // Functionary v3.2 - uses recipient-based format with >>>recipient\n{content} + // Detection: template has ">>>all" for content and ">>>" prefix for tool calls + if (src.find(">>>all") != std::string::npos && src.find(">>>${recipient}") != std::string::npos) { + LOG_DBG("Using specialized template: Functionary v3.2\n"); + return common_chat_params_init_functionary_v3_2(tmpl, params); + } + try { - LOG_INF("Using autoparser for template analysis\n"); - template_analysis_result analysis = template_analyzer::analyze_template(tmpl); - auto auto_params = universal_peg_generator::generate_parser(analysis, tmpl, params); + LOG_DBG("Using differential autoparser\n"); + auto auto_params = universal_peg_generator::generate_parser(tmpl, params); return auto_params; } catch (const std::exception & e) { LOG_WRN("Automatic parser generation failed: %s\n", e.what()); @@ -1227,22 +1343,24 @@ common_chat_params common_chat_templates_apply(const struct common_chat_template common_chat_templates_apply_legacy(tmpls, inputs); } -common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax) { - return common_chat_peg_parse(syntax.parser, input, is_partial, syntax); +common_chat_msg common_chat_parse(const std::string & input, + bool is_partial, + const common_chat_parser_params & params) { + return common_chat_peg_parse(params.parser, input, is_partial, params); } -common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, - const std::string & input, - bool is_partial, - const common_chat_syntax & syntax) { +common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, + const std::string & input, + bool is_partial, + const common_chat_parser_params & params) { if (parser.empty()) { throw std::runtime_error("Failed to parse due to missing parser definition."); } - LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(syntax.format), input.c_str()); + LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), input.c_str()); common_peg_parse_context ctx(input, is_partial); - ctx.debug = syntax.debug; + ctx.debug = params.debug; auto result = parser.parse(ctx); if (result.fail()) { @@ -1252,13 +1370,9 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, // Try to extract any partial results from what was successfully parsed common_chat_msg msg; msg.role = "assistant"; - if (syntax.format == COMMON_CHAT_FORMAT_PEG_NATIVE) { - auto mapper = common_chat_peg_unified_mapper(msg); - mapper.from_ast(ctx.ast, result); - } else { - auto mapper = common_chat_peg_mapper(msg); - mapper.from_ast(ctx.ast, result); - } + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + if (ctx.debug) { fprintf(stderr, "\nAST for partial parse (fail):\n%s\n", ctx.ast.dump().c_str()); fflush(stderr); @@ -1272,21 +1386,16 @@ common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, common_chat_msg msg; msg.role = "assistant"; - if (syntax.format == COMMON_CHAT_FORMAT_PEG_NATIVE) { - auto mapper = common_chat_peg_unified_mapper(msg); - mapper.from_ast(ctx.ast, result); - } else { - // Generic mapper - auto mapper = common_chat_peg_mapper(msg); - mapper.from_ast(ctx.ast, result); - } + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + if (ctx.debug) { fprintf(stderr, "\nAST for %s parse:\n%s\n", is_partial ? "partial" : "full", ctx.ast.dump().c_str()); fflush(stderr); } if (!is_partial) { - LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat({ msg }).at(0).dump().c_str()); + LOG_DBG("Parsed message: %s\n", common_chat_msgs_to_json_oaicompat({ msg }).at(0).dump().c_str()); } return msg; } @@ -1296,3 +1405,4 @@ std::map common_chat_templates_get_caps(const common_chat_tem GGML_ASSERT(chat_templates->template_default != nullptr); return chat_templates->template_default->caps.to_map(); } + diff --git a/common/chat.h b/common/chat.h index feaebedce7..a4dc21b756 100644 --- a/common/chat.h +++ b/common/chat.h @@ -267,17 +267,12 @@ std::string common_chat_format_example(const struct common_chat_templates * const std::map & chat_template_kwargs); const char * common_chat_format_name(common_chat_format format); -common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_parser_params & syntax); -common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, const std::string & input, bool is_partial, const common_chat_parser_params & syntax); +common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_parser_params & params); +common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, const std::string & input, bool is_partial, const common_chat_parser_params & params); // used by arg and server const char * common_reasoning_format_name(common_reasoning_format format); common_reasoning_format common_reasoning_format_from_name(const std::string & format); -common_chat_msg common_chat_parse(const std::string & input, bool is_partial, const common_chat_syntax & syntax); -common_chat_msg common_chat_peg_parse(const common_peg_arena & parser, - const std::string & input, - bool is_partial, - const common_chat_syntax & syntax); common_chat_tool_choice common_chat_tool_choice_parse_oaicompat(const std::string & tool_choice); diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 05712d029b..94ec2b6a2c 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -1,3 +1,4 @@ +#include "log.h" #include "value.h" #include "runtime.h" #include "caps.h" @@ -16,7 +17,7 @@ using json = nlohmann::ordered_json; namespace jinja { using caps_json_fn = std::function; -using caps_analyze_fn = std::function; +using caps_analyze_fn = std::function; static void caps_try_execute(jinja::program & prog, const caps_json_fn & messages_fn, @@ -36,16 +37,20 @@ static void caps_try_execute(jinja::program & prog, auto tools = ctx.get_val("tools"); bool success = false; + std::string result; try { jinja::runtime runtime(ctx); - runtime.execute(prog); + auto results = runtime.execute(prog); + auto parts = jinja::runtime::gather_string_parts(results); + std::string result = parts->as_string().str(); success = true; } catch (const std::exception & e) { JJ_DEBUG("Exception during execution: %s", e.what()); + result = ""; // ignore exceptions during capability analysis } - analyze_fn(success, messages, tools); + analyze_fn(success, messages, tools, result); } // for debugging only @@ -64,7 +69,6 @@ static void caps_print_stats(value & v, const std::string & path) { std::map caps::to_map() const { return { {"requires_typed_content", requires_typed_content}, - {"requires_non_null_content", requires_non_null_content}, {"supports_tools", supports_tools}, {"supports_tool_calls", supports_tool_calls}, {"supports_parallel_tool_calls", supports_parallel_tool_calls}, @@ -106,7 +110,7 @@ caps caps_get(jinja::program & prog) { // tools return json{nullptr}; }, - [&](bool, value & messages, value &) { + [&](bool, value & messages, value &, const std::string &) { auto & content = messages->at(0)->at("content"); caps_print_stats(content, "messages[0].content"); if (has_op(content, "selectattr") || has_op(content, "array_access")) { @@ -137,7 +141,7 @@ caps caps_get(jinja::program & prog) { // tools return json::array(); }, - [&](bool, value & messages, value &) { + [&](bool, value & messages, value &, const std::string &) { auto & content = messages->at(0)->at("content"); caps_print_stats(content, "messages[0].content"); if (!content->stats.used) { @@ -182,6 +186,15 @@ caps caps_get(jinja::program & prog) { } })} }, + { + {"role", "tool"}, + {"content", "Tool response"}, + {"tool_call_id", "call00001"} + }, + { + {"role", "assistant"}, + {"content", "The tool response was 'tool response'"} + }, { {"role", "user"}, {"content", "User message"}, @@ -211,7 +224,7 @@ caps caps_get(jinja::program & prog) { }, }); }, - [&](bool success, value & messages, value & tools) { + [&](bool success, value & messages, value & tools, const std::string & res) { if (!success) { result.supports_tool_calls = false; result.supports_tools = false; @@ -220,8 +233,11 @@ caps caps_get(jinja::program & prog) { auto & tool_name = tools->at(0)->at("function")->at("name"); caps_print_stats(tool_name, "tools[0].function.name"); + caps_print_stats(tools, "tools"); if (!tool_name->stats.used) { - result.supports_tools = false; + if (!tools->stats.used && res.find(tool_name->as_string().str()) == std::string::npos) { + result.supports_tools = false; + } } auto & tool_calls = messages->at(1)->at("tool_calls");; @@ -239,83 +255,6 @@ caps caps_get(jinja::program & prog) { } ); - // case: requires non-null content in tool calls - if (result.supports_tool_calls) { - caps_try_execute( - prog, - [&]() { - // messages - return json::array({ - { - { "role", "user" }, - { "content", "User message" }, - }, - { - { "role", "assistant" }, - { "tool_calls", - json::array({ - { - { "id", "call00001" }, - { "type", "function" }, - { "function", - { - { "name", "tool1" }, - { "arguments", - { - { "arg", "value" } - } - } - } - } - }, - }) - } - }, - { - { "role", "user" }, - { "content", "User message" }, - }, - }); - }, - [&]() { - // tools - return json::array({ - { - { "name", "tool" }, - { "type", "function" }, - { "function", - { - { "name", "tool1" }, - { "description", "Tool description" }, - { "parameters", - { - { "type", "object" }, - { "properties", - { - { "arg", - { - { "type", "string" }, - { "description", "Arg description" }, - } - }, - } - }, - { "required", json::array({ "arg" }) }, - } - }, - } - }, - }, - }); - }, - [&](bool success, value & /* messages */, value & /* tools */) { - if (!success) { - result.requires_non_null_content = true; - } - } - ); - } - // case: preserve reasoning content in chat history caps_try_execute( prog, @@ -341,7 +280,7 @@ caps caps_get(jinja::program & prog) { // tools return json::array(); }, - [&](bool, value & messages, value &) { + [&](bool, value & messages, value &, const std::string &) { auto & content = messages->at(1)->at("reasoning_content"); caps_print_stats(content, "messages[1].reasoning_content"); if (content->stats.used) { diff --git a/common/jinja/caps.h b/common/jinja/caps.h index c077ed49e6..77df117baa 100644 --- a/common/jinja/caps.h +++ b/common/jinja/caps.h @@ -15,7 +15,6 @@ struct caps { bool supports_preserve_reasoning = false; // support assistant message with reasoning_content bool requires_typed_content = false; // default: use string content - bool requires_non_null_content = false; // requires "" instead of null for content in tool calls // for reporting on server std::map to_map() const; diff --git a/common/json-schema-to-grammar.cpp b/common/json-schema-to-grammar.cpp index 2f67c74d79..efd2c8ef95 100644 --- a/common/json-schema-to-grammar.cpp +++ b/common/json-schema-to-grammar.cpp @@ -27,11 +27,11 @@ static std::string build_repetition(const std::string & item_rule, int min_items if (separator_rule.empty()) { if (min_items == 1 && !has_max) { return item_rule + "+"; - } else if (min_items == 0 && !has_max) { + } + if (min_items == 0 && !has_max) { return item_rule + "*"; - } else { - return item_rule + "{" + std::to_string(min_items) + "," + (has_max ? std::to_string(max_items) : "") + "}"; - } + } + return item_rule + "{" + std::to_string(min_items) + "," + (has_max ? std::to_string(max_items) : "") + "}"; } auto result = item_rule + " " + build_repetition("(" + separator_rule + " " + item_rule + ")", min_items == 0 ? 0 : min_items - 1, has_max ? max_items - 1 : max_items); @@ -41,7 +41,7 @@ static std::string build_repetition(const std::string & item_rule, int min_items return result; } -static void _build_min_max_int(int64_t min_value, int64_t max_value, std::stringstream & out, int decimals_left = 16, bool top_level = true) { +static void build_min_max_int(int64_t min_value, int64_t max_value, std::stringstream & out, int decimals_left = 16, bool top_level = true) { auto has_min = min_value != std::numeric_limits::min(); auto has_max = max_value != std::numeric_limits::max(); @@ -128,14 +128,14 @@ static void _build_min_max_int(int64_t min_value, int64_t max_value, std::string if (has_min && has_max) { if (min_value < 0 && max_value < 0) { out << "\"-\" ("; - _build_min_max_int(-max_value, -min_value, out, decimals_left, /* top_level= */ true); + build_min_max_int(-max_value, -min_value, out, decimals_left, /* top_level= */ true); out << ")"; return; } if (min_value < 0) { out << "\"-\" ("; - _build_min_max_int(0, -min_value, out, decimals_left, /* top_level= */ true); + build_min_max_int(0, -min_value, out, decimals_left, /* top_level= */ true); out << ") | "; min_value = 0; } @@ -159,7 +159,7 @@ static void _build_min_max_int(int64_t min_value, int64_t max_value, std::string if (has_min) { if (min_value < 0) { out << "\"-\" ("; - _build_min_max_int(std::numeric_limits::min(), -min_value, out, decimals_left, /* top_level= */ false); + build_min_max_int(std::numeric_limits::min(), -min_value, out, decimals_left, /* top_level= */ false); out << ") | [0] | [1-9] "; more_digits(0, decimals_left - 1); } else if (min_value == 0) { @@ -194,7 +194,7 @@ static void _build_min_max_int(int64_t min_value, int64_t max_value, std::string } digit_range(c, c); out << " ("; - _build_min_max_int(std::stoll(min_s.substr(1)), std::numeric_limits::max(), out, less_decimals, /* top_level= */ false); + build_min_max_int(std::stoll(min_s.substr(1)), std::numeric_limits::max(), out, less_decimals, /* top_level= */ false); out << ")"; if (c < '9') { out << " | "; @@ -213,10 +213,10 @@ static void _build_min_max_int(int64_t min_value, int64_t max_value, std::string more_digits(0, less_decimals); out << " | "; } - _build_min_max_int(0, max_value, out, decimals_left, /* top_level= */ true); + build_min_max_int(0, max_value, out, decimals_left, /* top_level= */ true); } else { out << "\"-\" ("; - _build_min_max_int(-max_value, std::numeric_limits::max(), out, decimals_left, /* top_level= */ false); + build_min_max_int(-max_value, std::numeric_limits::max(), out, decimals_left, /* top_level= */ false); out << ")"; } return; @@ -232,7 +232,7 @@ struct BuiltinRule { std::vector deps; }; -std::unordered_map PRIMITIVE_RULES = { +static std::unordered_map PRIMITIVE_RULES = { {"boolean", {"(\"true\" | \"false\") space", {}}}, {"decimal-part", {"[0-9]{1,16}", {}}}, {"integral-part", {"[0] | [1-9] [0-9]{0,15}", {}}}, @@ -247,7 +247,7 @@ std::unordered_map PRIMITIVE_RULES = { {"null", {"\"null\" space", {}}}, }; -std::unordered_map STRING_FORMAT_RULES = { +static std::unordered_map STRING_FORMAT_RULES = { {"date", {"[0-9]{4} \"-\" ( \"0\" [1-9] | \"1\" [0-2] ) \"-\" ( \"0\" [1-9] | [1-2] [0-9] | \"3\" [0-1] )", {}}}, {"time", {"([01] [0-9] | \"2\" [0-3]) \":\" [0-5] [0-9] \":\" [0-5] [0-9] ( \".\" [0-9]{3} )? ( \"Z\" | ( \"+\" | \"-\" ) ( [01] [0-9] | \"2\" [0-3] ) \":\" [0-5] [0-9] )", {}}}, {"date-time", {"date \"T\" time", {"date", "time"}}}, @@ -260,22 +260,26 @@ static bool is_reserved_name(const std::string & name) { static const std::unordered_set RESERVED_NAMES = [] { std::unordered_set s; s.insert("root"); - for (const auto & p : PRIMITIVE_RULES) s.insert(p.first); - for (const auto & p : STRING_FORMAT_RULES) s.insert(p.first); + for (const auto & p : PRIMITIVE_RULES) { + s.insert(p.first); + } + for (const auto & p : STRING_FORMAT_RULES) { + s.insert(p.first); + } return s; }(); return RESERVED_NAMES.find(name) != RESERVED_NAMES.end(); } -std::regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+"); -std::regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"\\\\]"); -std::regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]"); -std::unordered_map GRAMMAR_LITERAL_ESCAPES = { +static std::regex INVALID_RULE_CHARS_RE("[^a-zA-Z0-9-]+"); +static std::regex GRAMMAR_LITERAL_ESCAPE_RE("[\r\n\"\\\\]"); +static std::regex GRAMMAR_RANGE_LITERAL_ESCAPE_RE("[\r\n\"\\]\\-\\\\]"); +static std::unordered_map GRAMMAR_LITERAL_ESCAPES = { {'\r', "\\r"}, {'\n', "\\n"}, {'"', "\\\""}, {'-', "\\-"}, {']', "\\]"}, {'\\', "\\\\"} }; -std::unordered_set NON_LITERAL_SET = {'|', '.', '(', ')', '[', ']', '{', '}', '*', '+', '?'}; -std::unordered_set ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = {'^', '$', '.', '[', ']', '(', ')', '|', '{', '}', '*', '+', '?'}; +static std::unordered_set NON_LITERAL_SET = {'|', '.', '(', ')', '[', ']', '{', '}', '*', '+', '?'}; +static std::unordered_set ESCAPED_IN_REGEXPS_BUT_NOT_IN_LITERALS = {'^', '$', '.', '[', ']', '(', ')', '|', '{', '}', '*', '+', '?'}; static std::string replacePattern(const std::string & input, const std::regex & regex, const std::function & replacement) { std::smatch match; @@ -322,19 +326,19 @@ private: if (_rules.find(esc_name) == _rules.end() || _rules[esc_name] == rule) { _rules[esc_name] = rule; return esc_name; - } else { - int i = 0; - while (_rules.find(esc_name + std::to_string(i)) != _rules.end() && _rules[esc_name + std::to_string(i)] != rule) { - i++; - } - std::string key = esc_name + std::to_string(i); - _rules[key] = rule; - return key; } + int i = 0; + while (_rules.find(esc_name + std::to_string(i)) != _rules.end() && _rules[esc_name + std::to_string(i)] != rule) { + i++; + } + std::string key = esc_name + std::to_string(i); + _rules[key] = rule; + return key; } std::string _generate_union_rule(const std::string & name, const std::vector & alt_schemas) { std::vector rules; + rules.reserve(alt_schemas.size()); for (size_t i = 0; i < alt_schemas.size(); i++) { rules.push_back(visit(alt_schemas[i], name + (name.empty() ? "alternative-" : "-") + std::to_string(i))); } @@ -398,6 +402,7 @@ private: flush_literal(); std::vector results; + results.reserve(ret.size()); for (const auto & item : ret) { results.push_back(to_rule(item)); } @@ -551,7 +556,7 @@ private: TrieNode() : is_end_of_string(false) {} void insert(const std::string & string) { - auto node = this; + auto *node = this; for (char c : string) { node = &node->children[c]; } @@ -676,7 +681,7 @@ private: if (ks.empty()) { return res; } - std::string k = ks[0]; + const std::string& k = ks[0]; std::string kv_rule_name = prop_kv_rule_names[k]; std::string comma_ref = "( \",\" space " + kv_rule_name + " )"; if (first_is_optional) { @@ -779,7 +784,7 @@ public: std::string pointer = ref.substr(ref.find('#') + 1); std::vector tokens = string_split(pointer, "/"); for (size_t i = 1; i < tokens.size(); ++i) { - std::string sel = tokens[i]; + const std::string& sel = tokens[i]; if (target.is_object() && target.contains(sel)) { target = target[sel]; } else if (target.is_array()) { @@ -802,7 +807,7 @@ public: _refs[ref] = target; } } else { - for (auto & kv : n.items()) { + for (const auto & kv : n.items()) { visit_refs(kv.value()); } } @@ -812,7 +817,7 @@ public: visit_refs(schema); } - std::string _generate_constant_rule(const json & value) { + static std::string _generate_constant_rule(const json & value) { return format_literal(value.dump()); } @@ -823,10 +828,12 @@ public: if (schema.contains("$ref")) { return _add_rule(rule_name, _resolve_ref(schema["$ref"])); - } else if (schema.contains("oneOf") || schema.contains("anyOf")) { + } + if (schema.contains("oneOf") || schema.contains("anyOf")) { std::vector alt_schemas = schema.contains("oneOf") ? schema["oneOf"].get>() : schema["anyOf"].get>(); return _add_rule(rule_name, _generate_union_rule(name, alt_schemas)); - } else if (schema_type.is_array()) { + } + if (schema_type.is_array()) { std::vector schema_types; for (const auto & t : schema_type) { json schema_copy(schema); @@ -834,15 +841,18 @@ public: schema_types.push_back(schema_copy); } return _add_rule(rule_name, _generate_union_rule(name, schema_types)); - } else if (schema.contains("const")) { + } + if (schema.contains("const")) { return _add_rule(rule_name, _generate_constant_rule(schema["const"]) + " space"); - } else if (schema.contains("enum")) { + } + if (schema.contains("enum")) { std::vector enum_values; for (const auto & v : schema["enum"]) { enum_values.push_back(_generate_constant_rule(v)); } return _add_rule(rule_name, "(" + string_join(enum_values, " | ") + ") space"); - } else if ((schema_type.is_null() || schema_type == "object") + } + if ((schema_type.is_null() || schema_type == "object") && (schema.contains("properties") || (schema.contains("additionalProperties") && schema["additionalProperties"] != true))) { std::unordered_set required; @@ -863,11 +873,12 @@ public: _build_object_rule( properties, required, name, schema.contains("additionalProperties") ? schema["additionalProperties"] : json())); - } else if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) { + } + if ((schema_type.is_null() || schema_type == "object" || schema_type == "string") && schema.contains("allOf")) { std::unordered_set required; std::vector> properties; std::map enum_values; - std::string hybrid_name = name; + const std::string& hybrid_name = name; std::function add_component = [&](const json & comp_schema, bool is_required) { if (comp_schema.contains("$ref")) { add_component(_refs[comp_schema["$ref"]], is_required); @@ -890,9 +901,9 @@ public: // todo warning } }; - for (auto & t : schema["allOf"]) { + for (const auto & t : schema["allOf"]) { if (t.contains("anyOf")) { - for (auto & tt : t["anyOf"]) { + for (const auto & tt : t["anyOf"]) { add_component(tt, false); } } else { @@ -911,7 +922,8 @@ public: } } return _add_rule(rule_name, _build_object_rule(properties, required, hybrid_name, json())); - } else if ((schema_type.is_null() || schema_type == "array") && (schema.contains("items") || schema.contains("prefixItems"))) { + } + if ((schema_type.is_null() || schema_type == "array") && (schema.contains("items") || schema.contains("prefixItems"))) { json items = schema.contains("items") ? schema["items"] : schema["prefixItems"]; if (items.is_array()) { std::string rule = "\"[\" space "; @@ -923,27 +935,31 @@ public: } rule += " \"]\" space"; return _add_rule(rule_name, rule); - } else { - std::string item_rule_name = visit(items, name + (name.empty() ? "" : "-") + "item"); - int min_items = schema.contains("minItems") ? schema["minItems"].get() : 0; - json max_items_json = schema.contains("maxItems") ? schema["maxItems"] : json(); - int max_items = max_items_json.is_number_integer() ? max_items_json.get() : std::numeric_limits::max(); - - return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " \"]\" space"); } - } else if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) { + std::string item_rule_name = visit(items, name + (name.empty() ? "" : "-") + "item"); + int min_items = schema.contains("minItems") ? schema["minItems"].get() : 0; + json max_items_json = schema.contains("maxItems") ? schema["maxItems"] : json(); + int max_items = max_items_json.is_number_integer() ? max_items_json.get() : std::numeric_limits::max(); + + return _add_rule(rule_name, "\"[\" space " + build_repetition(item_rule_name, min_items, max_items, "\",\" space") + " \"]\" space"); + } + if ((schema_type.is_null() || schema_type == "string") && schema.contains("pattern")) { return _visit_pattern(schema["pattern"], rule_name); - } else if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) { + } + if ((schema_type.is_null() || schema_type == "string") && std::regex_match(schema_format, std::regex("^uuid[1-5]?$"))) { return _add_primitive(rule_name == "root" ? "root" : schema_format, PRIMITIVE_RULES.at("uuid")); - } else if ((schema_type.is_null() || schema_type == "string") && STRING_FORMAT_RULES.find(schema_format + "-string") != STRING_FORMAT_RULES.end()) { + } + if ((schema_type.is_null() || schema_type == "string") && STRING_FORMAT_RULES.find(schema_format + "-string") != STRING_FORMAT_RULES.end()) { auto prim_name = schema_format + "-string"; return _add_rule(rule_name, _add_primitive(prim_name, STRING_FORMAT_RULES.at(prim_name))); - } else if (schema_type == "string" && (schema.contains("minLength") || schema.contains("maxLength"))) { + } + if (schema_type == "string" && (schema.contains("minLength") || schema.contains("maxLength"))) { std::string char_rule = _add_primitive("char", PRIMITIVE_RULES.at("char")); int min_len = schema.contains("minLength") ? schema["minLength"].get() : 0; int max_len = schema.contains("maxLength") ? schema["maxLength"].get() : std::numeric_limits::max(); return _add_rule(rule_name, "\"\\\"\" " + build_repetition(char_rule, min_len, max_len) + " \"\\\"\" space"); - } else if (schema_type == "integer" && (schema.contains("minimum") || schema.contains("exclusiveMinimum") || schema.contains("maximum") || schema.contains("exclusiveMaximum"))) { + } + if (schema_type == "integer" && (schema.contains("minimum") || schema.contains("exclusiveMinimum") || schema.contains("maximum") || schema.contains("exclusiveMaximum"))) { int64_t min_value = std::numeric_limits::min(); int64_t max_value = std::numeric_limits::max(); if (schema.contains("minimum")) { @@ -958,19 +974,19 @@ public: } std::stringstream out; out << "("; - _build_min_max_int(min_value, max_value, out); + build_min_max_int(min_value, max_value, out); out << ") space"; return _add_rule(rule_name, out.str()); - } else if (schema.empty() || schema_type == "object") { + } + if (schema.empty() || schema_type == "object") { return _add_rule(rule_name, _add_primitive("object", PRIMITIVE_RULES.at("object"))); - } else { - if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get()) == PRIMITIVE_RULES.end()) { - _errors.push_back("Unrecognized schema: " + schema.dump()); - return ""; - } - // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero - return _add_primitive(rule_name == "root" ? "root" : schema_type.get(), PRIMITIVE_RULES.at(schema_type.get())); + } + if (!schema_type.is_string() || PRIMITIVE_RULES.find(schema_type.get()) == PRIMITIVE_RULES.end()) { + _errors.push_back("Unrecognized schema: " + schema.dump()); + return ""; } + // TODO: support minimum, maximum, exclusiveMinimum, exclusiveMaximum at least for zero + return _add_primitive(rule_name == "root" ? "root" : schema_type.get(), PRIMITIVE_RULES.at(schema_type.get())); } void check_errors() { @@ -985,7 +1001,7 @@ public: std::string format_grammar() { std::stringstream ss; for (const auto & kv : _rules) { - ss << kv.first << " ::= " << kv.second << std::endl; + ss << kv.first << " ::= " << kv.second << '\n'; } return ss.str(); } diff --git a/common/peg-parser.cpp b/common/peg-parser.cpp index 80dd105246..7a4c1cc398 100644 --- a/common/peg-parser.cpp +++ b/common/peg-parser.cpp @@ -692,6 +692,7 @@ struct parser_executor { switch (ctx.input[pos]) { case '"': + case '\'': case '\\': case '/': case 'b': @@ -768,6 +769,48 @@ struct parser_executor { return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_NEED_MORE_INPUT, start_pos, pos); } + common_peg_parse_result operator()(const common_peg_python_dict_string_parser & /* p */) { + auto pos = start_pos; + + // Parse string content (without quotes) + while (pos < ctx.input.size()) { + char c = ctx.input[pos]; + + if (c == '\'') { + // Found closing quote - success (don't consume it) + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_SUCCESS, start_pos, pos); + } + + if (c == '\\') { + auto result = handle_escape_sequence(ctx, start_pos, pos); + if (!result.success()) { + return result; + } + } else { + auto utf8_result = parse_utf8_codepoint(ctx.input, pos); + + if (utf8_result.status == utf8_parse_result::INCOMPLETE) { + if (!ctx.is_partial) { + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_FAIL, start_pos); + } + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_NEED_MORE_INPUT, start_pos, pos); + } + + if (utf8_result.status == utf8_parse_result::INVALID) { + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_FAIL, start_pos); + } + + pos += utf8_result.bytes_consumed; + } + } + + // Reached end without finding closing quote + if (!ctx.is_partial) { + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_FAIL, start_pos, pos); + } + return common_peg_parse_result(COMMON_PEG_PARSE_RESULT_NEED_MORE_INPUT, start_pos, pos); + } + common_peg_parse_result operator()(const common_peg_until_parser & p) const { trie matcher(p.delimiters); @@ -955,6 +998,7 @@ void common_peg_arena::resolve_refs() { std::is_same_v || std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v) { @@ -1036,6 +1080,8 @@ std::string common_peg_arena::dump_impl(common_peg_parser_id std::to_string(p.max_count) + ")"; } else if constexpr (std::is_same_v) { return "JsonString()"; + } else if constexpr (std::is_same_v) { + return "PythonDictString()"; } else if constexpr (std::is_same_v) { return "Until(" + string_join(p.delimiters, " | ") + ")"; } else if constexpr (std::is_same_v) { @@ -1266,10 +1312,28 @@ common_peg_parser common_peg_parser_builder::json_number() { } common_peg_parser common_peg_parser_builder::json_string() { + // When allow_python_dict_format is true, accept both single and double quotes + if (allow_python_dict_format_) { + return rule("json-string-flex", [this]() { + auto json_str = sequence({ literal("\""), json_string_content(), literal("\""), space() }); + auto python_str = sequence({ literal("'"), python_dict_string_content(), literal("'"), space() }); + return choice({ json_str, python_str }); + }); + } + // Standard JSON strings with double quotes only return rule("json-string", [this]() { return sequence({ literal("\""), json_string_content(), literal("\""), space() }); }); } +common_peg_parser common_peg_parser_builder::flexible_string() { + // Always returns a choice of both quote styles regardless of flag + return rule("flexible-string", [this]() { + auto json_str = sequence({ literal("\""), json_string_content(), literal("\""), space() }); + auto python_str = sequence({ literal("'"), python_dict_string_content(), literal("'"), space() }); + return choice({ json_str, python_str }); + }); +} + common_peg_parser common_peg_parser_builder::json_bool() { return rule("json-bool", [this]() { return sequence({ choice({ literal("true"), literal("false") }), space() }); }); } @@ -1305,6 +1369,57 @@ common_peg_parser common_peg_parser_builder::json_string_content() { return wrap(arena_.add_parser(common_peg_json_string_parser{})); } +common_peg_parser common_peg_parser_builder::python_dict_string_content() { + return wrap(arena_.add_parser(common_peg_python_dict_string_parser{})); +} + +common_peg_parser common_peg_parser_builder::python_dict_string() { + return rule("python-dict-string", + [this]() { return sequence({ literal("'"), python_dict_string_content(), literal("'"), space() }); }); +} + +common_peg_parser common_peg_parser_builder::python_dict_number() { + // Same as JSON number + return json_number(); +} + +common_peg_parser common_peg_parser_builder::python_dict_bool() { + // Same as JSON bool + return json_bool(); +} + +common_peg_parser common_peg_parser_builder::python_dict_null() { + // Same as JSON null + return json_null(); +} + +common_peg_parser common_peg_parser_builder::python_dict_object() { + return rule("python-dict-object", [this]() { + auto ws = space(); + auto member = sequence({ python_dict_string(), ws, literal(":"), ws, python_dict() }); + auto members = sequence({ member, zero_or_more(sequence({ ws, literal(","), ws, member })) }); + return sequence({ literal("{"), ws, choice({ literal("}"), sequence({ members, ws, literal("}") }) }) }); + }); +} + +common_peg_parser common_peg_parser_builder::python_dict_array() { + return rule("python-dict-array", [this]() { + auto ws = space(); + auto elements = sequence({ python_dict(), zero_or_more(sequence({ literal(","), ws, python_dict() })) }); + return sequence({ literal("["), ws, choice({ literal("]"), sequence({ elements, ws, literal("]") }) }) }); + }); +} + +common_peg_parser common_peg_parser_builder::python_dict() { + return rule("python-dict-value", [this]() { + std::vector parsers = { + python_dict_object(), python_dict_array(), python_dict_string(), python_dict_number(), + python_dict_bool(), python_dict_null() + }; + return choice(parsers); + }); +} + common_peg_parser common_peg_parser_builder::json_member(const std::string & key, const common_peg_parser & p) { auto ws = space(); return sequence({ @@ -1435,7 +1550,8 @@ static std::unordered_set collect_reachable_rules(const common_peg_ std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || - std::is_same_v) { + std::is_same_v || + std::is_same_v) { // These parsers do not have any children } else if constexpr (std::is_same_v) { for (auto child : p.children) { @@ -1579,6 +1695,8 @@ void common_peg_arena::build_grammar(const common_grammar_builder & builder, boo return result + "{" + std::to_string(p.min_count) + "," + std::to_string(p.max_count) + "}"; } else if constexpr (std::is_same_v) { return R"(( [^"\\] | "\\" ( ["\\/ bfnrt] | "u" [0-9a-fA-F]{4} ) )*)"; + } else if constexpr (std::is_same_v) { + return R"(( [^'\\] | "\\" ( ['"\\/ bfnrt] | "u" [0-9a-fA-F]{4} ) )*)"; } else if constexpr (std::is_same_v) { if (p.delimiters.empty()) { return ".*"; @@ -1743,6 +1861,10 @@ static nlohmann::json serialize_parser_variant(const common_peg_parser_variant & return json{ { "type", "json_string" } }; + } else if constexpr (std::is_same_v) { + return json{ + { "type", "python_dict_string" } + }; } else if constexpr (std::is_same_v) { return json{ { "type", "until" }, @@ -1876,6 +1998,9 @@ static common_peg_parser_variant deserialize_parser_variant(const nlohmann::json if (type == "json_string") { return common_peg_json_string_parser{}; } + if (type == "python_dict_string") { + return common_peg_python_dict_string_parser{}; + } if (type == "until") { if (!j.contains("delimiters") || !j["delimiters"].is_array()) { throw std::runtime_error("until parser missing or invalid 'delimiters' field"); diff --git a/common/peg-parser.h b/common/peg-parser.h index 5d08cf6d47..9bd5e05838 100644 --- a/common/peg-parser.h +++ b/common/peg-parser.h @@ -211,6 +211,7 @@ struct common_peg_chars_parser { }; struct common_peg_json_string_parser {}; +struct common_peg_python_dict_string_parser {}; struct common_peg_until_parser { std::vector delimiters; @@ -259,6 +260,7 @@ using common_peg_parser_variant = std::variant< common_peg_space_parser, common_peg_chars_parser, common_peg_json_string_parser, + common_peg_python_dict_string_parser, common_peg_until_parser, common_peg_schema_parser, common_peg_rule_parser, @@ -316,9 +318,16 @@ class common_peg_parser_builder { common_peg_parser wrap(common_peg_parser_id id) { return common_peg_parser(id, *this); } common_peg_parser add(const common_peg_parser_variant & p) { return wrap(arena_.add_parser(p)); } + + bool allow_python_dict_format_ = false; public: common_peg_parser_builder(); + + // Enable/disable Python dict format support (single-quoted strings). + // When enabled, JSON parsers will also accept Python dict-style single-quoted strings. + void set_allow_python_dict_format(bool allow) { allow_python_dict_format_ = allow; } + bool get_allow_python_dict_format() const { return allow_python_dict_format_; } // Match nothing, always succeed. // S -> ε @@ -424,10 +433,29 @@ class common_peg_parser_builder { // Useful for extracting content within a JSON string. common_peg_parser json_string_content(); + // Matches a string that accepts both JSON double-quoted and Python dict single-quoted styles. + // This is useful when you explicitly want to accept both formats regardless of the allow_python_dict_format flag. + common_peg_parser flexible_string(); + + // Matches a Python dict-style single-quoted string content without the surrounding quotes. + // Useful for extracting content within a Python dict string. + common_peg_parser python_dict_string_content(); + // Matches a JSON object member with a key and associated parser as the // value. common_peg_parser json_member(const std::string & key, const common_peg_parser & p); + // Creates a complete Python dict format parser supporting objects, arrays, single-quoted strings, + // numbers, booleans, and null. Similar to JSON but uses single quotes for strings. + // value -> object | array | string | number | true | false | null + common_peg_parser python_dict(); + common_peg_parser python_dict_object(); + common_peg_parser python_dict_string(); + common_peg_parser python_dict_array(); + common_peg_parser python_dict_number(); + common_peg_parser python_dict_bool(); + common_peg_parser python_dict_null(); + // Wraps a parser with JSON schema metadata for grammar generation. // Used internally to convert JSON schemas to GBNF grammar rules. common_peg_parser schema(const common_peg_parser & p, const std::string & name, const nlohmann::ordered_json & schema, bool raw = false); diff --git a/docs/autoparser.md b/docs/autoparser.md index 3c77c4d304..4b48cceb76 100644 --- a/docs/autoparser.md +++ b/docs/autoparser.md @@ -4,7 +4,15 @@ The auto-parser automatically analyzes chat templates to determine how to parse ## Overview -The unified auto-parser uses a two-phase incremental analysis approach: +The unified auto-parser uses a **pure differential, compositional approach** to analyze chat templates: + +**Core Philosophy**: + +- **Zero Hardcoded Patterns**: All markers extracted through template comparison (the **only heuristic** is JSON detection) +- **Compositional Architecture**: Separate parsers for reasoning, content, and tools that compose cleanly +- **Variant Types**: Structural descriptions (strings) instead of forced enum classification + +**Two-Phase Analysis**: 1. **Phase 1: Content & Reasoning Analysis** - Analyzes how the template handles basic content and reasoning, without considering tools 2. **Phase 2: Tool Call Analysis** - Analyzes tool calling patterns, layered on top of Phase 1 @@ -40,73 +48,210 @@ struct content_structure { }; ``` -### tool_call_structure (Phase 2 Result) +### diff_analysis_result (Analysis Result) -Describes how the template formats tool calls: +The result of differential analysis contains all extracted markers and format classifications: ```cpp -struct tool_call_structure { - bool supports_tools = false; +struct diff_analysis_result { + // Classification results + reasoning_mode reasoning = reasoning_mode::NONE; + content_mode content = content_mode::PLAIN; + tool_format tools = tool_format::NONE; + argument_format args = argument_format::JSON; - // Container markers (what wraps all tool calls) - std::string tool_section_start; // e.g., "", "[TOOL_CALLS]", "", "" - std::string tool_section_end; // e.g., "", "]", "", "" + // All extracted markers (see marker_registry below) + marker_registry markers; - // Function format (how individual functions are structured) - enum function_format { - FUNC_JSON_OBJECT, // {"name": "X", "arguments": {...}} - FUNC_TAG_WITH_NAME, // {...} - FUNC_TAG_NAME_ONLY, // ... where X is function name (rare) - FUNC_PREFIXED_INDEXED, // <|tool_call_begin|>functions.X:0<|tool_call_argument_begin|>{...}<|tool_call_end|> - FUNC_NAME_AS_KEY, // [{"function_name": {...arguments...}}] (Apertus-style) - FUNC_BRACKET_TAG, // [TOOL_CALLS]X[CALL_ID]id[ARGS]{...} (Mistral Small 3.2 style) - FUNC_RECIPIENT_BASED, // >>>recipient\n{content} where recipient is "all" (content) or function name (tools) - FUNC_MARKDOWN_CODE_BLOCK, // Action:\n```json\n[{"tool_name": "X", ...}]\n``` (Cohere Command-R Plus) - }; - function_format function_format = FUNC_JSON_OBJECT; + // JSON field names (for JSON-based formats) + std::string name_field = "name"; + std::string args_field = "arguments"; + std::string id_field; - // For FUNC_JSON_OBJECT format - field names (may vary between templates) - std::string name_field = "name"; // Could be "tool_name", "function" - std::string args_field = "arguments"; // Could be "parameters", "params", "input" - std::string id_field; // Optional: "id", "tool_call_id", "" - - // For FUNC_TAG_WITH_NAME format - std::string function_prefix; // e.g., "" - std::string function_close; // e.g., "" - - // For FUNC_PREFIXED_INDEXED format (e.g., Kimi-K2) - std::string per_call_start; // e.g., "<|tool_call_begin|>" - std::string function_namespace; // e.g., "functions." (prefix before function name) - std::string args_marker; // e.g., "<|tool_call_argument_begin|>" - std::string per_call_end; // e.g., "<|tool_call_end|>" - - // For FUNC_BRACKET_TAG format (e.g., Mistral Small 3.2) - std::string id_marker; // e.g., "[CALL_ID]" - marker before tool call ID - - // For FUNC_MARKDOWN_CODE_BLOCK format (Cohere Command-R Plus) - std::string code_block_marker; // e.g., "Action:" - text marker before code block - std::string code_block_language; // e.g., "json" - language identifier in code fence - - // Argument format (how arguments are structured within a function) - enum argument_format { - ARGS_JSON, // Standard JSON object: {"key": "value", ...} - ARGS_TAGGED, // XML-style: value - ARGS_KEY_VALUE_TAGS, // keyvalue (GLM-4.6) - }; - argument_format argument_format = ARGS_JSON; - - // For ARGS_TAGGED format - std::string arg_prefix; // e.g., "" - std::string arg_close; // e.g., "", "" - std::string arg_separator; // e.g., "", "\n" - - // Flag: template renders null content as "None" string, requires empty string instead + // Flags + bool supports_tools = false; + bool supports_parallel_calls = false; bool requires_nonnull_content = false; + + // Preserved tokens for tokenizer + std::vector preserved_tokens; }; ``` +### marker_registry (Extracted Markers) + +All markers are extracted via differential analysis without hardcoded patterns: + +```cpp +struct marker_registry { + // === Reasoning markers === + std::string reasoning_start; // e.g., "", "[THINK]", "<|START_THINKING|>" + std::string reasoning_end; // e.g., "", "[/THINK]", "<|END_THINKING|>" + + // === Content markers === + std::string content_start; // e.g., "", ">>>all\n" + std::string content_end; // e.g., "" + + // === Tool section markers === + std::string tool_section_start; // e.g., "", "[TOOL_CALLS]" + std::string tool_section_end; // e.g., "", "]" + std::string per_call_start; // e.g., "\u2985" (for multi-call templates) + std::string per_call_end; // e.g., " \u2985" + std::string call_separator; // e.g., ",", "\n" + + // === Function markers === + std::string func_name_prefix; // e.g., "", "\"" + std::string func_close; // e.g., "" + std::string args_start; // e.g., "{", " \u300b" + std::string args_end; // e.g., "}", "" + + // === Argument markers (for tagged args format) === + std::string arg_name_prefix; // e.g., "" + std::string arg_name_suffix; // e.g., ">", "" + std::string arg_value_prefix; // e.g., "", "" + std::string arg_value_suffix; // e.g., "", "" + std::string arg_separator; + + // === Special markers === + std::string code_block_marker; // e.g., "Action:" (markdown code block format) + std::string id_marker; // e.g., "[CALL_ID]" (bracket-tag format) + std::string function_namespace; // e.g., "functions." (prefixed-indexed format) +}; +``` + +## Tool Calling Formats + +The auto-parser recognizes three primary tool calling formats. Other formats may be deprecated in future versions. + +### JSON_NATIVE + +**Structure**: The entire tool call (function name, arguments, and values) is in JSON format. There may be enclosing tags around the tool calling section. + +**Characteristics**: +- Function name is a JSON field: `"name": "function_name"` +- Arguments are a JSON object: `"arguments": {"key": "value"}` +- May be wrapped in section markers like `...` or `[TOOL_CALLS]...]` + +**Examples**: + +Standard OpenAI-style: +```json + +{"name": "get_weather", "arguments": {"location": "Paris", "unit": "celsius"}} + +``` + +Mistral Nemo with array wrapper: +```json +[TOOL_CALLS] +[{"name": "calculate", "arguments": {"expr": "2+2"}}] +``` + +Hermes-style with tool_calls wrapper: +```json + +{"name": "search", "arguments": {"query": "llama.cpp"}} + +``` + +**Detection**: `args_start == "{"`, `args_end == "}"`, no function name prefix markers + +--- + +### TAG_WITH_JSON + +**Structure**: The function name is outside the JSON structure, typically within quasi-XML markers. Arguments are still provided as a JSON object. + +**Characteristics**: +- Function name appears in tag attributes: `` or `` +- Arguments are a JSON object following the tag +- Has closing tags: `` or `` +- Arguments remain valid JSON + +**Examples**: + +Nemotron-style: +```xml +get_weather{"location": "Paris"} +``` + +Functionary v3.1: +```xml +{"location": "Paris", "unit": "celsius"} +``` + +ByteDance Seed-OSS: +```xml + +get_weather +{"location": "Paris"} + +``` + +MiniMax: +```xml + +calculate +{"expr": "2+2"} + +``` + +**Detection**: `func_name_prefix` starts with `<`, `args_start == "{"`, arguments are JSON + +--- + +### TAG_WITH_TAGGED + +**Structure**: Both the function name AND argument names are in XML-style tags. Argument values may be JSON or unquoted primitives depending on schema type. + +**Characteristics**: +- Function name in tag: `` or `` +- Each argument has its own tag: `value` +- String values are **unquoted** (raw text content of the tag) +- Non-string values (objects, arrays, numbers, booleans) are still JSON-formatted +- Supports streaming: partial arguments can be parsed incrementally + +**Examples**: + +Qwen/Hermes XML format: +```xml + +Paris +celsius + +``` + +Note how string values (`Paris`, `celsius`) are unquoted inside the tags. + +Mixed types example: +```xml + +2+2 +2 +{"round": true} + +``` + +Here: +- `expr` and `precision` are strings (unquoted) +- `options` is an object (JSON-formatted inside the tag) + +**Detection**: `arg_name_prefix` is non-empty, arguments use tagged format rather than JSON object + +--- + +### Other Formats (To Be Deprecated) + +The following formats are currently supported but will likely be deprecated: + +| Format | Description | Example | +|--------|-------------|---------| +| `BRACKET_TAG` | Bracket-based markers | `[TOOL_CALLS]func[ARGS]{...}` | +| `PREFIXED_INDEXED` | Namespace prefix with index | `functions.name:0{...}` | +| `RECIPIENT_BASED` | Recipient routing | `>>>recipient\n{content}` | +| `MARKDOWN_BLOCK` | Markdown code blocks | `Action:\n\`\`\`json\n[...]` | + ## Analysis Flow ```console @@ -129,13 +274,13 @@ Phase 2: analyze_tool_structure() |-- Classify argument format (JSON vs tagged) | v -tool_call_structure +diff_analysis_result | v -generate_parser(content_structure, tool_call_structure) - |-- build_reasoning_block(content_structure) - |-- build_content_block(content_structure) - |-- build_tool_section(tool_call_structure, tools) +generate_parser(diff_analysis_result) + |-- build_reasoning_block(diff_analysis_result) + |-- build_content_block(diff_analysis_result) + |-- build_tool_section(diff_analysis_result, tools) |-- Compose into final parser | v @@ -148,14 +293,13 @@ The mechanism starts in `common/chat.cpp`, in `common_chat_templates_apply_jinja ```cpp // 1. Analyze the template (two-phase) -template_analysis_result analysis = template_analyzer::analyze_template(tmpl); +auto analysis = differential_analyzer::analyze(tmpl); // 2. Generate the parser and grammar -auto auto_params = universal_peg_generator::generate_parser(analysis, tmpl, params); +auto auto_params = universal_peg_generator::generate_parser(tmpl, params); // 3. Use if it provides more than basic content handling if (auto_params.format != COMMON_CHAT_FORMAT_CONTENT_ONLY || - auto_params.thinking_forced_open || !auto_params.parser.empty()) { return auto_params; } @@ -165,32 +309,32 @@ if (auto_params.format != COMMON_CHAT_FORMAT_CONTENT_ONLY || The unified builder (`common_chat_peg_unified_builder`) provides high-level methods: -- `build_reasoning_block(cs, reasoning_format, thinking_forced_open)` - Build reasoning parser -- `build_content_block(cs, reasoning_format)` - Build content parser -- `build_tool_section(ts, tools, parallel_tool_calls, force_tool_calls)` - Build tool section -- `build_function(ts, name, schema)` - Build single function parser -- `build_arguments(ts, schema)` - Build arguments parser +- `build_reasoning_block(analysis, reasoning_format, thinking_forced_open)` - Build reasoning parser +- `build_content_block(analysis, reasoning_format)` - Build content parser +- `build_tool_section(analysis, tools, parallel_tool_calls, force_tool_calls)` - Build tool section +- `build_function(analysis, name, schema)` - Build single function parser +- `build_arguments(analysis, schema)` - Build arguments parser ## Key Templates Supported - **Granite** - `` + `` with tool calls - **Nemotron** - JSON tools with `` wrapper -- **Qwen/Hermes** - XML-style `` format +- **Qwen/Hermes** - XML-style `` format (TAG_WITH_TAGGED) - **Command-R7B** - `<|START_THINKING|>`/`<|START_RESPONSE|>` + `<|START_ACTION|>` tools - **DeepSeek R1** - Forced thinking + complex tools -- **Mistral Nemo** - `[TOOL_CALLS]` wrapper -- **MiniMax** - `` wrapper with XML tools +- **Mistral Nemo** - `[TOOL_CALLS]` wrapper (JSON_NATIVE) +- **MiniMax** - `` wrapper with JSON args (TAG_WITH_JSON) - **GLM-4.6** - `` + `name\n......` format -- **Kimi-K2** - `FUNC_PREFIXED_INDEXED` format with namespace and indices -- **Mistral Small 3.2** - `FUNC_BRACKET_TAG` format with `[TOOL_CALLS]` markers -- **Functionary v3.2** - `FUNC_RECIPIENT_BASED` format with `>>>` routing +- **Kimi-K2** - `PREFIXED_INDEXED` format with namespace and indices +- **Mistral Small 3.2** - `BRACKET_TAG` format with `[TOOL_CALLS]` markers +- **Functionary v3.2** - `RECIPIENT_BASED` format with `>>>` routing ## Files | File | Purpose | |------|---------| | `common/chat-auto-parser.h` | Data structures and API declarations | -| `common/chat-auto-parser-analyzer.cpp` | Phase 1 and Phase 2 analysis implementation | +| `common/chat-diff-analyzer.h/cpp` | Differential analysis implementation | | `common/chat-auto-parser-generator.cpp` | PEG parser generator | | `common/chat-auto-parser-helpers.h/cpp` | Shared helper functions | | `common/chat-peg-parser.h/cpp` | Unified builder and mapper classes | @@ -205,7 +349,7 @@ The unified builder (`common_chat_peg_unified_builder`) provides high-level meth **Method 1: Differential Reasoning Content Analysis** - Render template with `reasoning_content` field present vs absent -- Compare outputs to find markers between `THOUGHT_MARKER` and `CONTENT_MARKER` +- Compare outputs to find markers between reasoning and content - If only closing tag found, derive opening tag using patterns: - XML: `` → `` - Special tokens: `<|END_X|>` → `<|START_X|>`, `<|/X|>` → `<|X|>` @@ -260,85 +404,121 @@ The unified builder (`common_chat_peg_unified_builder`) provides high-level meth ### Phase 2: Tool Call Structure Analysis -#### Differential Analysis Algorithm +#### Pure Differential Analysis Algorithm -**Test Payload Strategy**: +**Key Principle**: All patterns are extracted through template comparison. The **only heuristic** is detecting JSON vs marker-based structures (via JSON parse attempt). No hardcoded pattern lists. -1. **Base**: User + Assistant with content only (no tools) -2. **Tool 1**: User + Assistant with tool_calls (empty args) -3. **Tool 2**: User + Assistant with tool_calls (with args) -4. **Tool 3**: User + Assistant with multiple tool calls +**Comparison Matrix**: -**Pattern Extraction Process**: +| Comparison | Purpose | What's Extracted | +|------------|---------|------------------| +| **T1**: No tools vs tools | Tool section markers | `tool_section_start`, `tool_section_end` | +| **T2**: 1 call vs 2 calls | Call separators | `per_call_start`, `call_separator` | +| **T3**: func_alpha vs func_beta | Function boundaries | `func_name_prefix`, `func_name_suffix` | +| **T4**: 1 arg vs 2 args | Argument separator | `arg_separator` | +| **T5**: No args vs args | Args container | `args_start`, `args_end` | +| **A1**: key1 vs key2 | Arg name boundaries | `arg_name_prefix`, `arg_name_suffix` | +| **A2**: value A vs B | Arg value boundaries | `arg_value_prefix`, `arg_value_suffix` | +| **A3**: number vs string | Quoting behavior | Value type handling | -1. Compute string differences between base and tool outputs -2. Use `test_function_name` as reliable search anchor (using `rfind` for last occurrence) -3. Extract structural elements: - - `tool_call_opener`: Common prefix before function name - - `tool_call_closer`: Common suffix after function calls - - `function_opener`: Tag immediately before function name - - `function_closer`: Tag after function content - - `parameter_key_prefix/suffix`: Argument wrapping patterns +**Structural Extraction Helpers**: -#### Format Classification Logic +```cpp +// Extract last structural marker from string (finds last <, [, {, or ") +std::string extract_structural_suffix(const std::string & str); -**FORMAT_JSON_NATIVE**: +// Extract first structural marker from string (finds first >, ], }, or ") +std::string extract_structural_prefix(const std::string & str); -- Detected by `{"name":` pattern in `tool_call_opener` -- Or XML markers with JSON structure +// The only heuristic: detect if content is valid JSON +bool is_json_based(const std::string & content); +``` -**FORMAT_XML_CONSTRUCTED**: +**Pattern Extraction Process** (Example - T1: Tool Section Markers): -- `function_opener` starts with `<` -- No substantial parameter markers +1. Render template with/without tool calls +2. Compute diff: `calculate_diff_split(output_no_tools, output_with_tools)` +3. Use controlled function name (`func_alpha`) as anchor in `diff.right` +4. Extract structural prefix before function name → `tool_section_start` +5. Extract structural suffix after tool content → `tool_section_end` -**FORMAT_RECIPIENT_BASED**: +**No Pattern Lists**: Unlike the old approach, there are no hardcoded lists like `["", "[TOOL_CALLS]", ...]`. All markers are discovered through differential comparison. -- `tool_call_start_marker == function_opener` -- No parameter markers -- Opener doesn't start with structural chars +#### Variant Detection Logic -**FORMAT_BRACKET_TAG**: +Instead of forcing patterns into enum types, the analyzer detects **variant types** as strings that describe the structural characteristics: -- `function_name_suffix` contains bracket tags like `[CALL_ID]...[ARGS]` -- `tool_call_start_marker` matches `[TOOL_CALLS]` pattern +**Variant Types**: -**FORMAT_PREFIXED_INDEXED**: +- `"json-native"`: Pure JSON tool calls (Llama, Mistral Nemo) +- `"tagged-json"`: Function name in markers, args in JSON (Functionary v3.1, Nemotron) +- `"tagged-args"`: Full XML-style with tagged arguments (Qwen, Hermes, MiniMax) +- `"bracket-tag"`: Bracket markers (Mistral Small 3.2: `[TOOL_CALLS]func[ARGS]{...}`) +- `"recipient-based"`: Recipient routing (Functionary v3.2: `>>>func_name`) +- `"markdown-block"`: Markdown code blocks (Cohere Command-R Plus) +- `"prefixed-indexed"`: Namespace prefix with indices (Kimi-K2: `functions.name:0`) -- `function_opener` ends with `.` (namespace separator) -- `function_name_suffix` starts with `:` followed by digit -- Example: `functions.name:0<|tool_call_argument_begin|>` +**Detection Strategy** (from most to least distinctive): -#### Specialized Format Handling +```cpp +void detect_tool_variant(diff_analysis_result & result) { + // 1. Check for unique markers (most distinctive) + if (!result.markers.id_marker.empty()) + → "bracket-tag" -**FUNC_PREFIXED_INDEXED (Kimi-K2)**: + if (markers contain ">>>") + → "recipient-based" -- Splits `function_opener` at last `>` to get `per_call_start` + `function_namespace` -- Extracts `args_marker` from `function_name_suffix` -- Derives `per_call_end` by matching structural patterns in `tool_call_closer` + if (code_block_marker present) + → "markdown-block" -**FUNC_TAG_WITH_NAME (Functionary/Nemotron)**: + if (function_namespace or suffix contains ':') + → "prefixed-indexed" -- Detects nested vs non-nested formats -- Uses overlap detection between `tool_section_start` and `function_prefix` -- Handles double-wrapping prevention + // 2. Check argument structure (JSON variants) + if (arg_name_prefix starts with '<') + → "tagged-args" -**ARGS_KEY_VALUE_TAGS (GLM-4.6)**: + if (func_name_prefix starts with '<') + → "tagged-json" -- Detects `keyvalue` pattern -- Cleans up suffix to extract just the key closer + // 3. Default + → "json-native" +} +``` -**FUNC_RECIPIENT_BASED (Functionary v3.2)**: +#### Compositional Parser Building -- Detects `>>>` recipient delimiter format -- Routes to "all" for content, function name for tools -- Uses same delimiter for both content and tool routing +The analyzer builds separate, composable parsers for each component: -**FUNC_BRACKET_TAG (Mistral Small 3.2/Devstral)**: +**Reasoning Parser**: -- Detects `[TOOL_CALLS]function_name[ARGS]{...}` pattern -- Optional `[CALL_ID]id` marker for tool call identification -- No section wrapper - each call starts independently +- Built from `reasoning_start` and `reasoning_end` markers +- Supports tag-based, delimiter, and forced-open modes + +**Content Parser**: + +- Built from `content_start` and `content_end` markers +- Supports plain, always-wrapped, and conditionally-wrapped modes + +**Tool Parser** (variant-specific): + +- Built based on `variant_type` detection +- Each variant has its own builder that uses the extracted markers +- No enum forcing - structure preserved as discovered + +**Final Composition**: + +```cpp +sequence({ + reasoning_parser, + space(), + content_parser, + space(), + tool_parser, + end() +}) +``` ### Generator Algorithms @@ -386,13 +566,13 @@ The test suite covers: **Tool Call Formats**: -- JSON: Llama 3.x, Mistral Nemo, Hermes, MiMo-VL -- XML: Nemotron, Qwen3-Coder, MiniMax -- Tagged: GLM-4.6 (key-value tags) -- Bracket-tag: Mistral Small 3.2, Devstral -- Prefixed-indexed: Kimi-K2 variants -- Name-as-key: Apertus-8B -- Recipient-based: Functionary v3.2 +- JSON_NATIVE: Llama 3.x, Mistral Nemo, Hermes, MiMo-VL +- TAG_WITH_JSON: Nemotron, Qwen3-Coder, MiniMax +- TAG_WITH_TAGGED: Qwen, Hermes (XML), ByteDance Seed-OSS +- BRACKET_TAG: Mistral Small 3.2, Devstral +- PREFIXED_INDEXED: Kimi-K2 variants +- RECIPIENT_BASED: Functionary v3.2 +- MARKDOWN_BLOCK: Cohere Command-R Plus **Edge Cases**: @@ -433,11 +613,11 @@ tst.test("input") To support a new template format: -1. **If it follows standard patterns** - The auto-parser should detect it automatically -2. **If it has unique markers** - Add the markers to the detection patterns in: - - `detect_reasoning_markers()` for reasoning tags - - `detect_content_markers()` for content wrappers - - `extract_patterns_from_differences()` for tool call patterns +1. **If it follows standard patterns** - The auto-parser should detect it automatically using the three main formats (JSON_NATIVE, TAG_WITH_JSON, TAG_WITH_TAGGED) +2. **If it has unique markers** - Add differential analysis patterns in: + - `compare_reasoning_presence()` for reasoning tags + - `compare_content_values()` for content wrappers + - `extract_tool_section()` for tool call patterns 3. **If it needs special handling** - Add a dedicated handler in `chat.cpp` before the auto-parser block ## Edge Cases and Quirks @@ -458,28 +638,28 @@ The following templates have active tests in `tests/test-chat.cpp`: | Template | Format | Notes | |----------|--------|-------| -| DeepSeek V3.1 | `FUNC_JSON_OBJECT` | Forced thinking mode | +| DeepSeek V3.1 | `JSON_NATIVE` | Forced thinking mode | | DeepSeek R1 Distill (Llama/Qwen) | Reasoning only | Forced-open thinking | | llama-cpp-deepseek-r1 | Reasoning only | Forced-open thinking | -| GLM-4.6 | `ARGS_KEY_VALUE_TAGS` | `name\n......` format | -| Kimi-K2 / Kimi-K2-Instruct / Kimi-K2-Thinking | `FUNC_PREFIXED_INDEXED` | `functions.name:0` with special markers | -| Apertus-8B-Instruct | `FUNC_NAME_AS_KEY` | `{"function_name": {...}}` format | -| MiniMax-M2 | `FUNC_TAG_WITH_NAME` | XML invoke with parameter tags | -| NVIDIA-Nemotron-Nano-v2 | `FUNC_JSON_OBJECT` | `` wrapper (nested) | -| Mistral-Nemo-Instruct-2407 | `FUNC_JSON_OBJECT` | `[TOOL_CALLS]` wrapper with id field | -| Functionary v3.1 | `FUNC_TAG_WITH_NAME` | `` non-nested format | -| Functionary v3.2 | `FUNC_RECIPIENT_BASED` | `>>>` recipient delimiter format | -| MiMo-VL / Hermes 3 / Qwen 2.5 | `FUNC_JSON_OBJECT` | `` wrapper | -| Apriel 1.5 | `FUNC_JSON_OBJECT` | `` wrapper with JSON array | +| GLM-4.6 | `TAGGED` | `name\n......` format | +| Kimi-K2 / Kimi-K2-Instruct / Kimi-K2-Thinking | `PREFIXED_INDEXED` | `functions.name:0` with special markers | +| Apertus-8B-Instruct | `NAME_AS_KEY` | `{"function_name": {...}}` format | +| MiniMax-M2 | `TAG_WITH_JSON` | XML invoke with parameter tags | +| NVIDIA-Nemotron-Nano-v2 | `JSON_NATIVE` | `` wrapper (nested) | +| Mistral-Nemo-Instruct-2407 | `JSON_NATIVE` | `[TOOL_CALLS]` wrapper with id field | +| Functionary v3.1 | `TAG_WITH_JSON` | `` non-nested format | +| Functionary v3.2 | `RECIPIENT_BASED` | `>>>` recipient delimiter format | +| MiMo-VL / Hermes 3 / Qwen 2.5 | `JSON_NATIVE` | `` wrapper | +| Apriel 1.5 | `JSON_NATIVE` | `` wrapper with JSON array | | Apriel 1.6 Thinker | Reasoning only | Implicit reasoning start | -| Cohere Command-R7B | `FUNC_JSON_OBJECT` | `START_RESPONSE/ACTION/THINKING` markers | -| Mistral Small 3.2 | `FUNC_BRACKET_TAG` | `[TOOL_CALLS]func[ARGS]{...}` with ID | -| Devstral | `FUNC_BRACKET_TAG` | `[TOOL_CALLS]func[ARGS]{...}` without ID | +| Cohere Command-R7B | `JSON_NATIVE` | START_RESPONSE/ACTION/THINKING markers | +| Mistral Small 3.2 | `BRACKET_TAG` | `[TOOL_CALLS]func[ARGS]{...}` with ID | +| Devstral | `BRACKET_TAG` | `[TOOL_CALLS]func[ARGS]{...}` without ID | | Ministral-3-14B-Reasoning | Custom reasoning | `[THINK]...[/THINK]` tags | -| IBM Granite | `FUNC_JSON_OBJECT` | `` + `` | -| ByteDance Seed-OSS | `FUNC_TAG_WITH_NAME` | Custom `` and `` tags | -| Qwen3-Coder | `FUNC_TAG_WITH_NAME` | XML-style tool format | -| Cohere Command-R Plus | `FUNC_MARKDOWN_CODE_BLOCK` | `Action:\n\`\`\`json\n[...]\n\`\`\`` format | +| IBM Granite | `JSON_NATIVE` | `` + `` | +| ByteDance Seed-OSS | `TAG_WITH_TAGGED` | Custom `` and `` tags | +| Qwen3-Coder | `TAG_WITH_TAGGED` | XML-style tool format | +| Cohere Command-R Plus | `MARKDOWN_BLOCK` | `Action:\n`\`\`\`json\n[...]\n`\`\`` format | ### Currently Unsupported Templates @@ -496,18 +676,25 @@ Some templates genuinely don't support tool calls (this is not a detection bug): ### TODO / Roadmap -- [ ] **Fix OpenAI GPT-OSS**: Add `FUNC_CHANNEL_BASED` format for channel marker structure. -- [x] **~~Fix Cohere Command-R Plus~~**: Added `FUNC_MARKDOWN_CODE_BLOCK` format for `Action:\n\`\`\`json` structure. +- [ ] **Fix OpenAI GPT-OSS**: Add handling for channel marker structure. +- [x] **~~Fix Cohere Command-R Plus~~**: Added `MARKDOWN_BLOCK` format for `Action:\n`\`\`\`json` structure. ### Recent Additions (Dec 2025 - Jan 2026) -- **FUNC_RECIPIENT_BASED**: Support for Functionary v3.2's `>>>` recipient delimiter format -- **FUNC_BRACKET_TAG**: Support for Mistral Small 3.2 and Devstral's `[TOOL_CALLS]...` format +- **RECIPIENT_BASED**: Support for Functionary v3.2's `>>>` recipient delimiter format +- **BRACKET_TAG**: Support for Mistral Small 3.2 and Devstral's `[TOOL_CALLS]...` format - **Enhanced Content Detection**: Better handling of custom reasoning tags and content wrappers - **Improved Streaming Support**: Better handling of partial parsing for all supported formats - **Custom Tag Support**: Support for non-standard reasoning tags like `` (ByteDance) - **Multi-line Tool Arguments**: Better parsing of complex tool arguments with code blocks -- **FUNC_MARKDOWN_CODE_BLOCK**: Support for Cohere Command-R Plus markdown code block format +- **MARKDOWN_BLOCK**: Support for Cohere Command-R Plus markdown code block format - **Implicit Reasoning Support**: Support for templates where reasoning starts implicitly without a start marker. +- **Pure Differential Refactoring (Jan 2026)**: Complete refactoring to eliminate hardcoded patterns: + - Removed all hardcoded pattern lists (previously had `["", "[TOOL_CALLS]", ...]`) + - Added structural extraction helpers (`extract_structural_suffix`, `extract_structural_prefix`) + - Replaced enum-based classification with string-based variant types + - Only remaining heuristic: JSON detection via parse attempt + - All markers now discovered through differential template comparison +- **Three Primary Tool Formats**: Consolidated tool calling formats to JSON_NATIVE, TAG_WITH_JSON, and TAG_WITH_TAGGED for clarity and maintainability The auto-parser now successfully handles 25+ different template formats across reasoning-only, tool-calling, and hybrid models, with comprehensive test coverage ensuring robust parsing across streaming and non-streaming scenarios. diff --git a/models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja b/models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja index 078e9f5458..e144cfcf69 100644 --- a/models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja +++ b/models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja @@ -132,7 +132,7 @@ The following instructions take precedence over instructions in the default prea {%- elif message.role|lower == 'user' %} <|START_OF_TURN_TOKEN|><|USER_TOKEN|>{{ message.content }}<|END_OF_TURN_TOKEN|>{%- if documents and not sent_documents.value %}{%- set sent_documents.value = true %}{% set tool_idx.value = tool_idx.value + 1 %}{{ document_turn(documents) }}{% endif %} {%- elif message.role|lower == 'assistant' or message.role|lower == 'chatbot' %} -<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.tool_plan}}<|END_THINKING|><|START_ACTION|>[ +<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{% if message.tool_calls %}<|START_THINKING|>{{message.reasoning_content}}<|END_THINKING|><|START_ACTION|>[ {% for tc in message.tool_calls %} {"tool_call_id": "{{ tool_idx.value }}", "tool_name": "{{ tc['function']['name'] }}", "parameters": {{ tc['function']['arguments']|tojson }}}{% if not loop.last %},{% endif %} diff --git a/models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja b/models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja index fff2b755e2..0c8d81e107 100644 --- a/models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja +++ b/models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja @@ -11,7 +11,7 @@ {%- if message['role'] == 'user' -%} {%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}} {%- endif -%} - {%- if message['role'] == 'assistant' and message['content'] is none -%} + {%- if message['role'] == 'assistant' and message['tool_calls'] -%} {%- set ns.is_tool = false -%} {%- for tool in message['tool_calls']-%} {%- if not ns.is_first -%} diff --git a/template.ans b/template.ans new file mode 100644 index 0000000000..da602b1a07 --- /dev/null +++ b/template.ans @@ -0,0 +1,7774 @@ + +================================================================================ + TEMPLATE ANALYSIS TOOL +================================================================================ +Analyzing 37 template(s) + +================================================================================ + ANALYZING TEMPLATE: models/templates/Apertus-8B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities:' +Common Suffix: '<|developer_end|><|user_start|>Hello, please help me.<|user_end|>' +Left (difference): ' disabled' +Right (difference): ' +// +type = () => any;' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: disabled<|developer_end|><|user_start|>Hello, please help me.<|user_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|assistant_start|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: disabled<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|>I can help you with that.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: disabled<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|>I can help you with that.<|assistant_end|><|user_start|>Thank you.<|user_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: +// +type = () => any;<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|>' +Common Suffix: '' +Left (difference): 'Let me help you.' +Right (difference): '<|tools_prefix|>[{"test_function_name": {"param1":0x6414d8ab7770, "param2":0x6414d8b07d80}}]<|tools_suffix|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: +// +type = () => any;<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|>' +Common Suffix: '<|assistant_end|><|user_start|>Continue.<|user_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tools_prefix|>[{"test_function_name": {"param1":0x6414d8b0a5f0, "param2":0x6414d8b03350}}]<|tools_suffix|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: +// +type = () => any;<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|><|tools_prefix|>[{"test_function_name": {"param1":0x6414d8ab' +Common Suffix: '0}}]<|tools_suffix|>' +Left (difference): '8210, "param2":0x6414d8b1315' +Right (difference): '7220, "param2":0x6414d8b06690}}, {"test_function_name": {"param1":0x6414d8ae81e0, "param2":0x6414d8b0d43' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: +// +type = () => any;<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|><|tools_prefix|>[{"test_function_name": {"param1":0x6414d8b0' +Common Suffix: '0}}]<|tools_suffix|><|assistant_end|><|user_start|>Continue.<|user_end|>' +Left (difference): '6690, "param2":0x6414d8b0e31' +Right (difference): 'd430, "param2":0x6414d8b06540}}, {"test_function_name": {"param1":0x6414d8b04530, "param2":0x6414d8b05b8' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|system_start|>You are Apertus, a helpful assistant created by the SwissAI initiative. +Knowledge cutoff: 2024-04 +Current date: 2026-01-26<|system_end|><|developer_start|>Deliberation: enabled +Tool Capabilities: +// +type = () => any;<|developer_end|><|user_start|>Hello, please help me.<|user_end|><|assistant_start|><|tools_prefix|>[{"test_function_name": {"param1":0x6414d8' +Common Suffix: '0}}]<|tools_suffix|>' +Left (difference): 'b05b80, "param2":0x6414d8b0a1c' +Right (difference): 'ae81e0, "param2":0x6414d8b0d43' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Apriel-1.6-15b-Thinker-fixed.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +' +Common Suffix: '<|begin_user|> +Hello, please help me. +<|begin_assistant|> +Here are my reasoning steps: +' +Left (difference): '' +Right (difference): 'You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +Here are my reasoning steps: +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +' +Common Suffix: 'I can help you with that.' +Left (difference): '' +Right (difference): 'The user is asking for help. I should respond positively. +[BEGIN FINAL RESPONSE] +' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +I can help you with that. +<|end|> +<|begin_user|> +Thank you. +<|begin_assistant|> +Here are my reasoning steps: +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +' +Common Suffix: '' +Left (difference): 'Let me help you.' +Right (difference): ' +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}]' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +' +Common Suffix: ' +<|end|> +<|begin_user|> +Continue. +<|begin_assistant|> +Here are my reasoning steps: +' +Left (difference): 'Let me help you.' +Right (difference): ' +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}, "id": "call_001"}]' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +<|begin_user|> +Hello, please help me. +<|begin_assistant|> + +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}' +Common Suffix: ']' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}}' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +<|begin_user|> +Hello, please help me. +<|begin_assistant|> + +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}, "id": "call_001"}' +Common Suffix: '] +<|end|> +<|begin_user|> +Continue. +<|begin_assistant|> +Here are my reasoning steps: +' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}, "id": "call_002"}' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|begin_system|> +You are a thoughtful, systematic AI assistant from ServiceNow Language Models (SLAM) lab. + Analyze each question carefully, present your reasoning step-by-step, then provide the final + response after the marker [BEGIN FINAL RESPONSE]. +You are provided with function signatures within XML tags. + You may call one or more functions to assist with the user query. + Don't make assumptions about the arguments. You should infer the argument values from previous + user responses and the system message. + Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + . + + Return all function calls as a list of JSON objects within XML tags. + Each JSON object should contain a function name and arguments as follows: + [ + {"name": , "arguments": }, + {"name": , "arguments": }, + ... + ] +<|begin_user|> +Hello, please help me. +<|begin_assistant|> +' +Common Suffix: ' +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}]' +Left (difference): '' +Right (difference): 'I need to call the tool first. +[BEGIN FINAL RESPONSE] +' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/ByteDance-Seed-OSS.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '' +Common Suffix: 'user +Hello, please help me.' +Left (difference): '' +Right (difference): 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: 'user +Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): 'assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: 'user +Hello, please help me.assistant +' +Common Suffix: 'I can help you with that.' +Left (difference): '' +Right (difference): 'The user is asking for help. I should respond positively. +' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: 'user +Hello, please help me.assistant +' +Common Suffix: 'I can help you with that.user +Thank you.' +Left (difference): '' +Right (difference): 'The user is asking for help. I should respond positively. +' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +user +Hello, please help me.assistant +' +Common Suffix: '' +Left (difference): 'Let me help you.' +Right (difference): ' + +value1 +value2 + +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +user +Hello, please help me.assistant +' +Common Suffix: 'user +Continue.' +Left (difference): 'Let me help you.' +Right (difference): ' + +value1 +value2 + +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +user +Hello, please help me.assistant + + +value1 +value2 + +' +Common Suffix: '' +Left (difference): '' +Right (difference): ' + + +value3 +value4 + +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +user +Hello, please help me.assistant + + +value1 +value2 + +' +Common Suffix: 'user +Continue.' +Left (difference): '' +Right (difference): ' + + +value3 +value4 + +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: 'system +You are Doubao, a helpful AI assistant. You may call one or more functions to assist with the user query. + +Function: +def test_function_name(param1: str,param2: str): + """ + A test function for debugging + + Args: + - param1 (str) [必填]: First parameter + - param2 (str) [必填]: Second parameter + + """ +工具调用请遵循如下格式: + + +value_1 +This is the value for the second parameter +that can span +multiple lines + + +user +Hello, please help me.assistant +' +Common Suffix: ' + +value1 +value2 + +' +Left (difference): '' +Right (difference): 'I need to call the tool first. +' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +' +Common Suffix: '<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Left (difference): '' +Right (difference): '```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>I can help you with that.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>I can help you with that.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Thank you.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Left (difference): 'Let me help you.<|END_OF_TURN_TOKEN|>' +Right (difference): ' +Action: +```json +[ + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value1", + "param2": "value2" +} + } +]``` +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '<|START_OF_TURN_TOKEN|><|USER_TOKEN|>Continue.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Left (difference): 'Let me help you.<|END_OF_TURN_TOKEN|>' +Right (difference): ' +Action: +```json +[ + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value1", + "param2": "value2" +} + } +]``` +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> +Action: +```json +[ + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value1", + "param2": "value2" +} + }' +Common Suffix: ' +]``` +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Left (difference): '' +Right (difference): ', + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value3", + "param2": "value4" +} + }' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> +Action: +```json +[ + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value1", + "param2": "value2" +} + }' +Common Suffix: ' +]``` +<|START_OF_TURN_TOKEN|><|USER_TOKEN|>Continue.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Left (difference): '' +Right (difference): ', + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value3", + "param2": "value4" +} + }' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># Safety Preamble +The instructions in this section override those in the task description and style guide sections. Don't answer questions that are harmful or immoral. + +# System Preamble +## Basic Rules +You are a powerful conversational AI trained by Cohere to help people. You are augmented by a number of tools, and your job is to use and consume the output of these tools to best help the user. You will see a conversation history between yourself and a user, ending with an utterance from the user. You will then see a specific instruction instructing you what kind of response to generate. When you answer the user's requests, you cite your sources in your answers, according to those instructions. + +# User Preamble +## Task and Context +You help people answer their questions and other requests interactively. You will be asked a very wide array of requests on all kinds of topics. You will be equipped with a wide range of search engines or similar tools to help you, which you use to research your answer. You should focus on serving the user's needs as best you can, which will be wide-ranging. + +## Style Guide +Unless the user asks for a different style of answer, you should answer in full sentences, using proper grammar and spelling. + +## Available Tools +Here is a list of tools that you have available to you: + +```python +def test_function_name(param1: str, param2: str) -> List[Dict]: + """A test function for debugging + + Args: + param1 (str): First parameter + param2 (str): Second parameter + """ + pass +```<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> +Action: +```json +[ + { + "tool_name": "test_function_name", + "parameters": { + "param1": "value1", + "param2": "value2" +} + } +]``` +<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>Write 'Action:' followed by a json-formatted list of actions that you want to perform in order to produce a good response to the user's last input. You can use any of the supplied tools any number of times, but you should aim to execute the minimum number of necessary actions for the input. You should use the `directly-answer` tool if calling the other tools is unnecessary. The list of actions you want to call should be formatted as a list of json objects, for example: +```json +[ + { + "tool_name": title of the tool in the specification, + "parameters": a dict of parameters to input into the tool as they are defined in the specs, or {} if it takes no parameters + } +]```<|END_OF_TURN_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +' +Common Suffix: '# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Left (difference): '' +Right (difference): ' +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>I can help you with that.<|END_RESPONSE|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>I can help you with that.<|END_RESPONSE|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Thank you.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Left (difference): '<|START_RESPONSE|>Let me help you.<|END_RESPONSE|>' +Right (difference): '<|START_THINKING|><|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}} +]<|END_ACTION|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Continue.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Left (difference): '<|START_RESPONSE|>Let me help you.<|END_RESPONSE|>' +Right (difference): '<|START_THINKING|><|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}} +]<|END_ACTION|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|><|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' +Common Suffix: ' +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Left (difference): '' +Right (difference): ', + {"tool_call_id": "1", "tool_name": "test_function_name", "parameters": {"param1": "value3", "param2": "value4"}}' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|><|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' +Common Suffix: ' +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Continue.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Left (difference): '' +Right (difference): ', + {"tool_call_id": "1", "tool_name": "test_function_name", "parameters": {"param1": "value3", "param2": "value4"}}' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble +You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. + +Your information cutoff date is June 2024. + +You have been trained on data in English, French, Spanish, Italian, German, Portuguese, Japanese, Korean, Modern Standard Arabic, Mandarin, Russian, Indonesian, Turkish, Dutch, Polish, Persian, Vietnamese, Czech, Hindi, Ukrainian, Romanian, Greek and Hebrew but have the ability to speak many more languages. + +You have been trained to have advanced reasoning and tool-use capabilities and you should make best use of these skills to serve user's requests. + +## Tool Use +Think about how you can make best use of the provided tools to help with the task and come up with a high level plan that you will execute first. + +0. Start by writing <|START_THINKING|> followed by a detailed step by step plan of how you will solve the problem. For each step explain your thinking fully and give details of required tool calls (if needed). Unless specified otherwise, you write your plan in natural language. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when the user request is so straightforward to address that only a trivial plan would be needed. + NOTE: You MUST skip this step when you are directly responding to the user's request without using any tools. + +Then carry out your plan by repeatedly executing the following steps. +1. Action: write <|START_ACTION|> followed by a list of JSON-formatted tool calls, with each one containing "tool_name" and "parameters" fields. + When there are multiple tool calls which are completely independent of each other (i.e. they can be executed in parallel), you should list them out all together in one step. When you finish, close it out with <|END_ACTION|>. +2. Observation: you will then receive results of those tool calls in JSON format in the very next turn, wrapped around by <|START_TOOL_RESULT|> and <|END_TOOL_RESULT|>. Carefully observe those results and think about what to do next. Note that these results will be provided to you in a separate turn. NEVER hallucinate results. + Every tool call produces a list of results (when a tool call produces no result or a single result, it'll still get wrapped inside a list). Each result is clearly linked to its originating tool call via its "tool_call_id". +3. Reflection: start the next turn by writing <|START_THINKING|> followed by what you've figured out so far, any changes you need to make to your plan, and what you will do next. When you finish, close it out with <|END_THINKING|>. + You can optionally choose to skip this step when everything is going according to plan and no special pieces of information or reasoning chains need to be recorded. + NOTE: You MUST skip this step when you are done with tool-use actions and are ready to respond to the user. + +You can repeat the above 3 steps multiple times (could be 0 times too if no suitable tool calls are available or needed), until you decide it's time to finally respond to the user. + +4. Response: then break out of the loop and write <|START_RESPONSE|> followed by a piece of text which serves as a response to the user's last request. Use all previous tool calls and results to help you when formulating your response. When you finish, close it out with <|END_RESPONSE|>. + +## Available Tools +Here is the list of tools that you have available to you. +You can ONLY use the tools listed here. When a tool is not listed below, it is NOT available and you should NEVER attempt to use it. +Each tool is represented as a JSON object with fields like "name", "description", "parameters" (per JSON Schema), and optionally, "responses" (per JSON Schema). + +```json +[ + {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}, "responses": null} +] +``` + +# Default Preamble +The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. +- Your name is Command. +- You are a large language model built by Cohere. +- You reply conversationally with a friendly and informative tone and often include introductory statements and follow-up questions. +- If the input is ambiguous, ask clarifying follow-up questions. +- Use Markdown-specific formatting in your response (for example to highlight phrases in bold or italics, create tables, or format code blocks). +- Use LaTeX to generate mathematical notation for complex equations. +- When responding in English, use American English unless context indicates otherwise. +- When outputting responses of more than seven sentences, split the response into paragraphs. +- Prefer the active voice. +- Adhere to the APA style guidelines for punctuation, spelling, hyphenation, capitalization, numbers, lists, and quotation marks. Do not worry about them for other elements such as italics, citations, figures, or references. +- Use gender-neutral pronouns for unspecified persons. +- Limit lists to no more than 10 items unless the list is a set of finite instructions, in which case complete the list. +- Use the third person when asked to write a summary. +- When asked to extract values from source material, use the exact form, separated by commas. +- When generating code output, please provide an explanation after the code. +- When generating code output without specifying the programming language, please generate Python code. +- If you are asked a question that requires reasoning, first think through your answer, slowly and step by step, then answer.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello, please help me.<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_THINKING|><|END_THINKING|><|START_ACTION|>[ + {"tool_call_id": "0", "tool_name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}} +]<|END_ACTION|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/GLM-4.6.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[gMASK]' +Common Suffix: '<|user|> +Hello, please help me.' +Left (difference): '' +Right (difference): '<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[gMASK]<|user|> +Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|assistant|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[gMASK]<|user|> +Hello, please help me.<|assistant|> +' +Common Suffix: ' +I can help you with that.' +Left (difference): '' +Right (difference): 'The user is asking for help. I should respond positively.' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[gMASK]<|user|> +Hello, please help me.<|assistant|> + +I can help you with that.<|user|> +Thank you.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +<|user|> +Hello, please help me.<|assistant|> + +' +Common Suffix: '' +Left (difference): 'Let me help you.' +Right (difference): 'test_function_name +param1 +value1 +param2 +value2 +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +<|user|> +Hello, please help me.<|assistant|> + +' +Common Suffix: '<|user|> +Continue.' +Left (difference): 'Let me help you.' +Right (difference): 'test_function_name +param1 +value1 +param2 +value2 +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +<|user|> +Hello, please help me.<|assistant|> + +test_function_name +param1 +value1 +param2 +value2 +' +Common Suffix: '' +Left (difference): '' +Right (difference): ' +test_function_name +param1 +value3 +param2 +value4 +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +<|user|> +Hello, please help me.<|assistant|> + +test_function_name +param1 +value1 +param2 +value2 +' +Common Suffix: '<|user|> +Continue.' +Left (difference): '' +Right (difference): ' +test_function_name +param1 +value3 +param2 +value4 +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name} +{arg-key-1} +{arg-value-1} +{arg-key-2} +{arg-value-2} +... +<|user|> +Hello, please help me.<|assistant|> +' +Common Suffix: ' +test_function_name +param1 +value1 +param2 +value2 +' +Left (difference): '' +Right (difference): 'I need to call the tool first.' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/GLM-4.7-Flash.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[gMASK]' +Common Suffix: '<|user|>Hello, please help me.' +Left (difference): '' +Right (difference): '<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[gMASK]<|user|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|assistant|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[gMASK]<|user|>Hello, please help me.<|assistant|>' +Common Suffix: 'I can help you with that.' +Left (difference): '' +Right (difference): 'The user is asking for help. I should respond positively.' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[gMASK]<|user|>Hello, please help me.<|assistant|>I can help you with that.<|user|>Thank you.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>Hello, please help me.<|assistant|>' +Common Suffix: '' +Left (difference): 'Let me help you.' +Right (difference): 'test_function_nameparam1value1param2value2' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>Hello, please help me.<|assistant|>' +Common Suffix: '<|user|>Continue.' +Left (difference): 'Let me help you.' +Right (difference): 'test_function_nameparam1value1param2value2' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>Hello, please help me.<|assistant|>test_function_nameparam1value1param2value2' +Common Suffix: '' +Left (difference): '' +Right (difference): 'test_function_nameparam1value3param2value4' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>Hello, please help me.<|assistant|>test_function_nameparam1value1param2value2' +Common Suffix: '<|user|>Continue.' +Left (difference): '' +Right (difference): 'test_function_nameparam1value3param2value4' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '[gMASK]<|system|> +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, output the function name and arguments within the following XML format: +{function-name}{arg-key-1}{arg-value-1}{arg-key-2}{arg-value-2}...<|user|>Hello, please help me.<|assistant|>' +Common Suffix: 'test_function_nameparam1value1param2value2' +Left (difference): '' +Right (difference): 'I need to call the tool first.' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Kimi-K2-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_system|>' +Common Suffix: 'system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Left (difference): '' +Right (difference): 'tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_assistant|>assistant<|im_middle|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|><|im_user|>user<|im_middle|>Thank you.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|> +<|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|><|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Kimi-K2-Thinking.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_system|>' +Common Suffix: 'system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Left (difference): '' +Right (difference): 'tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_assistant|>assistant<|im_middle|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|><|im_user|>user<|im_middle|>Thank you.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|><|im_end|>' +Left (difference): '' +Right (difference): 'I need to call the tool first.' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/MiMo-VL.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi.' +Common Suffix: '<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are MiMo, an AI assistant developed by Xiaomi. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/MiniMax-M2.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant.' +Common Suffix: '[e~[ +]~b]user +Hello, please help me.[e~[ +' +Left (difference): '' +Right (difference): ' + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant.[e~[ +]~b]user +Hello, please help me.[e~[ +' +Common Suffix: '' +Left (difference): '' +Right (difference): ']~b]ai + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant.[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai +' +Common Suffix: 'I can help you with that.[e~[ +' +Left (difference): '' +Right (difference): ' +The user is asking for help. I should respond positively. + + +' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant.[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai +I can help you with that.[e~[ +]~b]user +Thank you.[e~[ +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant. + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai +' +Common Suffix: '[e~[ +' +Left (difference): 'Let me help you.' +Right (difference): ' + + +value1 +value2 + +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant. + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai +' +Common Suffix: '[e~[ +]~b]user +Continue.[e~[ +' +Left (difference): 'Let me help you.' +Right (difference): ' + + +value1 +value2 + +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant. + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai + + + +value1 +value2 + +' +Common Suffix: '[e~[ +' +Left (difference): '' +Right (difference): ' +value3 +value4 + +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant. + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai + + + +value1 +value2 + +' +Common Suffix: '[e~[ +]~b]user +Continue.[e~[ +' +Left (difference): '' +Right (difference): ' +value3 +value4 + +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: ']~!b[]~b]system +You are a helpful assistant. + +# Tools +You may call one or more tools to assist with the user query. +Here are the tools available in JSONSchema format: + + +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +When making tool calls, use XML format to invoke tools and pass parameters: + + + +param-value-1 +param-value-2 +... + +[e~[ +]~b]user +Hello, please help me.[e~[ +]~b]ai +' +Common Suffix: ' + + +value1 +value2 + +[e~[ +' +Left (difference): '' +Right (difference): ' +I need to call the tool first. + + +' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Mistral-Small-3.2-24B-Instruct-2506.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[SYSTEM_PROMPT]You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris. +Your knowledge base was last updated on 2023-10-01. The current date is 2026-01-26. + +When you're not sure about some information or when the user's request requires up-to-date or specific data, you must use the available tools to fetch the information. Do not hesitate to use tools whenever they can provide a more accurate or complete response. If no relevant tools are available, then clearly state that you don't have the information and avoid making up anything. + +If the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. "What are some good restaurants around me?" => "Where are you?" or "When is the next flight to Tokyo" => "Where do you travel from?"). +You are always very attentive to dates, and when asked about information at specific dates, you discard information that is at another date. +You follow these instructions in all languages, and always respond to the user in the language they use or request. +Next sections describe the capabilities that you have. + +# WEB BROWSING INSTRUCTIONS + +You cannot perform any web search or access internet to open URLs, links etc. If it seems like the user is expecting you to do so, you clarify the situation and ask the user to copy paste the text directly in the chat. + +# MULTI-MODAL INSTRUCTIONS + +You have the ability to read images, but you cannot generate images. You also cannot transcribe audio files or videos. +You cannot read nor transcribe audio files or videos. + +# TOOL CALLING INSTRUCTIONS + +You may have access to tools that you can use to fetch information or perform actions. You must use these tools in the following situations: + +1. When the request requires up-to-date information. +2. When the request requires specific data that you do not have in your knowledge base. +3. When the request involves actions that you cannot perform without tools. + +Always prioritize using tools to provide the most accurate and helpful response. If tools are not available, inform the user that you cannot perform the requested action at the moment.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[SYSTEM_PROMPT]You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris. +Your knowledge base was last updated on 2023-10-01. The current date is 2026-01-26. + +When you're not sure about some information or when the user's request requires up-to-date or specific data, you must use the available tools to fetch the information. Do not hesitate to use tools whenever they can provide a more accurate or complete response. If no relevant tools are available, then clearly state that you don't have the information and avoid making up anything. + +If the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. "What are some good restaurants around me?" => "Where are you?" or "When is the next flight to Tokyo" => "Where do you travel from?"). +You are always very attentive to dates, and when asked about information at specific dates, you discard information that is at another date. +You follow these instructions in all languages, and always respond to the user in the language they use or request. +Next sections describe the capabilities that you have. + +# WEB BROWSING INSTRUCTIONS + +You cannot perform any web search or access internet to open URLs, links etc. If it seems like the user is expecting you to do so, you clarify the situation and ask the user to copy paste the text directly in the chat. + +# MULTI-MODAL INSTRUCTIONS + +You have the ability to read images, but you cannot generate images. You also cannot transcribe audio files or videos. +You cannot read nor transcribe audio files or videos. + +# TOOL CALLING INSTRUCTIONS + +You may have access to tools that you can use to fetch information or perform actions. You must use these tools in the following situations: + +1. When the request requires up-to-date information. +2. When the request requires specific data that you do not have in your knowledge base. +3. When the request involves actions that you cannot perform without tools. + +Always prioritize using tools to provide the most accurate and helpful response. If tools are not available, inform the user that you cannot perform the requested action at the moment.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[SYSTEM_PROMPT]You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris. +Your knowledge base was last updated on 2023-10-01. The current date is 2026-01-26. + +When you're not sure about some information or when the user's request requires up-to-date or specific data, you must use the available tools to fetch the information. Do not hesitate to use tools whenever they can provide a more accurate or complete response. If no relevant tools are available, then clearly state that you don't have the information and avoid making up anything. + +If the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. "What are some good restaurants around me?" => "Where are you?" or "When is the next flight to Tokyo" => "Where do you travel from?"). +You are always very attentive to dates, and when asked about information at specific dates, you discard information that is at another date. +You follow these instructions in all languages, and always respond to the user in the language they use or request. +Next sections describe the capabilities that you have. + +# WEB BROWSING INSTRUCTIONS + +You cannot perform any web search or access internet to open URLs, links etc. If it seems like the user is expecting you to do so, you clarify the situation and ask the user to copy paste the text directly in the chat. + +# MULTI-MODAL INSTRUCTIONS + +You have the ability to read images, but you cannot generate images. You also cannot transcribe audio files or videos. +You cannot read nor transcribe audio files or videos. + +# TOOL CALLING INSTRUCTIONS + +You may have access to tools that you can use to fetch information or perform actions. You must use these tools in the following situations: + +1. When the request requires up-to-date information. +2. When the request requires specific data that you do not have in your knowledge base. +3. When the request involves actions that you cannot perform without tools. + +Always prioritize using tools to provide the most accurate and helpful response. If tools are not available, inform the user that you cannot perform the requested action at the moment.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[SYSTEM_PROMPT]You are Mistral Small 3, a Large Language Model (LLM) created by Mistral AI, a French startup headquartered in Paris. +Your knowledge base was last updated on 2023-10-01. The current date is 2026-01-26. + +When you're not sure about some information or when the user's request requires up-to-date or specific data, you must use the available tools to fetch the information. Do not hesitate to use tools whenever they can provide a more accurate or complete response. If no relevant tools are available, then clearly state that you don't have the information and avoid making up anything. + +If the user's question is not clear, ambiguous, or does not provide enough context for you to accurately answer the question, you do not try to answer it right away and you rather ask the user to clarify their request (e.g. "What are some good restaurants around me?" => "Where are you?" or "When is the next flight to Tokyo" => "Where do you travel from?"). +You are always very attentive to dates, and when asked about information at specific dates, you discard information that is at another date. +You follow these instructions in all languages, and always respond to the user in the language they use or request. +Next sections describe the capabilities that you have. + +# WEB BROWSING INSTRUCTIONS + +You cannot perform any web search or access internet to open URLs, links etc. If it seems like the user is expecting you to do so, you clarify the situation and ask the user to copy paste the text directly in the chat. + +# MULTI-MODAL INSTRUCTIONS + +You have the ability to read images, but you cannot generate images. You also cannot transcribe audio files or videos. +You cannot read nor transcribe audio files or videos. + +# TOOL CALLING INSTRUCTIONS + +You may have access to tools that you can use to fetch information or perform actions. You must use these tools in the following situations: + +1. When the request requires up-to-date information. +2. When the request requires specific data that you do not have in your knowledge base. +3. When the request involves actions that you cannot perform without tools. + +Always prioritize using tools to provide the most accurate and helpful response. If tools are not available, inform the user that you cannot perform the requested action at the moment.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.[INST]Thank you.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' +Analysis failed: +------------ +While executing CallExpression at line 91, column 40 in source: +...↵ {{- raise_exception("Tool call IDs should be alphanumeric s... + ^ +Error: Jinja Exception: Tool call IDs should be alphanumeric strings with length 9! + +================================================================================ + ANALYZING TEMPLATE: models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>system +' +Common Suffix: '<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): '# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>system +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: 'I can help you with that.<|im_end|> +' +Left (difference): '' +Right (difference): ' +The user is asking for help. I should respond positively. + +' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>system +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: 'I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Left (difference): '' +Right (difference): ' +' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' + + + +value1 + + +value2 + + + +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' + + + +value1 + + +value2 + + + +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + + + + +value1 + + +value2 + + + +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + + +value3 + + +value4 + + + +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + + + + +value1 + + +value2 + + + +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + + +value3 + + +value4 + + + +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +# Tools + +You have access to the following functions: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a function ONLY reply in the following format with NO suffix: + + + + +value_1 + + +This is the value for the second parameter +that can span +multiple lines + + + + + +Reminder: +- Function calls MUST follow the specified format: an inner block must be nested within XML tags +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: ' + + + +value1 + + +value2 + + + +<|im_end|> +' +Left (difference): '' +Right (difference): ' +I need to call the tool first. +' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/NVIDIA-Nemotron-Nano-v2.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: 'System +' +Common Suffix: ' + +User +Hello, please help me. +' +Left (difference): '' +Right (difference): 'You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user.' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: 'System + + +User +Hello, please help me. +' +Common Suffix: '' +Left (difference): '' +Right (difference): 'Assistant + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: 'System + + +User +Hello, please help me. +Assistant + +I can help you with that. + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: 'System + + +User +Hello, please help me. +Assistant +I can help you with that. + +User +Thank you. +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: 'System +You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user. + +User +Hello, please help me. +Assistant +' +Common Suffix: ' +' +Left (difference): ' +Let me help you. +' +Right (difference): '[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}] +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: 'System +You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user. + +User +Hello, please help me. +Assistant +' +Common Suffix: ' + +User +Continue. +' +Left (difference): 'Let me help you.' +Right (difference): '[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}]' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: 'System +You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user. + +User +Hello, please help me. +Assistant +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}' +Common Suffix: '] + +' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}}' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: 'System +You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user. + +User +Hello, please help me. +Assistant +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}' +Common Suffix: '] + +User +Continue. +' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}}' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: 'System +You can use the following tools to assist the user if required: +[{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}] + +If you decide to call any tool(s), use the following format: +[{{"name": "tool_name1", "arguments": "tool_args1"}}, {{"name": "tool_name2", "arguments": "tool_args2"}}] + +The user will execute tool-calls and return responses from tool(s) in this format: +[{{"tool_response1"}}, {{"tool_response2"}}] + +Based on the tool responses, you can call additional tools if needed, correct tool calls if any errors are found, or just respond to the user. + +User +Hello, please help me. +Assistant +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}] + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: ' +Common Suffix: ' Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): '{"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: ' +Common Suffix: ' Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): '{"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are a function calling AI model. You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about what values to plug into functions. Here are the available tools: {"type": "function", "function": {"name": "test_function_name", "description": "test_function_name(param1: str, param2: str) - A test function for debugging + + Args: + param1(str): First parameter param2(str): Second parameter", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} Use the following pydantic model json schema for each tool call you will make: {"properties": {"name": {"title": "Name", "type": "string"}, "arguments": {"title": "Arguments", "type": "object"}}, "required": ["name", "arguments"], "title": "FunctionCall", "type": "object"}} +For each function call return a json object with function name and arguments within XML tags as follows: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Qwen-QwQ-32B.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>' +Common Suffix: 'user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): 'system + + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system + + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Analysis failed: +------------ +While executing CallExpression at line 31, column 52 in source: +... {%- set content = message.content.split('')[-1].lstrip('\n') %}↵ ... + ^ +Error: Callee is not a function: got Undefined (hint: 'split') + +================================================================================ + ANALYZING TEMPLATE: models/templates/Qwen-Qwen2.5-7B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' +Common Suffix: '<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + +{"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}} +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, created by Alibaba Cloud. You are a helpful assistant. + +# Tools + +You may call one or more functions to assist with the user query. + +You are provided with function signatures within XML tags: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + + +For each function call, return a json object with function name and arguments within XML tags: + +{"name": , "arguments": } +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + +{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}} +<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/Qwen3-Coder.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_start|>' +Common Suffix: 'user +Hello, please help me.<|im_end|> +' +Left (difference): '' +Right (difference): 'system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_start|>assistant +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +I can help you with that.<|im_end|> +<|im_start|>user +Thank you.<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' + + +value1 + + +value2 + + +' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' + + +value1 + + +value2 + + +' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + + + +value1 + + +value2 + + +' +Common Suffix: '<|im_end|> +' +Left (difference): '' +Right (difference): ' + + + +value3 + + +value4 + + +' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_start|>system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + + + +value1 + + +value2 + + +' +Common Suffix: '<|im_end|> +<|im_start|>user +Continue.<|im_end|> +' +Left (difference): '' +Right (difference): ' + + + +value3 + + +value4 + + +' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_start|>system +You are Qwen, a helpful AI assistant that can interact with a computer to solve tasks. + +# Tools + +You have access to the following tools: + + + +test_function_name +A test function for debugging + + +param1 +string +First parameter + + +param2 +string +Second parameter + +["param1", "param2"] + + + + +If you choose to call a tool ONLY reply in the following format with NO suffix: + + + + +value_1 + + +value_2 + + + + + +Reminder: +- Function calls MUST follow the specified format: the tool calling block MUST begin with an opening tag and end with a closing tag. +- Required parameters MUST be specified +- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after +- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls +<|im_end|> +<|im_start|>user +Hello, please help me.<|im_end|> +<|im_start|>assistant + + + +value1 + + +value2 + + +<|im_end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|Assistant|> +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|><|User|>Thank you.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '' +Left (difference): 'Let me help you.<|end▁of▁sentence|>' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8b028d0, "param2":0x6414d8abda40} +```<|tool▁call▁end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '<|User|>Continue.' +Left (difference): 'Let me help you.<|end▁of▁sentence|>' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8abea30, "param2":0x6414d8aba0e0} +```<|tool▁call▁end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8a' +Common Suffix: '' +Left (difference): 'ce970, "param2":0x6414d8abbb70} +```<|tool▁call▁end|>' +Right (difference): 'b5ac0, "param2":0x6414d8aba960} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8b09df0, "param2":0x6414d8b0d3a0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8' +Common Suffix: '<|User|>Continue.' +Left (difference): 'abb400, "param2":0x6414d8aff760} +```<|tool▁call▁end|>' +Right (difference): 'b17be0, "param2":0x6414d8ab7550} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8aba960, "param2":0x6414d8ab5ac0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8a' +Common Suffix: '0} +```<|tool▁call▁end|>' +Left (difference): 'def30, "param2":0x6414d8aba0e' +Right (difference): 'be4e0, "param2":0x6414d8ae266' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|Assistant|> +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|><|User|>Thank you.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '<|end▁of▁sentence|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8ab9ec0, "param2":0x6414d8ac6240} +```<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '<|end▁of▁sentence|><|User|>Continue.' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8aeabd0, "param2":0x6414d8abda40} +```<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8ab' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): 'b620, "param2":0x6414d8abd82' +Right (difference): '9420, "param2":0x6414d8ac14b0} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8abbb70, "param2":0x6414d8abda4' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8a' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|><|User|>Continue.' +Left (difference): 'c4150, "param2":0x6414d8abf2b' +Right (difference): 'bea30, "param2":0x6414d8aba410} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8ab9420, "param2":0x6414d8ac46a' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8ab' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): '5ac0, "param2":0x6414d8adef3' +Right (difference): 'a960, "param2":0x6414d8ac4bf' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/deepseek-ai-DeepSeek-V3.1.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|User|>Hello, please help me.' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|Assistant|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>I can help you with that.<|end▁of▁sentence|><|User|>Thank you.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '<|end▁of▁sentence|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8ae5a10, "param2":0x6414d8abe810}<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|>' +Common Suffix: '<|end▁of▁sentence|><|User|>Continue.' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8ac7cb0, "param2":0x6414d8abe4e0}<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8a' +Common Suffix: '0}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): 'c5f10, "param2":0x6414d8ac59c' +Right (difference): 'be4e0, "param2":0x6414d8ac2e30}<|tool▁call▁end|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8acbf00, "param2":0x6414d8ac7cb' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8' +Common Suffix: '0}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|><|User|>Continue.' +Left (difference): 'b0a810, "param2":0x6414d8adf7b' +Right (difference): 'ac2e30, "param2":0x6414d8ac46a0}<|tool▁call▁end|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8ac3e20, "param2":0x6414d8abe4e' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>test_function_name<|tool▁sep|>{"param1":0x6414d8ac' +Common Suffix: '0}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): '7cb0, "param2":0x6414d8ad63e' +Right (difference): '59c0, "param2":0x6414d8abc0c' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/fireworks-ai-llama-3-firefunction-v2.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Left (difference): 'Let me help you.' +Right (difference): ' functools[{"name": "test_function_name", "arguments": {"param1":0x6414d8ab9970, "param2":0x6414d8ac3380}}]' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Left (difference): 'Let me help you.' +Right (difference): ' functools[{"name": "test_function_name", "arguments": {"param1":0x6414d8b04f50, "param2":0x6414d8ab6cd0}}]' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + functools[{"name": "test_function_name", "arguments": {"param1":0x6414d8a' +Common Suffix: '0}}]<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Left (difference): 'bb950, "param2":0x6414d8abb40' +Right (difference): 'c1290, "param2":0x6414d8abf2b0}}, {"name": "test_function_name", "arguments": {"param1":0x6414d8b04f50, "param2":0x6414d8adef3' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + functools[{"name": "test_function_name", "arguments": {"param1":0x6414d8a' +Common Suffix: '0}}]<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Left (difference): 'c4150, "param2":0x6414d8b103a' +Right (difference): 'bed60, "param2":0x6414d8ae0430}}, {"name": "test_function_name", "arguments": {"param1":0x6414d8adef30, "param2":0x6414d8b04f5' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are a helpful assistant with access to functions. +In addition to plain text responses, you can chose to call one or more of the provided functions. + +Use the following rule to decide when to call a function: + * if the response can be generated from your internal knowledge (e.g., as in the case of queries like "What is the capital of Poland?"), do so + * if you need external information that can be obtained by calling one or more of the provided functions, generate a function calls + +If you decide to call functions: + * prefix function calls with functools marker (no closing marker required) + * all function calls should be generated in a single JSON list formatted as functools[{"name": [function name], "arguments": [function arguments as JSON]}, ...] + * follow the provided JSON schema. Do not hallucinate arguments or values. Do to blindly copy values from the provided samples + * respect the argument type formatting. E.g., if the type if number and format is float, write value 7 as 7.0 + * make sure you pick the right functions that match the user intent + +Available functions as JSON spec: + +Today is .<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + + functools[{"name": "test_function_name", "arguments": {"param1":0x6414d8a' +Common Suffix: '0}}]<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Left (difference): 'c46a0, "param2":0x6414d8b2231' +Right (difference): 'bb950, "param2":0x6414d8abb40' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/google-gemma-2-2b-it.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: false +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: 'user +Hello, please help me. +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: 'user +Hello, please help me. +' +Common Suffix: '' +Left (difference): '' +Right (difference): 'model +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: 'user +Hello, please help me. +model +I can help you with that. +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: 'user +Hello, please help me. +model +I can help you with that. +user +Thank you. +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: 'user +Hello, please help me. +model +' +Common Suffix: ' +' +Left (difference): 'Let me help you.' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: 'user +Hello, please help me. +model +' +Common Suffix: ' +user +Continue. +' +Left (difference): 'Let me help you.' +Right (difference): '' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: 'user +Hello, please help me. +model + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: 'user +Hello, please help me. +model + +user +Continue. +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: 'user +Hello, please help me. +model + +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful ' +Common Suffix: '<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +' +Left (difference): 'AI assistant.' +Right (difference): 'assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful AI assistant.<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_of_role|>assistant<|end_of_role|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful AI assistant.<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|>I can help you with that.<|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful AI assistant.<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|>I can help you with that.<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Thank you.<|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|>' +Common Suffix: '<|end_of_text|> +' +Left (difference): 'Let me help you.' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|>' +Common Suffix: '<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Continue.<|end_of_text|> +' +Left (difference): 'Let me help you.' +Right (difference): '' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|><|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|><|end_of_text|> +<|start_of_role|>user<|end_of_role|>Continue.<|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|start_of_role|>system<|end_of_role|>Knowledge Cutoff Date: April 2024. Today's Date: January 26, 2026. You are Granite, developed by IBM. You are a helpful assistant with access to the following tools. When a tool is required to answer the user's query, respond only with <|tool_call|> followed by a JSON list of tools used. If a tool does not exist in the provided list of tools, notify the user that you do not have the ability to fulfill the request.<|end_of_text|> +<|start_of_role|>available_tools<|end_of_role|>[ + { + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } + } +]<|end_of_text|> +<|start_of_role|>user<|end_of_role|>Hello, please help me.<|end_of_text|> +<|start_of_role|>assistant<|end_of_role|><|end_of_text|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/llama-cpp-deepseek-r1.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '' +Common Suffix: '<|User|>Hello, please help me.<|end▁of▁sentence|>' +Left (difference): '' +Right (difference): 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|User|>Hello, please help me.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|Assistant|> +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|>I can help you with that.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|>I can help you with that.<|end▁of▁sentence|><|User|>Thank you.<|end▁of▁sentence|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|>' +Common Suffix: '<|end▁of▁sentence|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8b06690, "param2":0x6414d8abf070} +```<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|>' +Common Suffix: '<|end▁of▁sentence|><|User|>Continue.<|end▁of▁sentence|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8acb270, "param2":0x6414d8ab6cd0} +```<|tool▁call▁end|><|tool▁calls▁end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): 'b09420, "param2":0x6414d8ac46a' +Right (difference): 'ac1290, "param2":0x6414d8ab6cd0} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8b17be0, "param2":0x6414d8ae5f2' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|><|User|>Continue.<|end▁of▁sentence|>' +Left (difference): 'ac2c10, "param2":0x6414d8aba63' +Right (difference): 'b06690, "param2":0x6414d8ab6cd0} +```<|tool▁call▁end|> +<|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8aba960, "param2":0x6414d8aba0e' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: 'You can call any of the following function tools to satisfy the user's requests: [ + { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +] + +Example function tool call syntax: + +<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>example_function_name +```json +{ + "arg1": "some_value" + ... +} +``` +<|tool▁call▁end|><|tool▁calls▁end|> + +<|User|>Hello, please help me.<|end▁of▁sentence|><|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>test_function_name +```json +{"param1":0x6414d8' +Common Suffix: '0} +```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>' +Left (difference): 'abf070, "param2":0x6414d8aba63' +Right (difference): 'b09420, "param2":0x6414d8b17be' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/meetkai-functionary-medium-v3.1.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + +' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|>' +Left (difference): '' +Right (difference): ' +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_header_id|>assistant<|end_header_id|> + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + + +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '' +Left (difference): 'Let me help you.<|eot_id|>' +Right (difference): '{"param1":0x6414d8ae7330, "param2":0x6414d8aaf400}<|eom_id|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + + +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'Let me help you.<|eot_id|>' +Right (difference): '{"param1":0x6414d8ac1f50, "param2":0x6414d8aba630}<|eom_id|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + + +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +{"param1":0x6414d8ab' +Common Suffix: '0}<|eom_id|>' +Left (difference): '7aa0, "param2":0x6414d8abb62' +Right (difference): '62f0, "param2":0x6414d8aba410}{"param1":0x6414d8ac5360, "param2":0x6414d8b05ef' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + + +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +{"param1":0x6414d8ab' +Common Suffix: '0}<|eom_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): '9420, "param2":0x6414d8ae027' +Right (difference): 'e5f0, "param2":0x6414d8b24fa0}{"param1":0x6414d8ab7000, "param2":0x6414d8ac492' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + + +Cutting Knowledge Date: December 2023 + + +You have access to the following functions: + +Use the function 'test_function_name' to 'A test function for debugging' +{"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}} + + +Think very carefully before calling functions. +If a you choose to call a function ONLY reply in the following format: +<{start_tag}={function_name}>{parameters}{end_tag} +where + +start_tag => ` a JSON dict with the function argument name as key and function argument value as value. +end_tag => `` + +Here is an example, +{"example_name": "example_value"} + +Reminder: +- If looking for real time information use relevant functions before falling back to brave_search +- Function calls MUST follow the specified format, start with +- Required parameters MUST be specified +- Only call one function at a time +- Put the entire function call reply on one line + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +{"param1":0x6414d8ac' +Common Suffix: '0}<|eom_id|>' +Left (difference): 'b270, "param2":0x6414d8abf80' +Right (difference): '1290, "param2":0x6414d8abaeb' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/meetkai-functionary-medium-v3.2.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +' +Common Suffix: '} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|>' +Left (difference): '' +Right (difference): '// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_header_id|>assistant<|end_header_id|> + +>>>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>all +I can help you with that.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>all +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>' +Common Suffix: '<|eot_id|>' +Left (difference): 'all +Let me help you.' +Right (difference): 'test_function_name +{"param1":0x6414d8af9280, "param2":0x6414d8af8a90}' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'all +Let me help you.' +Right (difference): 'test_function_name +{"param1":0x6414d8ae3c80, "param2":0x6414d8b39240}' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>test_function_name +{"param1":0x6414d8afff80, "param2":0x6414d8b3' +Common Suffix: '0}<|eot_id|>' +Left (difference): 'de2' +Right (difference): 'e8b0}>>>test_function_name +{"param1":0x6414d8ad7ff0, "param2":0x6414d8ae3c8' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>test_function_name +{"param1":0x6414d8a' +Common Suffix: '0}<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'e4d40, "param2":0x6414d8abf04' +Right (difference): 'b5ac0, "param2":0x6414d8b3e8b0}>>>test_function_name +{"param1":0x6414d8b210c0, "param2":0x6414d8b0e31' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +You are capable of executing available function(s) if required. +Only execute function(s) when absolutely necessary. +Ask for the required input to:recipient==all +Use JSON for function arguments. +Respond in this format: +>>>${recipient} +${content} +Available functions: +// Supported function definitions that should be called when necessary. +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter. +param1: string, +// Second parameter. +param2: string, +}) => any; + +} // namespace functions<|eot_id|><|start_header_id|>user<|end_header_id|> + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +>>>test_function_name +{"param1":0x6414d8a' +Common Suffix: '0}<|eot_id|>' +Left (difference): 'ba960, "param2":0x6414d8ab9ca' +Right (difference): 'c4bf0, "param2":0x6414d8ae4d4' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +' +Common Suffix: 'Hello, please help me.<|eot_id|>' +Left (difference): '' +Right (difference): '{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_header_id|>assistant<|end_header_id|> + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' +Analysis failed: +------------ +While executing CallExpression at line 71, column 32 in source: +... == 1 %}↵ {{- raise_exception("This model only supports single tool-c... + ^ +Error: Jinja Exception: This model only supports single tool-calls at once! + +================================================================================ + ANALYZING TEMPLATE: models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +' +Common Suffix: 'Hello, please help me.<|eot_id|>' +Left (difference): '' +Right (difference): '{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_header_id|>assistant<|end_header_id|> + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jan 2026 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' +Analysis failed: +------------ +While executing CallExpression at line 72, column 32 in source: +... == 1 %}↵ {{- raise_exception("This model only supports single tool-c... + ^ +Error: Jinja Exception: This model only supports single tool-calls at once! + +================================================================================ + ANALYZING TEMPLATE: models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +' +Common Suffix: 'Hello, please help me.<|eot_id|>' +Left (difference): '' +Right (difference): '{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start_header_id|>assistant<|end_header_id|> + +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +I can help you with that.<|eot_id|><|start_header_id|>user<|end_header_id|> + +Thank you.<|eot_id|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|start_header_id|>system<|end_header_id|> + +Environment: ipython +Cutting Knowledge Date: December 2023 +Today Date: 26 Jul 2024 + +<|eot_id|><|start_header_id|>user<|end_header_id|> + +Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt. + +Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables. + +{ + "type": "function", + "function": { + "name": "test_function_name", + "description": "A test function for debugging", + "parameters": { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "First parameter" + }, + "param2": { + "type": "string", + "description": "Second parameter" + } + }, + "required": [ + "param1", + "param2" + ] + } + } +} + +Hello, please help me.<|eot_id|><|start_header_id|>assistant<|end_header_id|> + +' +Common Suffix: '<|eot_id|><|start_header_id|>user<|end_header_id|> + +Continue.<|eot_id|>' +Left (difference): 'Let me help you.' +Right (difference): '{"name": "test_function_name", "parameters": {"param1": "value1", "param2": "value2"}}' +Analysis failed: +------------ +While executing CallExpression at line 71, column 32 in source: +... == 1 %}↵ {{- raise_exception("This model only supports single tool-c... + ^ +Error: Jinja Exception: This model only supports single tool-calls at once! + +================================================================================ + ANALYZING TEMPLATE: models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[SYSTEM_PROMPT]# HOW YOU SHOULD THINK AND ANSWER + +First draft your thinking process (inner monologue) until you arrive at a response. Format your response using Markdown, and use LaTeX for any mathematical equations. Write both your thoughts and the response in the same language as the input. + +Your thinking process must follow the template below:[THINK]Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate the response to the user.[/THINK]Here, provide a self-contained response.[/SYSTEM_PROMPT]' +Common Suffix: '[INST]Hello, please help me.[/INST]' +Left (difference): '' +Right (difference): '[AVAILABLE_TOOLS][{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}][/AVAILABLE_TOOLS]' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[SYSTEM_PROMPT]# HOW YOU SHOULD THINK AND ANSWER + +First draft your thinking process (inner monologue) until you arrive at a response. Format your response using Markdown, and use LaTeX for any mathematical equations. Write both your thoughts and the response in the same language as the input. + +Your thinking process must follow the template below:[THINK]Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate the response to the user.[/THINK]Here, provide a self-contained response.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[SYSTEM_PROMPT]# HOW YOU SHOULD THINK AND ANSWER + +First draft your thinking process (inner monologue) until you arrive at a response. Format your response using Markdown, and use LaTeX for any mathematical equations. Write both your thoughts and the response in the same language as the input. + +Your thinking process must follow the template below:[THINK]Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate the response to the user.[/THINK]Here, provide a self-contained response.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[SYSTEM_PROMPT]# HOW YOU SHOULD THINK AND ANSWER + +First draft your thinking process (inner monologue) until you arrive at a response. Format your response using Markdown, and use LaTeX for any mathematical equations. Write both your thoughts and the response in the same language as the input. + +Your thinking process must follow the template below:[THINK]Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate the response to the user.[/THINK]Here, provide a self-contained response.[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.[INST]Thank you.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' +Analysis failed: +------------ +While executing FilterExpression at line 90, column 37 in source: +... }}↵ {%- elif message['content'] | length > 0 %}↵ {%- for bloc... + ^ +Error: Unknown (built-in) filter 'length' for type None + +================================================================================ + ANALYZING TEMPLATE: models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: false +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[INST]Hello, please help me.[/INST]I can help you with that.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[INST]Hello, please help me.[/INST]I can help you with that.[INST]Thank you.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' +Analysis failed: +------------ +While executing CallExpression at line 62, column 36 in source: +...9 %}↵ {{- raise_exception("Tool call IDs should be alphanumeric s... + ^ +Error: Jinja Exception: Tool call IDs should be alphanumeric strings with length 9! + +================================================================================ + ANALYZING TEMPLATE: models/templates/moonshotai-Kimi-K2.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|im_system|>' +Common Suffix: 'system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Left (difference): '' +Right (difference): 'tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|im_assistant|>assistant<|im_middle|>' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>I can help you with that.<|im_end|><|im_user|>user<|im_middle|>Thank you.<|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|>' +Common Suffix: '<|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): 'Let me help you.' +Right (difference): '<|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|>' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|>' +Common Suffix: '<|tool_calls_section_end|><|im_end|><|im_user|>user<|im_middle|>Continue.<|im_end|>' +Left (difference): '' +Right (difference): '<|tool_call_begin|>functions.test_function_name:1<|tool_call_argument_begin|>{"param1": "value3", "param2": "value4"}<|tool_call_end|>' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|im_system|>tool_declare<|im_middle|>[{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are a helpful assistant<|im_end|><|im_user|>user<|im_middle|>Hello, please help me.<|im_end|><|im_assistant|>assistant<|im_middle|><|tool_calls_section_begin|><|tool_call_begin|>functions.test_function_name:0<|tool_call_argument_begin|>{"param1": "value1", "param2": "value2"}<|tool_call_end|><|tool_calls_section_end|><|im_end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/openai-gpt-oss-120b.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: true +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: false +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2024-06 +Current date: 2026-01-26 + +Reasoning: medium + +# Valid channels: analysis, commentary, final. Channel must be included for every message.' +Common Suffix: '<|end|><|start|>user<|message|>Hello, please help me.<|end|>' +Left (difference): '' +Right (difference): ' +Calls to these tools must go to the commentary channel: 'functions'.<|end|><|start|>developer<|message|># Tools + +## functions + +namespace functions { + +// A test function for debugging +type test_function_name = (_: { +// First parameter +param1: string, +// Second parameter +param2: string, +}) => any; + +} // namespace functions' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2024-06 +Current date: 2026-01-26 + +Reasoning: medium + +# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>Hello, please help me.<|end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|start|>assistant' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2024-06 +Current date: 2026-01-26 + +Reasoning: medium + +# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>Hello, please help me.<|end|><|start|>assistant<|channel|>final<|message|>I can help you with that.<|return|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|start|>system<|message|>You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2024-06 +Current date: 2026-01-26 + +Reasoning: medium + +# Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>user<|message|>Hello, please help me.<|end|><|start|>assistant<|channel|>final<|message|>I can help you with that.<|end|><|start|>user<|message|>Thank you.<|end|>' +Common Suffix: '' +Left (difference): '' +Right (difference): '' +Analysis failed: +------------ +While executing BinaryExpression at line 264, column 53 in source: +...{%- if "<|channel|>analysis<|message|>" in message.content or "<|channel|>final<... + ^ +Error: Cannot perform operation on null values + +================================================================================ + ANALYZING TEMPLATE: models/templates/unsloth-Apriel-1.5.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].' +Common Suffix: ' +<|end|> +<|user|> +Hello, please help me. +<|end|> +' +Left (difference): '' +Right (difference): 'You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '<|assistant|> +' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> +I can help you with that. +<|end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> +I can help you with that. +<|end|> +<|user|> +Thank you. +<|end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without tool call (user, assistant) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> +' +Common Suffix: ' +<|end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}]' + +=== Diff: With vs Without tool call (user, assistant, user) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> +' +Common Suffix: ' +<|end|> +<|user|> +Continue. +<|end|> +' +Left (difference): 'Let me help you.' +Right (difference): ' +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}, "id": "call_001"}]' + +=== Diff: One vs Two tool calls (user, assistant) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> + +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}' +Common Suffix: '] +<|end|> +' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}}' + +=== Diff: One vs Two tool calls (user, assistant, user) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> + +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}, "id": "call_001"}' +Common Suffix: '] +<|end|> +<|user|> +Continue. +<|end|> +' +Left (difference): '' +Right (difference): ', {"name": "test_function_name", "arguments": {"param1": "value3", "param2": "value4"}, "id": "call_002"}' + +=== Diff: Tool call with vs without reasoning_content (user, assistant) === +Common Prefix: '<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE].You are provided with function signatures within XML tags. You may call one or more functions to assist with the user query. Don't make assumptions about the arguments. You should infer the argument values from previous user responses and the system message. Here are the available tools: + +{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}} + +Return all function calls as a list of json objects within XML tags. Each json object should contain a function name and arguments as follows: +[{"name": , "arguments": }, {"name": , "arguments": },...] +<|end|> +<|system|> +You are a thoughtful and systematic AI assistant built by ServiceNow Language Models (SLAM) lab. Before providing an answer, analyze the problem carefully and present your reasoning step by step. After explaining your thought process, provide the final solution in the following format: [BEGIN FINAL RESPONSE] ... [END FINAL RESPONSE]. +<|end|> +<|user|> +Hello, please help me. +<|end|> +<|assistant|> + +[{"name": "test_function_name", "arguments": {"param1": "value1", "param2": "value2"}}] +<|end|> +' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Checking Reasoning Variables === +No reasoning/thinking-related variables were queried by the template + +================================================================================ + ANALYZING TEMPLATE: models/templates/unsloth-mistral-Devstral-Small-2507.jinja +================================================================================ + +=== Template Capabilities (from jinja::caps) === +supports_tools: false +supports_tool_calls: true +supports_system_role: true +supports_parallel_tool_calls: true +requires_typed_content: false + +=== Diff: With vs Without Tools (single user message) === +Common Prefix: '[SYSTEM_PROMPT]You are Devstral, a helpful agentic model trained by Mistral AI and using the OpenHands scaffold. You can interact with a computer to solve tasks. + + +Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed. +* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question. + + + +* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once. +* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations. + + + +* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it. +* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename. +* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times. + + + +* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself. +* When implementing solutions, focus on making the minimal changes needed to solve the problem. +* Before implementing any changes, first thoroughly understand the codebase through exploration. +* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate. + + + +* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so. +* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. +* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. +* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. + + + +* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise. +* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue. +* When updating a PR, preserve the original PR title and purpose, updating description only when necessary. + + + +1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions +2. ANALYSIS: Consider multiple approaches and select the most promising one +3. TESTING: + * For bug fixes: Create tests to verify issues before implementing fixes + * For new features: Consider test-driven development when appropriate + * If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure + * If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies +4. IMPLEMENTATION: Make focused, minimal changes to address the problem +5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests. + + + +* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect. +* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing. + + + +* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again. +* If you encounter missing dependencies: + 1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.) + 2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.) + 3. Only install individual packages directly if no dependency files are found or if only specific packages are needed +* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible. + + + +* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken: + 1. Step back and reflect on 5-7 different possible sources of the problem + 2. Assess the likelihood of each possible cause + 3. Methodically address the most likely causes, starting with the highest probability + 4. Document your reasoning process +* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding. +[/SYSTEM_PROMPT]' +Common Suffix: '[INST]Hello, please help me.[/INST]' +Left (difference): '' +Right (difference): '[AVAILABLE_TOOLS][{"type": "function", "function": {"name": "test_function_name", "description": "A test function for debugging", "parameters": {"type": "object", "properties": {"param1": {"type": "string", "description": "First parameter"}, "param2": {"type": "string", "description": "Second parameter"}}, "required": ["param1", "param2"]}}}][/AVAILABLE_TOOLS]' + +=== Diff: With vs Without add_generation_prompt (single user message) === +Common Prefix: '[SYSTEM_PROMPT]You are Devstral, a helpful agentic model trained by Mistral AI and using the OpenHands scaffold. You can interact with a computer to solve tasks. + + +Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed. +* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question. + + + +* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once. +* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations. + + + +* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it. +* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename. +* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times. + + + +* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself. +* When implementing solutions, focus on making the minimal changes needed to solve the problem. +* Before implementing any changes, first thoroughly understand the codebase through exploration. +* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate. + + + +* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so. +* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. +* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. +* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. + + + +* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise. +* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue. +* When updating a PR, preserve the original PR title and purpose, updating description only when necessary. + + + +1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions +2. ANALYSIS: Consider multiple approaches and select the most promising one +3. TESTING: + * For bug fixes: Create tests to verify issues before implementing fixes + * For new features: Consider test-driven development when appropriate + * If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure + * If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies +4. IMPLEMENTATION: Make focused, minimal changes to address the problem +5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests. + + + +* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect. +* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing. + + + +* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again. +* If you encounter missing dependencies: + 1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.) + 2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.) + 3. Only install individual packages directly if no dependency files are found or if only specific packages are needed +* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible. + + + +* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken: + 1. Step back and reflect on 5-7 different possible sources of the problem + 2. Assess the likelihood of each possible cause + 3. Methodically address the most likely causes, starting with the highest probability + 4. Document your reasoning process +* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding. +[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant) === +Common Prefix: '[SYSTEM_PROMPT]You are Devstral, a helpful agentic model trained by Mistral AI and using the OpenHands scaffold. You can interact with a computer to solve tasks. + + +Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed. +* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question. + + + +* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once. +* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations. + + + +* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it. +* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename. +* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times. + + + +* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself. +* When implementing solutions, focus on making the minimal changes needed to solve the problem. +* Before implementing any changes, first thoroughly understand the codebase through exploration. +* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate. + + + +* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so. +* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. +* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. +* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. + + + +* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise. +* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue. +* When updating a PR, preserve the original PR title and purpose, updating description only when necessary. + + + +1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions +2. ANALYSIS: Consider multiple approaches and select the most promising one +3. TESTING: + * For bug fixes: Create tests to verify issues before implementing fixes + * For new features: Consider test-driven development when appropriate + * If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure + * If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies +4. IMPLEMENTATION: Make focused, minimal changes to address the problem +5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests. + + + +* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect. +* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing. + + + +* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again. +* If you encounter missing dependencies: + 1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.) + 2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.) + 3. Only install individual packages directly if no dependency files are found or if only specific packages are needed +* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible. + + + +* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken: + 1. Step back and reflect on 5-7 different possible sources of the problem + 2. Assess the likelihood of each possible cause + 3. Methodically address the most likely causes, starting with the highest probability + 4. Document your reasoning process +* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding. +[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.' +Common Suffix: '' +Left (difference): '' +Right (difference): '' + +=== Diff: With vs Without reasoning_content (user, assistant, user) === +Common Prefix: '[SYSTEM_PROMPT]You are Devstral, a helpful agentic model trained by Mistral AI and using the OpenHands scaffold. You can interact with a computer to solve tasks. + + +Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed. +* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question. + + + +* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once. +* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations. + + + +* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it. +* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename. +* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times. + + + +* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself. +* When implementing solutions, focus on making the minimal changes needed to solve the problem. +* Before implementing any changes, first thoroughly understand the codebase through exploration. +* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate. + + + +* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so. +* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. +* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. +* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. + + + +* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise. +* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue. +* When updating a PR, preserve the original PR title and purpose, updating description only when necessary. + + + +1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions +2. ANALYSIS: Consider multiple approaches and select the most promising one +3. TESTING: + * For bug fixes: Create tests to verify issues before implementing fixes + * For new features: Consider test-driven development when appropriate + * If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure + * If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies +4. IMPLEMENTATION: Make focused, minimal changes to address the problem +5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests. + + + +* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect. +* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing. + + + +* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again. +* If you encounter missing dependencies: + 1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.) + 2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.) + 3. Only install individual packages directly if no dependency files are found or if only specific packages are needed +* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible. + + + +* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken: + 1. Step back and reflect on 5-7 different possible sources of the problem + 2. Assess the likelihood of each possible cause + 3. Methodically address the most likely causes, starting with the highest probability + 4. Document your reasoning process +* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding. +[/SYSTEM_PROMPT][INST]Hello, please help me.[/INST]I can help you with that.[INST]Thank you.[/INST]' +Common Suffix: '' +Left (difference): '' +Right (difference): '' +Analysis failed: +------------ +While executing MemberExpression at line 74, column 24 in source: +... {%- else %}↵ {{- message['content'][0]['text'] }}↵ {%- en... + ^ +Error: Cannot access property with non-string: got Integer + +================================================================================ + ANALYSIS COMPLETE +================================================================================ + \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5607055990..b45ed04e6e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -186,6 +186,7 @@ endif() llama_build_and_test(test-chat-peg-parser.cpp peg-parser/simple-tokenize.cpp) llama_build_and_test(test-jinja.cpp) llama_test(test-jinja NAME test-jinja-py ARGS -py LABEL python) +llama_build_and_test(test-chat-auto-parser.cpp) llama_build_and_test(test-json-partial.cpp) llama_build_and_test(test-log.cpp) llama_build_and_test( @@ -195,6 +196,7 @@ llama_build_and_test( peg-parser/test-gbnf-generation.cpp peg-parser/test-json-parser.cpp peg-parser/test-json-serialization.cpp + peg-parser/test-python-dict-parser.cpp peg-parser/test-unicode.cpp peg-parser/tests.h ) diff --git a/tests/peg-parser/test-python-dict-parser.cpp b/tests/peg-parser/test-python-dict-parser.cpp new file mode 100644 index 0000000000..9db1154b45 --- /dev/null +++ b/tests/peg-parser/test-python-dict-parser.cpp @@ -0,0 +1,279 @@ +#include "tests.h" + +void test_python_dict_parser(testing &t) { + // Test parsing a simple Python dict object with single quotes + t.test("simple Python dict object parsing", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'name': 'test', 'value': 42, 'flag': true}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test parsing a Python dict array with mixed types + t.test("Python dict array with mixed types", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "[1, 'hello', true, null, 3.14]"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test parsing nested Python dict with objects and arrays + t.test("nested Python dict with objects and arrays", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = + "{'users': [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}], 'count': 2, 'metadata': {'version': '1.0', 'tags': ['admin', 'user']}}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test parsing Python dict with escaped single quotes + t.test("Python dict with escaped single quotes", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'message': 'It\\'s working!'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test parsing Python dict with double quotes inside single quotes + t.test("Python dict with double quotes inside single quotes", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'quote': 'He said \"Hello\"'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test the example from the requirements + t.test("complex Python dict example from requirements", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{ 'obj' : { 'something': 1, 'other \"something\"' : 'foo\\'s bar' } }"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test need_more_input() parsing - incomplete object + t.test("need_more_input() parsing - incomplete object", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'name': 'test', 'value': "; + common_peg_parse_context ctx(input, true); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_need_more_input", true, result.need_more_input()); + }); + + // Test need_more_input() parsing - incomplete single-quoted string + t.test("need_more_input() parsing - incomplete single-quoted string", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'name': 'test"; + common_peg_parse_context ctx(input, true); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_need_more_input", true, result.need_more_input()); + }); + + // Test unicode in Python dict strings + t.test("unicode in Python dict strings", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'message': 'Hello, 世界!'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test Python dict with unicode escapes + t.test("Python dict with unicode escapes", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{'unicode': 'Hello\\u0041'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_success", true, result.success()); + t.assert_equal("result_end", input.size(), result.end); + }); + + // Test that JSON double-quoted strings fail with Python dict parser + t.test("JSON double-quoted strings fail with Python dict parser", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { return p.python_dict(); }); + + std::string input = "{\"name\": \"test\"}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + + t.assert_equal("result_is_fail", true, result.fail()); + }); + + // Test Python dict string content parser directly + t.test("python dict string content parser", [](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + return p.sequence({ p.literal("'"), p.python_dict_string_content(), p.literal("'"), p.space() }); + }); + + t.test("simple string", [&](testing &t) { + std::string input = "'hello'"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("string with escaped single quote", [&](testing &t) { + std::string input = "'it\\'s'"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("string with double quotes", [&](testing &t) { + std::string input = "'say \"hello\"'"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("incomplete string", [&](testing &t) { + std::string input = "'hello"; + common_peg_parse_context ctx(input, true); + + auto result = parser.parse(ctx); + t.assert_true("need_more_input", result.need_more_input()); + }); + }); + + // Test allow_python_dict_format flag usage + t.test("allow_python_dict_format flag", [](testing &t) { + t.test("flag is false by default", [&](testing &t) { + common_peg_parser_builder builder; + t.assert_equal("default_value", false, builder.get_allow_python_dict_format()); + }); + + t.test("flag can be set to true", [&](testing &t) { + common_peg_parser_builder builder; + builder.set_allow_python_dict_format(true); + t.assert_equal("after_set", true, builder.get_allow_python_dict_format()); + }); + + t.test("flag can be set back to false", [&](testing &t) { + common_peg_parser_builder builder; + builder.set_allow_python_dict_format(true); + builder.set_allow_python_dict_format(false); + t.assert_equal("after_reset", false, builder.get_allow_python_dict_format()); + }); + }); + + // Test that the flag actually affects json() parser behavior + t.test("json() parser with allow_python_dict_format flag", [](testing &t) { + t.test("json() rejects single quotes when flag is false", [&](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + p.set_allow_python_dict_format(false); + return p.json(); + }); + + std::string input = "{'name': 'test'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("fail", result.fail()); + }); + + t.test("json() accepts single quotes when flag is true", [&](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + p.set_allow_python_dict_format(true); + return p.json(); + }); + + std::string input = "{'name': 'test'}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("json() still accepts double quotes when flag is true", [&](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + p.set_allow_python_dict_format(true); + return p.json(); + }); + + std::string input = "{\"name\": \"test\"}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("json() accepts mixed quote styles when flag is true", [&](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + p.set_allow_python_dict_format(true); + return p.json(); + }); + + std::string input = "{\"name\": 'test', 'value': \"hello\"}"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + + t.test("complex nested structure with flag true", [&](testing &t) { + auto parser = build_peg_parser([](common_peg_parser_builder & p) { + p.set_allow_python_dict_format(true); + return p.json(); + }); + + std::string input = "{ 'obj' : { 'something': 1, 'other \"something\"' : 'foo\\'s bar' } }"; + common_peg_parse_context ctx(input); + + auto result = parser.parse(ctx); + t.assert_true("success", result.success()); + t.assert_equal("end", input.size(), result.end); + }); + }); +} diff --git a/tests/peg-parser/tests.h b/tests/peg-parser/tests.h index 4d3f4e9eaf..debd4286c5 100644 --- a/tests/peg-parser/tests.h +++ b/tests/peg-parser/tests.h @@ -22,3 +22,4 @@ void test_json_parser(testing &t); void test_gbnf_generation(testing &t); void test_unicode(testing &t); void test_json_serialization(testing &t); +void test_python_dict_parser(testing &t); diff --git a/tests/test-chat-auto-parser.cpp b/tests/test-chat-auto-parser.cpp new file mode 100644 index 0000000000..015c90d408 --- /dev/null +++ b/tests/test-chat-auto-parser.cpp @@ -0,0 +1,1845 @@ +#include "chat-auto-parser-helpers.h" +#include "chat-diff-analyzer.h" +#include "chat-peg-parser.h" +#include "chat.h" +#include "peg-parser.h" +#include "testing.h" + +#include +#include +#include +#include + +static void test_calculate_diff_split_basic(testing & t); +static void test_calculate_diff_split_identical(testing & t); +static void test_calculate_diff_split_common_prefix(testing & t); +static void test_calculate_diff_split_common_suffix(testing & t); +static void test_calculate_diff_split_common_both(testing & t); +static void test_calculate_diff_split_empty_cases(testing & t); +static void test_calculate_diff_split_no_common(testing & t); +static void test_calculate_diff_split_single_char(testing & t); +static void test_calculate_diff_split_overlaps(testing & t); +static void test_calculate_diff_split_tag_boundaries(testing & t); +static void test_calculate_diff_split(testing & t); + +static void test_until_common_prefix_basic(testing & t); +static void test_until_common_prefix(testing & t); + +static void test_after_common_suffix_basic(testing & t); +static void test_after_common_suffix(testing & t); + +static void test_analyze_tool_call_pure_json(testing & t); +static void test_analyze_tool_call_function_name_markers(testing & t); +static void test_analyze_tool_call_full_markers(testing & t); +static void test_analyze_tool_call_edge_cases(testing & t); + +static void test_compare_variants_basic(testing & t); +static void test_compare_variants_messages_modifier(testing & t); +static void test_compare_variants_tools_modifier(testing & t); +static void test_compare_variants_both_modifiers(testing & t); +static void test_compare_variants_template_failure(testing & t); +static void test_compare_variants_identity(testing & t); +static void test_compare_variants(testing & t); + +// Seed-OSS template tool calling analysis tests +static void test_seed_oss_tool_analysis(testing & t); +static void test_seed_oss_tool_presence(testing & t); +static void test_seed_oss_call_count(testing & t); +static void test_seed_oss_function_names(testing & t); +static void test_seed_oss_argument_count(testing & t); +static void test_seed_oss_args_presence(testing & t); +static void test_seed_oss_tool_with_reasoning(testing & t); + +// Nemotron template analysis tests +static void test_nemotron_analysis(testing & t); +static void test_nemotron_reasoning_detection(testing & t); +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 +static void test_marker_separation(testing & t); + +// standard_json_tools format tests +static void test_standard_json_tools_formats(testing & t); +static void test_standard_json_tools_openai(testing & t); +static void test_standard_json_tools_cohere(testing & t); +static void test_standard_json_tools_function_key(testing & t); + +// normalize_quotes_to_json tests +static void test_normalize_quotes_to_json(testing & t); +static void test_normalize_quotes_with_embedded_quotes(testing & t); + +// TAG_WITH_TAGGED argument parsing tests +static void test_tagged_args_with_embedded_quotes(testing & t); + +int main(int argc, char * argv[]) { + testing t(std::cout); + t.verbose = true; + + // usage: test-chat-auto-parser-helpers [filter_regex] + + if (argc > 1) { + t.set_filter(argv[1]); + } + + t.test("diff_split", test_calculate_diff_split); + t.test("common_prefix", test_until_common_prefix); + t.test("common_suffix", test_after_common_suffix); + t.test("compare_variants", test_compare_variants); + t.test("segments", test_marker_separation); + t.test("seed_oss_diffs", test_seed_oss_tool_analysis); + t.test("cohere", test_cohere_analysis); + t.test("nemotron", test_nemotron_analysis); + t.test("standard_json_tools", test_standard_json_tools_formats); + t.test("normalize_quotes_to_json", test_normalize_quotes_to_json); + t.test("tagged_args_embedded_quotes", test_tagged_args_with_embedded_quotes); + + return t.summary(); +} + +static void test_marker_separation(testing & t) { + auto single_square_marker = segmentize_markers("pre_marker[marker]post_marker"); + auto single_diag_marker = segmentize_markers("pre_markerpost_marker"); + auto paired_markers = segmentize_markers("world"); + auto double_different_markers = segmentize_markers("[hello][world]"); + auto in_between = segmentize_markers("imdabada[hey]"); + + t.test("single_square_marker", [&] (testing & t) { + t.assert_equal("first is text", segment_type::TEXT, single_square_marker[0].type); + t.assert_equal("second is marker", segment_type::MARKER, single_square_marker[1].type); + t.assert_equal("last is text", segment_type::TEXT, single_square_marker[2].type); + + t.assert_equal("first is 'pre_marker'", "pre_marker", single_square_marker[0].value); + t.assert_equal("second is '[marker]'", "[marker]", single_square_marker[1].value); + t.assert_equal("last is 'post_marker'", "post_marker", single_square_marker[2].value); + }); + + t.test("single_diagonal_marker", [&] (testing & t) { + t.assert_equal("first is text", segment_type::TEXT, single_diag_marker[0].type); + t.assert_equal("second is marker", segment_type::MARKER, single_diag_marker[1].type); + t.assert_equal("last is text", segment_type::TEXT, single_diag_marker[2].type); + + t.assert_equal("first is 'pre_marker'", "pre_marker", single_diag_marker[0].value); + t.assert_equal("second is ''", "", single_diag_marker[1].value); + t.assert_equal("last is 'post_marker'", "post_marker", single_diag_marker[2].value); + }); + + t.test("paired_markers", [&] (testing & t) { + t.assert_equal("first is marker", segment_type::MARKER, paired_markers[0].type); + t.assert_equal("second is text", segment_type::TEXT, paired_markers[1].type); + t.assert_equal("third is marker", segment_type::MARKER, paired_markers[2].type); + + t.assert_equal("first is ''", "", paired_markers[0].value); + t.assert_equal("second is 'world'", "world", paired_markers[1].value); + t.assert_equal("third is ''", "", paired_markers[2].value); + }); + + t.test("double_different_markers", [&] (testing & t) { + t.assert_equal("first is marker", segment_type::MARKER, double_different_markers[0].type); + t.assert_equal("second is marker", segment_type::MARKER, double_different_markers[1].type); + t.assert_equal("third is marker", segment_type::MARKER, double_different_markers[2].type); + t.assert_equal("fourth is marker", segment_type::MARKER, double_different_markers[3].type); + + t.assert_equal("first is ''", "", double_different_markers[0].value); + t.assert_equal("second is '[hello]'", "[hello]", double_different_markers[1].value); + t.assert_equal("third is ''", "", double_different_markers[2].value); + t.assert_equal("fourth is '[world]'", "[world]", double_different_markers[3].value); + }); + + t.test("in_between", [&] (testing & t) { + t.assert_equal("first is text", segment_type::TEXT, in_between[0].type); + t.assert_equal("second is marker", segment_type::MARKER, in_between[1].type); + t.assert_equal("third is text", segment_type::TEXT, in_between[2].type); + t.assert_equal("fourth is marker", segment_type::MARKER, in_between[3].type); + t.assert_equal("fifth is text", segment_type::TEXT, in_between[4].type); + t.assert_equal("sixth is marker", segment_type::MARKER, in_between[5].type); + + t.assert_equal("first is 'im'", "im", in_between[0].value); + t.assert_equal("second is ''", "", in_between[1].value); + t.assert_equal("third is 'daba'", "daba", in_between[2].value); + t.assert_equal("fourth is ''", "", in_between[3].value); + t.assert_equal("fifth is 'da'", "da", in_between[4].value); + t.assert_equal("sixth is '[hey]'", "[hey]", in_between[5].value); + }); +} + +static void test_calculate_diff_split(testing & t) { + t.test("calculate_diff_split basic", test_calculate_diff_split_basic); + t.test("calculate_diff_split identical", test_calculate_diff_split_identical); + t.test("calculate_diff_split common prefix", test_calculate_diff_split_common_prefix); + t.test("calculate_diff_split common suffix", test_calculate_diff_split_common_suffix); + t.test("calculate_diff_split common both", test_calculate_diff_split_common_both); + t.test("calculate_diff_split empty cases", test_calculate_diff_split_empty_cases); + t.test("calculate_diff_split no common", test_calculate_diff_split_no_common); + t.test("calculate_diff_split single char", test_calculate_diff_split_single_char); + t.test("calculate_diff_split overlaps", test_calculate_diff_split_overlaps); + t.test("calculate_diff_split tag boundaries", test_calculate_diff_split_tag_boundaries); +} + +static void test_calculate_diff_split_basic(testing & t) { + diff_split result = calculate_diff_split("hello world", "hello test"); + t.assert_equal("prefix should be 'hello '", "hello ", result.prefix); + t.assert_equal("left should be 'world'", "world", result.left); + t.assert_equal("right should be 'test'", "test", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("abc", "xyz"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'abc'", "abc", result.left); + t.assert_equal("right should be 'xyz'", "xyz", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("prefixA suffix", "prefixB suffix"); + t.assert_equal("prefix should be 'prefix'", "prefix", result.prefix); + t.assert_equal("left should be 'A'", "A", result.left); + t.assert_equal("right should be 'B'", "B", result.right); + t.assert_equal("suffix should be ' suffix'", " suffix", result.suffix); +} + +static void test_calculate_diff_split_identical(testing & t) { + diff_split result = calculate_diff_split("hello", "hello"); + t.assert_equal("prefix should be 'hello'", "hello", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("", ""); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("a", "a"); + t.assert_equal("prefix should be 'a'", "a", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_common_prefix(testing & t) { + diff_split result = calculate_diff_split("abcdef", "abcxyz"); + t.assert_equal("prefix should be 'abc'", "abc", result.prefix); + t.assert_equal("left should be 'def'", "def", result.left); + t.assert_equal("right should be 'xyz'", "xyz", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("same", "sameagain"); + t.assert_equal("prefix should be 'same'", "same", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'again'", "again", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("test", "testing"); + t.assert_equal("prefix should be 'test'", "test", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'ing'", "ing", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_common_suffix(testing & t) { + diff_split result = calculate_diff_split("123end", "456end"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be '123'", "123", result.left); + t.assert_equal("right should be '456'", "456", result.right); + t.assert_equal("suffix should be 'end'", "end", result.suffix); + + result = calculate_diff_split("start", "end"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'start'", "start", result.left); + t.assert_equal("right should be 'end'", "end", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("abcsuffix", "xyzsuffix"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'abc'", "abc", result.left); + t.assert_equal("right should be 'xyz'", "xyz", result.right); + t.assert_equal("suffix should be 'suffix'", "suffix", result.suffix); +} + +static void test_calculate_diff_split_common_both(testing & t) { + diff_split result = calculate_diff_split("helloXworld", "helloYworld"); + t.assert_equal("prefix should be 'hello'", "hello", result.prefix); + t.assert_equal("left should be 'X'", "X", result.left); + t.assert_equal("right should be 'Y'", "Y", result.right); + t.assert_equal("suffix should be 'world'", "world", result.suffix); + + result = calculate_diff_split("ABCmiddleXYZ", "ABCdifferentXYZ"); + t.assert_equal("prefix should be 'ABC'", "ABC", result.prefix); + t.assert_equal("left should be 'middle'", "middle", result.left); + t.assert_equal("right should be 'different'", "different", result.right); + t.assert_equal("suffix should be 'XYZ'", "XYZ", result.suffix); + + result = calculate_diff_split("startAend", "startBend"); + t.assert_equal("prefix should be 'start'", "start", result.prefix); + t.assert_equal("left should be 'A'", "A", result.left); + t.assert_equal("right should be 'B'", "B", result.right); + t.assert_equal("suffix should be 'end'", "end", result.suffix); + + // Edge case: common prefix and suffix overlap + result = calculate_diff_split("aa", "ab"); + t.assert_equal("prefix should be 'a'", "a", result.prefix); + t.assert_equal("left should be 'a'", "a", result.left); + t.assert_equal("right should be 'b'", "b", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_empty_cases(testing & t) { + // Empty left, non-empty right + diff_split result = calculate_diff_split("", "hello"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'hello'", "hello", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Non-empty left, empty right + result = calculate_diff_split("hello", ""); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'hello'", "hello", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Both empty + result = calculate_diff_split("", ""); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Left single char, empty right + result = calculate_diff_split("a", ""); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'a'", "a", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Empty left, right single char + result = calculate_diff_split("", "a"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'a'", "a", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_no_common(testing & t) { + diff_split result = calculate_diff_split("abc", "xyz"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'abc'", "abc", result.left); + t.assert_equal("right should be 'xyz'", "xyz", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("left", "right"); + // The algorithm finds "t" as a common suffix since both strings end with 't' + // This is the algorithm's actual behavior - it finds maximal common suffix + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'lef'", "lef", result.left); + t.assert_equal("right should be 'righ'", "righ", result.right); + t.assert_equal("suffix should be 't'", "t", result.suffix); + + result = calculate_diff_split("123", "456"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be '123'", "123", result.left); + t.assert_equal("right should be '456'", "456", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_single_char(testing & t) { + diff_split result = calculate_diff_split("a", "b"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'a'", "a", result.left); + t.assert_equal("right should be 'b'", "b", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("a", "a"); + t.assert_equal("prefix should be 'a'", "a", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("a", "ab"); + t.assert_equal("prefix should be 'a'", "a", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'b'", "b", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("ab", "a"); + t.assert_equal("prefix should be 'a'", "a", result.prefix); + t.assert_equal("left should be 'b'", "b", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_overlaps(testing & t) { + // One string is substring of another + diff_split result = calculate_diff_split("test", "testing"); + t.assert_equal("prefix should be 'test'", "test", result.prefix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be 'ing'", "ing", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + result = calculate_diff_split("testing", "test"); + t.assert_equal("prefix should be 'test'", "test", result.prefix); + t.assert_equal("left should be 'ing'", "ing", result.left); + t.assert_equal("right should be empty", "", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Similar strings with one extra char at start + result = calculate_diff_split("Xtest", "Ytest"); + // The algorithm finds "test" as a common suffix since both strings end with "test" + // This is the algorithm's actual behavior - it finds maximal common suffix + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'X'", "X", result.left); + t.assert_equal("right should be 'Y'", "Y", result.right); + t.assert_equal("suffix should be 'test'", "test", result.suffix); + + // Similar strings with one extra char at end + result = calculate_diff_split("testX", "testY"); + t.assert_equal("prefix should be 'test'", "test", result.prefix); + t.assert_equal("left should be 'X'", "X", result.left); + t.assert_equal("right should be 'Y'", "Y", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Strings that are reverses + result = calculate_diff_split("abc", "cba"); + t.assert_equal("prefix should be empty", "", result.prefix); + t.assert_equal("left should be 'abc'", "abc", result.left); + t.assert_equal("right should be 'cba'", "cba", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); +} + +static void test_calculate_diff_split_tag_boundaries(testing & t) { + // Test with unclosed XML tags + diff_split result = calculate_diff_split("testcontent"); + // The fix_tag_boundaries should move incomplete tags appropriately + t.assert_true("prefix should start with 'test'", result.prefix.find("test") == 0); + t.assert_true("should handle tag boundaries", result.left != "" || result.right != "" || result.suffix != ""); + + // Test with unclosed brackets + result = calculate_diff_split("test[", "test]value"); + t.assert_true("should handle bracket boundaries", result.left != "" || result.right != "" || result.suffix != ""); + + // Test with partial tags on both sides + result = calculate_diff_split("prefix", "prefixsuffix"); + // fix_tag_boundaries moves the incomplete '<' from prefix to left/right + t.assert_equal("prefix should be 'prefix'", "prefix", result.prefix); + t.assert_equal("left should be ''", "", result.left); + t.assert_equal("right should be 'suffix'", "suffix", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Test with complex nested tags + result = calculate_diff_split("prefix
content
", "prefix
different
"); + // Algorithm finds "ent" as a common suffix because both strings end with it + // This is the actual algorithm behavior, though not semantically ideal + t.assert_equal("prefix should be 'prefix
'", "prefix
", result.prefix); + t.assert_equal("left should be 'cont'", "cont", result.left); + t.assert_equal("right should be 'differ'", "differ", result.right); + t.assert_equal("suffix should be 'ent
'", "ent
", result.suffix); + + // Test with unclosed angle bracket + result = calculate_diff_split("Hello ", "Hello test"); + t.assert_equal("prefix should be 'Hello '", "Hello ", result.prefix); + t.assert_true("left should contain ''", result.left.find("") != std::string::npos); + t.assert_equal("right should be 'test'", "test", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Test with unclosed square bracket + result = calculate_diff_split("test [array]", "test other"); + t.assert_equal("prefix should be 'test '", "test ", result.prefix); + t.assert_true("left should contain '[array]'", result.left.find("[array]") != std::string::npos); + t.assert_equal("right should be 'other'", "other", result.right); + t.assert_equal("suffix should be empty", "", result.suffix); + + // Test empty prefix and suffix with tags + result = calculate_diff_split("left", "righ"); + t.assert_equal("prefix should be ''", "", result.prefix); + t.assert_equal("left should be 'left'", "left", result.left); + t.assert_equal("right should be 'righ'", "righ", result.right); + t.assert_equal("suffix should be ''", "", result.suffix); + + { + // real case from template tests, simplified + std::string left = "PREFIX
Sure"; + std::string right = "PREFIXLemme thinkSure"; + result = calculate_diff_split(left, right); + t.assert_equal("prefix should be PREFIX", "PREFIX", result.prefix); + t.assert_equal("suffix should be
Sure", "Sure", result.suffix); + t.assert_equal("left should be empty", "", result.left); + t.assert_equal("right should be Lemme think", "Lemme think", result.right); + } + + { + // Real case: special tokens with |> boundary issue + // The suffix starts with |> which should be moved to complete <|END_RESPONSE and <|END_ACTION + std::string prefix = "SOME_PREFIX"; + std::string suffix = "|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>"; + std::string left_diff = "<|START_RESPONSE|>Let me help you.<|END_RESPONSE"; + std::string right_diff = + "<|START_THINKING|><|END_THINKING|><|START_ACTION|>[\n" + " {\"tool_call_id\": \"0\", \"tool_name\": \"test_function_name\", " + "\"parameters\": {\"param1\": \"value1\", \"param2\": \"value2\"}}\n" + "]<|END_ACTION"; + + std::string left = prefix + left_diff + suffix; + std::string right = prefix + right_diff + suffix; + result = calculate_diff_split(left, right); + + t.assert_equal("special token prefix", prefix, result.prefix); + // The |> should be moved from suffix to complete the tokens + t.assert_equal("special token left", "<|START_RESPONSE|>Let me help you.<|END_RESPONSE|>", result.left); + t.assert_true("special token right ends with |>", result.right.find("<|END_ACTION|>") != std::string::npos); + t.assert_equal("special token suffix", "<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", + result.suffix); + } +} + +static void test_until_common_prefix(testing & t) { + t.test("until_common_prefix basic", test_until_common_prefix_basic); +} + +static void test_until_common_prefix_basic(testing & t) { + // Test case from the user request + std::string result = until_common_prefix("", "", ""); + t.assert_equal("untilCommonPrefix should return ''", "", result); + + // Additional test cases to ensure robustness + // Test with different common prefix lengths + result = until_common_prefix("prefixsuffix", "different", "other"); + t.assert_equal("should return 'prefix'", "prefix", result); + + // Test when common prefix is at the start + result = until_common_prefix("rest", "left", "right"); + t.assert_equal("should return empty string when common prefix at start", "", result); + + // Test when there's no common prefix + result = until_common_prefix("something", "left", "right"); + t.assert_equal("should return empty string when no common prefix", "", result); + + // Test with empty strings + result = until_common_prefix("test", "", "right"); + t.assert_equal("should return empty string when left is empty", "", result); + + // Test with longer common prefix + result = until_common_prefix("abcXYZrest", "left", "right"); + t.assert_equal("should return 'abcXYZ'", "abcXYZ", result); +} + +static void test_after_common_suffix(testing & t) { + t.test("after_common_suffix basic", test_after_common_suffix_basic); +} + +static void test_after_common_suffix_basic(testing & t) { + // Test case from the user request + std::string result = after_common_suffix("100", + "100", + "535"); + t.assert_equal("afterCommonSuffix should return ''", "", result); + + // Test when common suffix is at the end + result = after_common_suffix("rest", "left", "right"); + t.assert_equal("should return empty string when common suffix at end", "", result); + + // Test with empty strings + result = after_common_suffix("test", "left", ""); + t.assert_equal("should return empty string when right is empty", "", result); + + // Test case with XML-like structure similar to the main example + result = after_common_suffix("value", + "value", + "different"); + t.assert_equal("should return ''", "", result); + + // Test with longer common suffix appearing at the end of full + result = after_common_suffix("prefixrest", "prefixleft", "prefixright"); + t.assert_equal("should return '' when common suffix is at end of full", "", result); + + // Test with common suffix appearing in middle but not at end + result = after_common_suffix("content", "value", "other"); + t.assert_equal("should return '' when common suffix appears before end", "", result); + + // Test with multi-character common suffix at the very end of full + result = after_common_suffix("startend", "prefixleft", "prefixright"); + t.assert_equal("should return '' when common suffix is at end of full", "", result); +} + +static void test_compare_variants(testing & t) { + t.test("compare_variants basic", test_compare_variants_basic); + t.test("compare_variants messages modifier", test_compare_variants_messages_modifier); + t.test("compare_variants tools modifier", test_compare_variants_tools_modifier); + t.test("compare_variants both modifiers", test_compare_variants_both_modifiers); + t.test("compare_variants template failure", test_compare_variants_template_failure); + t.test("compare_variants identity", test_compare_variants_identity); +} + +static void test_compare_variants_basic(testing & t) { + // Create a simple template that just echoes messages + common_chat_template tmpl("{{ messages[0]['content'] }}", "", ""); + + template_params params; + params.messages = json::array({ + json {{"role", "user"}, {"content", "Hello"}} + }); + + auto modifier = [](template_params & p) { + p.messages[0]["content"] = "World"; + }; + + auto result = differential_analyzer::compare_variants(tmpl, params, modifier); + + t.assert_true("result should have value", result.has_value()); + // The template might not output anything if messages is empty or format is different + // Check that we get a valid result + t.assert_true("prefix or left should have content", !result->diff.prefix.empty() || !result->diff.left.empty()); +} + +static void test_compare_variants_messages_modifier(testing & t) { + // Test with messages modifier only + common_chat_template tmpl("{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}", "", ""); + + template_params params; + params.messages = json::array({ + json {{"role", "user"}, {"content", "A"}} + }); + + auto modifier = [](template_params & p) { + p.messages[0]["content"] = "B"; + }; + + std::optional result = differential_analyzer::compare_variants(tmpl, params, modifier); + + t.assert_true("result should have value", result.has_value()); + t.assert_equal("left should be 'A'", "A", result->diff.left); + t.assert_equal("right should be 'B'", "B", result->diff.right); +} + +static void test_compare_variants_tools_modifier(testing & t) { + // Test with tools modifier only + common_chat_template tmpl( + "{% for tool in tools %}{{ tool['name'] }}{% endfor %}", "", ""); + + template_params params; + params.tools = json::array({ + json {{"name", "foo"}} + }); + + auto modifier = [](template_params & p) { + p.tools[0]["name"] = "bar"; + }; + + auto result = differential_analyzer::compare_variants(tmpl, params, modifier); + + t.assert_true("result should have value", result.has_value()); + t.assert_equal("left should be 'foo'", "foo", result->diff.left); + t.assert_equal("right should be 'bar'", "bar", result->diff.right); +} + +static void test_compare_variants_both_modifiers(testing & t) { + // Test with both messages and tools modifiers using the for loop approach + common_chat_template tmpl( + "{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}", "", ""); + + template_params params; + params.messages = json::array({ + json {{"role", "user"}, {"content", "A"}} + }); + + auto modifier = [](template_params & p) { + p.messages[0]["content"] = "B"; + p.messages[0]["role"] = "newuser"; + }; + + auto result = differential_analyzer::compare_variants(tmpl, params, modifier); + + t.assert_true("result should have value", result.has_value()); + t.assert_equal("left should be 'user:A'", "user:A", result->diff.left); + t.assert_equal("right should be 'newuser:B'", "newuser:B", result->diff.right); +} + +static void test_compare_variants_template_failure(testing & t) { + // Test with template that causes failure during application (not construction) + // We use a valid template syntax but one that will fail during application + common_chat_template tmpl("{{ messages[0]['nonexistent_field'] }}", "", ""); + + template_params params; + params.messages = json::array({ + json {{"role", "user"}, {"content", "Hello"}} + }); + + auto modifier = [](template_params & p) { + p.messages[0]["content"] = "World"; + }; + + auto result = differential_analyzer::compare_variants(tmpl, params, modifier); + + t.assert_true("result should be nullopt on template failure", !result.has_value()); +} + +static void test_compare_variants_identity(testing & t) { + // Test with identity modifier (no change) + common_chat_template tmpl("{{ messages[0]['content'] }}", "", ""); + + template_params params; + params.messages = json::array({ + json {{"role", "user"}, {"content", "Hello"}} + }); + + // No modifier - should use identity + auto result = differential_analyzer::compare_variants(tmpl, params, nullptr); + + t.assert_true("result should have value", result.has_value()); + t.assert_equal("prefix should be 'Hello'", "Hello", result->diff.prefix); + t.assert_equal("left should be empty", "", result->diff.left); + t.assert_equal("right should be empty", "", result->diff.right); + t.assert_equal("suffix should be empty", "", result->diff.suffix); +} + +// ============================================================================ +// Seed-OSS Template Tool Calling Analysis Tests +// ============================================================================ + +static void test_seed_oss_tool_analysis(testing & t) { + t.test("Seed-OSS tool presence", test_seed_oss_tool_presence); + t.test("Seed-OSS call count", test_seed_oss_call_count); + t.test("Seed-OSS function names", test_seed_oss_function_names); + t.test("Seed-OSS argument count", test_seed_oss_argument_count); + t.test("Seed-OSS args presence", test_seed_oss_args_presence); + t.test("Seed-OSS tool with reasoning", test_seed_oss_tool_with_reasoning); +} + +// Helper to load Seed-OSS template +static common_chat_template load_seed_oss_template(testing & t) { + std::string template_path = "models/templates/ByteDance-Seed-OSS.jinja"; + std::ifstream fin(template_path, std::ios::binary); + std::ostringstream buf; + if (fin.is_open()) { + buf << fin.rdbuf(); + } + std::string template_source = buf.str(); + common_chat_template tmpl(template_source, "", ""); + t.assert_true("Seed-OSS template loaded successfully", template_source.length() > 0); + return tmpl; +} + +// Helper to build tool call JSON +static json build_tool_call(const std::string & name, const json & args, const std::string & id = "call_001") { + return json{ + {"id", id}, + {"type", "function"}, + {"function", json{ + {"name", name}, + {"arguments", args} + }} + }; +} + +// Helper to build tools definition +static json build_tools_definition() { + json parameters_schema = json::object(); + parameters_schema["type"] = "object"; + parameters_schema["properties"] = json::object(); + parameters_schema["properties"]["param1"] = json::object({ + {"type", "string"}, + {"description", "First parameter"} + }); + parameters_schema["properties"]["param2"] = json::object({ + {"type", "string"}, + {"description", "Second parameter"} + }); + parameters_schema["required"] = json::array({"param1", "param2"}); + + return json::array({ + json{ + {"type", "function"}, + {"function", json{ + {"name", "test_function_name"}, + {"description", "A test function for debugging"}, + {"parameters", parameters_schema} + }} + } + }); +} + +// T1: Compare with/without tool call (user, assistant) +static void test_seed_oss_tool_presence(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + json assistant_no_tools = json{ + {"role", "assistant"}, + {"content", "Let me help you."} + }; + + json assistant_with_tools = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello, please help me."} + }; + + template_params params_no_tools; + params_no_tools.messages = json::array({user_msg, assistant_no_tools}); + params_no_tools.tools = build_tools_definition(); + params_no_tools.add_generation_prompt = false; + params_no_tools.enable_thinking = true; + + template_params params_with_tools; + params_with_tools.messages = json::array({user_msg, assistant_with_tools}); + params_with_tools.tools = build_tools_definition(); + params_with_tools.add_generation_prompt = false; + params_with_tools.enable_thinking = true; + + auto result = differential_analyzer::compare_variants(tmpl, params_no_tools, + [&](template_params & p) { + p.messages = params_with_tools.messages; + }); + + t.assert_true("T1 result should have value", result.has_value()); + + const auto & diff = result->diff; + t.assert_true("T1 prefix should contain system", diff.prefix.find("system") != std::string::npos); + t.assert_true("T1 prefix should contain user", diff.prefix.find("user") != std::string::npos); + t.assert_true("T1 prefix should contain assistant", diff.prefix.find("assistant") != std::string::npos); + + // Left should be the assistant content without tool + t.assert_equal("T1 left should contain 'Let me help you.'", "Let me help you.", diff.left); + + // Right should contain the tool call markers + t.assert_true("T1 right should contain tool_call begin", diff.right.find("") != std::string::npos); + t.assert_true("T1 right should contain function tag", diff.right.find("") != std::string::npos); + t.assert_true("T1 right should contain parameter=param1", diff.right.find("") != std::string::npos); + t.assert_true("T1 right should contain parameter=param2", diff.right.find("") != std::string::npos); + t.assert_true("T1 right should contain value1", diff.right.find("value1") != std::string::npos); + t.assert_true("T1 right should contain value2", diff.right.find("value2") != std::string::npos); + t.assert_true("T1 right should contain tool_call end", diff.right.find("") != std::string::npos); + + // Suffix should be the eos token + t.assert_equal("T1 suffix should be ''", "", diff.suffix); +} + +// T2: Compare one vs two tool calls +static void test_seed_oss_call_count(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + json assistant_one_call = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })} + }; + + json assistant_two_calls = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})), + build_tool_call("test_function_name", json::object({{"param1", "value3"}, {"param2", "value4"}}), "call_002") + })} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello, please help me."} + }; + + template_params params_one; + params_one.messages = json::array({user_msg, assistant_one_call}); + params_one.tools = build_tools_definition(); + params_one.add_generation_prompt = false; + params_one.enable_thinking = true; + + auto result = differential_analyzer::compare_variants(tmpl, params_one, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_two_calls}); + }); + + t.assert_true("T2 result should have value", result.has_value()); + + const auto & diff = result->diff; + + // Prefix should include the first tool call + t.assert_true("T2 prefix should contain first tool_call begin", diff.prefix.find("") != std::string::npos); + t.assert_true("T2 prefix should contain first function", diff.prefix.find("") != std::string::npos); + t.assert_true("T2 prefix should contain value1", diff.prefix.find("value1") != std::string::npos); + t.assert_true("T2 prefix should contain value2", diff.prefix.find("value2") != std::string::npos); + t.assert_true("T2 prefix should contain first tool_call end", diff.prefix.find("") != std::string::npos); + + // Left should be empty (no second tool call in variant A) + t.assert_equal("T2 left should be empty", "", diff.left); + + // Right should contain the second tool call + t.assert_true("T2 right should contain second tool_call begin", diff.right.find("") != std::string::npos); + t.assert_true("T2 right should contain second function", diff.right.find("") != std::string::npos); + t.assert_true("T2 right should contain value3", diff.right.find("value3") != std::string::npos); + t.assert_true("T2 right should contain value4", diff.right.find("value4") != std::string::npos); + t.assert_true("T2 right should contain second tool_call end", diff.right.find("") != std::string::npos); + + // Suffix should be the eos token + t.assert_equal("T2 suffix should be ''", "", diff.suffix); +} + +// T3: Compare different function names +static void test_seed_oss_function_names(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + // Build tools with two different function names + json parameters_schema = json::object(); + parameters_schema["type"] = "object"; + parameters_schema["properties"] = json::object(); + parameters_schema["properties"]["arg1"] = json::object({ + {"type", "string"}, + {"description", "Argument 1"} + }); + parameters_schema["required"] = json::array({"arg1"}); + + json tools = json::array({ + json{ + {"type", "function"}, + {"function", json{ + {"name", "func_alpha"}, + {"description", "First function"}, + {"parameters", parameters_schema} + }} + }, + json{ + {"type", "function"}, + {"function", json{ + {"name", "func_beta"}, + {"description", "Second function"}, + {"parameters", parameters_schema} + }} + } + }); + + json assistant_func_alpha = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("func_alpha", json::object({{"arg1", "test_value"}})) + })} + }; + + json assistant_func_beta = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("func_beta", json::object({{"arg1", "test_value"}})) + })} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello"} + }; + + template_params params_alpha; + params_alpha.messages = json::array({user_msg, assistant_func_alpha}); + params_alpha.tools = tools; + params_alpha.add_generation_prompt = false; + params_alpha.enable_thinking = true; + + auto result = differential_analyzer::compare_variants(tmpl, params_alpha, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_func_beta}); + }); + + t.assert_true("T3 result should have value", result.has_value()); + + const auto & diff = result->diff; + + bool func_alpha_in_left = diff.left.find("func_alpha") != std::string::npos; + bool func_alpha_in_prefix = diff.prefix.find("func_alpha") != std::string::npos; + bool func_beta_in_right = diff.right.find("func_beta") != std::string::npos; + bool func_beta_in_prefix = diff.prefix.find("func_beta") != std::string::npos; + bool func_beta_in_suffix = diff.suffix.find("func_beta") != std::string::npos; + + // Left should contain func_alpha (or be in prefix) + t.assert_true("T3 left should contain func_alpha (or prefix)", func_alpha_in_left || func_alpha_in_prefix); + + // Right should contain func_beta + t.assert_true("T3 right should contain func_beta", func_beta_in_right || func_beta_in_prefix || func_beta_in_suffix); + + // Both should have the same parameter value (in common parts, not in diffs) + // Since both have same args, test_value will be in prefix/suffix + t.assert_true("T3 diff should contain test_value (in prefix or suffix)", + diff.prefix.find("test_value") != std::string::npos || diff.suffix.find("test_value") != std::string::npos); +} + +// T4: Compare different argument counts (zero, one, two parameters) +static void test_seed_oss_argument_count(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + // Build tools with 0, 1, or 2 required parameters + json params_2_required = json::object(); + params_2_required["type"] = "object"; + params_2_required["properties"] = json::object(); + params_2_required["properties"]["arg1"] = json::object({ + {"type", "string"}, + {"description", "Argument 1"} + }); + params_2_required["properties"]["arg2"] = json::object({ + {"type", "string"}, + {"description", "Argument 2"} + }); + params_2_required["required"] = json::array({"arg1", "arg2"}); + + json params_1_required = json::object(); + params_1_required["type"] = "object"; + params_1_required["properties"] = json::object(); + params_1_required["properties"]["arg1"] = json::object({ + {"type", "string"}, + {"description", "Argument 1"} + }); + params_1_required["required"] = json::array({"arg1"}); + + json tools = json::array({ + json{ + {"type", "function"}, + {"function", json{ + {"name", "test_func"}, + {"description", "Test function"}, + {"parameters", params_2_required} + }} + } + }); + + // Test: zero args vs one arg + json assistant_zero_args = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_func", json::object()) + })} + }; + + json assistant_one_arg = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_func", json::object({{"arg1", "value1"}})) + })} + }; + + json assistant_two_args = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_func", json::object({{"arg1", "value1"}, {"arg2", "value2"}})) + })} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello"} + }; + + // Test zero vs one + template_params params_zero; + params_zero.messages = json::array({user_msg, assistant_zero_args}); + params_zero.tools = tools; + params_zero.add_generation_prompt = false; + params_zero.enable_thinking = true; + + auto result_zero_one = differential_analyzer::compare_variants(tmpl, params_zero, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_one_arg}); + }); + + t.assert_true("T4 zero vs one result should have value", result_zero_one.has_value()); + t.assert_true("T4 zero vs one left should be empty or minimal", result_zero_one->diff.left.empty() || result_zero_one->diff.left == ""); + t.assert_true("T4 zero vs one right should contain arg1", result_zero_one->diff.right.find("arg1") != std::string::npos); + + // Test one vs two + template_params params_one; + params_one.messages = json::array({user_msg, assistant_one_arg}); + params_one.tools = tools; + params_one.add_generation_prompt = false; + params_one.enable_thinking = true; + + auto result_one_two = differential_analyzer::compare_variants(tmpl, params_one, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_two_args}); + }); + + t.assert_true("T4 one vs two result should have value", result_one_two.has_value()); + + const auto & diff4 = result_one_two->diff; + t.assert_true("T4 one vs two left should contain arg1 (or prefix)", + diff4.left.find("arg1") != std::string::npos || diff4.prefix.find("arg1") != std::string::npos); + t.assert_true("T4 one vs two right should contain arg1 (or prefix)", + diff4.right.find("arg1") != std::string::npos || diff4.prefix.find("arg1") != std::string::npos); + t.assert_true("T4 one vs two right should contain arg2 (or prefix/suffix)", + diff4.right.find("arg2") != std::string::npos || diff4.prefix.find("arg2") != std::string::npos || diff4.suffix.find("arg2") != std::string::npos); +} + +// T5: Compare different argument values +static void test_seed_oss_args_presence(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + json assistant_same_arg = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}})) + })} + }; + + json assistant_other_arg = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param2", "value2"}})) + })} + }; + + json assistant_both_args = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello"} + }; + + template_params params_same; + params_same.messages = json::array({user_msg, assistant_same_arg}); + params_same.tools = build_tools_definition(); + params_same.add_generation_prompt = false; + params_same.enable_thinking = true; + + // Test same arg vs other arg + auto result_same_other = differential_analyzer::compare_variants(tmpl, params_same, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_other_arg}); + }); + + t.assert_true("T5 same vs other result should have value", result_same_other.has_value()); + const auto & diff5a = result_same_other->diff; + t.assert_true("T5 same vs other left should contain param1 (or prefix/suffix)", + diff5a.left.find("param1") != std::string::npos || diff5a.prefix.find("param1") != std::string::npos || diff5a.suffix.find("param1") != std::string::npos); + t.assert_true("T5 same vs other left should contain value1 (or prefix/suffix)", + diff5a.left.find("value1") != std::string::npos || diff5a.prefix.find("value1") != std::string::npos); + t.assert_true("T5 same vs other right should contain param2 (or prefix/suffix)", + diff5a.right.find("param2") != std::string::npos || diff5a.prefix.find("param2") != std::string::npos || diff5a.suffix.find("param2") != std::string::npos); + t.assert_true("T5 same vs other right should contain value2 (or prefix/suffix)", + diff5a.right.find("value2") != std::string::npos || diff5a.prefix.find("value2") != std::string::npos || diff5a.suffix.find("value2") != std::string::npos); + + // Test same arg vs both args + auto result_same_both = differential_analyzer::compare_variants(tmpl, params_same, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_both_args}); + }); + + t.assert_true("T5 same vs both result should have value", result_same_both.has_value()); + const auto & diff5b = result_same_both->diff; + t.assert_true("T5 same vs both left should contain param1 (or prefix/suffix)", + diff5b.left.find("param1") != std::string::npos || diff5b.prefix.find("param1") != std::string::npos || diff5b.suffix.find("param1") != std::string::npos); + t.assert_true("T5 same vs both right should contain param1 (or prefix/suffix)", + diff5b.right.find("param1") != std::string::npos || diff5b.prefix.find("param1") != std::string::npos || diff5b.suffix.find("param1") != std::string::npos); + t.assert_true("T5 same vs both right should contain param2 (or prefix/suffix)", + diff5b.right.find("param2") != std::string::npos || diff5b.prefix.find("param2") != std::string::npos || diff5b.suffix.find("param2") != std::string::npos); +} + +// T6: Tool call with vs without reasoning_content +static void test_seed_oss_tool_with_reasoning(testing & t) { + common_chat_template tmpl = load_seed_oss_template(t); + + json assistant_tool_only = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })} + }; + + json assistant_tool_with_reasoning = json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })}, + {"reasoning_content", "I need to call the tool first."} + }; + + json user_msg = json{ + {"role", "user"}, + {"content", "Hello, please help me."} + }; + + template_params params_tool_only; + params_tool_only.messages = json::array({user_msg, assistant_tool_only}); + params_tool_only.tools = build_tools_definition(); + params_tool_only.add_generation_prompt = false; + params_tool_only.enable_thinking = true; + + auto result = differential_analyzer::compare_variants(tmpl, params_tool_only, + [&](template_params & p) { + p.messages = json::array({user_msg, assistant_tool_with_reasoning}); + }); + + t.assert_true("T6 result should have value", result.has_value()); + + const auto & diff = result->diff; + + // Left should be empty (no reasoning in variant A) + t.assert_equal("T6 left should be empty", "", diff.left); + + // Right should contain the thinking token with reasoning content + t.assert_true("T6 right should contain think begin", diff.right.find("") != std::string::npos); + t.assert_true("T6 right should contain reasoning content", diff.right.find("I need to call the tool first.") != std::string::npos); + t.assert_true("T6 right should contain think end", diff.right.find("") != std::string::npos); + + // Prefix should contain the assistant role + t.assert_true("T6 prefix should contain assistant", diff.prefix.find("assistant") != std::string::npos); + + // Suffix should contain the tool call + t.assert_true("T6 suffix should contain tool_call begin", diff.suffix.find("") != std::string::npos); + t.assert_true("T6 suffix should contain function name", diff.suffix.find("test_function_name") != std::string::npos); + t.assert_true("T6 suffix should contain eos", diff.suffix.find("") != std::string::npos); +} + +static common_chat_template load_template(testing & t, const std::string & template_path) { + std::ifstream fin(template_path, std::ios::binary); + std::ostringstream buf; + if (fin.is_open()) { + buf << fin.rdbuf(); + } + std::string template_source = buf.str(); + common_chat_template tmpl(template_source, "", ""); + t.assert_true("Nemotron template loaded successfully", template_source.length() > 0); + return tmpl; +} + +// ============================================================================ +// Nemotron Template Analysis Tests +// ============================================================================ +static common_chat_template load_nemotron_template(testing & t) { + return load_template(t, "models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja"); +} + +static void test_nemotron_analysis(testing & t) { + t.test("Nemotron reasoning detection", test_nemotron_reasoning_detection); + t.test("Nemotron tool format", test_nemotron_tool_format); +} + +static void test_nemotron_reasoning_detection(testing & t) { + common_chat_template tmpl = load_nemotron_template(t); + + // Test the comparison manually to see what's happening + json user_msg = json{ { "role", "user" }, { "content", "Hello" } }; + json assistant_no_reasoning = json{ + { "role", "assistant" }, + { "content", "I can help." } + }; + json assistant_with_reasoning = json{ + { "role", "assistant" }, + { "content", "I can help." }, + { "reasoning_content", "Let me think about this." } + }; + + template_params params; + params.messages = json::array({ user_msg, assistant_no_reasoning }); + params.add_generation_prompt = false; + params.enable_thinking = true; + + // Run differential analysis + 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); + + // 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); + + // 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 should be PLAIN", content_mode::PLAIN, analysis.content); +} + +static void test_nemotron_tool_format(testing & t) { + common_chat_template tmpl = load_nemotron_template(t); + + // Run differential analysis + 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); + + // 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 ''", "", analysis.markers.func_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); + + // Check format classification + t.assert_true("tool format should be TAG_WITH_TAGGED", analysis.tools == tool_format::TAG_WITH_TAGGED); + + // Verify tool support + t.assert_true("should support tools", analysis.supports_tools); +} + +static common_chat_template load_cohere_template(testing & t) { + return load_template(t, "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja"); +} + +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) { + common_chat_template tmpl = load_cohere_template(t); + + // Run differential analysis + 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); + + // 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); + + // 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); + + // Content is always wrapped (both with and without tools) + t.assert_equal("content should be ALWAYS_WRAPPED", content_mode::ALWAYS_WRAPPED, analysis.content); +} + +static void test_cohere_tool_format(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); + + // 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); + + // 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); + + // 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); + + // 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); + + // 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); + // 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); + + // Check format classification + t.assert_equal("tool format should be JSON_NATIVE", tool_format::JSON_NATIVE, analysis.tools); + + // 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); +} + +// ============================================================================ +// standard_json_tools Format Tests +// ============================================================================ + +// Helper to build tools definition for tests +static json build_test_tools() { + json parameters_schema = json::object(); + parameters_schema["type"] = "object"; + parameters_schema["properties"] = json::object(); + parameters_schema["properties"]["location"] = json::object({ + {"type", "string"}, + {"description", "The city and state"} + }); + parameters_schema["properties"]["unit"] = json::object({ + {"type", "string"}, + {"description", "Temperature unit"}, + {"enum", json::array({"celsius", "fahrenheit"})} + }); + parameters_schema["required"] = json::array({"location"}); + + return json::array({ + json{ + {"type", "function"}, + {"function", json{ + {"name", "get_current_weather"}, + {"description", "Get the current weather in a given location"}, + {"parameters", parameters_schema} + }} + } + }); +} + +static void test_standard_json_tools_formats(testing & t) { + t.test("OpenAI format", test_standard_json_tools_openai); + t.test("Cohere format", test_standard_json_tools_cohere); + t.test("function-as-key format", test_standard_json_tools_function_key); +} + +// Test 1: OpenAI Standard Format +// {"id": "call_abc", "function": {"name": "get_weather", "arguments": {"location": "NYC"}}} +static void test_standard_json_tools_openai(testing & t) { + json tools = build_test_tools(); + + auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { + auto tool_call = p.standard_json_tools( + "", "", tools, + /* parallel */ true, + /* force */ false, + /* name_key */ "function.name", + /* args_key */ "function.arguments", + /* array_wrapped */ false, + /* function_is_key */ false, + /* call_id_key */ "id", + /* gen_call_id_key */ "", + /* parameters_order */ {} + ); + return p.content(p.until("")) + p.optional(tool_call) + p.end(); + }); + + std::string input = + "Let me check the weather." + "" + R"({"id": "call_abc123", "function": {"name": "get_current_weather", "arguments": {"location": "NYC"}}})" + ""; + + common_peg_parse_context ctx(input, false); + auto result = parser.parse(ctx); + + t.assert_true("parse success", result.success()); + + common_chat_msg msg; + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + + t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); + if (!msg.tool_calls.empty()) { + t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); + t.assert_equal("tool id", "call_abc123", msg.tool_calls[0].id); + } + t.assert_true("content present", msg.content.find("Let me check the weather") != std::string::npos); +} + +// Test 2: Cohere Format +// {"tool_call_id": 0, "tool_name": "get_weather", "parameters": {"location": "NYC"}} +static void test_standard_json_tools_cohere(testing & t) { + json tools = build_test_tools(); + + auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { + auto tool_call = p.standard_json_tools( + "<|START_ACTION|>[", "]<|END_ACTION|>", tools, + /* parallel */ true, + /* force */ false, + /* name_key */ "tool_name", + /* args_key */ "parameters", + /* array_wrapped */ false, // Brackets are part of section markers + /* function_is_key */ false, + /* call_id_key */ "", + /* gen_call_id_key */ "tool_call_id", + /* parameters_order */ {"tool_call_id", "tool_name", "parameters"} + ); + return p.content(p.until("<|START_ACTION|>")) + p.optional(tool_call) + p.end(); + }); + + std::string input = + "Let me search for that." + "<|START_ACTION|>[" + R"({"tool_call_id": 0, "tool_name": "get_current_weather", "parameters": {"location": "NYC", "unit": "celsius"}})" + "]<|END_ACTION|>"; + + common_peg_parse_context ctx(input, false); + auto result = parser.parse(ctx); + + t.assert_true("parse success", result.success()); + + common_chat_msg msg; + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + + t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); + if (!msg.tool_calls.empty()) { + t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); + t.assert_equal("tool id", "0", msg.tool_calls[0].id); + } + t.assert_true("content present", msg.content.find("Let me search") != std::string::npos); +} + +// Test 3: Function-as-Key Format +// {"get_current_weather": {"id": "call-0001", "args": {"location": "NYC"}}} +static void test_standard_json_tools_function_key(testing & t) { + json tools = build_test_tools(); + + auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { + auto tool_call = p.standard_json_tools( + "[", "]", tools, + /* parallel */ true, + /* force */ false, + /* name_key */ "", // Name is the key itself + /* args_key */ "args", + /* array_wrapped */ false, + /* function_is_key */ true, + /* call_id_key */ "id", + /* gen_call_id_key */ "", + /* parameters_order */ {} + ); + return p.content(p.until("")) + p.optional(tool_call) + p.end(); + }); + + std::string input = + "I'll call the weather function." + "[" + R"({"get_current_weather": {"id": "call-0001", "args": {"location": "NYC", "unit": "celsius"}}})" + "]"; + + common_peg_parse_context ctx(input, false); + auto result = parser.parse(ctx); + + t.assert_true("parse success", result.success()); + + common_chat_msg msg; + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + + t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); + if (!msg.tool_calls.empty()) { + t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); + t.assert_equal("tool id", "call-0001", msg.tool_calls[0].id); + } + t.assert_true("content present", msg.content.find("I'll call the weather") != std::string::npos); +} + +// ============================================================================ +// normalize_quotes_to_json Tests +// ============================================================================ + +// Copy of the function for isolated testing (original is static in chat-peg-parser.cpp) +static std::string normalize_quotes_to_json(const std::string & input) { + std::string result; + result.reserve(input.size() + 16); + + bool in_single_quoted = false; + bool in_double_quoted = false; + + for (size_t i = 0; i < input.size(); ++i) { + char c = input[i]; + + if (c == '\\' && i + 1 < input.size()) { + char next = input[i + 1]; + + if (in_single_quoted) { + if (next == '\'') { + result += '\''; + ++i; + continue; + } + if (next == '"') { + result += "\\\""; + ++i; + continue; + } + result += c; + result += next; + ++i; + continue; + } + + if (in_double_quoted) { + result += c; + result += next; + ++i; + continue; + } + + result += c; + continue; + } + + if (c == '"') { + if (in_single_quoted) { + result += "\\\""; + } else { + in_double_quoted = !in_double_quoted; + result += c; + } + } else if (c == '\'') { + if (in_double_quoted) { + result += c; + } else if (in_single_quoted) { + in_single_quoted = false; + result += '"'; + } else { + in_single_quoted = true; + result += '"'; + } + } else { + result += c; + } + } + + return result; +} + +static void test_normalize_quotes_to_json(testing & t) { + t.test("basic single to double quotes", [](testing & t) { + std::string input = "{'key': 'value'}"; + std::string expected = "{\"key\": \"value\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("basic conversion", expected, result); + }); + + t.test("escaped single quote inside single-quoted string", [](testing & t) { + std::string input = "{'code': 'print(\\'hello\\')'}"; + std::string expected = "{\"code\": \"print('hello')\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("escaped single quote", expected, result); + }); + + t.test("double quote inside single-quoted string", [](testing & t) { + std::string input = "{'msg': 'He said \"hi\"'}"; + std::string expected = "{\"msg\": \"He said \\\"hi\\\"\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("double quote escaping", expected, result); + }); + + t.test("nested backslash escapes", [](testing & t) { + std::string input = "{'path': 'C:\\\\Users\\\\test'}"; + std::string expected = "{\"path\": \"C:\\\\Users\\\\test\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("backslash escaping", expected, result); + }); + + t.test("newline escapes", [](testing & t) { + std::string input = "{'text': 'line1\\nline2'}"; + std::string expected = "{\"text\": \"line1\\nline2\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("newline escaping", expected, result); + }); + + t.test("mixed quotes", [](testing & t) { + std::string input = "{\"already_double\": 'single_value'}"; + std::string expected = "{\"already_double\": \"single_value\"}"; + std::string result = normalize_quotes_to_json(input); + t.assert_equal("mixed quotes", expected, result); + }); + + t.test("embedded quotes - the test case", test_normalize_quotes_with_embedded_quotes); +} + +// Test case that mirrors the Seed-OSS failing test scenario +static void test_normalize_quotes_with_embedded_quotes(testing & t) { + // This is similar to the Seed-OSS template test case + // The input has embedded double quotes like "14" and "bar" inside string values + std::string input = "{'filename': 'foo.cpp', 'oldString': 'def foo(arg = \"14\"):\\n return arg + \"bar\"\\n', 'newString': 'def foo(arg = \"15\"):\\n pass\\n'}"; + + // Expected: Python single quotes -> JSON double quotes, internal double quotes escaped + std::string expected = "{\"filename\": \"foo.cpp\", \"oldString\": \"def foo(arg = \\\"14\\\"):\\n return arg + \\\"bar\\\"\\n\", \"newString\": \"def foo(arg = \\\"15\\\"):\\n pass\\n\"}"; + + std::string result = normalize_quotes_to_json(input); + + t.assert_equal("normalize quotes with embedded double quotes", expected, result); + + // Also verify the result is valid JSON + try { + json parsed = json::parse(result); + t.assert_true("result is valid JSON", true); + t.assert_equal("filename field", "foo.cpp", parsed["filename"].get()); + t.assert_true("oldString contains embedded quotes", + parsed["oldString"].get().find("\"14\"") != std::string::npos); + t.assert_true("newString contains embedded quotes", + parsed["newString"].get().find("\"15\"") != std::string::npos); + } catch (const std::exception & e) { + t.assert_true(std::string("JSON parse failed: ") + e.what(), false); + } +} + +// ============================================================================ +// TAG_WITH_TAGGED Argument Parsing Tests +// ============================================================================ + +// Build tools definition for edit function +static json build_edit_tool() { + json parameters_schema = json::object(); + parameters_schema["type"] = "object"; + parameters_schema["properties"] = json::object(); + parameters_schema["properties"]["filename"] = json::object({ + {"type", "string"}, + {"description", "Path of file to edit"} + }); + parameters_schema["properties"]["oldString"] = json::object({ + {"type", "string"}, + {"description", "String to replace"} + }); + parameters_schema["properties"]["newString"] = json::object({ + {"type", "string"}, + {"description", "New (replacement) value"} + }); + parameters_schema["required"] = json::array({"filename", "oldString", "newString"}); + + return json::array({ + json{ + {"type", "function"}, + {"function", json{ + {"name", "edit"}, + {"description", "Edit a file"}, + {"parameters", parameters_schema} + }} + } + }); +} + +// Test that reproduces the Seed-OSS template issue with embedded quotes +static void test_tagged_args_with_embedded_quotes(testing & t) { + json tools = build_edit_tool(); + + // Build a parser for TAG_WITH_TAGGED format like Seed-OSS/Nemotron + auto parser = build_chat_peg_unified_parser([&](common_chat_peg_unified_builder & p) { + // Build tool choice for the edit function + auto tool_choice = p.choice(); + + for (const auto & tool_def : tools) { + if (!tool_def.contains("function")) continue; + const auto & function = tool_def.at("function"); + std::string name = function.at("name"); + const auto & params = function.at("parameters"); + + if (!params.contains("properties") || !params.at("properties").is_object()) continue; + + const auto & properties = params.at("properties"); + + // Build argument parsers + std::vector arg_parsers; + for (const auto & [param_name, param_schema] : properties.items()) { + auto arg = p.tool_arg( + p.tool_arg_open(p.literal("")) + + p.space() + + p.tool_arg_string_value(p.until("")) + + p.space() + + p.tool_arg_close(p.literal("")) + ); + arg_parsers.push_back(p.optional(p.rule("arg-" + param_name, arg))); + } + + // Build arg sequence with space() between + common_peg_parser args_seq = p.eps(); + for (size_t i = 0; i < arg_parsers.size(); i++) { + if (i > 0) { + args_seq = args_seq + p.space(); + } + args_seq = args_seq + arg_parsers[i]; + } + + auto func_parser = + p.tool_open(p.literal("")) + + p.space() + args_seq + p.space() + + p.tool_close(p.literal("")); + + tool_choice |= p.rule("tool-" + name, p.tool(func_parser)); + } + + auto tool_section = + p.literal("") + p.space() + + tool_choice + + p.space() + p.literal(""); + + return p.content(p.until("")) + p.optional(tool_section) + p.end(); + }); + + // The exact input from the failing test + std::string input = + "\n" + "\n" + "\n" + "foo.cpp\n" + "\n" + "" + "def foo(arg = \"14\"):\n" + " return arg + \"bar\"\n" + "\n" + "\n" + "" + "def foo(arg = \"15\"):\n" + " pass\n" + "\n" + "\n" + "\n" + ""; + + common_peg_parse_context ctx(input, false); + auto result = parser.parse(ctx); + + t.assert_true("parse success", result.success()); + + common_chat_msg msg; + auto mapper = common_chat_peg_unified_mapper(msg); + mapper.from_ast(ctx.ast, result); + + t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); + + if (!msg.tool_calls.empty()) { + t.assert_equal("tool name", "edit", msg.tool_calls[0].name); + + // Parse the arguments as JSON to verify they're valid + std::string args = msg.tool_calls[0].arguments; + + try { + json parsed = json::parse(args); + t.assert_true("arguments is valid JSON", true); + + // Verify each field has proper value + t.assert_equal("filename", "foo.cpp", parsed.value("filename", "")); + + std::string oldString = parsed.value("oldString", ""); + t.assert_true("oldString contains embedded quotes", + oldString.find("\"14\"") != std::string::npos); + t.assert_true("oldString contains bar with quotes", + oldString.find("\"bar\"") != std::string::npos); + + std::string newString = parsed.value("newString", ""); + t.assert_true("newString contains embedded quotes", + newString.find("\"15\"") != std::string::npos); + + } catch (const std::exception & e) { + t.assert_true(std::string("arguments should be valid JSON: ") + e.what(), false); + } + } +} + diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index ad2953f6da..38798ec9d7 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -14,6 +14,7 @@ #include "log.h" #include +#include #include #include #include @@ -439,7 +440,7 @@ const common_chat_msg message_assist_call_idx = const common_chat_msg message_assist_thoughts_call_idx = simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1\": 1}", /* id = */ "0"); const common_chat_msg message_assist_thoughts_partial_call = - simple_assist_msg("", "I'm\nthinking", "", "", /* id = */ "0"); + simple_assist_msg("", "I'm\nthinking", "special_function", "", /* id = */ "0"); const common_chat_msg message_assist_call_python = simple_assist_msg("", "", "python", "{\"code\":\"print('hey')\"}"); const common_chat_msg message_assist_call_python_lines = simple_assist_msg("", "", "python", "{\"code\":\"# This is a program:\\nprint('hey')\"}"); @@ -541,14 +542,14 @@ static void test_templates(const struct common_chat_templates * tmpls, if (expect_grammar_triggered) { // TODO @ngxson : refactor common_chat_parse to avoid passing format/reasoning_format every time - common_chat_parser_params params; - params.format = data.params.format; - params.reasoning_format = reasoning_format; - if (!params.parser.empty()) { - syntax.parser = common_peg_arena(); - syntax.parser.load(params.parser); + common_chat_parser_params parser_params; + parser_params.format = data.params.format; + parser_params.reasoning_format = reasoning_format; + if (!parser_params.parser.empty()) { + parser_params.parser = common_peg_arena(); + parser_params.parser.load(params.parser); } - const auto msg = common_chat_parse(data.delta, /* is_partial= */ false, params); + const auto msg = common_chat_parse(data.delta, /* is_partial= */ false, parser_params); assert_msg_equals(test_message, msg, ignore_whitespace_differences); } @@ -670,10 +671,12 @@ static void test_parser_with_streaming(const common_chat_msg & expected, const s auto last_msg = parse_msg(""); for (size_t i = 1; i <= raw_message.size(); ++i) { auto curr_msg = parse_msg(std::string(utf8_truncate_safe_view(std::string_view(raw_message).substr(0, i)))); - if (curr_msg == simple_assist_msg("")) continue; - LOG_INF("Streaming msg: %s\n", common_chat_msgs_to_json_oaicompat({curr_msg}).dump().c_str()); - for (auto diff: common_chat_msg_diff::compute_diffs(last_msg, curr_msg)) { - LOG_INF("Streaming diff: %s\n", common_chat_msg_diff_to_json_oaicompat(diff).dump().c_str()); + if (curr_msg == simple_assist_msg("")) { + continue; + } + LOG_INF("Streaming msg: %s\n", common_chat_msgs_to_json_oaicompat({ curr_msg }).dump().c_str()); + for (auto diff : common_chat_msg_diff::compute_diffs(last_msg, curr_msg)) { + LOG_INF("Streaming diff: %s\n", common_chat_msg_diff_to_json_oaicompat(diff).dump().c_str()); if (!diff.reasoning_content_delta.empty()) { merged.reasoning_content += diff.reasoning_content_delta; } @@ -689,7 +692,7 @@ static void test_parser_with_streaming(const common_chat_msg & expected, const s merged.tool_calls.back().arguments += diff.tool_call_delta.arguments; } } - LOG_INF("Streaming merged: %s\n", common_chat_msgs_to_json_oaicompat({merged}).dump().c_str()); + LOG_INF("Streaming merged: %s\n", common_chat_msgs_to_json_oaicompat({ merged }).dump().c_str()); } assert_msg_equals(curr_msg, merged, true); last_msg = curr_msg; @@ -719,10 +722,12 @@ struct make_peg_parser { arena_.load(params_.parser); } - common_chat_msg parse(const std::string & msg, bool is_partial) { -common_chat_parser_params parser_params; + common_chat_msg parse(const std::string & msg, bool is_partial) const { + common_chat_parser_params parser_params; parser_params.format = params_.format; + parser_params.debug = detailed_debug_; return common_chat_peg_parse(arena_, msg, is_partial, parser_params); + } }; static void test_peg_parser(common_chat_templates * tmpls, @@ -809,7 +814,12 @@ static void test_peg_parser(common_chat_templates * tmpls, } } } - assert_msg_equals(msg_current, msg_accum, true); + try { + assert_msg_equals(msg_current, msg_accum, true); + } catch (std::exception & e) { + throw std::runtime_error((std::string("Error comparing accumulated message to current: ") + e.what()).c_str()); + } + msg_prev = msg_current; } @@ -916,8 +926,7 @@ class peg_test_builder { return; } } - LOG_DBG("\n================================\nRunning test for template: %s\n================================\n", - tester_.template_path().c_str()); + LOG_INF("\n\x1b[38;5;126m[%s]\x1b[0m\n%s\n\n", tester_.template_path().c_str(), tc_.input.c_str()); test_peg_parser(tester_.tmpls_.get(), [this](peg_test_case & t) { t = tc_; }, tester_.detailed_debug_); } }; @@ -940,51 +949,47 @@ static void test_msgs_oaicompat_json_conversion() { message_assist_call_python, }; for (const auto & msg : msgs) { - auto oai_json = common_chat_msgs_to_json_oaicompat({msg}); - auto msgs2 = common_chat_msgs_parse_oaicompat(oai_json); + auto oai_json = common_chat_msgs_to_json_oaicompat({ msg }); + auto msgs2 = common_chat_msgs_parse_oaicompat(oai_json); assert_equals((size_t) 1, msgs2.size()); const auto & msg2 = msgs2[0]; assert_msg_equals(msg, msg2); } - assert_equals( - std::string( - "[\n" - " {\n" - " \"role\": \"user\",\n" - " \"content\": [\n" - " {\n" - " \"type\": \"text\",\n" - " \"text\": \"Hey\"\n" - " },\n" - " {\n" - " \"type\": \"text\",\n" - " \"text\": \"there\"\n" - " }\n" - " ]\n" - " }\n" - "]" - ), - common_chat_msgs_to_json_oaicompat({message_user_parts}).dump(2)); + assert_equals(std::string("[\n" + " {\n" + " \"role\": \"user\",\n" + " \"content\": [\n" + " {\n" + " \"type\": \"text\",\n" + " \"text\": \"Hey\"\n" + " },\n" + " {\n" + " \"type\": \"text\",\n" + " \"text\": \"there\"\n" + " }\n" + " ]\n" + " }\n" + "]"), + common_chat_msgs_to_json_oaicompat({ message_user_parts }).dump(2)); - assert_equals( - std::string( - "[\n" - " {\n" - " \"role\": \"assistant\",\n" - " \"content\": \"\",\n" - " \"tool_calls\": [\n" - " {\n" - " \"type\": \"function\",\n" - " \"function\": {\n" - " \"name\": \"python\",\n" - " \"arguments\": \"{\\\"code\\\":\\\"print('hey')\\\"}\"\n" - " }\n" - " }\n" - " ]\n" - " }\n" - "]" - ), - common_chat_msgs_to_json_oaicompat({message_assist_call_python}).dump(2)); + // Note: content is "" instead of null due to workaround for templates that render null as "None" + // Arguments are serialized as string for OAI compatibility + assert_equals(std::string("[\n" + " {\n" + " \"role\": \"assistant\",\n" + " \"content\": \"\",\n" + " \"tool_calls\": [\n" + " {\n" + " \"type\": \"function\",\n" + " \"function\": {\n" + " \"name\": \"python\",\n" + " \"arguments\": \"{\\\"code\\\":\\\"print('hey')\\\"}\"\n" + " }\n" + " }\n" + " ]\n" + " }\n" + "]"), + common_chat_msgs_to_json_oaicompat({ message_assist_call_python }).dump(2)); auto res = common_chat_msgs_parse_oaicompat(json::parse("[{\"role\": \"assistant\", \"tool_calls\": []}]")); assert_equals(1, res.size()); @@ -1010,8 +1015,8 @@ static void test_tools_oaicompat_json_conversion() { }; for (const auto & tool : tools) { - auto oai_json = common_chat_tools_to_json_oaicompat({tool}); - auto tools2 = common_chat_tools_parse_oaicompat(oai_json); + auto oai_json = common_chat_tools_to_json_oaicompat({ tool }); + auto tools2 = common_chat_tools_parse_oaicompat(oai_json); assert_equals((size_t) 1, tools2.size()); auto tool2 = tools2[0]; assert_equals(tool.name, tool2.name); @@ -1019,364 +1024,267 @@ static void test_tools_oaicompat_json_conversion() { assert_equals(json::parse(tool.parameters).dump(2), json::parse(tool2.parameters).dump(2)); } - assert_equals( - std::string( - "[\n" - " {\n" - " \"type\": \"function\",\n" - " \"function\": {\n" - " \"name\": \"special_function\",\n" - " \"description\": \"I'm special\",\n" - " \"parameters\": {\n" - " \"type\": \"object\",\n" - " \"properties\": {\n" - " \"arg1\": {\n" - " \"type\": \"integer\",\n" - " \"description\": \"The arg.\"\n" - " }\n" - " },\n" - " \"required\": [\n" - " \"arg1\"\n" - " ]\n" - " }\n" - " }\n" - " }\n" - "]" - ), - common_chat_tools_to_json_oaicompat({special_function_tool}).dump(2)); - - { - auto tools_no_params = common_chat_tools_parse_oaicompat(json::parse( - R"([{"type": "function", "function": {"name": "test_func", "description": "A test"}}])")); - assert_equals((size_t) 1, tools_no_params.size()); - assert_equals(std::string("test_func"), tools_no_params[0].name); - assert_equals(std::string("A test"), tools_no_params[0].description); - assert_equals(std::string("{}"), tools_no_params[0].parameters); - } - { - auto tools_no_desc = common_chat_tools_parse_oaicompat(json::parse( - R"([{"type": "function", "function": {"name": "test_func", "parameters": {"type": "object"}}}])")); - assert_equals((size_t) 1, tools_no_desc.size()); - assert_equals(std::string("test_func"), tools_no_desc[0].name); - assert_equals(std::string(""), tools_no_desc[0].description); - } - { - auto tools_minimal = common_chat_tools_parse_oaicompat(json::parse( - R"([{"type": "function", "function": {"name": "test_func"}}])")); - assert_equals((size_t) 1, tools_minimal.size()); - assert_equals(std::string("test_func"), tools_minimal[0].name); - assert_equals(std::string(""), tools_minimal[0].description); - assert_equals(std::string("{}"), tools_minimal[0].parameters); - } + assert_equals(std::string("[\n" + " {\n" + " \"type\": \"function\",\n" + " \"function\": {\n" + " \"name\": \"special_function\",\n" + " \"description\": \"I'm special\",\n" + " \"parameters\": {\n" + " \"type\": \"object\",\n" + " \"properties\": {\n" + " \"arg1\": {\n" + " \"type\": \"integer\",\n" + " \"description\": \"The arg.\"\n" + " }\n" + " },\n" + " \"required\": [\n" + " \"arg1\"\n" + " ]\n" + " }\n" + " }\n" + " }\n" + "]"), + common_chat_tools_to_json_oaicompat({ special_function_tool }).dump(2)); } -// for compat; ref: https://github.com/ggml-org/llama.cpp/pull/18961 -struct test_parser_params { - common_chat_format format = COMMON_CHAT_FORMAT_CONTENT_ONLY; - common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_NONE; - bool reasoning_in_content = false; - bool thinking_forced_open = false; - bool parse_tool_calls = true; -}; +static void test_template_output_peg_parsers(bool detailed_debug) { + LOG_DBG("%s\n", __func__); -static common_chat_msg test_chat_parse(const std::string & input, bool is_partial, const test_parser_params & syntax) { - common_chat_parser_params params; - params.format = syntax.format; - params.reasoning_format = syntax.reasoning_format; - params.reasoning_in_content = syntax.reasoning_in_content; - params.thinking_forced_open = syntax.thinking_forced_open; - params.parse_tool_calls = syntax.parse_tool_calls; - return common_chat_parse(input, is_partial, params); -} - -static void test_template_output_parsers() { - printf("[%s]\n", __func__); - - common_chat_templates_inputs inputs_no_tools; - inputs_no_tools.messages = {message_user}; - - common_chat_templates_inputs inputs_tools; - inputs_tools.messages = {message_user}; - inputs_tools.tools = {special_function_tool}; - - common_chat_templates_inputs inputs_tools_builtin; - inputs_tools_builtin.messages = {message_user}; - inputs_tools_builtin.tools = {python_tool}; - - { - // Not supported yet - auto tmpls = read_templates("models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja"); - assert_equals(COMMON_CHAT_FORMAT_CONTENT_ONLY, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format); - assert_equals(COMMON_CHAT_FORMAT_GENERIC, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - } - { - auto tmpls = read_templates("models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja"); - std::vector end_tokens{ "<|END_OF_TURN_TOKEN|>" }; - - for (const auto & inputs : { inputs_no_tools, inputs_tools }) { - auto params = common_chat_templates_apply(tmpls.get(), inputs); - assert_equals(COMMON_CHAT_FORMAT_COMMAND_R7B, params.format); - assert_equals(false, params.thinking_forced_open); + // JSON schemas + const char * invoice_schema = R"({ + "type": "object", + "properties": { + "amount": {"type": "number"}, + "date": {"type": "string"} } + })"; - assert_msg_equals(message_assist, - test_chat_parse( - "Hello, world!\nWhat's up?", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_COMMAND_R7B})); - assert_msg_equals(message_assist, - test_chat_parse( - "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_COMMAND_R7B})); - assert_msg_equals(message_assist_thoughts, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_thoughts_unparsed_deepseek, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - /* .reasoning_in_content = */ true, - /* .thinking_forced_open = */ false, - })); - assert_msg_equals(message_assist_thoughts_unparsed_r7b, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_COMMAND_R7B})); - assert_msg_equals(message_assist_thoughts, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_thoughts_call_idx, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_ACTION|>[\n" - " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" - "]<|END_ACTION|>", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_thoughts_no_content, - test_chat_parse( - "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" - "<|START_ACTION|>[\n" - " {\"tool_call_id\": \"0\", \"tool_name\": \"special", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_COMMAND_R7B, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - - test_templates(tmpls.get(), end_tokens, message_assist_call_idx, tools, - "<|START_THINKING|><|END_THINKING|>" - "<|START_ACTION|>[\n" - " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" - "]<|END_ACTION|>", - /* expect_grammar_triggered= */ true, - /* test_grammar_if_triggered= */ true, - COMMON_REASONING_FORMAT_DEEPSEEK); - test_templates(tmpls.get(), end_tokens, message_assist, tools, - "<|START_RESPONSE|>Hello, world!\n" - "What's up?<|END_RESPONSE|>", - /* expect_grammar_triggered= */ false); - } - // TODO @ngxson : generic tool calls is too costly to maintain, consider removing it in the future { - auto tmpls = read_templates("models/templates/google-gemma-2-2b-it.jinja"); - std::vector end_tokens{ "" }; + // Ministral-3-14B-Reasoning-2512 + auto tst = peg_tester("models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja", detailed_debug); - assert_equals(COMMON_CHAT_FORMAT_CONTENT_ONLY, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format); - assert_equals(COMMON_CHAT_FORMAT_GENERIC, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - assert_equals(COMMON_CHAT_FORMAT_GENERIC, - common_chat_templates_apply( - read_templates("models/templates/microsoft-Phi-3.5-mini-instruct.jinja").get(), - inputs_tools) - .format); + tst.test("Hello, world!\nWhat's up?").expect(message_assist).run(); - // Generic tool calls doesn't generate / parse content-only messages symmetrically. + tst.test("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?") + .expect_content("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?") + .run(); - assert_equals( - simple_assist_msg("{ \"tool_call\" : { \"name\" : \"t"), - test_chat_parse( - "{ \"tool_call\" : { \"name\" : \"t", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_GENERIC, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - /* .reasoning_in_content = */ false, - /* .thinking_forced_open = */ true, - /* .parse_tool_calls = */ false, - })); - assert_equals( - message_assist_empty, - test_chat_parse( - "{ \"tool_call\" : { \"name\" : \"t", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_GENERIC})); + tst.test("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .expect(message_assist_thoughts) + .run(); - assert_equals( - simple_assist_msg("", "", "puppeteer_screenshot", "{\"name\":\"servethehome_homepage\","), - test_chat_parse( - R"({"tool_call": {"name": "puppeteer_screenshot", "arguments": {"name": "servethehome_homepage",)", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_GENERIC})); - - assert_equals( - message_assist_call_empty_args, - test_chat_parse( - "{ \"tool_call\" : { \"name\" : \"special_function\"", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_GENERIC})); - assert_equals( - message_assist_call_cutoff_args, - test_chat_parse( - "{ \"tool_call\" : { \"name\" : \"special_function\", \"arguments\" : { \"arg", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_GENERIC})); - - assert_msg_equals(message_assist, - test_chat_parse( - "{\n" - " \"response\": \"Hello, world!\\nWhat's up?\"\n" - "}", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_GENERIC})); -#if 0 - test_templates(tmpls.get(), end_tokens, message_assist_call_id, tools, - "{\n" - " \"tool_calls\": [\n" - " {\n" - " \"name\": \"special_function\",\n" - " \"arguments\": {\n" - " \"arg1\": 1\n" - " },\n" - " \"id\": \"123456789\"\n" - " }\n" - " ],\n" - " \"content\": \"\"\n" - "}"); -#endif - } - { - auto tmpls = read_templates("models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja"); - std::vector end_tokens{ "" }; - - assert_equals(COMMON_CHAT_FORMAT_MISTRAL_NEMO, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - - test_templates(tmpls.get(), end_tokens, message_assist, tools, "Hello, world!\nWhat's up?", /* expect_grammar_triggered= */ false); - test_templates( - tmpls.get(), end_tokens, message_assist_call_id, tools, - "[TOOL_CALLS][{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}, \"id\": \"123456789\"}]"); - } - { - assert_msg_equals( - simple_assist_msg("Réponse", "raisonnement"), - test_chat_parse( - message_assist_thoughts_unparsed_magistral.content, - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_MAGISTRAL, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO, - })); - } - { - auto tmpls = read_templates("models/templates/Qwen-QwQ-32B.jinja"); - std::vector end_tokens{ "<|im_end|>" }; - - assert_equals(COMMON_CHAT_FORMAT_HERMES_2_PRO, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format); - assert_equals(COMMON_CHAT_FORMAT_HERMES_2_PRO, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - } - { - auto tmpls = read_templates("models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja"); - std::vector end_tokens{ "<|im_end|>" }; - - assert_equals(COMMON_CHAT_FORMAT_HERMES_2_PRO, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format); - assert_equals(COMMON_CHAT_FORMAT_HERMES_2_PRO, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - assert_equals( - COMMON_CHAT_FORMAT_HERMES_2_PRO, - common_chat_templates_apply( - read_templates("models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja").get(), - inputs_tools) - .format); - assert_equals( - COMMON_CHAT_FORMAT_HERMES_2_PRO, - common_chat_templates_apply( - read_templates("models/templates/Qwen-Qwen2.5-7B-Instruct.jinja").get(), - inputs_tools) - .format); - - // Test parsing - assert_msg_equals( - simple_assist_msg("", "", "python", ""), - common_chat_parse( - "```json\n" - " { \"name\" : \"python\"", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_HERMES_2_PRO})); - assert_msg_equals( - simple_assist_msg("Let's call something\n"), - common_chat_parse( - "Let's call something\n" - "{\"name\"", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals( - simple_assist_msg("Let's call something\n"), - common_chat_parse( - "Let's call something\n" - "{\"name", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_call_thoughts, - common_chat_parse( - // QwQ-32B's template adds a trailing if add_generation_prompt - "I'm\nthinking\n" - "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_HERMES_2_PRO, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - /* .reasoning_in_content = */ false, - /* .thinking_forced_open = */ true, - })); - assert_msg_equals( - message_assist_call, - common_chat_parse( - "\n" - "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n" - "") - .enable_thinking(true) + tst.test(R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})") .reasoning_format(COMMON_REASONING_FORMAT_AUTO) .tools({ special_function_tool }) - .expect(simple_assist_msg("", "I should use a tool", "special_function", R"({"arg1": 1})")) + .expect(message_assist_call) + .run(); + + tst.test( + "[THINK]I'm\nthinking[/THINK]" + R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .tools({ special_function_tool }) + .expect(message_assist_call_thoughts) + .run(); + + tst.test(R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})" + R"([TOOL_CALLS]special_function_with_opt[ARGS]{"arg1": 1, "arg2": 2})") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .parallel_tool_calls(true) + .tools({ + special_function_tool, special_function_tool_with_optional_param + }) + .expect_tool_calls({ + { "special_function", R"({"arg1": 1})", {} }, + { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", {} }, + }) + .run(); + + tst.test( + "[THINK]I need to output the invoice details in JSON[/THINK]" + "```json\n" + R"({"amount": 123.45, "date": "2025-12-03"})" + "\n```") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .json_schema(invoice_schema) + .expect_reasoning("I need to output the invoice details in JSON") + .expect_content(R"({"amount": 123.45, "date": "2025-12-03"})") .run(); } + { + // NVIDIA Nemotron-3 Nano + auto tst = peg_tester("models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja", detailed_debug); + + tst.test("Hello, world!\nWhat's up?").enable_thinking(false).expect(message_assist).run(); + + tst.test("I'm\nthinking\n\nHello, world!\nWhat's up?") + .enable_thinking(false) + .reasoning_format(COMMON_REASONING_FORMAT_NONE) + .expect_content("I'm\nthinking\n\nHello, world!\nWhat's up?") + .run(); + + tst.test("I'm\nthinking\n\nHello, world!\nWhat's up?") + .enable_thinking(true) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .expect(message_assist_thoughts) + .run(); + + tst.test( + "\n" + "\n" + "\n1\n\n" + "\n" + "") + .enable_thinking(false) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .tools({ special_function_tool }) + .expect(message_assist_call) + .run(); + + tst.test( + "I'm\nthinking\n\n" + "\n" + "\n" + "\n1\n\n" + "\n" + "") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .tools({ special_function_tool }) + .expect(message_assist_call_thoughts) + .run(); + + tst.test( + "\n" + "\n" + "\n1\n\n" + "\n" + "\n" + "\n" + "\n" + "\n1\n\n" + "\n2\n\n" + "\n" + "") + .enable_thinking(false) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .parallel_tool_calls(true) + .tools({ + special_function_tool, special_function_tool_with_optional_param + }) + .expect_tool_calls({ + { "special_function", R"({"arg1": 1})", {} }, + { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", {} }, + }) + .run(); + + tst.test( + "\n" + "\n" + "\n" + "def hello():\n" + " print(\"Hello, world!\")\n" + "\n" + "hello()\n" + "\n" + "\n" + "") + .enable_thinking(false) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .tools({ + python_tool + }) + .expect_tool_calls({ + { "python", "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}", {} }, + }) + .run(); + + tst.test( + "I need to output the invoice details in JSON\n" + "\n" + R"({"amount": 123.45, "date": "2025-12-03"})") + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .enable_thinking(true) + .json_schema(invoice_schema) + .expect_reasoning("I need to output the invoice details in JSON") + .expect_content(R"({"amount": 123.45, "date": "2025-12-03"})") + .run(); + } + + { + // CohereForAI Command-R 7B (2024-tool_use) + auto tst = peg_tester("models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja", detailed_debug); + + tst.test("<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>").expect(message_assist).run(); + + tst.test( + "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" + "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>") + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .expect(message_assist_thoughts) + .run(); + + tst.test( + "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" + "<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>") + .expect(message_assist_thoughts_unparsed_r7b) + .run(); + + tst.test( + "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" + "<|START_ACTION|>[\n" + " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" + "]<|END_ACTION|>") + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ special_function_tool }) + .expect(message_assist_thoughts_call_idx) + .run(); + + tst.test( + "<|START_THINKING|>I'm\nthinking<|END_THINKING|>" + "<|START_ACTION|>[\n" + " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", ") + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ special_function_tool }) + .is_partial(true) + .expect(message_assist_thoughts_partial_call) + .run(); + + tst.test( + "<|START_THINKING|><|END_THINKING|>" + "<|START_ACTION|>[\n" + " {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" + "]<|END_ACTION|>") + .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + .tools({ special_function_tool }) + .expect(message_assist_call_idx) + .run(); + } + + { + // Google Gemma 2 2B - does not support tool calling + auto tst = peg_tester("models/templates/google-gemma-2-2b-it.jinja"); + + tst.test("Hello, world!").expect(simple_assist_msg("Hello, world!")).run(); + + tst.test("Line 1\nLine 2\nLine 3").expect(simple_assist_msg("Line 1\nLine 2\nLine 3")).run(); + } + + { + // Qwen-QwQ-32B (reasoning model) + auto tst = peg_tester("models/templates/Qwen-QwQ-32B.jinja"); + + // QwQ always has thinking forced open - input starts after the \n in the prompt + tst.test("Let me think about this...\n\nThe answer is 42.") + .enable_thinking(true) + .reasoning_format(COMMON_REASONING_FORMAT_AUTO) + .expect(simple_assist_msg("The answer is 42.", "Let me think about this...")) + .run(); + + tst.test("Hello, world!").expect(simple_assist_msg("Hello, world!")).run(); + } { // NousResearch-Hermes-2-Pro and Hermes-3 (tool calling models) auto tst = peg_tester("models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja", detailed_debug); @@ -1400,99 +1308,12 @@ static void test_template_output_parsers() { // Note: Hermes template doesn't support thinking/reasoning natively // Note: We only support one tool calling format per template, no alternate formats } - { - auto tmpls = read_templates("models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja"); - std::vector end_tokens{ "<|end_of_text|>" }; - - assert_equals(COMMON_CHAT_FORMAT_GRANITE, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format); - - assert_equals(COMMON_CHAT_FORMAT_GRANITE, common_chat_templates_apply(tmpls.get(), inputs_tools).format); - - // Test parsing regular content - assert_msg_equals(message_assist, - common_chat_parse( - "Hello, world!\nWhat's up?", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_GRANITE})); - assert_msg_equals( - message_assist, - common_chat_parse( - "Hello, world!\nWhat's up?", - /* is_partial= */ true, - {COMMON_CHAT_FORMAT_GRANITE})); - - // Test parsing content with thinking - assert_msg_equals(message_assist_thoughts, - common_chat_parse( - "I'm\nthinkingHello, world!\nWhat's up?", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_GRANITE, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_thoughts_unparsed_deepseek, - common_chat_parse( - "I'm\nthinkingHello, world!\nWhat's up?", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_GRANITE})); - assert_msg_equals(message_assist_thoughts, - common_chat_parse( - "I'm\nthinkingHello, world!\nWhat's up?", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_GRANITE, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(message_assist_thoughts, - common_chat_parse( - "I'm\nthinkingHello, world!\nWhat's up?", - /* is_partial= */ false, - { - /* .format = */ COMMON_CHAT_FORMAT_GRANITE, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals(simple_assist_msg("I'm\nthinkingHello, world!\nWhat's up?"), - common_chat_parse( - "I'm\nthinkingHello, world!\nWhat's up?", - /* is_partial= */ false, - {COMMON_CHAT_FORMAT_GRANITE})); - assert_msg_equals(message_assist_empty, - common_chat_parse( - "I'm\nthinking", - /* is_partial= */ true, - { - /* .format = */ COMMON_CHAT_FORMAT_GRANITE, - /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK, - })); - assert_msg_equals( - message_assist_empty, - common_chat_parse( - "I'm\nthinkingI'm\nthinkingHello, world!\nWhat's up?") - .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) - .expect(message_assist_thoughts) - .run(); + // TODO: pending support for WRAPPED_WITH_REASONING + // tst.test("I'm\nthinkingHello, world!\nWhat's up?") + // .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) + // .expect(message_assist_thoughts) + // .run(); } { @@ -1531,38 +1353,6 @@ static void test_template_output_parsers() { .expect(message_assist_call) .run(); - // Test deltas: the number of tool calls in partial parses should never decrease - std::string tool_msg = "\n" - "\n" - "[1, 2, 3]\n" - ""; - std::size_t previousToolCalls = 0; - for (std::size_t i = std::string("").length(); i < tool_msg.length() - 1; i++) { - auto partial = tool_msg.substr(0, i); - auto partial_res = common_chat_parse(partial, true, { COMMON_CHAT_FORMAT_SEED_OSS, COMMON_REASONING_FORMAT_DEEPSEEK }); - if (partial_res.tool_calls.size() < previousToolCalls) { - throw std::runtime_error("Tool call size decreased on partial: " + partial + " from " + std::to_string(previousToolCalls) + " to " + std::to_string(partial_res.tool_calls.size())); - } - previousToolCalls = partial_res.tool_calls.size(); - } - - // Test multiple parameters in tool call - common_chat_msg msg_multi_param; - msg_multi_param.role = "assistant"; - msg_multi_param.tool_calls.push_back({"process_data", "{\"input\": \"test\", \"format\": \"json\"}", ""}); - assert_msg_equals( - msg_multi_param, - common_chat_parse( - "\n" - "\n" - "1\n" - "\n" - "") - .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) - .tools({ special_function_tool }) - .expect(simple_assist_msg("", "I need to call a function", "special_function", R"({"arg1": 1})")) - .run(); - tst.test( "\n" "\n" @@ -1588,9 +1378,7 @@ static void test_template_output_parsers() { tst.test( "\n" "\n" - "\n" - "[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]\n" - "\n" + "[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]\n" "\n" "") .tools({ @@ -1605,9 +1393,7 @@ static void test_template_output_parsers() { tst.test( "\n" "\n" - "\n" - "[{'item': 'Check stuff', 'selected': false}, {'item': 'Prepare stuff', 'selected': true}]\n" - "\n" + "[{'item': 'Check stuff', 'selected': false}, {'item': 'Prepare stuff', 'selected': true}]\n" "\n" "") .tools({ @@ -1618,19 +1404,19 @@ static void test_template_output_parsers() { }) .run(); - // single-quote normalization and tool call with inside quotes + // tool call with inside quotes tst.test( "\n" "\n" "\n" "foo.cpp\n" "\n" - "\n" + "" "def foo(arg = \"14\"):\n" " return arg + \"bar\"\n" "\n" "\n" - "\n" + "" "def foo(arg = \"15\"):\n" " pass\n" "\n" @@ -1809,7 +1595,7 @@ static void test_template_output_parsers() { // GLM-4.6 tests - format: function_name\n...\n...\n { - auto tst = peg_tester("models/templates/GLM-4.6.jinja"); + auto tst = peg_tester("models/templates/GLM-4.6.jinja", detailed_debug); tst.test( "special_function\n" "arg1\n1\n" @@ -1904,8 +1690,8 @@ static void test_template_output_parsers() { { auto tst = peg_tester("models/templates/MiniMax-M2.jinja", detailed_debug); tst.test( - "\n1\n") + "\n\n1\n\n") .tools({ special_function_tool }) .expect(message_assist_call) .run(); @@ -1915,7 +1701,7 @@ static void test_template_output_parsers() { // Format: [{"name": "func", "arguments": {...}}] { auto tst = peg_tester("models/templates/NVIDIA-Nemotron-Nano-v2.jinja", detailed_debug); - tst.test("[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]") + tst.test("[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]") .tools({ special_function_tool }) .expect(message_assist_call) .run(); @@ -2011,7 +1797,7 @@ static void test_template_output_parsers() { .run(); tst.test( "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n" - "```json\n{\"arg1\": 1}\n```<|tool▁call▁end|><|tool▁calls▁end|>") + "```json\n{\"arg1\": 1}```<|tool▁call▁end|><|tool▁calls▁end|>") .tools({ special_function_tool }) .parallel_tool_calls(true) .expect(message_assist_call) @@ -2028,7 +1814,7 @@ static void test_template_output_parsers() { .run(); tst.test( "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n" - "```json\n{\"arg1\": 1}\n```<|tool▁call▁end|><|tool▁calls▁end|>") + "```json\n{\"arg1\": 1}```<|tool▁call▁end|><|tool▁calls▁end|>") .tools({ special_function_tool }) .expect(message_assist_call) .run(); @@ -2361,7 +2147,6 @@ static void test_msg_diffs_compute() { } int main(int argc, char ** argv) { - common_log_set_verbosity_thold(999); bool detailed_debug = false; bool only_run_filtered = false; @@ -2375,6 +2160,7 @@ int main(int argc, char ** argv) { } if (arg == "--detailed") { detailed_debug = true; + common_log_set_verbosity_thold(999); } } diff --git a/tests/test-peg-parser.cpp b/tests/test-peg-parser.cpp index 220745d029..7d22d77612 100644 --- a/tests/test-peg-parser.cpp +++ b/tests/test-peg-parser.cpp @@ -20,6 +20,7 @@ int main(int argc, char *argv[]) { t.test("json", test_json_parser); t.test("gbnf", test_gbnf_generation); t.test("serialization", test_json_serialization); + t.test("python-dict", test_python_dict_parser); return t.summary(); } diff --git a/tools/parser/CMakeLists.txt b/tools/parser/CMakeLists.txt index 4bf40a8717..73157b0a0e 100644 --- a/tools/parser/CMakeLists.txt +++ b/tools/parser/CMakeLists.txt @@ -6,3 +6,12 @@ target_compile_features(${TARGET} PRIVATE cxx_std_17) if(LLAMA_TOOLS_INSTALL) install(TARGETS ${TARGET} RUNTIME) endif() + +set(TARGET llama-template-analysis) +add_executable(${TARGET} template-analysis.cpp) +target_link_libraries(${TARGET} PRIVATE common llama ${CMAKE_THREAD_LIBS_INIT}) +target_compile_features(${TARGET} PRIVATE cxx_std_17) + +if(LLAMA_TOOLS_INSTALL) + install(TARGETS ${TARGET} RUNTIME) +endif() diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp index 551d2bcf9d..b8b4f3dfd3 100644 --- a/tools/parser/debug-template-parser.cpp +++ b/tools/parser/debug-template-parser.cpp @@ -1,15 +1,19 @@ #include "../src/llama-grammar.h" #include "chat-auto-parser.h" +#include "chat-diff-analyzer.h" #include "chat.h" #include "common.h" #include "gguf.h" +#include "jinja/runtime.h" #include "log.h" #include +#include #include #include #include "nlohmann/json.hpp" +#include "peg-parser.h" using json = nlohmann::ordered_json; @@ -239,7 +243,7 @@ static json build_tools_definition() { { "type", "string" }, { "description", "Second parameter" } }); - parameters_schema["required"] = json::array({ "param1", "param2" }); + parameters_schema["required"] = json::array({ "param1" }); return json::array({ json{ { "type", "function" }, @@ -324,68 +328,21 @@ static void render_all_scenarios(const common_chat_template & tmpl, } } -static const char * reasoning_mode_to_str(content_structure::reasoning_mode_type mode) { - switch (mode) { - case content_structure::REASONING_NONE: - return "NONE"; - case content_structure::REASONING_OPTIONAL: - return "OPTIONAL"; - case content_structure::REASONING_FORCED_OPEN: - return "FORCED_OPEN"; - } - return "UNKNOWN"; -} - -static const char * content_mode_to_str(content_structure::content_mode_type mode) { - switch (mode) { - case content_structure::CONTENT_PLAIN: - return "PLAIN"; - case content_structure::CONTENT_ALWAYS_WRAPPED: - return "ALWAYS_WRAPPED"; - case content_structure::CONTENT_WRAPPED_WITH_REASONING: - return "WRAPPED_WITH_REASONING"; - } - return "UNKNOWN"; -} - -static const char * function_format_to_str(enum tool_call_structure::function_format fmt) { - switch (fmt) { - case tool_call_structure::FUNC_JSON_OBJECT: - return "JSON_OBJECT"; - case tool_call_structure::FUNC_TAG_WITH_NAME: - return "TAG_WITH_NAME"; - case tool_call_structure::FUNC_TAG_NAME_ONLY: - return "TAG_NAME_ONLY"; - case tool_call_structure::FUNC_PREFIXED_INDEXED: - return "PREFIXED_INDEXED"; - case tool_call_structure::FUNC_NAME_AS_KEY: - return "NAME_AS_KEY"; - case tool_call_structure::FUNC_BRACKET_TAG: - return "BRACKET_TAG"; - case tool_call_structure::FUNC_RECIPIENT_BASED: - return "RECIPIENT_BASED"; - case tool_call_structure::FUNC_MARKDOWN_CODE_BLOCK: - return "MARKDOWN_CODE_BLOCK"; - } - return "UNKNOWN"; -} - -static const char * argument_format_to_str(enum tool_call_structure::argument_format fmt) { - switch (fmt) { - case tool_call_structure::ARGS_JSON: - return "JSON"; - case tool_call_structure::ARGS_TAGGED: - return "TAGGED"; - case tool_call_structure::ARGS_KEY_VALUE_TAGS: - return "KEY_VALUE_TAGS"; - } - return "UNKNOWN"; +template +static std::string mode_to_str(T mode) { + std::ostringstream os; + os << mode; + return os.str(); } int main(int argc, char ** argv) { // Set log level to most verbose to capture all debug output common_log_set_verbosity_thold(99); + if (std::getenv("LLAMA_DEBUG_JINJA") != nullptr) { + jinja::enable_debug(true); + } + debug_options opts; if (!parse_options(argc, argv, opts)) { return 1; @@ -434,48 +391,7 @@ int main(int argc, char ** argv) { LOG_ERR(" TEMPLATE ANALYSIS\n"); LOG_ERR("================================================================================\n"); - template_analysis_result analysis = template_analyzer::analyze_template(chat_template); - - LOG_ERR("\n=== Analysis Results ===\n"); - - LOG_ERR("\n--- Content Structure (Phase 1) ---\n"); - LOG_ERR("reasoning_mode: %s\n", reasoning_mode_to_str(analysis.content.reasoning_mode)); - LOG_ERR("reasoning_start: '%s'\n", analysis.content.reasoning_start.c_str()); - LOG_ERR("reasoning_end: '%s'\n", analysis.content.reasoning_end.c_str()); - LOG_ERR("content_mode: %s\n", content_mode_to_str(analysis.content.content_mode)); - LOG_ERR("content_start: '%s'\n", analysis.content.content_start.c_str()); - LOG_ERR("content_end: '%s'\n", analysis.content.content_end.c_str()); - - LOG_ERR("\n--- Tool Structure (Phase 2) ---\n"); - LOG_ERR("supports_tools: %s\n", analysis.tools.supports_tools ? "true" : "false"); - LOG_ERR("function_format: %s\n", function_format_to_str(analysis.tools.function_format)); - LOG_ERR("argument_format: %s\n", argument_format_to_str(analysis.tools.argument_format)); - LOG_ERR("tool_section_start: '%s'\n", analysis.tools.tool_section_start.c_str()); - LOG_ERR("tool_section_end: '%s'\n", analysis.tools.tool_section_end.c_str()); - LOG_ERR("function_prefix: '%s'\n", analysis.tools.function_prefix.c_str()); - LOG_ERR("function_suffix: '%s'\n", analysis.tools.function_suffix.c_str()); - LOG_ERR("function_close: '%s'\n", analysis.tools.function_close.c_str()); - LOG_ERR("arg_prefix: '%s'\n", analysis.tools.arg_prefix.c_str()); - LOG_ERR("arg_suffix: '%s'\n", analysis.tools.arg_suffix.c_str()); - LOG_ERR("arg_close: '%s'\n", analysis.tools.arg_close.c_str()); - LOG_ERR("name_field: '%s'\n", analysis.tools.name_field.c_str()); - LOG_ERR("args_field: '%s'\n", analysis.tools.args_field.c_str()); - LOG_ERR("id_field: '%s'\n", analysis.tools.id_field.c_str()); - - // Additional fields for special formats - if (analysis.tools.function_format == tool_call_structure::FUNC_PREFIXED_INDEXED) { - LOG_ERR("\n--- Prefixed-Indexed Format Details ---\n"); - LOG_ERR("per_call_start: '%s'\n", analysis.tools.per_call_start.c_str()); - LOG_ERR("function_namespace: '%s'\n", analysis.tools.function_namespace.c_str()); - LOG_ERR("args_marker: '%s'\n", analysis.tools.args_marker.c_str()); - LOG_ERR("per_call_end: '%s'\n", analysis.tools.per_call_end.c_str()); - } - if (analysis.tools.function_format == tool_call_structure::FUNC_BRACKET_TAG) { - LOG_ERR("\n--- Bracket-Tag Format Details ---\n"); - LOG_ERR("per_call_start: '%s'\n", analysis.tools.per_call_start.c_str()); - LOG_ERR("id_marker: '%s'\n", analysis.tools.id_marker.c_str()); - LOG_ERR("args_marker: '%s'\n", analysis.tools.args_marker.c_str()); - } + diff_analysis_result analysis = differential_analyzer::analyze(chat_template); // Generate Parser templates_params params; @@ -494,10 +410,45 @@ int main(int argc, char ** argv) { } params.parallel_tool_calls = false; - auto parser_data = universal_peg_generator::generate_parser(analysis, chat_template, params); + auto parser_data = universal_peg_generator::generate_parser(chat_template, params, analysis); + + 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("\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(), + std::string(""), [] (const std::string & a, const std::string & b) { return a.empty() ? b : a + ", " + b; } + ).c_str()); LOG_ERR("\n=== Generated Parser ===\n"); - LOG_ERR("%s\n", json::parse(parser_data.parser).dump(4).c_str()); + common_peg_arena arena; + arena.load(parser_data.parser); + LOG_ERR("%s\n", arena.dump(arena.root()).c_str()); LOG_ERR("\n=== Generated Grammar ===\n"); LOG_ERR("%s\n", parser_data.grammar.c_str()); diff --git a/tools/parser/template-analysis.cpp b/tools/parser/template-analysis.cpp new file mode 100644 index 0000000000..0fbcc09390 --- /dev/null +++ b/tools/parser/template-analysis.cpp @@ -0,0 +1,610 @@ +#include "chat-auto-parser.h" +#include "chat-auto-parser-helpers.h" +#include "chat.h" +#include "log.h" +#include "jinja/caps.h" +#include "jinja/runtime.h" + +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" + +using json = nlohmann::ordered_json; + +// ANSI color codes - using 256-color palette for brighter colors (all bold) +#define ANSI_RESET "\033[0m" +#define ANSI_PURPLE "\033[1m\x1b[38;5;126m" // Bold bright purple for main headers +#define ANSI_CYAN "\033[1m\x1b[38;5;81m" // Bold bright cyan for section headers +#define ANSI_BLUE "\033[1m\x1b[38;5;12m" // Bold bright blue for labels +#define ANSI_ORANGE "\033[1m\x1b[38;5;209m" // Bold orange for right differences +#define ANSI_GREEN "\033[1m\x1b[38;5;83m" // Bold bright green for left differences +#define ANSI_GRAY "\033[1m\x1b[38;5;240m" // Bold gray (used for "no variables" message) +#define ANSI_BOLD "\033[1m" // Standalone bold +#define ANSI_PREFIX "\033[1m\x1b[38;5;176m" // Bold color for common prefix +#define ANSI_SUFFIX "\033[1m\x1b[38;5;61m" // Bold color for common suffix + +// All template paths extracted from tests/test-chat.cpp +static const std::vector ALL_TEMPLATE_PATHS = { + "models/templates/Apertus-8B-Instruct.jinja", + "models/templates/Apriel-1.6-15b-Thinker-fixed.jinja", + "models/templates/ByteDance-Seed-OSS.jinja", + "models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja", + "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja", + "models/templates/GLM-4.6.jinja", + "models/templates/GLM-4.7-Flash.jinja", + "models/templates/Kimi-K2-Instruct.jinja", + "models/templates/Kimi-K2-Thinking.jinja", + "models/templates/MiMo-VL.jinja", + "models/templates/MiniMax-M2.jinja", + "models/templates/Mistral-Small-3.2-24B-Instruct-2506.jinja", + "models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja", + "models/templates/NVIDIA-Nemotron-Nano-v2.jinja", + "models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja", + "models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja", + "models/templates/Qwen-QwQ-32B.jinja", + "models/templates/Qwen-Qwen2.5-7B-Instruct.jinja", + "models/templates/Qwen3-Coder.jinja", + "models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja", + "models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja", + "models/templates/deepseek-ai-DeepSeek-V3.1.jinja", + "models/templates/fireworks-ai-llama-3-firefunction-v2.jinja", + "models/templates/google-gemma-2-2b-it.jinja", + "models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja", + "models/templates/llama-cpp-deepseek-r1.jinja", + "models/templates/meetkai-functionary-medium-v3.1.jinja", + "models/templates/meetkai-functionary-medium-v3.2.jinja", + "models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja", + "models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja", + "models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja", + "models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja", + "models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja", + "models/templates/moonshotai-Kimi-K2.jinja", + "models/templates/openai-gpt-oss-120b.jinja", + "models/templates/unsloth-Apriel-1.5.jinja", + "models/templates/unsloth-mistral-Devstral-Small-2507.jinja", +}; + +struct analysis_options { + std::vector template_paths; + bool analyze_all = false; +}; + +static std::string read_file(const std::string & path) { + std::ifstream fin(path, std::ios::binary); + if (!fin.is_open()) { + throw std::runtime_error("Could not open file: " + path); + } + std::ostringstream buf; + buf << fin.rdbuf(); + return buf.str(); +} + +static void print_usage(const char * program_name) { + LOG_ERR("Usage: %s [options]\n", program_name); + LOG_ERR("\nOptions:\n"); + LOG_ERR(" --template Analyze specific template from test suite (e.g., 'deepseek' or 'DeepSeek-V3.1')\n"); + LOG_ERR(" --template-file Analyze custom template file\n"); + LOG_ERR(" --all Analyze all templates from test suite\n"); + LOG_ERR("\nExamples:\n"); + LOG_ERR(" %s --all\n", program_name); + LOG_ERR(" %s --template deepseek\n", program_name); + LOG_ERR(" %s --template-file my-template.jinja\n", program_name); +} + +static bool parse_options(int argc, char ** argv, analysis_options & opts) { + if (argc < 2) { + print_usage(argv[0]); + return false; + } + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--all") { + opts.analyze_all = true; + } else if (arg == "--template") { + if (i + 1 >= argc) { + LOG_ERR("--template requires an argument\n"); + return false; + } + std::string pattern = argv[++i]; + std::transform(pattern.begin(), pattern.end(), pattern.begin(), ::tolower); + + // Find matching templates + bool found = false; + for (const auto & path : ALL_TEMPLATE_PATHS) { + std::string path_lower = path; + std::transform(path_lower.begin(), path_lower.end(), path_lower.begin(), ::tolower); + if (path_lower.find(pattern) != std::string::npos) { + opts.template_paths.push_back(path); + found = true; + } + } + + if (!found) { + LOG_ERR("No templates found matching: %s\n", pattern.c_str()); + return false; + } + } else if (arg == "--template-file") { + if (i + 1 >= argc) { + LOG_ERR("--template-file requires an argument\n"); + return false; + } + opts.template_paths.push_back(argv[++i]); + } else { + LOG_ERR("Unknown option: %s\n", arg.c_str()); + print_usage(argv[0]); + return false; + } + } + + if (opts.analyze_all) { + opts.template_paths = ALL_TEMPLATE_PATHS; + } + + if (opts.template_paths.empty()) { + LOG_ERR("No templates specified\n"); + print_usage(argv[0]); + return false; + } + + return true; +} + +static json build_tools_definition() { + json parameters_schema = json::object(); + parameters_schema["type"] = "object"; + parameters_schema["properties"] = json::object(); + parameters_schema["properties"]["param1"] = json::object({ + { "type", "string" }, + { "description", "First parameter" } + }); + parameters_schema["properties"]["param2"] = json::object({ + { "type", "string" }, + { "description", "Second parameter" } + }); + parameters_schema["required"] = json::array({ "param1", "param2" }); + + return json::array({ + json{ { "type", "function" }, + { "function", json{ { "name", "test_function_name" }, + { "description", "A test function for debugging" }, + { "parameters", parameters_schema } } } } + }); +} + +// Helper to create a tool call with arguments as JSON object +static json build_tool_call(const std::string & name, const json & args_object, const std::string & id = "call_001") { + return json{ + {"id", id}, + {"type", "function"}, + {"function", json{ + {"name", name}, + {"arguments", args_object} // Pass as JSON object, not serialized string + }} + }; +} + +// Helper functions to create repeating message definitions +static json make_user_msg() { + return json{ + {"role", "user"}, + {"content", "Hello, please help me."} + }; +} + +static json make_user_msg2() { + return json{ + {"role", "user"}, + {"content", "Thank you."} + }; +} + +static json make_user_msg2_continue() { + return json{ + {"role", "user"}, + {"content", "Continue."} + }; +} + +static json make_assistant_no_tool() { + return json{ + {"role", "assistant"}, + {"content", "Let me help you."} + }; +} + +static json make_assistant_one_tool() { + return json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })} + }; +} + +static json make_assistant_two_tools() { + return json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})), + build_tool_call("test_function_name", json::object({{"param1", "value3"}, {"param2", "value4"}}), "call_002") + })} + }; +} + +static json make_assistant_no_reasoning() { + return json{ + {"role", "assistant"}, + {"content", "I can help you with that."} + }; +} + +static json make_assistant_with_reasoning() { + return json{ + {"role", "assistant"}, + {"content", "I can help you with that."}, + {"reasoning_content", "The user is asking for help. I should respond positively."} + }; +} + +static json make_assistant_one_tool_with_reasoning() { + return json{ + {"role", "assistant"}, + {"content", nullptr}, + {"tool_calls", json::array({ + build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) + })}, + {"reasoning_content", "I need to call the tool first."} + }; +} + +static void print_diff_split(const std::string & title, const diff_split & diff) { + LOG_ERR("\n%s=== %s ===%s\n", ANSI_CYAN, title.c_str(), ANSI_RESET); + LOG_ERR("%sCommon Prefix:%s '%s'\n", ANSI_PREFIX, ANSI_RESET, diff.prefix.c_str()); + LOG_ERR("%sCommon Suffix:%s '%s'\n", ANSI_SUFFIX, ANSI_RESET, diff.suffix.c_str()); + LOG_ERR("%sLeft (difference):%s '%s'\n", ANSI_GREEN, ANSI_RESET, diff.left.c_str()); + LOG_ERR("%sRight (difference):%s '%s'\n", ANSI_ORANGE, ANSI_RESET, diff.right.c_str()); +} + +static void check_reasoning_variables(const common_chat_template & tmpl) { + LOG_ERR("\n%s=== Checking Reasoning Variables ===%s\n", ANSI_CYAN, ANSI_RESET); + + try { + // Create a list of candidate reasoning/thinking variable names to probe + std::vector candidate_vars = { + "enable_reasoning", + "use_reasoning", + "reasoning_enabled", + "has_reasoning", + "reasoning_mode", + "reasoning_format", + "reasoning_active", + "with_reasoning", + "use_thinking", + "thinking_enabled", + "has_thinking", + "thinking_mode", + "thinking_format", + "thinking_active", + "with_thinking", + "enable_reason", + "reason_enabled", + "enable_think", + "think_enabled", + }; + + jinja::context ctx; + ctx.is_get_stats = true; + + json messages = json::array({ + json{ + {"role", "user"}, + {"content", "Test message"} + }, + json{ + {"role", "assistant"}, + {"content", "Response"}, + {"reasoning_content", "Some reasoning"} + } + }); + + // Set up base context + jinja::global_from_json(ctx, json{ + {"messages", messages}, + {"tools", json::array()}, + {"bos_token", ""}, + {"eos_token", ""}, + {"add_generation_prompt", false}, + {"enable_thinking", true} // Already passed, so we'll exclude this from results + }, true); + + // Add candidate variables as undefined to probe which ones are accessed + for (const auto & var_name : candidate_vars) { + ctx.set_val(var_name, jinja::mk_val(var_name)); + } + + try { + jinja::runtime runtime(ctx); + runtime.execute(tmpl.prog); + } catch (const std::exception & e) { + // Execution may fail, that's okay - we just want to see what variables were accessed + } + + // Check which candidate variables were accessed (stats.used = true) + std::vector accessed_vars; + for (const auto & var_name : candidate_vars) { + auto val = ctx.get_val(var_name); + if (!val->is_undefined()) { + // Variable was overwritten, skip it + continue; + } + if (val->stats.used) { + accessed_vars.push_back(var_name); + } + } + + if (accessed_vars.empty()) { + LOG_ERR("%sNo reasoning/thinking-related variables were queried by the template%s\n", ANSI_GRAY, ANSI_RESET); + } else { + LOG_ERR("Template queries the following reasoning/thinking-related variables:\n"); + for (const auto & var : accessed_vars) { + LOG_ERR(" %s- %s%s\n", ANSI_ORANGE, var.c_str(), ANSI_RESET); + } + } + + } catch (const std::exception & e) { + LOG_ERR("Error checking reasoning variables: %s\n", e.what()); + } +} + +static void analyze_template(const std::string & template_path) { + LOG_ERR("\n"); + LOG_ERR("%s", ANSI_PURPLE); + LOG_ERR("================================================================================\n"); + LOG_ERR(" ANALYZING TEMPLATE: %s\n", template_path.c_str()); + LOG_ERR("================================================================================\n"); + LOG_ERR("%s", ANSI_RESET); + + std::string template_source; + try { + template_source = read_file(template_path); + } catch (const std::exception & e) { + LOG_ERR("Error reading template: %s\n", e.what()); + return; + } + + try { + common_chat_template chat_template(template_source, "", ""); + json tools = build_tools_definition(); + + // ===== CAPABILITIES ANALYSIS ===== + LOG_ERR("\n%s=== Template Capabilities (from jinja::caps) ===%s\n", ANSI_CYAN, ANSI_RESET); + auto caps = chat_template.original_caps(); + LOG_ERR("%ssupports_tools:%s %s\n", ANSI_BLUE, ANSI_RESET, caps.supports_tools ? "true" : "false"); + LOG_ERR("%ssupports_tool_calls:%s %s\n", ANSI_BLUE, ANSI_RESET, caps.supports_tool_calls ? "true" : "false"); + LOG_ERR("%ssupports_system_role:%s %s\n", ANSI_BLUE, ANSI_RESET, caps.supports_system_role ? "true" : "false"); + LOG_ERR("%ssupports_parallel_tool_calls:%s %s\n", ANSI_BLUE, ANSI_RESET, caps.supports_parallel_tool_calls ? "true" : "false"); + LOG_ERR("%srequires_typed_content:%s %s\n", ANSI_BLUE, ANSI_RESET, caps.requires_typed_content ? "true" : "false"); + + // ===== DIFFERENTIAL ANALYSIS ===== + + // Test 1: With and without tools (single user message) + { + json user_msg = make_user_msg(); + + templates_params params_no_tools; + params_no_tools.messages = json::array({ user_msg }); + params_no_tools.add_generation_prompt = false; + params_no_tools.tools = json::array(); + + templates_params params_with_tools = params_no_tools; + params_with_tools.tools = tools; + + std::string output_no_tools = common_chat_template_direct_apply(chat_template, params_no_tools); + std::string output_with_tools = common_chat_template_direct_apply(chat_template, params_with_tools); + + auto diff = calculate_diff_split(output_no_tools, output_with_tools); + print_diff_split("Diff: With vs Without Tools (single user message)", diff); + } + + // Test 2: With and without add_generation_prompt (single user message) + { + json user_msg = make_user_msg(); + + templates_params params_no_prompt; + params_no_prompt.messages = json::array({ user_msg }); + params_no_prompt.add_generation_prompt = false; + params_no_prompt.tools = json::array(); + + templates_params params_with_prompt = params_no_prompt; + params_with_prompt.add_generation_prompt = true; + + std::string output_no_prompt = common_chat_template_direct_apply(chat_template, params_no_prompt); + std::string output_with_prompt = common_chat_template_direct_apply(chat_template, params_with_prompt); + + auto diff = calculate_diff_split(output_no_prompt, output_with_prompt); + print_diff_split("Diff: With vs Without add_generation_prompt (single user message)", diff); + } + + // Test 3: Assistant with reasoning_content (user, assistant) + { + json user_msg = make_user_msg(); + + templates_params params_no_reasoning; + params_no_reasoning.messages = json::array({ user_msg, make_assistant_no_reasoning() }); + params_no_reasoning.add_generation_prompt = false; + params_no_reasoning.enable_thinking = true; + + templates_params params_with_reasoning = params_no_reasoning; + params_with_reasoning.messages = json::array({ user_msg, make_assistant_with_reasoning() }); + + std::string output_no_reasoning = common_chat_template_direct_apply(chat_template, params_no_reasoning); + std::string output_with_reasoning = common_chat_template_direct_apply(chat_template, params_with_reasoning); + + auto diff = calculate_diff_split(output_no_reasoning, output_with_reasoning); + print_diff_split("Diff: With vs Without reasoning_content (user, assistant)", diff); + } + + // Test 4: Assistant with reasoning_content (user, assistant, user) + { + json user_msg = make_user_msg(); + json user_msg2 = make_user_msg2(); + + templates_params params_no_reasoning; + params_no_reasoning.messages = json::array({ user_msg, make_assistant_no_reasoning(), user_msg2 }); + params_no_reasoning.add_generation_prompt = false; + params_no_reasoning.enable_thinking = true; + + templates_params params_with_reasoning = params_no_reasoning; + params_with_reasoning.messages = json::array({ user_msg, make_assistant_with_reasoning(), user_msg2 }); + + std::string output_no_reasoning = common_chat_template_direct_apply(chat_template, params_no_reasoning); + std::string output_with_reasoning = common_chat_template_direct_apply(chat_template, params_with_reasoning); + + auto diff = calculate_diff_split(output_no_reasoning, output_with_reasoning); + print_diff_split("Diff: With vs Without reasoning_content (user, assistant, user)", diff); + } + + // Test 5: Tool call in last assistant message (user, assistant) + { + json user_msg = make_user_msg(); + + templates_params params_no_tool; + params_no_tool.messages = json::array({ user_msg, make_assistant_no_tool() }); + params_no_tool.add_generation_prompt = false; + params_no_tool.tools = tools; + + templates_params params_with_tool = params_no_tool; + params_with_tool.messages = json::array({ user_msg, make_assistant_one_tool() }); + + std::string output_no_tool = common_chat_template_direct_apply(chat_template, params_no_tool); + std::string output_with_tool = common_chat_template_direct_apply(chat_template, params_with_tool); + + auto diff = calculate_diff_split(output_no_tool, output_with_tool); + print_diff_split("Diff: With vs Without tool call (user, assistant)", diff); + } + + // Test 6: Tool call in last assistant message (user, assistant, user) + { + json user_msg = make_user_msg(); + json user_msg2 = make_user_msg2_continue(); + + templates_params params_no_tool; + params_no_tool.messages = json::array({ user_msg, make_assistant_no_tool(), user_msg2 }); + params_no_tool.add_generation_prompt = false; + params_no_tool.tools = tools; + + templates_params params_with_tool = params_no_tool; + params_with_tool.messages = json::array({ user_msg, make_assistant_one_tool(), user_msg2 }); + + std::string output_no_tool = common_chat_template_direct_apply(chat_template, params_no_tool); + std::string output_with_tool = common_chat_template_direct_apply(chat_template, params_with_tool); + + auto diff = calculate_diff_split(output_no_tool, output_with_tool); + print_diff_split("Diff: With vs Without tool call (user, assistant, user)", diff); + } + + // Test 7: One vs two tool calls (user, assistant) + { + json user_msg = make_user_msg(); + + templates_params params_one_tool; + params_one_tool.messages = json::array({ user_msg, make_assistant_one_tool() }); + params_one_tool.add_generation_prompt = false; + params_one_tool.tools = tools; + + templates_params params_two_tools = params_one_tool; + params_two_tools.messages = json::array({ user_msg, make_assistant_two_tools() }); + + std::string output_one_tool = common_chat_template_direct_apply(chat_template, params_one_tool); + std::string output_two_tools = common_chat_template_direct_apply(chat_template, params_two_tools); + + auto diff = calculate_diff_split(output_one_tool, output_two_tools); + print_diff_split("Diff: One vs Two tool calls (user, assistant)", diff); + } + + // Test 8: One vs two tool calls (user, assistant, user) + { + json user_msg = make_user_msg(); + json user_msg2 = make_user_msg2_continue(); + + templates_params params_one_tool; + params_one_tool.messages = json::array({ user_msg, make_assistant_one_tool(), user_msg2 }); + params_one_tool.add_generation_prompt = false; + params_one_tool.tools = tools; + + templates_params params_two_tools = params_one_tool; + params_two_tools.messages = json::array({ user_msg, make_assistant_two_tools(), user_msg2 }); + + std::string output_one_tool = common_chat_template_direct_apply(chat_template, params_one_tool); + std::string output_two_tools = common_chat_template_direct_apply(chat_template, params_two_tools); + + auto diff = calculate_diff_split(output_one_tool, output_two_tools); + print_diff_split("Diff: One vs Two tool calls (user, assistant, user)", diff); + } + + // Test 9: Tool call with vs without reasoning_content (user, assistant) + { + json user_msg = make_user_msg(); + + templates_params params_no_reasoning; + params_no_reasoning.messages = json::array({ user_msg, make_assistant_one_tool() }); + params_no_reasoning.add_generation_prompt = false; + params_no_reasoning.tools = tools; + params_no_reasoning.enable_thinking = true; + + templates_params params_with_reasoning = params_no_reasoning; + params_with_reasoning.messages = json::array({ user_msg, make_assistant_one_tool_with_reasoning() }); + + std::string output_no_reasoning = common_chat_template_direct_apply(chat_template, params_no_reasoning); + std::string output_with_reasoning = common_chat_template_direct_apply(chat_template, params_with_reasoning); + + auto diff = calculate_diff_split(output_no_reasoning, output_with_reasoning); + print_diff_split("Diff: Tool call with vs without reasoning_content (user, assistant)", diff); + } + + // Check reasoning variables + check_reasoning_variables(chat_template); + + } catch (const std::exception & e) { + LOG_ERR("Analysis failed: %s\n", e.what()); + } +} + +int main(int argc, char ** argv) { + // Set log level to capture all output + common_log_set_verbosity_thold(99); + + analysis_options opts; + if (!parse_options(argc, argv, opts)) { + return 1; + } + + LOG_ERR("\n"); + LOG_ERR("%s", ANSI_PURPLE); + LOG_ERR("================================================================================\n"); + LOG_ERR(" TEMPLATE ANALYSIS TOOL\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR("%s", ANSI_RESET); + LOG_ERR("Analyzing %s%zu%s template(s)\n", ANSI_CYAN, opts.template_paths.size(), ANSI_RESET); + + for (const auto & path : opts.template_paths) { + analyze_template(path); + } + + LOG_ERR("\n"); + LOG_ERR("%s", ANSI_GREEN); + LOG_ERR("================================================================================\n"); + LOG_ERR(" ANALYSIS COMPLETE\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR("%s", ANSI_RESET); + + return 0; +}