//  Tests chat handling, including grammar genration and parsing for tool calling, for various templates.
//
//  Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates,
//  e.g. given Minja (http://github.com/google/minja) checked out in parent dir:
//
//    cmake -B build && cmake --build build --parallel && ./build/bin/test-chat ../minja/build/tests/*.jinja 2>/dev/null
//
#include "../src/llama-grammar.h"
#include "../src/unicode.h"
#include "../tools/server/server-chat.h"
#include "chat-auto-parser.h"
#include "chat.h"
#include "common.h"
#include "ggml.h"
#include "log.h"

#include <algorithm>
#include <exception>
#include <fstream>
#include <functional>
#include <iostream>
#include <nlohmann/json.hpp>
#include <set>
#include <stdexcept>
#include <string>

using json = nlohmann::ordered_json;

static std::ostream & operator<<(std::ostream & os, const common_chat_msg_diff & diff) {
    os << "{ content_delta: " << diff.content_delta << "; ";
    os << "reasoning_content_delta: " << diff.reasoning_content_delta << "; ";
    if (diff.tool_call_index != std::string::npos) {
        os << "tool_call_index: " << diff.tool_call_index << "; ";
        os << "tool_call_delta.name: " << diff.tool_call_delta.name << "; ";
        os << "tool_call_delta.id: " << diff.tool_call_delta.id << "; ";
        os << "tool_call_delta.arguments: " << diff.tool_call_delta.arguments << "; ";
    }
    os << "}";
    return os;
}

// operator<< for vector<common_chat_msg_diff>:
static std::ostream & operator<<(std::ostream & os, const std::vector<common_chat_msg_diff> & diffs) {
    os << "[\n";
    for (const auto & diff : diffs) {
        os << "  " << diff << ",\n";
    }
    os << "]";
    return os;
}

static std::ostream & operator<<(std::ostream & os, const common_chat_msg & msg) {
    os << "{ role: " << msg.role << "; ";
    os << "content: " << msg.content << "; ";
    os << "content_parts: [\n";
    for (const auto & part : msg.content_parts) {
        os << "  { type: " << part.type << "; text: " << part.text << " },\n";
    }
    os << "]; ";
    os << "reasoning_content: " << msg.reasoning_content << "; ";
    os << "tool_calls: [\n";
    for (const auto & tool_call : msg.tool_calls) {
        os << "  { name: " << tool_call.name << "; arguments: " << tool_call.arguments << "; id: " << tool_call.id
           << " },\n";
    }
    os << "]";
    os << "}";
    return os;
}

template <class T> static bool equals(const T & expected, const T & actual) {
    return expected == actual;
}

static common_chat_msg normalize(const common_chat_msg & msg) {
    common_chat_msg normalized = msg;
    for (auto & tool_call : normalized.tool_calls) {
        try {
            tool_call.arguments = json::parse(tool_call.arguments).dump();
        } catch (const std::exception &) {
        }
    }
    return normalized;
}

template <> bool equals(const common_chat_msg & expected, const common_chat_msg & actual) {
    return normalize(expected) == normalize(actual);
}

template <class T> static void assert_equals(const T & expected, const T & actual) {
    if (!equals(expected, actual)) {
        std::ostringstream oss_expected;
        oss_expected << expected;
        std::ostringstream oss_actual;
        oss_actual << actual;
        LOG_ERR("Expected: %s\n", oss_expected.str().c_str());
        LOG_ERR("Actual: %s\n", oss_actual.str().c_str());
        common_log_flush(common_log_main());
        throw std::runtime_error("Test failed");
    }
}

static void assert_contains(const std::string & haystack, const std::string & needle) {
    if (haystack.find(needle) == std::string::npos) {
        LOG_ERR("Expected to contain: %s\n", needle.c_str());
        LOG_ERR("Actual: %s\n", haystack.c_str());
        common_log_flush(common_log_main());
        throw std::runtime_error("Test failed");
    }
}

static void assert_not_contains(const std::string & haystack, const std::string & needle) {
    if (haystack.find(needle) != std::string::npos) {
        LOG_ERR("Expected NOT to contain: %s\n", needle.c_str());
        LOG_ERR("Actual: %s\n", haystack.c_str());
        common_log_flush(common_log_main());
        throw std::runtime_error("Test failed");
    }
}

static void assert_ends_with(const std::string & str, const std::string & suffix) {
    if (str.size() < suffix.size() ||
        str.compare(str.size() - suffix.size(), suffix.size(), suffix) != 0) {
        LOG_ERR("Expected to end with: %s\n", suffix.c_str());
        LOG_ERR("Actual: %s\n", str.c_str());
        common_log_flush(common_log_main());
        throw std::runtime_error("Test failed");
    }
}

static std::string read_file(const std::string & path) {
    std::ifstream fs(path, std::ios_base::binary);
    if (!fs.is_open()) {
        fs = std::ifstream("../" + path, std::ios_base::binary);
        if (!fs.is_open()) {
            throw std::runtime_error("Failed to open file: " + path);
        }
    }
    fs.seekg(0, std::ios_base::end);
    auto size = fs.tellg();
    fs.seekg(0);
    std::string out;
    out.resize(static_cast<size_t>(size));
    fs.read(out.data(), static_cast<std::streamsize>(size));
    return out;
}

static common_chat_templates_ptr read_templates(const std::string & path) {
    return common_chat_templates_ptr(common_chat_templates_init(/* model= */ nullptr, read_file(path)));
}

static std::unique_ptr<llama_grammar> build_grammar(const std::string & grammar_str) {
    return std::unique_ptr<llama_grammar>(
        llama_grammar_init_impl(nullptr, grammar_str.c_str(), "root", false, nullptr, 0, nullptr, 0));
}

// Helper to format a code point as a readable string
static std::string format_codepoint(uint32_t cp) {
    if (cp >= 32 && cp < 127) {
        return std::string("'") + static_cast<char>(cp) + "'";
    } else if (cp == '\n') {
        return "'\\n'";
    } else if (cp == '\r') {
        return "'\\r'";
    } else if (cp == '\t') {
        return "'\\t'";
    } else {
        return "U+" + std::to_string(cp);
    }
}

// Helper to format expected element from grammar stack
static std::string format_expected_element(const llama_grammar_rules & /* rules*/, const llama_grammar_element * elem) {
    if (!elem) {
        return "<end>";
    }

    switch (elem->type) {
        case LLAMA_GRETYPE_END:
            return "<end of rule>";
        case LLAMA_GRETYPE_ALT:
            return "<alternative>";
        case LLAMA_GRETYPE_RULE_REF:
            {
                // Find rule name - just show rule ID for now
                return "<rule-" + std::to_string(elem->value) + ">";
            }
        case LLAMA_GRETYPE_CHAR:
            {
                std::string                   result;
                const llama_grammar_element * pos   = elem;
                bool                          first = true;

                do {
                    if (!first) {
                        result += " | ";
                    }
                    first = false;

                    if (pos[1].type == LLAMA_GRETYPE_CHAR_RNG_UPPER) {
                        // Range like [a-z]
                        result += "[" + format_codepoint(pos->value) + "-" + format_codepoint(pos[1].value) + "]";
                        pos += 2;
                    } else {
                        result += format_codepoint(pos->value);
                        pos += 1;
                    }
                } while (pos->type == LLAMA_GRETYPE_CHAR_ALT);

                return result;
            }
        case LLAMA_GRETYPE_CHAR_NOT:
            {
                std::string                   result = "[^";
                const llama_grammar_element * pos    = elem;
                bool                          first  = true;

                do {
                    if (!first) {
                        result += " ";
                    }
                    first = false;

                    if (pos[1].type == LLAMA_GRETYPE_CHAR_RNG_UPPER) {
                        result += format_codepoint(pos->value) + "-" + format_codepoint(pos[1].value);
                        pos += 2;
                    } else {
                        result += format_codepoint(pos->value);
                        pos += 1;
                    }
                } while (pos->type == LLAMA_GRETYPE_CHAR_ALT);

                return result + "]";
            }
        case LLAMA_GRETYPE_CHAR_ANY:
            return "<any char>";
        case LLAMA_GRETYPE_TOKEN:
            return "<token-" + std::to_string(elem->value) + ">";
        case LLAMA_GRETYPE_TOKEN_NOT:
            return "<not-token-" + std::to_string(elem->value) + ">";
        default:
            return "<unknown>";
    }
}

// Get description of what the grammar expects at current position
static std::string get_expected_description(const llama_grammar_rules & rules, const llama_grammar_stacks & stacks) {
    if (stacks.empty()) {
        return "<no valid continuations>";
    }

    std::string           result;
    std::set<std::string> seen;

    for (const auto & stack : stacks) {
        if (stack.empty()) {
            if (seen.insert("<end>").second) {
                if (!result.empty()) {
                    result += " OR ";
                }
                result += "<end>";
            }
            continue;
        }

        const llama_grammar_element * elem = stack.back();
        std::string                   desc = format_expected_element(rules, elem);
        if (seen.insert(desc).second) {
            if (!result.empty()) {
                result += " OR ";
            }
            result += desc;
        }
    }

    return result;
}

// Result of a detailed grammar match attempt
struct grammar_match_result {
    bool        success            = false;  // Did the string fully match the grammar?
    size_t      matched_bytes      = 0;      // Bytes successfully matched before failure
    size_t      matched_codepoints = 0;      // Codepoints successfully matched before failure
    size_t      total_bytes        = 0;      // Total bytes in input
    size_t      total_codepoints   = 0;      // Total codepoints in input
    std::string matched_prefix;              // The portion that was successfully matched
    std::string failing_char;                // The character that caused failure (if any)
    std::string expected_description;        // What the grammar expected at failure point
    bool        incomplete = false;          // True if matched all input but grammar expects more
};

// Detailed version of match_string that returns failure information
static grammar_match_result match_string_detailed(const std::string & input, llama_grammar * grammar) {
    grammar_match_result result;
    result.total_bytes = input.size();

    const auto cpts         = unicode_cpts_from_utf8(input);
    result.total_codepoints = cpts.size();

    auto &       stacks_cur = llama_grammar_get_stacks(grammar);
    const auto & rules      = llama_grammar_get_rules(grammar);

    size_t byte_pos = 0;

    for (size_t i = 0; i < cpts.size(); i++) {
        const auto & cpt = cpts[i];

        // Get expected before accepting (for error reporting)
        std::string expected_before = get_expected_description(rules, stacks_cur);

        llama_grammar_accept(grammar, cpt);

        // Calculate byte position for this codepoint
        size_t cpt_bytes = 0;
        if (cpt < 0x80) {
            cpt_bytes = 1;
        } else if (cpt < 0x800) {
            cpt_bytes = 2;
        } else if (cpt < 0x10000) {
            cpt_bytes = 3;
        } else {
            cpt_bytes = 4;
        }

        if (stacks_cur.empty()) {
            // Grammar failed to match at this point
            result.matched_bytes        = byte_pos;
            result.matched_codepoints   = i;
            result.matched_prefix       = input.substr(0, byte_pos);
            result.failing_char         = format_codepoint(cpt);
            result.expected_description = expected_before;
            result.incomplete           = false;
            return result;
        }

        byte_pos += cpt_bytes;
    }

    // All input matched - check if grammar is complete
    result.matched_bytes      = input.size();
    result.matched_codepoints = cpts.size();
    result.matched_prefix     = input;

    if (std::any_of(stacks_cur.begin(), stacks_cur.end(), [](const auto & stack) { return stack.empty(); })) {
        // An empty stack means that the grammar has been completed
        result.success    = true;
        result.incomplete = false;
    } else {
        // Grammar expects more input
        result.success              = false;
        result.incomplete           = true;
        result.expected_description = get_expected_description(rules, stacks_cur);
    }

    return result;
}

// TODO: extract to common helper (copied from test-grammar-integration.cpp)
static bool match_string(const std::string & input, llama_grammar * grammar) {
    const auto cpts = unicode_cpts_from_utf8(input);

    auto & stacks_cur = llama_grammar_get_stacks(grammar);

    for (const auto & cpt : cpts) {
        llama_grammar_accept(grammar, cpt);

        if (stacks_cur.empty()) {
            // no stacks means that the grammar failed to match at this point
            return false;
        }
    }

    if (std::any_of(stacks_cur.begin(), stacks_cur.end(), [](const auto & stack) { return stack.empty(); })) {
        // An empty stack means that the grammar has been completed
        return true;
    }

    return false;
}

static std::string renormalize_json(const std::string & json_str) {
    try {
        auto json_obj = json::parse(json_str);
        return json_obj.dump();
    } catch (const std::exception & e) {
        return "";  // ignore parial JSON contents for comparison purposes
    }
}

static void assert_msg_equals(const common_chat_msg & expected,
                              const common_chat_msg & actual,
                              bool                    ignore_whitespace_differences = false) {
    assert_equals(expected.role, actual.role);
    if (ignore_whitespace_differences) {
        assert_equals(string_strip(expected.content), string_strip(actual.content));
    } else {
        assert_equals(expected.content, actual.content);
    }
    assert_equals(expected.content_parts.size(), actual.content_parts.size());
    for (size_t i = 0; i < expected.content_parts.size(); i++) {
        const auto & expected_part = expected.content_parts[i];
        const auto & actual_part   = actual.content_parts[i];
        assert_equals(expected_part.type, actual_part.type);
        if (ignore_whitespace_differences) {
            assert_equals(string_strip(expected_part.text), string_strip(actual_part.text));
        } else {
            assert_equals(expected_part.text, actual_part.text);
        }
    }
    if (ignore_whitespace_differences) {
        assert_equals(string_strip(expected.reasoning_content), string_strip(actual.reasoning_content));
    } else {
        assert_equals(expected.reasoning_content, actual.reasoning_content);
    }
    assert_equals(expected.tool_calls.size(), actual.tool_calls.size());
    for (size_t i = 0; i < expected.tool_calls.size(); i++) {
        const auto & expected_tool_call = expected.tool_calls[i];
        const auto & actual_tool_call   = actual.tool_calls[i];
        assert_equals(expected_tool_call.name, actual_tool_call.name);
        assert_equals(renormalize_json(expected_tool_call.arguments), renormalize_json(actual_tool_call.arguments));
        assert_equals(expected_tool_call.id, actual_tool_call.id);
    }
}

static common_chat_tool special_function_tool{
    /* .name = */ "special_function",
    /* .description = */ "I'm special",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "arg1": {
                "type": "integer",
                "description": "The arg."
            }
        },
        "required": ["arg1"]
    })",
};
static common_chat_tool special_function_tool_with_optional_param{
    /* .name = */ "special_function_with_opt",
    /* .description = */ "I'm special but have optional stuff",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "arg1": {
                "type": "integer",
                "description": "The arg."
            },
            "arg2": {
                "type": "integer",
                "description": "The optional arg."
            }
        },
        "required": ["arg1"]
    })",
};

static common_chat_tool empty_args_tool{
    /* .name = */ "empty_args",
    /* .description = */ "A tool that takes no arguments",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {}
    })",
};

static common_chat_tool empty_args_tool_no_properties{
    /* .name = */ "empty_args_no_props",
    /* .description = */ "A tool that takes no arguments and has no properties",
    /* .parameters = */ R"({
        "type": "object"
    })",
};

static common_chat_tool python_tool{
    /* .name = */ "python",
    /* .description = */ "an ipython interpreter",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "code": {
                "type": "string",
                "description": "Python code to execute."
            }
        },
        "required": ["code"]
    })",
};

static common_chat_tool html_tool{
    /* .name = */ "html",
    /* .description = */ "an html validator",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "markup": {
                "type": "string",
                "description": "HTML markup to validate."
            }
        },
        "required": ["markup"]
    })",
};

static common_chat_tool get_time_tool{
    /* .name = */ "get_time",
    /* .description = */ "Get the current time in a city",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "City name"
            }
        },
        "required": ["city"]
    })",
};

static common_chat_tool get_weather_tool{
    /* .name = */ "get_weather",
    /* .description = */ "Get the current weather in a city",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "City name"
            }
        },
        "required": ["city"]
    })",
};

static common_chat_tool todo_list{
    /* .name = */ "todo_list",
    /* .description = */ "Create or update the todo list",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "todos": {
                "type": "array",
                "description": "List of TODO list items"
            }
        },
        "required": ["todos"]
    })",
};

static common_chat_tool edit_tool{
    /* .name = */ "edit",
    /* .description = */ "Edit file",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "filename": {
                "type": "string",
                "description": "Path of file to edit"
            },
            "oldString": {
                "type": "string",
                "description": "String to replace"
            },
            "newString": {
                "type": "string",
                "description": "New (replacement) value"
            }
        },
        "required": ["filename", "oldString", "newString"]
    })",
};

static common_chat_tool manage_todo_list_tool{
    /* .name = */ "manage_todo_list",
    /* .description = */ "Create or update the todo list",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "todos": {
                "type": "array",
                "description": "List of TODO list items"
            }
        },
        "required": ["todos"]
    })",
};

static common_chat_tool run_in_terminal_tool{
    /* .name = */ "run_in_terminal",
    /* .description = */ "Run a shell command.",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "command": {
                "type": "string",
                "description": "Shell command to run"
            }
        },
        "required": ["command"]
    })",
};

static common_chat_tool magic_tool{
    /* .name = */ "magic",
    /* .description = */ "Magic tool that takes a hash",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "name": {
                "type": "string"
            },
            "ref": {
                "type": "string"
            }
        },
        "required": ["name", "ref"]
    })",
};

static common_chat_tool magic_int_tool{
    /* .name = */ "magic_int",
    /* .description = */ "Magic tool that takes a hash",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "ref": {
                "type": "integer"
            },
            "name": {
                "type": "string"
            }
        },
        "required": ["ref"]
    })",
};

static common_chat_tool amount_tool{
    /* .name = */ "amount",
    /* .description = */ "Amount converter",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "orig": {
                "type": "number"
            }
        },
        "required": ["orig"]
    })",
};

static common_chat_tool toggle_tool{
    /* .name = */ "toggle",
    /* .description = */ "Toggle a feature",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "enabled": {
                "type": "boolean",
                "description": "Whether to enable the feature"
            }
        },
        "required": ["enabled"]
    })",
};

static common_chat_tool nullable_tool{
    /* .name = */ "set_nullable",
    /* .description = */ "Set a nullable value",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "value": {
                "type": "null",
                "description": "A null value"
            }
        },
        "required": ["value"]
    })",
};

static common_chat_tool config_tool{
    /* .name = */ "set_config",
    /* .description = */ "Set configuration",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "config": {
                "type": "object",
                "description": "Configuration dict"
            }
        },
        "required": ["config"]
    })",
};

static common_chat_tool calendar_create_event_tool{
    /* .name = */ "Calendar.create_event",
    /* .description = */ "Create a calendar event",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "title": { "type": "string" },
            "participants": { "type": "array", "items": { "type": "string" } },
            "metadata": { "type": "object" }
        },
        "required": ["title", "participants", "metadata"]
    })",
};

static common_chat_tool imaginary_number_tool{
    /* .name = */ "imaginary_number",
    /* .description = */ "Imaginary number converter",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "number": {
                "type": "object",
                "properties": {
                    "real": {
                        "type": "number"
                    },
                    "imaginary": {
                        "type": "number"
                    }
                },
                "required": ["real", "imaginary"]
            }
        },
        "required": ["number"]
    })",
};

static common_chat_tool nested_args_tool{
    /* .name = */ "nested_args",
    /* .description = */ "Tool with nested array arguments",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "tags": {
                "type": "array",
                "items": { "type": "string" }
            },
            "entries": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "id": { "type": "integer" },
                        "label": { "type": "string" }
                    },
                    "required": ["id", "label"]
                }
            }
        },
        "required": ["tags", "entries"]
    })",
};

static common_chat_tool union_args_tool{
    /* .name = */ "union_args",
    /* .description = */ "Tool with union arguments",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "filter": {
                "anyOf": [
                    { "type": "array", "items": { "type": "string" } },
                    {
                        "type": "object",
                        "properties": {
                            "field": { "type": "string" },
                            "op": { "type": "string" }
                        },
                        "required": ["field", "op"]
                    }
                ]
            },
            "label": {
                "oneOf": [
                    { "type": "string" },
                    { "type": "object", "properties": { "text": { "type": "string" } } }
                ]
            },
            "limit": {
                "oneOf": [
                    { "type": "integer" },
                    {
                        "type": "object",
                        "properties": { "max": { "type": "integer" } },
                        "required": ["max"]
                    }
                ]
            }
        }
    })",
};

static common_chat_tool nullable_string_tool{
    /* .name = */ "set_nullable_str",
    /* .description = */ "Set a nullable string value",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "name": {
                "type": ["string", "null"],
                "description": "A nullable string"
            }
        },
        "required": ["name"]
    })",
};

static common_chat_tool nullable_string_null_first_tool{
    /* .name = */ "set_nullable_str_nf",
    /* .description = */ "Set a nullable string value with null first in type array",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "name": {
                "type": ["null", "string"],
                "description": "A nullable string with null first"
            }
        },
        "required": ["name"]
    })",
};

static common_chat_tool nullable_int_tool{
    /* .name = */ "set_nullable_int",
    /* .description = */ "Set a nullable integer value",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "count": {
                "type": ["integer", "null"],
                "description": "A nullable integer"
            }
        },
        "required": ["count"]
    })",
};

static common_chat_tool enum_no_type_tool{
    /* .name = */ "set_unit",
    /* .description = */ "Set a temperature unit",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "unit": {
                "enum": ["celsius", "fahrenheit"],
                "description": "Temperature unit"
            }
        },
        "required": ["unit"]
    })",
};

static common_chat_tool string_param_tool{
    /* .name = */ "string_param",
    /* .description = */ "Tool with string parameter for testing",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "text": {
                "type": "string",
                "description": "A text parameter"
            }
        },
        "required": []
    })",
};

static common_chat_tool quoted_unquoted_tool{
    /* .name = */ "quoted_unquoted",
    /* .description = */ "Tool with two string parameters, one for quoted string, one for unquoted",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "quoted": {
                "type": "string",
                "description": "Quoted value"
            },
            "unquoted": {
                "type": "string",
                "description": "Unquoted value"
            }
        },
        "required": ["quoted", "unquoted"]
    })",
};


static common_chat_tool tool_2req_4opt{
    /* .name = */ "tool_2req_4opt",
    /* .description = */ "Tool with 2 required and 4 optional params",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "req1": { "type": "string", "description": "Required string" },
            "req2": { "type": "integer", "description": "Required int" },
            "opt1": { "type": "string", "description": "Optional string 1" },
            "opt2": { "type": "integer", "description": "Optional int 1" },
            "opt3": { "type": "string", "description": "Optional string 2" },
            "opt4": { "type": "integer", "description": "Optional int 2" }
        },
        "required": ["req1", "req2"]
    })",
};

static common_chat_tool tool_2req_5opt{
    /* .name = */ "tool_2req_5opt",
    /* .description = */ "Tool with 2 required and 5 optional params",
    /* .parameters = */ R"({
        "type": "object",
        "properties": {
            "req1": { "type": "string", "description": "Required string" },
            "req2": { "type": "integer", "description": "Required int" },
            "opt1": { "type": "string", "description": "Optional string 1" },
            "opt2": { "type": "integer", "description": "Optional int 1" },
            "opt3": { "type": "string", "description": "Optional string 2" },
            "opt4": { "type": "integer", "description": "Optional int 2" },
            "opt5": { "type": "string", "description": "Optional string 3" }
        },
        "required": ["req1", "req2"]
    })",
};

static std::vector<common_chat_tool> tools{ special_function_tool, special_function_tool_with_optional_param,
                                            python_tool, html_tool, todo_list };

const common_chat_msg message_user{
    "user",
    "Hey there!",
    /* .content_parts = */ {},
    /* .tool_calls = */ {},
    /* .reasoning_content = */ "",
    /* .tool_name = */ "",
    /* .tool_call_id = */ "",
};

const common_chat_msg message_user_parts{
    "user",
    /* .content = */ "",
    /* .content_parts = */
    {
     { "text", "Hey" },
     { "text", "there" },
     },
    /* .tool_calls = */
    {                 },
    /* .reasoning_content = */
    "",
    /* .tool_name = */ "",
    /* .tool_call_id = */ "",
};

static common_chat_msg simple_assist_msg(const std::string & content,
                                         const std::string & reasoning_content = "",
                                         const std::string & tool_name         = "",
                                         const std::string & arguments         = "",
                                         const std::string & id                = "") {
    common_chat_msg msg;
    msg.role              = "assistant";
    msg.content           = content;
    msg.reasoning_content = reasoning_content;
    if (!tool_name.empty() || !id.empty()) {
        msg.tool_calls.push_back({ tool_name, arguments, id });
    }
    return msg;
}

static common_chat_msg message_with_tool_calls(const std::string & tool_name, const std::string & arguments) {
    return simple_assist_msg("", "", tool_name, arguments);
}

static common_chat_msg message_with_tool_calls_and_reasoning(const std::string & tool_name,
                                                             const std::string & arguments,
                                                             const std::string & reasoning) {
    return simple_assist_msg("", reasoning, tool_name, arguments);
}

static common_chat_msg message_with_reasoning_content_and_multiple_tool_calls(
    const std::string &                                      reasoning,
    const std::string &                                      content,
    const std::vector<std::pair<std::string, std::string>> & tool_calls) {
    common_chat_msg msg;
    msg.role              = "assistant";
    msg.content           = content;
    msg.reasoning_content = reasoning;
    for (const auto & [name, args] : tool_calls) {
        msg.tool_calls.push_back({ name, args, "" });
    }
    return msg;
}

static common_chat_msg message_with_content_and_tool_call(const std::string & content,
                                                          const std::string & tool_name,
                                                          const std::string & arguments) {
    return simple_assist_msg(content, "", tool_name, arguments);
}

static common_chat_msg message_with_reasoning_and_tool_call(const std::string & reasoning,
                                                            const std::string & tool_name,
                                                            const std::string & arguments) {
    return simple_assist_msg("", reasoning, tool_name, arguments);
}

const common_chat_msg message_assist       = simple_assist_msg("Hello, world!\nWhat's up?");
const common_chat_msg message_assist_empty = simple_assist_msg("");
const common_chat_msg message_assist_thoughts_unparsed_deepseek =
    simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?");
const common_chat_msg message_assist_thoughts_unparsed_md =
    simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```");
const common_chat_msg message_assist_thoughts_unparsed_md_partial =
    simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}");

const common_chat_msg message_assist_thoughts_unparsed_r7b =
    simple_assist_msg("<|START_THINKING|>I'm\nthinking<|END_THINKING|>Hello, world!\nWhat's up?");
const common_chat_msg message_assist_thoughts_unparsed_magistral =
    simple_assist_msg("[THINK]raisonnement[/THINK]Réponse");
const common_chat_msg message_assist_thoughts = simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking");
const common_chat_msg message_assist_thoughts_unopened_unparsed =
    simple_assist_msg("I'm\nthinking</think>Hello, world!\nWhat's up?");
const common_chat_msg message_assist_thoughts_no_content = simple_assist_msg("", "I'm\nthinking");
const common_chat_msg message_assist_call = simple_assist_msg("", "", "special_function", "{\"arg1\": 1}");
const common_chat_msg message_assist_call_noopt =
    simple_assist_msg("", "", "special_function_with_opt", "{\"arg1\": 1}");
const common_chat_msg message_assist_call_withopt =
    simple_assist_msg("", "", "special_function_with_opt", "{\"arg1\": 1, \"arg2\": 2}");
const common_chat_msg message_assist_call_content =
    simple_assist_msg("Hello, world!\nWhat's up?", "", "special_function", "{\"arg1\":1}");
const common_chat_msg message_assist_call_empty_args  = simple_assist_msg("", "", "special_function");
const common_chat_msg message_assist_call_cutoff_args = simple_assist_msg("", "", "special_function", "{\"arg");
const common_chat_msg message_assist_call_thoughts =
    simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1\":1}");
const common_chat_msg message_assist_call_thoughts_unparsed =
    simple_assist_msg("<think>I'm\nthinking</think>\n\n", "", "special_function", "{\"arg1\": 1}");
const common_chat_msg message_assist_call_thoughts_content =
    simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": 1}");
const common_chat_msg message_assist_call_id =
    simple_assist_msg("", "", "special_function", "{\"arg1\":1}", /* .id = */ "123456789");
const common_chat_msg message_assist_call_idx =
    simple_assist_msg("", "", "special_function", "{\"arg1\":1}", /* .id = */ "0");
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", "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')\"}");
const common_chat_msg message_assist_call_python_lines_unclosed =
    simple_assist_msg("", "", "python", "{\"code\":\"# This is a program:\\nprint('hey')");
const common_chat_msg message_assist_json_content =
    simple_assist_msg("{\n  \"response\": \"Hello, world!\\nWhat's up?\"\n}");
const common_chat_msg message_assist_prefill_content   = simple_assist_msg("Hello, ", "I'm thinking");
const common_chat_msg message_assist_prefill_reasoning = simple_assist_msg("", "I'm");

// Use for PEG parser implementations
struct peg_test_case {
    common_chat_templates_inputs params;
    std::string                  input;
    common_chat_msg              expect;
    bool                         is_partial            = false;
    bool                         expect_reconstruction = false;
};

struct make_peg_parser {
    common_chat_params params_;
    common_peg_arena   arena_;
    bool               detailed_debug_;

    make_peg_parser(common_chat_templates *              tmpls,
                    const common_chat_templates_inputs & inputs,
                    bool                                 detailed_debug = false) {
        detailed_debug_ = detailed_debug;
        params_         = common_chat_templates_apply(tmpls, inputs);
        arena_.load(params_.parser);
    }

    common_chat_msg parse(const std::string & msg, bool is_partial) const {
        common_chat_parser_params parser_params(params_);
        parser_params.debug = detailed_debug_;
        return common_chat_peg_parse(arena_, msg, is_partial, parser_params);
    }
};

// Global template filter for --template flag
static std::string g_template_filter;

// When true, run reconstruction test on every non-partial test and report results
static bool g_force_reconstruction_test = false;

static void test_peg_parser(common_chat_templates *                      tmpls,
                            const std::function<void(peg_test_case &)> & init,
                            bool                                         detailed_debug) {
    // UTF-8-safe truncation helper (same as in test_parser_with_streaming)
    constexpr auto utf8_truncate_safe_len = [](const std::string_view s) -> size_t {
        auto len = s.size();
        if (len == 0) {
            return 0;
        }
        auto i = len;
        for (size_t back = 0; back < 4 && i > 0; ++back) {
            --i;
            unsigned char c = s[i];
            if ((c & 0x80) == 0) {
                return len;
            }
            if ((c & 0xC0) == 0xC0) {
                size_t expected_len = 0;
                if ((c & 0xE0) == 0xC0) {
                    expected_len = 2;
                } else if ((c & 0xF0) == 0xE0) {
                    expected_len = 3;
                } else if ((c & 0xF8) == 0xF0) {
                    expected_len = 4;
                } else {
                    return i;
                }
                if (len - i >= expected_len) {
                    return len;
                }
                return i;
            }
        }
        return len - std::min(len, size_t(3));
    };

    peg_test_case tc;
    init(tc);
    if (tc.params.messages.empty()) {
        tc.params.messages = { message_user };
    }
    if (tc.expect.role.empty()) {
        tc.expect.role = "assistant";
    }

    auto parser = make_peg_parser(tmpls, tc.params, detailed_debug);
    if (detailed_debug) {
        LOG_DBG("Using parser: \n%s\n", parser.arena_.dump(parser.arena_.root()).c_str());
        LOG_DBG("Generation prompt: '%s'\n", parser.params_.generation_prompt.c_str());
    }

    common_chat_msg msg_accum;
    common_chat_msg msg_prev;
    msg_accum.role = msg_prev.role = "assistant";

    for (size_t i = 1; i <= tc.input.size(); ++i) {
        auto            is_partial  = i < tc.input.size() || tc.is_partial;
        // Use UTF-8 safe truncation to avoid corrupting multi-byte characters
        size_t          safe_len    = utf8_truncate_safe_len(std::string_view(tc.input).substr(0, i));
        std::string     prefix      = tc.input.substr(0, safe_len);
        common_chat_msg msg_current = parser.parse(prefix, is_partial);

        for (const auto & diff : common_chat_msg_diff::compute_diffs(msg_prev, msg_current)) {
            if (!diff.reasoning_content_delta.empty()) {
                msg_accum.reasoning_content += diff.reasoning_content_delta;
            }
            if (!diff.content_delta.empty()) {
                msg_accum.content += diff.content_delta;
            }
            if (diff.tool_call_index != std::string::npos) {
                // During partial parsing, a new tool call may appear with empty name initially
                // The name gets filled in as more input is parsed
                while (msg_accum.tool_calls.size() <= diff.tool_call_index) {
                    msg_accum.tool_calls.push_back({ "", "", "" });
                }
                // Always update name and id from diff (may change during incremental parsing), but only if the delta
                // actually contains them
                if (!diff.tool_call_delta.name.empty()) {
                    msg_accum.tool_calls[diff.tool_call_index].name = diff.tool_call_delta.name;
                }
                if (!diff.tool_call_delta.id.empty()) {
                    msg_accum.tool_calls[diff.tool_call_index].id = diff.tool_call_delta.id;
                }
                if (!diff.tool_call_delta.arguments.empty()) {
                    msg_accum.tool_calls[diff.tool_call_index].arguments += diff.tool_call_delta.arguments;
                }
            }
        }
        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;
    }

    if (!tc.is_partial) {
        assert_msg_equals(tc.expect, parser.parse(tc.input, false), true);
    }
    assert_msg_equals(tc.expect, msg_accum, true);

    // Test grammar if present in params
    if (!parser.params_.grammar.empty()) {
        auto grammar = build_grammar(parser.params_.grammar);
        if (!grammar) {
            throw std::runtime_error("Failed to build grammar: " + parser.params_.grammar);
        }

        // In production, grammar triggers match against the full generated text
        // including the generation prompt. All positions are in full_input coordinates.
        const auto & gen_prompt = parser.params_.generation_prompt;
        std::string full_input = gen_prompt + tc.input;

        // Determine whether the reasoning-budget sampler path applies: tool-call grammar
        // with all WORD triggers and thinking tags present. In production, the reasoning
        // budget sampler inhibits grammar application while inside thinking blocks —
        // triggers inside <think>...</think> are suppressed.
        bool use_reasoning_budget_path = false;
        if (parser.params_.grammar_lazy && !parser.params_.thinking_end_tags.empty()) {
            use_reasoning_budget_path = true;
            for (const auto & trigger : parser.params_.grammar_triggers) {
                if (trigger.type != COMMON_GRAMMAR_TRIGGER_TYPE_WORD) {
                    use_reasoning_budget_path = false;
                    break;
                }
            }
        }

        // Find the earliest trigger position to determine the constrained portion
        auto earliest_trigger_pos = std::string::npos;

        if (use_reasoning_budget_path) {
            // Reasoning-budget path: simulate thinking-aware trigger detection.
            // Walk through full_input tracking thinking state; only match triggers
            // when outside thinking blocks.
            const auto & think_start = parser.params_.thinking_start_tag;
            const auto & think_ends  = parser.params_.thinking_end_tags;

            bool in_thinking = false;
            for (size_t i = 0; i < full_input.size(); ++i) {
                if (!in_thinking && !think_start.empty()
                        && full_input.compare(i, think_start.size(), think_start) == 0) {
                    in_thinking = true;
                    i += think_start.size() - 1;
                    continue;
                }
                if (in_thinking) {
                    for (const auto & think_end : think_ends) {
                        if (full_input.compare(i, think_end.size(), think_end) == 0) {
                            in_thinking = false;
                            i += think_end.size() - 1;
                            break;
                        }
                    }
                    continue;
                }
                // Outside thinking — check if any trigger word starts here
                for (const auto & trigger : parser.params_.grammar_triggers) {
                    if (full_input.compare(i, trigger.value.size(), trigger.value) == 0) {
                        if (earliest_trigger_pos == std::string::npos || i < earliest_trigger_pos) {
                            earliest_trigger_pos = i;
                        }
                    }
                }
                if (earliest_trigger_pos != std::string::npos) {
                    break;  // found the earliest
                }
            }

            // If the reasoning-budget path found no trigger outside thinking but the test
            // expects tool calls, this template nests tool calls inside thinking
            // blocks (e.g. Kimi). Fall back to the legacy path for this case.
            if (earliest_trigger_pos == std::string::npos && !tc.expect.tool_calls.empty()) {
                use_reasoning_budget_path = false;
            }
        }

        if (!use_reasoning_budget_path) {
            // Legacy path: find triggers without thinking-awareness
            for (const auto & trigger : parser.params_.grammar_triggers) {
                size_t      pos = std::string::npos;
                std::smatch match;
                switch (trigger.type) {
                    case COMMON_GRAMMAR_TRIGGER_TYPE_WORD:
                        {
                            const auto & word = trigger.value;
                            pos               = full_input.find(word);
                            break;
                        }
                    case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN:
                        {
                            const auto & compiled = std::regex(trigger.value);
                            if (std::regex_search(full_input, match, compiled)) {
                                pos = match.position(compiled.mark_count());
                            }
                            break;
                        }
                    case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL:
                        {
                            // In production, PATTERN_FULL triggers are checked against
                            // the text generated so far, growing token by token. Simulate
                            // by trying every prefix of full_input.
                            const auto & compiled = std::regex(trigger.value);
                            for (size_t end = gen_prompt.size(); end <= full_input.size(); ++end) {
                                std::string prefix = full_input.substr(0, end);
                                if (std::regex_match(prefix, match, compiled)) {
                                    pos = std::string::npos;
                                    for (size_t gi = 1; gi < match.size(); ++gi) {
                                        if (match[gi].length() > 0) {
                                            pos = match.position(gi);
                                            break;
                                        }
                                    }
                                    if (pos == std::string::npos) {
                                        pos = match.position(0);
                                    }
                                    break;
                                }
                            }
                            break;
                        }
                    default:
                        throw std::runtime_error("Unknown trigger type");
                }
                if (pos != std::string::npos) {
                    if (earliest_trigger_pos == std::string::npos || pos < earliest_trigger_pos) {
                        earliest_trigger_pos = pos;
                    }
                }
            }
        }

        // If the test expects tool calls and the grammar is lazy, the trigger must fire.
        // Otherwise the grammar would never activate in production and tool calls wouldn't
        // be constrained. A silent skip here would hide broken triggers.
        if (parser.params_.grammar_lazy && !tc.expect.tool_calls.empty() && !tc.is_partial
                && earliest_trigger_pos == std::string::npos) {
            std::string trigger_desc;
            for (const auto & trigger : parser.params_.grammar_triggers) {
                trigger_desc += "\n  [type=" + std::to_string(trigger.type) + "] " + trigger.value;
            }
            throw std::runtime_error(
                "Grammar trigger did not fire, but test expects tool calls (lazy grammar).\n"
                ">>> Input: " + full_input + "\n"
                ">>> Triggers (" + std::to_string(parser.params_.grammar_triggers.size()) + "):" + trigger_desc);
        }

        // Determine the constrained portion of input to test against grammar.
        // If the trigger position falls inside the generation prompt, the grammar
        // sampler was already active before model output began — constrain from the
        // start of the model output (i.e. tc.input).
        std::string constrained = full_input;
        bool grammar_triggered = false;
        if (earliest_trigger_pos != std::string::npos) {
            auto constrain_from = std::max(earliest_trigger_pos, gen_prompt.size());
            constrained = full_input.substr(constrain_from);
            grammar_triggered = true;
        } else if (!parser.params_.grammar_lazy) {
            // For non-lazy grammars, the entire input should match
            grammar_triggered = true;
        }

        // Test the constrained portion against the grammar
        if (grammar_triggered && !tc.is_partial) {
            auto result = match_string_detailed(constrained, grammar.get());
            if (!result.success) {
                std::string error_msg;
                if (result.incomplete) {
                    error_msg =
                        "Grammar matched all input but expects more:\n\n"
                        ">>> Input: " + tc.input +
                        "\n\n>>> Constrained: " + constrained +
                        "\n\n>>> Matched prefix (" + std::to_string(result.matched_bytes) + " bytes, " +
                        std::to_string(result.matched_codepoints) + " codepoints): " +
                        (result.matched_prefix.size() > 100 ? result.matched_prefix.substr(0, 100) + "..." : result.matched_prefix) +
                        "\n\n>>> Expected next: " + result.expected_description +
                        "\n\n>>> Grammar: " + parser.params_.grammar;
                } else {
                    error_msg =
                        "Grammar match failed:\n\n"
                        ">>> Input: " + tc.input +
                        "\n\n>>> Constrained: " + constrained +
                        "\n\n>>> Matched prefix (" + std::to_string(result.matched_bytes) + " bytes, " +
                        std::to_string(result.matched_codepoints) + " codepoints): " +
                        (result.matched_prefix.size() > 100 ? result.matched_prefix.substr(0, 100) + "..." : result.matched_prefix) +
                        "\n\n>>> Failing character: " + result.failing_char +
                        "\n\n>>> Expected: " + result.expected_description +
                        "\n\n>>> Grammar: " + parser.params_.grammar;
                }
                throw std::runtime_error(error_msg);
            }
        }
    }

    // Reconstruction test: verify that appending the parsed message to the original
    // messages and re-rendering the template (without generation prompt) reproduces
    // the original prompt + input exactly, or as a proper prefix (the template may
    // append end-of-turn tokens after the assistant message).
    if ((tc.expect_reconstruction || g_force_reconstruction_test) && !tc.is_partial) {
        // Start from tc.expect but copy tool call arguments from the actual parser
        // output, which preserves original JSON formatting (e.g. {"arg1":1} vs {"arg1": 1}).
        auto reconstruction_msg = tc.expect;
        auto parsed_msg         = parser.parse(tc.input, false);
        for (size_t i = 0; i < reconstruction_msg.tool_calls.size() && i < parsed_msg.tool_calls.size(); i++) {
            reconstruction_msg.tool_calls[i].arguments = parsed_msg.tool_calls[i].arguments;
        }
        common_chat_templates_inputs reconstruction_inputs = tc.params;
        reconstruction_inputs.messages.push_back(reconstruction_msg);
        reconstruction_inputs.add_generation_prompt = false;

        auto reconstruction_params = common_chat_templates_apply(tmpls, reconstruction_inputs);
        std::string expected_text  = parser.params_.prompt + tc.input;
        bool match = reconstruction_params.prompt == expected_text ||
            (reconstruction_params.prompt.size() > expected_text.size() &&
             reconstruction_params.prompt.compare(0, expected_text.size(), expected_text) == 0);
        if (!match && g_force_reconstruction_test && !tc.expect_reconstruction) {
            // In forced mode, report mismatch but don't fail
            // Find the first difference position
            size_t diff_pos = 0;
            size_t min_len  = std::min(expected_text.size(), reconstruction_params.prompt.size());
            while (diff_pos < min_len && expected_text[diff_pos] == reconstruction_params.prompt[diff_pos]) {
                diff_pos++;
            }
            size_t ctx_start = diff_pos > 60 ? diff_pos - 60 : 0;
            size_t ctx_end_e = std::min(expected_text.size(), diff_pos + 40);
            size_t ctx_end_r = std::min(reconstruction_params.prompt.size(), diff_pos + 40);
            LOG_ERR("\x1b[31m[RECONSTRUCTION FAIL]\x1b[0m "
                    "first diff at byte %zu (expected len=%zu, reconstructed len=%zu)\n"
                    "  expected:      ...%s...\n"
                    "  reconstructed: ...%s...\n",
                    diff_pos, expected_text.size(), reconstruction_params.prompt.size(),
                    expected_text.substr(ctx_start, ctx_end_e - ctx_start).c_str(),
                    reconstruction_params.prompt.substr(ctx_start, ctx_end_r - ctx_start).c_str());
        } else if (!match) {
            std::string error_msg =
                "Reconstruction mismatch:\n\n"
                ">>> Expected (prompt + input):\n" + expected_text +
                "\n\n>>> Reconstructed:\n" + reconstruction_params.prompt;
            throw std::runtime_error(error_msg);
        } else if (g_force_reconstruction_test) {
            LOG_INF("\x1b[32m[RECONSTRUCTION OK]\x1b[0m\n");
        }
    }
}

// Fluent builder for PEG parser tests
class peg_test_builder;

class peg_tester {
    common_chat_templates_ptr tmpls_;
    std::string               template_path_;
    bool                      detailed_debug_;
    friend class peg_test_builder;

  public:
    explicit peg_tester(const std::string & template_path, const bool detailed_debug = false) :
        tmpls_(read_templates(template_path)),
        template_path_(template_path),
        detailed_debug_(detailed_debug) {}

    const std::string & template_path() const { return template_path_; }

    peg_test_builder test(const std::string & input);
};

class peg_test_builder {
    peg_tester &  tester_;
    peg_test_case tc_;

  public:
    peg_test_builder(peg_tester & tester, const std::string & input) : tester_(tester) {
        tc_.input = input;
        tc_.params.add_generation_prompt = true;
    }

    // Parameter setters
    peg_test_builder & reasoning_format(common_reasoning_format fmt) {
        tc_.params.reasoning_format = fmt;
        return *this;
    }

    peg_test_builder & tools(std::vector<common_chat_tool> tools) {
        tc_.params.tools = std::move(tools);
        return *this;
    }

    peg_test_builder & enable_thinking(bool val) {
        tc_.params.enable_thinking = val;
        return *this;
    }

    peg_test_builder & parallel_tool_calls(bool val) {
        tc_.params.parallel_tool_calls = val;
        return *this;
    }

    peg_test_builder & add_generation_prompt(bool val) {
        tc_.params.add_generation_prompt = val;
        return *this;
    }

    peg_test_builder & continue_final_message(common_chat_continuation cont) {
        tc_.params.continue_final_message = cont;
        return *this;
    }

    peg_test_builder & json_schema(const std::string & schema) {
        tc_.params.json_schema = schema;
        return *this;
    }

    peg_test_builder & is_partial(bool val) {
        tc_.is_partial = val;
        return *this;
    }

    peg_test_builder & expect_reconstruction(bool val = true) {
        tc_.expect_reconstruction = val;
        return *this;
    }

    // Expect setters
    peg_test_builder & expect(const common_chat_msg & msg) {
        tc_.expect = msg;
        return *this;
    }

    peg_test_builder & expect_content(const std::string & content) {
        tc_.expect.content = content;
        return *this;
    }

    peg_test_builder & expect_reasoning(const std::string & reasoning) {
        tc_.expect.reasoning_content = reasoning;
        return *this;
    }

    peg_test_builder & expect_tool_calls(std::vector<common_chat_tool_call> calls) {
        tc_.expect.tool_calls = std::move(calls);
        return *this;
    }

    peg_test_builder & tool_choice(common_chat_tool_choice choice) {
        tc_.params.tool_choice = choice;
        return *this;
    }

    peg_test_builder & messages(std::vector<common_chat_msg> messages) {
        tc_.params.messages = std::move(messages);
        return *this;
    }

    // Execute the test
    void run() {
        // Check template filter
        if (!g_template_filter.empty()) {
            // Case-insensitive substring match
            std::string template_path_lower = tester_.template_path();
            std::string filter_lower        = g_template_filter;
            std::transform(template_path_lower.begin(), template_path_lower.end(), template_path_lower.begin(),
                           ::tolower);
            std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(), ::tolower);
            if (template_path_lower.find(filter_lower) == std::string::npos) {
                // Skip this test
                return;
            }
        }
        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_);
    }
};

peg_test_builder peg_tester::test(const std::string & input) {
    return peg_test_builder(*this, input);
}

static void test_msgs_oaicompat_json_conversion() {
    LOG_DBG("%s\n", __func__);
    std::vector<common_chat_msg> msgs{
        message_user,
        message_user_parts,
        message_assist_call,
        message_assist_call_thoughts,
        message_assist_call_thoughts_unparsed,
        message_assist_call_thoughts_content,
        message_assist_call_id,
        message_assist_call_idx,
        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);
        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));

    // Note: content is "" instead of null due to workaround for templates that render null as "None"
    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<size_t>(1, res.size());
    assert_equals<std::string>(res[0].role, "assistant");
    assert_equals(true, res[0].content.empty());
    assert_equals(true, res[0].tool_calls.empty());

    try {
        common_chat_msgs_parse_oaicompat(json::parse("[{\"role\": \"assistant\"}]"));
        throw std::runtime_error("Expected exception");
    } catch (const std::exception & e) {
        if (std::string(e.what()).find("'content'") == std::string::npos) {
            throw std::runtime_error("Expected exception about missing 'content'");
        }
    }
}

static void test_msg_token_delimiters_split() {
    LOG_DBG("%s\n", __func__);

    // Delimiters that share a leading token, distinguished by the second token,
    // to exercise the per-position token matching.
    const common_chat_msg_delimiters delims = {
        { { COMMON_CHAT_ROLE_USER,      "", { 10, 11 } },
          { COMMON_CHAT_ROLE_ASSISTANT, "", { 10, 12 } } }
    };

    // Empty inputs
    assert_equals<size_t>(0, common_chat_msg_delimiters{}.split({}).spans.size());
    assert_equals<size_t>(0, common_chat_msg_delimiters{}.split({ 10, 11 }).spans.size());
    assert_equals<size_t>(0, delims.split({}).spans.size());

    // No delimiters match -> no spans
    assert_equals<size_t>(0, delims.split({ 100, 101, 102 }).spans.size());

    // Multi-role conversation: <user>Hi<assistant>Hello<user>Bye
    {
        const llama_tokens tokens = {
            10, 11,            // <user>
            100, 101,          // Hi
            10, 12,            // <assistant>
            200, 201, 202,     // Hello
            10, 11,            // <user>
            300, 301,          // Bye
        };

        const auto result = delims.split(tokens);
        const auto & spans = result.spans;
        assert_equals<size_t>(3, spans.size());

        assert_equals(COMMON_CHAT_ROLE_USER, spans[0].role);
        assert_equals<size_t>(0, spans[0].pos);
        assert_equals<size_t>(4, spans[0].len);

        assert_equals(COMMON_CHAT_ROLE_ASSISTANT, spans[1].role);
        assert_equals<size_t>(4, spans[1].pos);
        assert_equals<size_t>(5, spans[1].len);

        assert_equals(COMMON_CHAT_ROLE_USER, spans[2].role);
        assert_equals<size_t>(9, spans[2].pos);
        assert_equals<size_t>(4, spans[2].len);

        // is_user_start() is true at the token position where a user span begins
        assert_equals(true,  result.is_user_start(0));
        assert_equals(false, result.is_user_start(4));  // assistant span
        assert_equals(true,  result.is_user_start(9));
    }

    // Content before the first delimiter is not captured as a span
    {
        const llama_tokens tokens = {
            500, 501,    // leading content (dropped)
            10, 11,      // <user>
            100,         // Hi
        };

        const auto spans = delims.split(tokens).spans;
        assert_equals<size_t>(1, spans.size());
        assert_equals(COMMON_CHAT_ROLE_USER, spans[0].role);
        assert_equals<size_t>(2, spans[0].pos);
        assert_equals<size_t>(3, spans[0].len);
    }

    // Skipped regions (media chunks) are jumped over but still count as span content
    {
        const llama_tokens tokens = {
            10, 11,             // <user>
            LLAMA_TOKEN_NULL,   // media chunk (3 tokens)
            LLAMA_TOKEN_NULL,
            LLAMA_TOKEN_NULL,
            100,                // Hi
            10, 12,             // <assistant>
        };

        const std::map<size_t, size_t> skips = { { 2, 3 } };

        const auto spans = delims.split(tokens, skips).spans;
        assert_equals<size_t>(2, spans.size());

        assert_equals(COMMON_CHAT_ROLE_USER, spans[0].role);
        assert_equals<size_t>(0, spans[0].pos);
        assert_equals<size_t>(6, spans[0].len);

        assert_equals(COMMON_CHAT_ROLE_ASSISTANT, spans[1].role);
        assert_equals<size_t>(6, spans[1].pos);
        assert_equals<size_t>(2, spans[1].len);
    }

    // A delimiter sequence inside a skipped region is not matched
    {
        const llama_tokens tokens = {
            10, 11,      // <user>
            10, 12,      // skipped region that happens to contain delimiter tokens
            100,         // Hi
        };

        const std::map<size_t, size_t> skips = { { 2, 2 } };

        const auto spans = delims.split(tokens, skips).spans;
        assert_equals<size_t>(1, spans.size());
        assert_equals(COMMON_CHAT_ROLE_USER, spans[0].role);
        assert_equals<size_t>(0, spans[0].pos);
        assert_equals<size_t>(5, spans[0].len);
    }
}

static void test_tools_oaicompat_json_conversion() {
    LOG_DBG("%s\n", __func__);
    std::vector<common_chat_tool> tools{
        special_function_tool,
        python_tool,
    };

    for (const auto & tool : tools) {
        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);
        assert_equals(tool.description, tool2.description);
        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));
}

static void test_convert_responses_to_chatcmpl() {
    LOG_DBG("%s\n", __func__);

    // Test basic conversion with input messages (user/assistant alternating)
    {
        json input = json::parse(R"({
            "input": [
                {
                    "type": "message",
                    "role": "user",
                    "content": "hi wassup"
                },
                {
                    "type": "message",
                    "role": "assistant",
                    "content": "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"
                },
                {
                    "type": "message",
                    "role": "user",
                    "content": "hi"
                }
            ],
            "model": "gpt-5-mini",
            "stream": false,
            "text": {},
            "reasoning": {
                "effort": "medium"
            }
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        // Verify messages were converted correctly
        assert_equals(true, result.contains("messages"));
        assert_equals(true, result.at("messages").is_array());
        assert_equals((size_t)3, result.at("messages").size());

        // Check first message (user)
        const auto & msg0 = result.at("messages")[0];
        assert_equals(std::string("user"), msg0.at("role").get<std::string>());
        assert_equals(true, msg0.at("content").is_array());
        assert_equals(std::string("text"), msg0.at("content")[0].at("type").get<std::string>());
        assert_equals(std::string("hi wassup"), msg0.at("content")[0].at("text").get<std::string>());

        // Check second message (assistant)
        const auto & msg1 = result.at("messages")[1];
        assert_equals(std::string("assistant"), msg1.at("role").get<std::string>());
        assert_equals(true, msg1.at("content").is_array());
        assert_equals(std::string("text"), msg1.at("content")[0].at("type").get<std::string>());
        assert_equals(std::string("Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"), msg1.at("content")[0].at("text").get<std::string>());

        // Check third message (user)
        const auto & msg2 = result.at("messages")[2];
        assert_equals(std::string("user"), msg2.at("role").get<std::string>());
        assert_equals(true, msg2.at("content").is_array());
        assert_equals(std::string("text"), msg2.at("content")[0].at("type").get<std::string>());
        assert_equals(std::string("hi"), msg2.at("content")[0].at("text").get<std::string>());

        // Verify other fields preserved
        assert_equals(std::string("gpt-5-mini"), result.at("model").get<std::string>());
        assert_equals(false, result.at("stream").get<bool>());
    }

    // Test string input
    {
        json input = json::parse(R"({
            "input": "Hello, world!",
            "model": "test-model"
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        assert_equals((size_t)1, result.at("messages").size());
        const auto & msg = result.at("messages")[0];
        assert_equals(std::string("user"), msg.at("role").get<std::string>());
        assert_equals(std::string("Hello, world!"), msg.at("content").get<std::string>());
    }

    // Test with instructions (system message)
    {
        json input = json::parse(R"({
            "input": "Hello",
            "instructions": "You are a helpful assistant.",
            "model": "test-model"
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        assert_equals((size_t)2, result.at("messages").size());
        const auto & sys_msg = result.at("messages")[0];
        assert_equals(std::string("system"), sys_msg.at("role").get<std::string>());
        assert_equals(std::string("You are a helpful assistant."), sys_msg.at("content").get<std::string>());
    }

    // Test with max_output_tokens conversion
    {
        json input = json::parse(R"({
            "input": "Hello",
            "model": "test-model",
            "max_output_tokens": 100
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        assert_equals(true, result.contains("max_tokens"));
        assert_equals(false, result.contains("max_output_tokens"));
        assert_equals(100, result.at("max_tokens").get<int>());
    }

    // Test mixed Responses tools: convert only function tools
    {
        json input = json::parse(R"({
            "input": "Hello",
            "model": "test-model",
            "tools": [
                {
                    "type": "web_search"
                },
                {
                    "type": "function",
                    "name": "get_weather",
                    "description": "Get weather for a location",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "location": {
                                "type": "string"
                            }
                        },
                        "required": ["location"]
                    }
                },
                {
                    "type": "image_generation"
                },
                {
                    "type": "mcp",
                    "server_label": "test-server"
                },
                {
                    "type": "namespace",
                    "name": "browser"
                }
            ]
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        assert_equals(true, result.contains("tools"));
        assert_equals(true, result.at("tools").is_array());
        assert_equals((size_t)1, result.at("tools").size());

        const auto & tool = result.at("tools")[0];
        assert_equals(std::string("function"), tool.at("type").get<std::string>());
        assert_equals(std::string("get_weather"), tool.at("function").at("name").get<std::string>());
        assert_equals(true, tool.at("function").at("strict").get<bool>());
    }

    // Test non-function Responses tools are ignored
    {
        json input = json::parse(R"({
            "input": "Hello",
            "model": "test-model",
            "tools": [
                {
                    "type": "web_search"
                },
                {
                    "type": "image_generation"
                },
                {
                    "type": "mcp",
                    "server_label": "test-server"
                },
                {
                    "type": "namespace",
                    "name": "browser"
                }
            ]
        })");

        json result = server_chat_convert_responses_to_chatcmpl(input);

        assert_equals(false, result.contains("tools"));
    }
}

// Shared LFM2 parser cases - all variants use one output format and parser
static void test_lfm2_parser(const std::string & template_path, bool detailed_debug) {
    auto tst = peg_tester(template_path, detailed_debug);

    // Basic content only
    tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

    // Single tool call without reasoning
    tst.test("<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>")
        .tools({ special_function_tool })
        .expect(message_assist_call)
        .run();

    // Tool call with string argument
    tst.test("<|tool_call_start|>[get_time(city=\"XYZCITY\")]<|tool_call_end|>")
        .tools({ get_time_tool })
        .expect(message_with_tool_calls("get_time", "{\"city\":\"XYZCITY\"}"))
        .run();

    // Python literals become JSON
    tst.test("<|tool_call_start|>[toggle(enabled=True)]<|tool_call_end|>")
        .tools({ toggle_tool })
        .expect(message_with_tool_calls("toggle", R"({"enabled": true})"))
        .run();

    tst.test("<|tool_call_start|>[set_nullable(value=None)]<|tool_call_end|>")
        .tools({ nullable_tool })
        .expect(message_with_tool_calls("set_nullable", R"({"value": null})"))
        .run();

    // Nested Python literal
    tst.test("<|tool_call_start|>[set_config(config={\"enabled\": True, \"count\": 3})]<|tool_call_end|>")
        .tools({ config_tool })
        .expect(message_with_tool_calls("set_config", R"({"config": {"enabled": true, "count": 3}})"))
        .run();

    // JSON literals are accepted too
    tst.test("<|tool_call_start|>[set_config(config={\"enabled\": true, \"note\": null})]<|tool_call_end|>")
        .tools({ config_tool })
        .expect(message_with_tool_calls("set_config", R"({"config": {"enabled": true, "note": null}})"))
        .run();

    // Dotted function name with structured args
    tst.test("<|tool_call_start|>[Calendar.create_event(title=\"demo\", participants=[\"Alice\", \"Bob\"], "
             "metadata={\"priority\": \"high\", \"reminder\": true})]<|tool_call_end|>")
        .tools({ calendar_create_event_tool })
        .expect(message_with_tool_calls(
            "Calendar.create_event",
            R"({"title": "demo", "participants": ["Alice", "Bob"], "metadata": {"priority": "high", "reminder": true}})"))
        .run();

    // Markdown links stay content
    tst.test("Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org).")
        .tools({ get_time_tool })
        .expect(simple_assist_msg("Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org)."))
        .run();

    // Python tool with multiline code in string: the \n in the literal decodes to a real
    // newline, emitted as a JSON \n escape (not a doubled backslash).
    tst.test("<|tool_call_start|>[python(code=\"def hello():\\n    print('hey')\")]<|tool_call_end|>")
        .tools({ python_tool })
        .expect_tool_calls({
            { "python", R"#({"code": "def hello():\n    print('hey')"})#", "" }
        })
        .run();

    // String escape sequences decode to their actual characters (newline + tab here),
    // so a "write a two line file" style call produces real line breaks, not literal "\n".
    tst.test("<|tool_call_start|>[python(code=\"First line\\nSecond line\\tindented\")]<|tool_call_end|>")
        .tools({ python_tool })
        .expect_tool_calls({
            { "python", R"#({"code": "First line\nSecond line\tindented"})#", "" }
        })
        .run();

    // Escaped quotes inside a string argument survive the round-trip.
    tst.test("<|tool_call_start|>[python(code=\"print(\\\"hi\\\")\")]<|tool_call_end|>")
        .tools({ python_tool })
        .expect_tool_calls({
            { "python", R"#({"code": "print(\"hi\")"})#", "" }
        })
        .run();

    // Content before tool call (no reasoning)
    tst.test("Let me check the time.<|tool_call_start|>[get_time(city=\"Paris\")]<|tool_call_end|>")
        .tools({ get_time_tool })
        .expect(message_with_reasoning_content_and_multiple_tool_calls(
            "", "Let me check the time.", { { "get_time", "{\"city\":\"Paris\"}" } }
        ))
        .run();

    // Multiple tool calls (parallel)
    tst.test("<|tool_call_start|>[special_function(arg1=1), special_function_with_opt(arg1=1, arg2=2)]<|tool_call_end|>")
        .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();

    // Partial tool call (streaming)
    tst.test("<|tool_call_start|>[special_function(arg1=")
        .tools({ special_function_tool })
        .is_partial(true)
        .expect(simple_assist_msg("", "", "special_function", "{\"arg1\": "))
        .run();

    // Tool call with empty arguments
    tst.test("<|tool_call_start|>[empty_args()]<|tool_call_end|>")
        .tools({ empty_args_tool })
        .expect(simple_assist_msg("", "", "empty_args", "{}"))
        .run();

}

static void test_template_output_peg_parsers(bool detailed_debug) {
    LOG_DBG("%s\n", __func__);

    // JSON schemas
    const char * invoice_schema = R"({
        "type": "object",
        "properties": {
            "amount": {"type": "number"},
            "date": {"type": "string"}
        }
    })";

    const char * const_schema = R"({
        "const": "42"
    })";

    {
        // Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case)
        auto tst = peg_tester("models/templates/Qwen3.5-4B.jinja", detailed_debug);

        tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .expect(message_assist_thoughts)
            .run();

        tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_NONE)
            .expect_content("<think>\nI'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
            .run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        tst.test(
               "I'm\nthinking\n</think>\n\n"
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>\n"
               "<tool_call>\n"
               "<function=special_function_with_opt>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "<parameter=arg2>\n2\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .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(
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Hello, world!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .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(
               "<tool_call>\n"
               "<function=edit>\n"
               "<parameter=filename>\n"
               "foo.c\n"
               "</parameter>\n"
               "<parameter=oldString>\n"
               "#iclunde\n"
               "</parameter>\n"
               "<parameter=newString>\n"
               "#include\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                edit_tool
        })
            .expect_tool_calls({
                { "edit", "{\"filename\": \"foo.c\", \"oldString\": \"#iclunde\", \"newString\": \"#include\"}", {} },
            })
            .run();

        // a parameter value that itself ends in a newline (e.g. a source file with a
        // trailing newline). The structural delimiter is "\n</parameter>\n", so the value
        // "#include\n" renders as "...#include\n\n</parameter>\n". The trailing newline must
        // be preserved faithfully (no stripping), and the generated grammar must admit a
        // value ending on a delimiter prefix. Regression test for gbnf_excluding_pattern.
        tst.test(
               "<tool_call>\n"
               "<function=edit>\n"
               "<parameter=filename>\n"
               "foo.c\n"
               "</parameter>\n"
               "<parameter=oldString>\n"
               "#iclunde\n"
               "</parameter>\n"
               "<parameter=newString>\n"
               "#include\n"
               "\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                edit_tool
        })
            .expect_tool_calls({
                { "edit", "{\"filename\": \"foo.c\", \"oldString\": \"#iclunde\", \"newString\": \"#include\\n\"}", {} },
            })
            .run();


        // test code that starts with indent
        tst.test(
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "    print(\"Hello, world!\")\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                python_tool
        })
            .expect_tool_calls({
                { "python", "{\"code\": \"    print(\\\"Hello, world!\\\")\"}", {} },
            })
            .run();

        tst.test(
               "I need to output the invoice details in JSON\n"
               "</think>\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();

        // tool call segment in reasoning
        tst.test(
               "Let's call a tool: <tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Not the real call!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\n</think>\n\n"
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Hello, world!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                python_tool
        })
            .expect_reasoning(
                "Let's call a tool: <tool_call>\n"
                "<function=python>\n"
                "<parameter=code>\n"
                "def hello():\n"
                "    print(\"Not the real call!\")\n"
                "\n"
                "hello()\n"
                "</parameter>\n"
                "</function>\n"
                "</tool_call>")
            .expect_tool_calls({
                { "python", "{\"code\": \"def hello():\\n    print(\\\"Hello, world!\\\")\\n\\nhello()\"}", {} },
            })
            .run();

        // No args tool
        tst.test(
               "<tool_call>\n"
               "<function=empty_args>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ empty_args_tool })
            .expect(message_with_tool_calls("empty_args", "{}"))
            .run();

        // No args tool with no properties defined
        tst.test(
               "<tool_call>\n"
               "<function=empty_args_no_props>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ empty_args_tool_no_properties })
            .expect(message_with_tool_calls("empty_args_no_props", "{}"))
            .run();

        // Edge cases when reasoning traces are not sent
        tst.test(
               "<think>\n\n</think>\n\n"
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                special_function_tool
        })
            .expect_reasoning("<think>\n\n")
            .expect_tool_calls({ { "special_function", "{\"arg1\": 1}", "" } })
            .run();

        tst.test(
               "</think>\n\n"
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                special_function_tool
        })
            .expect_reasoning("")
            .expect_tool_calls({ { "special_function", "{\"arg1\": 1}", "" } })
            .run();

        tst.test(
               "</think>\n\n"
               "<tool_call>\n"
               "<function=run_in_terminal>\n"
               "<parameter=command>\n"
               "pwd\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({
                run_in_terminal_tool
        })
            .expect_tool_calls({
                { "run_in_terminal", R"({"command": "pwd"})", {} },
            })
            .run();

        tst.test(
               "</think>\n\n"
               "Let me inspect the current directory.\n"
               "<tool_call>\n"
               "<function=run_in_terminal>\n"
               "<parameter=command>\n"
               "pwd\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({
                run_in_terminal_tool
        })
            .expect_content("Let me inspect the current directory.\n")
            .expect_tool_calls({
                { "run_in_terminal", R"({"command": "pwd"})", {} },
            })
            .run();

        tst.test(
               "</think>\n\n"
               "Let me inspect the current directory.\n"
               "<tool_call>\n"
               "<function=run_in_terminal>\n"
               "<parameter=command>\n"
               "pwd\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({
                run_in_terminal_tool
        })
            .tool_choice(COMMON_CHAT_TOOL_CHOICE_REQUIRED)
            .expect_content("Let me inspect the current directory.\n")
            .expect_tool_calls({
                { "run_in_terminal", R"({"command": "pwd"})", {} },
            })
            .run();

        tst.test(
               "I should inspect the directory.\n"
               "</think>\n\n"
               "Let me inspect it now.\n"
               "<tool_call>\n"
               "<function=run_in_terminal>\n"
               "<parameter=command>\n"
               "pwd\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({
                run_in_terminal_tool
        })
            .expect_reasoning("I should inspect the directory.")
            .expect_content("Let me inspect it now.\n")
            .expect_tool_calls({
                { "run_in_terminal", R"({"command": "pwd"})", {} },
            })
            .run();

        tst.test(
               "I might call <tool_call> later, but I am still thinking.\n"
               "</think>\n\n"
               "Final answer without tools.")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({ run_in_terminal_tool })
            .expect_reasoning("I might call <tool_call> later, but I am still thinking.")
            .expect_content("Final answer without tools.")
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\n\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        {
            common_chat_msg user_start;
            user_start.role    = "user";
            user_start.content = "Create a todo list, then inspect the repository.";

            common_chat_msg assistant_todos =
                simple_assist_msg("", "", "manage_todo_list",
                                  R"({"todos":[{"item":"Inspect repository","selected":false}]})", "call_todos");

            common_chat_msg tool_result;
            tool_result.role         = "tool";
            tool_result.content      = "Successfully wrote todo list";
            tool_result.tool_call_id = "call_todos";

            common_chat_msg user_continue;
            user_continue.role    = "user";
            user_continue.content = "Proceed.";

            tst.test(
                   "I need to run a terminal command.\n"
                   "</think>\n\n"
                   "<tool_call>\n"
                   "<function=run_in_terminal>\n"
                   "<parameter=command>\n"
                   "pwd\n"
                   "</parameter>\n"
                   "</function>\n"
                   "</tool_call>")
                .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
                .enable_thinking(true)
                .tools({
                    manage_todo_list_tool, run_in_terminal_tool
            })
                .messages({ user_start, assistant_todos, tool_result, user_continue })
                .expect_reasoning("I need to run a terminal command.")
                .expect_tool_calls({
                    { "run_in_terminal", R"({"command": "pwd"})", {} },
                })
                .run();

            tst.test(
                   "I need to run a terminal command.\n"
                   "</think>\n\n"
                   "Let me inspect the current directory.\n"
                   "<tool_call>\n"
                   "<function=run_in_terminal>\n"
                   "<parameter=command>\n"
                   "pwd\n"
                   "</parameter>\n"
                   "</function>\n"
                   "</tool_call>")
                .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
                .enable_thinking(true)
                .tools({
                    manage_todo_list_tool, run_in_terminal_tool
            })
                .tool_choice(COMMON_CHAT_TOOL_CHOICE_REQUIRED)
                .messages({ user_start, assistant_todos, tool_result, user_continue })
                .expect_reasoning("I need to run a terminal command.")
                .expect_content("Let me inspect the current directory.\n")
                .expect_tool_calls({
                    { "run_in_terminal", R"({"command": "pwd"})", {} },
                })
                .run();

            tst.test(
                   "</think>\n\n"
                   "<tool_call>\n"
                   "<function=run_in_terminal>\n"
                   "<parameter=command>\n"
                   "pwd\n"
                   "</parameter>\n"
                   "</function>\n"
                   "</tool_call>")
                .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
                .enable_thinking(true)
                .tools({
                    manage_todo_list_tool, run_in_terminal_tool
            })
                .messages({ user_start, assistant_todos, tool_result, user_continue })
                .expect_tool_calls({
                    { "run_in_terminal", R"({"command": "pwd"})", {} },
                })
                .run();
        }
    }

    {
        // Ministral-3-14B-Reasoning-2512
        auto tst = peg_tester("models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();

        tst.test("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?")
            .expect_content("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?")
            .expect_reconstruction()
            .run();

        tst.test("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .expect(message_assist_thoughts)
            .expect_reconstruction()
            .run();

        tst.test(R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({ special_function_tool })
            .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)
            .enable_thinking(true)
            .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})", {} },
            })
            .expect_reconstruction()
            .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();

        // fake tool call marker in reasoning
        tst.test(
               "[THINK]Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more[/THINK]"
               R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({ special_function_tool })
            .expect_reasoning("Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking[/THINK]Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .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).
            reasoning_format(COMMON_REASONING_FORMAT_AUTO).
            expect(message_assist).run();

        tst.test("I'm\nthinking\n</think>\nHello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_NONE)
            .expect_content("<think>\nI'm\nthinking\n</think>\nHello, world!\nWhat's up?")
            .run();

        tst.test("I'm\nthinking\n</think>\nHello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>\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</think>\n"
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>\n")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "</function>\n"
               "</tool_call>\n"
               "<tool_call>\n"
               "<function=special_function_with_opt>\n"
               "<parameter=arg1>\n1\n</parameter>\n"
               "<parameter=arg2>\n2\n</parameter>\n"
               "</function>\n"
               "</tool_call>\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(
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Hello, world!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\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"
               "</think>\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();

        // tool call segment in reasoning
        tst.test(
               "Let's call a tool: <tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Not the real call!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\n</think>\n"
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Hello, world!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\n"
            )
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                python_tool
        })
            .expect_reasoning("Let's call a tool: <tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Not the real call!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\n")
            .expect_tool_calls({
                { "python", "{\"code\": \"def hello():\\n    print(\\\"Hello, world!\\\")\\n\\nhello()\"}", {} },
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .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();
    }

    {
        // Cohere2 MoE (North Code) - dedicated parser.
        // Marker-wrapped format: <|START_THINKING|>...<|END_THINKING|> then either
        // <|START_TEXT|>...<|END_TEXT|> (content) or <|START_ACTION|>[json]<|END_ACTION|> (tools).
        // The generation prompt forces a leading <|START_THINKING|>, so model output begins inside
        // the thinking block: test inputs start with the reasoning body, not the <|START_THINKING|> tag.
        auto tst = peg_tester("models/templates/Cohere2MoE.jinja", detailed_debug);

        // Content with reasoning, extracted.
        tst.test("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();

        // Content with reasoning, reasoning_format=NONE -> thinking kept inline in content (markers preserved).
        tst.test("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>")
            .expect(message_assist_thoughts_unparsed_r7b)
            .run();

        // Content with empty thinking block.
        tst.test("<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist)
            .run();

        // JSON output schema
        tst.test(
                "I need to output the invoice details in JSON<|END_THINKING|>"
                "<|START_TEXT|>{\"amount\": 123.45, \"date\": \"2025-12-03\"}<|END_TEXT|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .json_schema(invoice_schema)
            .tools({ special_function_tool })
            .expect_reasoning("I need to output the invoice details in JSON")
            .expect_content(R"({"amount": 123.45, "date": "2025-12-03"})")
                .run();

        // Single tool call with reasoning.
        tst.test(
               "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();

        // Single tool call, empty thinking block (no reasoning content).
        tst.test(
               "<|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();

        // Tool call with an array argument (todo_list).
        tst.test(
               "<|END_THINKING|>"
               "<|START_ACTION|>[\n"
               "    {\"tool_call_id\": \"0\", \"tool_name\": \"todo_list\", \"parameters\": {\"todos\": [\"buy milk\", \"walk dog\"]}}\n"
               "]<|END_ACTION|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ todo_list })
            .expect(simple_assist_msg("", "", "todo_list", "{\"todos\": [\"buy milk\", \"walk dog\"]}", "0"))
            .run();

        // Parallel tool calls with reasoning.
        tst.test(
               "I'm\nthinking<|END_THINKING|>"
               "<|START_ACTION|>[\n"
               "    {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}},\n"
               "    {\"tool_call_id\": \"1\", \"tool_name\": \"python\", \"parameters\": {\"code\": \"print('hey')\"}}\n"
               "]<|END_ACTION|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .parallel_tool_calls(true)
            .tools({ special_function_tool, python_tool })
            .expect_reasoning("I'm\nthinking")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "0" },
                { "python", "{\"code\": \"print('hey')\"}", "1" },
            })
            .run();

        // Tools available but the model answers with content instead of calling a tool.
        tst.test("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect(message_assist_thoughts)
            .run();

        // Partial tool call (streaming): name/id resolved before arguments arrive.
        tst.test(
               "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();
    }

    {
        // 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!")).expect_reconstruction().run();

        tst.test("Line 1\nLine 2\nLine 3").expect(simple_assist_msg("Line 1\nLine 2\nLine 3")).expect_reconstruction().run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // Google Gemma 4 (tool calling with Gemma4 dict format)
        auto tst = peg_tester("models/templates/google-gemma-4-31B-it.jinja");

        tst.test("Hello, world!").expect(simple_assist_msg("Hello, world!")).run();

        // Reasoning and content
        tst.test(
                "<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        // Empty reasoning (budget=0: sampler forces end tag before newline)
        tst.test(
                "<|channel>thought<channel|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(simple_assist_msg("Hello, world!\nWhat's up?", ""))
            .run();

        // Reasoning and content with reasoning_format = none
        tst.test(
                "<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_NONE)
            .expect_content("<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?")
            .run();

        // Simple tool call with string argument
        tst.test(
                "<|tool_call>call:get_time{city:<|\"|>London<|\"|>}<tool_call|>")
            .tools({ get_time_tool })
            .expect(message_with_tool_calls("get_time", R"({"city": "London"})"))
            .run();

        // Tool call with string argument containing special chars
        tst.test(
                "<|tool_call>call:get_time{city:<|\"|>San Francisco<|\"|>}<tool_call|>")
            .tools({ get_time_tool })
            .expect(message_with_tool_calls("get_time", R"({"city": "San Francisco"})"))
            .run();

        // Tool call with empty args
        tst.test(
                "<|tool_call>call:empty_args{}<tool_call|>")
            .tools({ empty_args_tool })
            .expect(message_with_tool_calls("empty_args", "{}"))
            .run();

        // Tool call with string and content
        tst.test(
                "Hello, world!\nWhat's up?<|tool_call>call:get_time{city:<|\"|>Paris<|\"|>}<tool_call|>")
            .tools({ get_time_tool })
            .expect(message_with_content_and_tool_call("Hello, world!\nWhat's up?", "get_time", R"({"city": "Paris"})"))
            .run();

        // Parallel tool calls
        tst.test(
                "<|tool_call>call:get_time{city:<|\"|>London<|\"|>}<tool_call|>"
                "<|tool_call>call:get_weather{city:<|\"|>Paris<|\"|>}<tool_call|>")
            .tools({ get_time_tool, get_weather_tool })
            .parallel_tool_calls(true)
            .expect_tool_calls({
                { "get_time", R"({"city": "London"})", "" },
                { "get_weather", R"({"city": "Paris"})", "" },
            })
            .run();

        // Tool call with integer argument (number type)
        tst.test(
                "<|tool_call>call:special_function{arg1:42}<tool_call|>")
            .tools({ special_function_tool })
            .expect(message_with_tool_calls("special_function", R"({"arg1": 42})"))
            .run();

        // Tool call with negative number argument
        tst.test(
                "<|tool_call>call:special_function{arg1:-7}<tool_call|>")
            .tools({ special_function_tool })
            .expect(message_with_tool_calls("special_function", R"({"arg1": -7})"))
            .run();

        // Tool call with decimal number argument
        tst.test(
                "<|tool_call>call:amount{orig:3.14}<tool_call|>")
            .tools({ amount_tool })
            .expect(message_with_tool_calls("amount", R"({"orig": 3.14})"))
            .run();

        // Tool call with boolean argument (true)
        tst.test(
                "<|tool_call>call:toggle{enabled:true}<tool_call|>")
            .tools({ toggle_tool })
            .expect(message_with_tool_calls("toggle", R"({"enabled": true})"))
            .run();

        // Tool call with boolean argument (false)
        tst.test(
                "<|tool_call>call:toggle{enabled:false}<tool_call|>")
            .tools({ toggle_tool })
            .expect(message_with_tool_calls("toggle", R"({"enabled": false})"))
            .run();

        // Tool call with null argument
        tst.test(
                "<|tool_call>call:set_nullable{value:null}<tool_call|>")
            .tools({ nullable_tool })
            .expect(message_with_tool_calls("set_nullable", R"({"value": null})"))
            .run();

        // Tool call with array argument (todo list)
        tst.test(
                "<|tool_call>call:todo_list{todos:[<|\"|>buy milk<|\"|>,<|\"|>walk dog<|\"|>]}<tool_call|>")
            .tools({ todo_list })
            .expect(message_with_tool_calls("todo_list", R"({"todos":["buy milk","walk dog"]})"))
            .run();

        // Tool call with object/dict argument
        tst.test(
                "<|tool_call>call:set_config{config:{theme:<|\"|>dark<|\"|>,count:3}}<tool_call|>")
            .tools({ config_tool })
            .expect(message_with_tool_calls("set_config", R"({"config":{"theme":"dark","count":3}})"))
            .run();

        // Tool call with empty array
        tst.test(
                "<|tool_call>call:todo_list{todos:[]}<tool_call|>")
            .tools({ todo_list })
            .expect(message_with_tool_calls("todo_list", R"({"todos":[]})"))
            .run();

        // Tool call with empty dict
        tst.test(
                "<|tool_call>call:set_config{config:{}}<tool_call|>")
            .tools({ config_tool })
            .expect(message_with_tool_calls("set_config", R"({"config":{}})"))
            .run();

        // Tool call with scientific notation number
        tst.test(
                "<|tool_call>call:amount{orig:1.5e10}<tool_call|>")
            .tools({ amount_tool })
            .expect(message_with_tool_calls("amount", R"({"orig": 1.5e10})"))
            .run();

        // Edge cases
        tst.test(
                "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<channel|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .run();

        tst.test(
                "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .run();

        tst.test(
                "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|><channel|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .run();

        tst.test(
                "<|channel><|channel>thought\n<channel|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking<channel|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        {
            // additional tests for https://github.com/ggml-org/llama.cpp/pull/21760
            auto tmpls = read_templates("models/templates/google-gemma-4-31B-it.jinja");

            common_chat_msg tool_call_msg = simple_assist_msg(
                "Let me check.", "", "special_function", "{\"arg1\": 1}","c0");

            common_chat_msg tool_msg;
            tool_msg.role         = "tool";
            tool_msg.tool_name    = "special_function";
            tool_msg.tool_call_id = "c0";
            tool_msg.content      = "{\"r\":\"ok\"}";

            {
                common_chat_templates_inputs inputs;
                inputs.messages              = { message_user, tool_call_msg, tool_msg };
                inputs.tools                 = { special_function_tool };
                inputs.add_generation_prompt = true;

                auto params = common_chat_templates_apply(tmpls.get(), inputs);

                if (!string_ends_with(params.prompt, "<turn|>\n<|turn>model\n")) {
                    throw std::runtime_error("Missing generation prompt for Gemma 4");
                }
            }

            {
                common_chat_templates_inputs inputs;
                inputs.messages              = { message_user, tool_call_msg, tool_msg };
                inputs.tools                 = { special_function_tool };
                inputs.add_generation_prompt = false;

                auto params = common_chat_templates_apply(tmpls.get(), inputs);

                if (string_ends_with(params.prompt, "<|turn>model\n")) {
                    throw std::runtime_error("Gemma 4: generation prompt was modified despite add_generation_prompt=false");
                }
            }
        }

        {
            // StepFun trimming regression test (see https://github.com/ggml-org/llama.cpp/pull/25238)
            auto tmpls = read_templates("models/templates/StepFun3.5-Flash.jinja");

            common_chat_msg message_chatbot = simple_assist_msg("Let me check.\n\n", "I am thinking.\n\n");

            {
                common_chat_templates_inputs inputs;
                inputs.messages              = { message_chatbot };
                inputs.add_generation_prompt = true;

                auto params = common_chat_templates_apply(tmpls.get(), inputs);

                if (params.prompt.find("Let me check.\n\n") != std::string::npos) {
                    throw std::runtime_error("StepFun 3.5: content not trimmed");
                }

                if (params.prompt.find("I am thinking.\n\n") != std::string::npos) {
                    throw std::runtime_error("StepFun 3.5: reasoning_content not trimmed");
                }
            }

            {
                // Trimming must also reach typed (text) content parts, not just string content
                // (see https://github.com/ggml-org/llama.cpp/pull/25238)
                common_chat_msg message_parts;
                message_parts.role          = "user";
                message_parts.content_parts = {
                    { /* .type = */ "text", /* .text = */ "First part.\n\n" },
                    { /* .type = */ "media_marker", /* .text = */ "<__media__>" },
                    { /* .type = */ "text", /* .text = */ "Second part.\n\n" },
                };

                common_chat_templates_inputs inputs;
                inputs.messages              = { message_parts };
                inputs.add_generation_prompt = true;

                auto params = common_chat_templates_apply(tmpls.get(), inputs);

                if (params.prompt.find("First part.\n\n") != std::string::npos ||
                    params.prompt.find("Second part.\n\n") != std::string::npos) {
                    throw std::runtime_error("StepFun 3.5: text content parts not trimmed");
                }

                // the trimmed text itself must still be present
                if (params.prompt.find("First part.") == std::string::npos ||
                    params.prompt.find("Second part.") == std::string::npos) {
                    throw std::runtime_error("StepFun 3.5: text content parts missing after trim");
                }
            }
        }

    }

    {
        // 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 <think>\n in the prompt
        tst.test("Let me think about this...\n</think>\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("</think>Hello, world!").reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(simple_assist_msg("Hello, world!")).run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .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);

        tst.test(
               "<tool_call>\n"
               "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        tst.test(
               "Hello, world!\nWhat's up?<tool_call>\n"
               "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call_content)
            .run();

        // Note: Hermes template doesn't support thinking/reasoning natively
        // Note: We only support one tool calling format per template, no alternate formats

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    {
        // Test simple content-only template
        auto tst = peg_tester("models/templates/google-gemma-2-2b-it.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
    }
    {
        // IBM Granite (reasoning and tool calling model)
        auto tst = peg_tester("models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

        tst.test("<think>I'm\nthinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();

        // TODO: pending support for WRAPPED_WITH_REASONING
        // tst.test("<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>")
        //     .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
        //     .expect(message_assist_thoughts)
        //     .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // IBM Granite 4.0 (production template shared by h-tiny, h-small, micro)
        // Uses <tool_call> XML tags for tool calls, tools in system message
        auto tst = peg_tester("models/templates/ibm-granite-granite-4.0.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

        tst.test(
               "<tool_call>\n"
               "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // IBM Granite 4.1 (same format as 4.0)
        auto tst = peg_tester("models/templates/ibm-granite-granite-4.1.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

        tst.test(
               "<tool_call>\n"
               "{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();
    }

    {
        // ByteDance-Seed-OSS (reasoning and tool calling model)
        auto tst = peg_tester("models/templates/ByteDance-Seed-OSS.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

        tst.test("<seed:think>I'm thinking about the answer</seed:think>\nHello, world!")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(simple_assist_msg("Hello, world!", "I'm thinking about the answer"))
            .run();

        tst.test(
               "<seed:tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>1</parameter>\n"
               "</function>\n"
               "</seed:tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        tst.test(
               "<seed:tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>1</parameter>\n"
               "</function>\n"
               "</seed:tool_call>\n"
               "<seed:tool_call>\n"
               "<function=special_function_with_opt>\n"
               "<parameter=arg1>1</parameter>\n"
               "<parameter=arg2>2</parameter>\n"
               "</function>\n"
               "</seed:tool_call>")
            .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(
               "<seed:tool_call>\n"
               "<function=todo_list>\n"
               "<parameter=todos>[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]</parameter>\n"
               "</function>\n"
               "</seed:tool_call>")
            .tools({
                todo_list
        })
            .expect_tool_calls({
                { "todo_list", "{\"todos\": [{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]}", {} },
            })
            .run();

        // tool call with inside quotes
        tst.test(
               "<seed:tool_call>\n"
               "<function=edit>\n"
               "<parameter=filename>"
               "foo.cpp"
               "</parameter>\n"
               "<parameter=oldString>"
               "def foo(arg = \"14\"):\n"
               "    return arg + \"bar\"\n"
               "</parameter>\n"
               "<parameter=newString>"
               "def foo(arg = \"15\"):\n"
               "    pass\n"
               "</parameter>\n"
               "</function>\n"
               "</seed:tool_call>")
            .tools({
                edit_tool
        })
            .expect_tool_calls({
                { "edit", "{\"filename\": \"foo.cpp\", "
                    "\"oldString\": \"def foo(arg = \\\"14\\\"):\\n    return arg + \\\"bar\\\"\\n\", "
                    "\"newString\": \"def foo(arg = \\\"15\\\"):\\n    pass\\n\"}", {}
                }
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</seed:think>\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // Qwen3-Coder (tool calling with XML-style format)
        auto tst = peg_tester("models/templates/Qwen3-Coder.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n"
               "1\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        tst.test(
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n"
               "1\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>\n"
               "<tool_call>\n"
               "<function=special_function_with_opt>\n"
               "<parameter=arg1>\n"
               "1\n"
               "</parameter>\n"
               "<parameter=arg2>\n"
               "2\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .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})", {} },
            })
            .expect_reconstruction()
            .run();

        // Test with code content (multiline)
        tst.test(
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "def hello():\n"
               "    print(\"Hello, world!\")\n"
               "\n"
               "hello()\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({
                python_tool
        })
            .expect_tool_calls({
                { "python", "{\"code\": \"def hello():\\n    print(\\\"Hello, world!\\\")\\n\\nhello()\"}", {} },
            })
            .expect_reconstruction()
            .run();

        // Test with code content (asian unicode chars)
        tst.test(
               "<tool_call>\n"
               "<function=python>\n"
               "<parameter=code>\n"
               "格\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({
                python_tool
        })
            .expect_tool_calls({
                { "python", "{\"code\": \"格\"}", {} },
            })
            .expect_reconstruction()
            .run();

        // Test with HTML tag content
        tst.test(
               "<tool_call>\n"
               "<function=html>\n"
               "<parameter=markup>\n"
               "<html>\n"
               " <head>\n"
               "  <title>Hello!</title>\n"
               " </head>\n"
               "</html>\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({
                html_tool
        })
            .expect_tool_calls({
                { "html", "{\"markup\": \"<html>\\n <head>\\n  <title>Hello!</title>\\n </head>\\n</html>\"}", {} },
            })
            .expect_reconstruction()
            .run();

        // Test with TODO list (array of objects)
        tst.test(
               "<tool_call>\n"
               "<function=todo_list>\n"
               "<parameter=todos>\n"
               "[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({
                todo_list
        })
            .expect_tool_calls({
                { "todo_list", "{\"todos\": [{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]}", {} },
            })
            .expect_reconstruction()
            .run();

        // Test flexible optional argument ordering (2 required + 4 optional, reversed optional order)
        tst.test(
               "<tool_call>\n"
               "<function=tool_2req_4opt>\n"
               "<parameter=req1>\nhello\n</parameter>\n"
               "<parameter=req2>\n42\n</parameter>\n"
               "<parameter=opt4>\n100\n</parameter>\n"
               "<parameter=opt2>\n200\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ tool_2req_4opt })
            .expect_tool_calls({
                { "tool_2req_4opt", R"({"req1": "hello", "req2": 42, "opt4": 100, "opt2": 200})", {} },
            })
            .expect_reconstruction()
            .run();

        // Test flexible optional argument ordering (2 required + 5 optional, reversed optional order)
        tst.test(
               "<tool_call>\n"
               "<function=tool_2req_5opt>\n"
               "<parameter=req1>\nworld\n</parameter>\n"
               "<parameter=req2>\n7\n</parameter>\n"
               "<parameter=opt5>\nlast\n</parameter>\n"
               "<parameter=opt3>\nmiddle\n</parameter>\n"
               "<parameter=opt1>\nfirst\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ tool_2req_5opt })
            .expect_tool_calls({
                { "tool_2req_5opt", R"({"req1": "world", "req2": 7, "opt5": "last", "opt3": "middle", "opt1": "first"})", {} },
            })
            .expect_reconstruction()
            .run();

        // Test flexible optional argument ordering (2 required + 5 optional, all 5 in shuffled order)
        tst.test(
               "<tool_call>\n"
               "<function=tool_2req_5opt>\n"
               "<parameter=req1>\ntest\n</parameter>\n"
               "<parameter=req2>\n99\n</parameter>\n"
               "<parameter=opt3>\nc\n</parameter>\n"
               "<parameter=opt1>\na\n</parameter>\n"
               "<parameter=opt5>\ne\n</parameter>\n"
               "<parameter=opt4>\n4\n</parameter>\n"
               "<parameter=opt2>\n2\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ tool_2req_5opt })
            .expect_tool_calls({
                { "tool_2req_5opt", R"({"req1": "test", "req2": 99, "opt3": "c", "opt1": "a", "opt5": "e", "opt4": 4, "opt2": 2})", {} },
            })
            .expect_reconstruction()
            .run();

        // nullable string type ["string", "null"]
        tst.test(
               "<tool_call>\n"
               "<function=set_nullable_str>\n"
               "<parameter=name>\nhello world\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ nullable_string_tool })
            .expect_tool_calls({
                { "set_nullable_str", R"({"name": "hello world"})", {} },
            })
            .run();

        // nullable string with null first in type array ["null", "string"]
        tst.test(
               "<tool_call>\n"
               "<function=set_nullable_str_nf>\n"
               "<parameter=name>\nhello world\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ nullable_string_null_first_tool })
            .expect_tool_calls({
                { "set_nullable_str_nf", R"({"name": "hello world"})", {} },
            })
            .run();

        // nullable integer type ["integer", "null"] - should use JSON value path, not string
        tst.test(
               "<tool_call>\n"
               "<function=set_nullable_int>\n"
               "<parameter=count>\n42\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ nullable_int_tool })
            .expect_tool_calls({
                { "set_nullable_int", R"({"count": 42})", {} },
            })
            .run();

        // enum without explicit type key - should infer string from enum values
        tst.test(
               "<tool_call>\n"
               "<function=set_unit>\n"
               "<parameter=unit>\ncelsius\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .tools({ enum_no_type_tool })
            .expect_tool_calls({
                { "set_unit", R"({"unit": "celsius"})", {} },
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);
        tst.test(
               "<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>get_time<｜tool▁sep｜>{\"city\": "
               "\"XYZCITY\"}<｜tool▁call▁end｜><｜tool▁calls▁end｜>")
            .tools({ get_time_tool })
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_with_tool_calls("get_time", "{\"city\":\"XYZCITY\"}"))
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);
        tst.test(
               "REASONING</think><｜tool▁calls▁begin｜><｜tool▁call▁begin｜>get_time<｜tool▁sep｜>{\"city\": "
               "\"Tokyo\"}<｜tool▁call▁end｜><｜tool▁calls▁end｜>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls_and_reasoning("get_time", "{\"city\":\"Tokyo\"}", "REASONING"))
            .run();
    }

    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);
        tst.test(
               "REASONING</think>CONTENT<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>get_time<｜tool▁sep｜>{\"city\": "
               "\"Paris\"}<｜tool▁call▁end｜><｜tool▁call▁begin｜>get_weather<｜tool▁sep｜>{\"city\": "
               "\"Paris\"}<｜tool▁call▁end｜><｜tool▁calls▁end｜>")
            .tools({
                get_time_tool, get_weather_tool
        })
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .parallel_tool_calls(true)
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "REASONING", "CONTENT",
                { { "get_time", "{\"city\":\"Paris\"}" }, { "get_weather", "{\"city\":\"Paris\"}" } }))
            .run();
    }

    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);
        tst.test("REASONING</think>\nCONTENT")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(simple_assist_msg("CONTENT", "REASONING\n"))
            .run();
    }

    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.1.jinja", detailed_debug);
        tst.test("CONTENT").enable_thinking(false).reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK).
            expect(simple_assist_msg("CONTENT", "")).run();
    }

    // DeepSeek V3.2 tests - format uses DSML markup:
    //   <｜DSML｜function_calls>
    //   <｜DSML｜invoke name="foo">
    //   <｜DSML｜parameter name="bar" string="true|false">value</｜DSML｜parameter>
    //   </｜DSML｜invoke>
    //   </｜DSML｜function_calls>
    // Reasoning uses <think>...</think>. The generation prompt ends in <think> (thinking mode)
    // or <think></think> (non-thinking mode).
    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V3.2.jinja", detailed_debug);

        // Pure content (non-thinking mode)
        tst.test("Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist)
            .run();

        // Thinking + content
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();

        // Thinking + tool call (single, string param)
        tst.test(
               "Let me check the time</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"get_time\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Tokyo</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time"))
            .run();

        // Tool call without reasoning (non-thinking mode), integer param (string="false")
        tst.test(
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"special_function\">\n"
               "<｜DSML｜parameter name=\"arg1\" string=\"false\">1</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Multiple parallel tool calls with reasoning
        tst.test(
               "Calling both</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"get_time\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Paris</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "<｜DSML｜invoke name=\"get_weather\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Paris</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .parallel_tool_calls(true)
            .tools({ get_time_tool, get_weather_tool })
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "Calling both", "",
                { { "get_time", R"({"city": "Paris"})" }, { "get_weather", R"({"city": "Paris"})" } }))
            .run();

        // Tool call with content before tool calls
        tst.test(
               "Thinking about it</think>"
               "Let me call the function.\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"special_function\">\n"
               "<｜DSML｜parameter name=\"arg1\" string=\"false\">1</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect_reasoning("Thinking about it")
            .expect_content("Let me call the function.")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .run();

        // Tool call with negative number
        tst.test(
               "Test negative</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"magic_int\">\n"
               "<｜DSML｜parameter name=\"ref\" string=\"false\">-14</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_int_tool })
            .expect_reasoning("Test negative")
            .expect_tool_calls({
                { "magic_int", R"({"ref": -14})", {} },
            })
            .run();

        // Tool call with decimal number
        tst.test(
               "Test decimal</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"amount\">\n"
               "<｜DSML｜parameter name=\"orig\" string=\"false\">3.14</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ amount_tool })
            .expect_reasoning("Test decimal")
            .expect_tool_calls({
                { "amount", R"({"orig": 3.14})", {} },
            })
            .run();

        // Tool call with boolean
        tst.test(
               "Test boolean</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"toggle\">\n"
               "<｜DSML｜parameter name=\"enabled\" string=\"false\">true</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ toggle_tool })
            .expect_reasoning("Test boolean")
            .expect_tool_calls({
                { "toggle", R"({"enabled": true})", {} },
            })
            .run();

        // Tool call with array parameter (JSON-formatted)
        tst.test(
               "Test array</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"todo_list\">\n"
               "<｜DSML｜parameter name=\"todos\" string=\"false\">[\"buy milk\",\"walk dog\"]</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ todo_list })
            .expect_reasoning("Test array")
            .expect_tool_calls({
                { "todo_list", R"({"todos": ["buy milk", "walk dog"]})", {} },
            })
            .run();

        // Tool call with object parameter (JSON-formatted)
        tst.test(
               "Test object</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"set_config\">\n"
               "<｜DSML｜parameter name=\"config\" string=\"false\">{\"theme\":\"dark\",\"level\":2}</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ config_tool })
            .expect_reasoning("Test object")
            .expect_tool_calls({
                { "set_config", R"({"config": {"theme": "dark", "level": 2}})", {} },
            })
            .run();

        // Edge case: empty reasoning
        tst.test(
               "</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"get_time\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">XYZCITY</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls("get_time", R"({"city": "XYZCITY"})"))
            .run();

        // Edge case: tool call with multiple params (mixed types, string first)
        tst.test(
               "Multi-arg call</think>\n\n"
               "<｜DSML｜function_calls>\n"
               "<｜DSML｜invoke name=\"magic_int\">\n"
               "<｜DSML｜parameter name=\"ref\" string=\"false\">42</｜DSML｜parameter>\n"
               "<｜DSML｜parameter name=\"name\" string=\"true\">foo bar</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜function_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_int_tool })
            .expect_reasoning("Multi-arg call")
            .expect_tool_calls({
                { "magic_int", R"({"ref": 42, "name": "foo bar"})", {} },
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // DeepSeek V4 tests - same DSML markup as V3.2, but the tool call block is named
    // "tool_calls" and the non-thinking generation prompt ends in a bare </think>
    // instead of an empty <think></think> pair.
    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-V4.jinja", detailed_debug);

        // Pure content (non-thinking mode; generation prompt ends with </think>)
        tst.test("Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist)
            .run();

        // Thinking + content
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();

        // Thinking + tool call (single, string param)
        tst.test(
               "Let me check the time</think>\n\n"
               "<｜DSML｜tool_calls>\n"
               "<｜DSML｜invoke name=\"get_time\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Tokyo</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜tool_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time"))
            .run();

        // Tool call without reasoning (non-thinking mode), integer param (string="false")
        tst.test(
               "<｜DSML｜tool_calls>\n"
               "<｜DSML｜invoke name=\"special_function\">\n"
               "<｜DSML｜parameter name=\"arg1\" string=\"false\">1</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜tool_calls>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Multiple parallel tool calls with reasoning
        tst.test(
               "Calling both</think>\n\n"
               "<｜DSML｜tool_calls>\n"
               "<｜DSML｜invoke name=\"get_time\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Paris</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "<｜DSML｜invoke name=\"get_weather\">\n"
               "<｜DSML｜parameter name=\"city\" string=\"true\">Paris</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜tool_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .parallel_tool_calls(true)
            .tools({ get_time_tool, get_weather_tool })
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "Calling both", "",
                { { "get_time", R"({"city": "Paris"})" }, { "get_weather", R"({"city": "Paris"})" } }))
            .run();

        // Tool call with content before tool calls
        tst.test(
               "Thinking about it</think>"
               "Let me call the function.\n\n"
               "<｜DSML｜tool_calls>\n"
               "<｜DSML｜invoke name=\"special_function\">\n"
               "<｜DSML｜parameter name=\"arg1\" string=\"false\">1</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜tool_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect_reasoning("Thinking about it")
            .expect_content("Let me call the function.")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .run();

        // Tool call with multiple params (mixed types)
        tst.test(
               "Multi-arg call</think>\n\n"
               "<｜DSML｜tool_calls>\n"
               "<｜DSML｜invoke name=\"magic_int\">\n"
               "<｜DSML｜parameter name=\"ref\" string=\"false\">42</｜DSML｜parameter>\n"
               "<｜DSML｜parameter name=\"name\" string=\"true\">foo bar</｜DSML｜parameter>\n"
               "</｜DSML｜invoke>\n"
               "</｜DSML｜tool_calls>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_int_tool })
            .expect_reasoning("Multi-arg call")
            .expect_tool_calls({
                { "magic_int", R"({"ref": 42, "name": "foo bar"})", {} },
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // GLM-4.6 tests - format: <tool_call>function_name\n<arg_key>...</arg_key>\n<arg_value>...</arg_value>\n</tool_call>
    {
        auto tst = peg_tester("models/templates/GLM-4.6.jinja", detailed_debug);
        tst.test(
               "<tool_call>special_function\n"
               "<arg_key>arg1</arg_key>\n<arg_value>1</arg_value>\n"
               "</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();
    }

    // GLM-4.7-Flash tests - format: <tool_call>function_name<arg_key>...</arg_key><arg_value>...</arg_value></tool_call>
    // Note: Template uses forced-open thinking mode (prompt ends with <think>)
    {
        auto tst = peg_tester("models/templates/GLM-4.7-Flash.jinja", detailed_debug);

        // Pure content (no reasoning)
        tst.test("Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .expect(message_assist)
            .expect_reconstruction()
            .run();

        // Reasoning with content (forced-open mode - input starts after <think>)
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .expect_reconstruction()
            .run();

        // Tool call without reasoning
        tst.test(
               "<tool_call>special_function"
               "<arg_key>arg1</arg_key><arg_value>1</arg_value>"
               "</tool_call>")
            .enable_thinking(false)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Tool call with reasoning (forced-open mode)
        tst.test(
               "I'm\nthinking</think>"
               "<tool_call>special_function"
               "<arg_key>arg1</arg_key><arg_value>1</arg_value>"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .expect_reconstruction()
            .run();

        tst.test(
               "<tool_call>special_function"
               "<arg_key>arg1</arg_key><arg_value>1</arg_value>"
               "</tool_call>"
               "<tool_call>special_function_with_opt"
               "<arg_key>arg1</arg_key><arg_value>1</arg_value>"
               "<arg_key>arg2</arg_key><arg_value>2</arg_value>"
               "</tool_call>")
            .enable_thinking(false)
            .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})", {} },
            })
            .expect_reconstruction()
            .run();

        // #20650: tool with no required args, model emits <tool_call>name</tool_call> with no arg tags.
        {
            static common_chat_tool no_args_tool{
                "read_file_diff_md", "Reads a file diff",
                R"({"type":"object","properties":{"review_id":{"type":"string"},"file_id":{"type":"string"}}})",
            };
            tst.test(
                   "Let me read the diff content."
                   "</think>"
                   "<tool_call>read_file_diff_md</tool_call>")
                .enable_thinking(true)
                .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
                .tools({ no_args_tool })
                .expect_reasoning("Let me read the diff content.")
                .expect_tool_calls({{ "read_file_diff_md", "{}", {} }})
                .expect_reconstruction()
                .run();
        }

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Verify the throw path produces a readable error message, not std::out_of_range.
    // #20424 introduced effective_input = generation_prompt + input, but the throw
    // uses input.substr(result.end) where result.end is in effective_input space.
    {
        if (!g_template_filter.empty() && std::string("models/templates/GLM-4.7-Flash.jinja").find(g_template_filter) != std::string::npos) {
            auto tmpls = common_chat_templates_ptr(
                common_chat_templates_init(nullptr, read_file("models/templates/GLM-4.7-Flash.jinja")));

            static common_chat_tool weather_tool{
                "get_weather", "Get weather",
                R"({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})",
            };

            common_chat_templates_inputs inputs;
            inputs.tools = { weather_tool };
            inputs.enable_thinking = true;
            inputs.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
            inputs.add_generation_prompt = true;
            inputs.use_jinja = true;
            common_chat_msg msg;
            msg.role = "user";
            msg.content = "get_weather";
            inputs.messages = { msg };

            auto params = common_chat_templates_apply(tmpls.get(), inputs);
            common_peg_arena arena;
            arena.load(params.parser);
            common_chat_parser_params pp(params);

            // generation_prompt is non-empty for thinking models, so result.end
            // will be offset by generation_prompt.size() into effective_input space.
            assert(!pp.generation_prompt.empty());

            std::string bad_input =
                "Thinking.\n"
                "</think>"
                "<tool_call>get_weather"
                "<arg_key>city</arg_key><arg_value>Tokyo</arg_value>"
                "</tool_call>\n";

            bool got_runtime_error = false;
            bool got_out_of_range = false;
            std::string error_msg;
            try {
                common_chat_peg_parse(arena, bad_input, /*is_partial=*/false, pp);
            } catch (const std::out_of_range & e) {
                got_out_of_range = true;
                error_msg = e.what();
            } catch (const std::runtime_error & e) {
                got_runtime_error = true;
                error_msg = e.what();
            }
            GGML_ASSERT(!got_out_of_range && "throw path crashed with out_of_range (input.substr in effective_input space)");
            GGML_ASSERT(got_runtime_error  && "throw path should produce std::runtime_error with parse position");
        }
    }

    // Kimi-K2-Thinking tests - custom parser
    // Unique feature: tool call ID embeds function name as functions.<name>:<counter>
    {
        auto tst = peg_tester("models/templates/Kimi-K2-Thinking.jinja", detailed_debug);

        // Basic content only
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();

        // Single tool call
        tst.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ special_function_tool })
            .expect(simple_assist_msg("", "", "special_function", "{\"arg1\": 1}", "functions.special_function:0"))
            .run();

        // Single tool call with reasoning
        tst.test(
               "<think>I'm thinking about this</think>"
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(simple_assist_msg("", "I'm thinking about this", "special_function", "{\"arg1\": 1}", "functions.special_function:0"))
            .run();

        // Tool call with content
        tst.test(
               "Hello, world!\nWhat's up?"
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ special_function_tool })
            .expect(simple_assist_msg("Hello, world!\nWhat's up?", "", "special_function", "{\"arg1\": 1}", "functions.special_function:0"))
            .run();

        // Multiple tool calls (parallel) - tests the indexing behavior
        tst.test(
               "<|tool_calls_section_begin|>"
               "<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>"
               "<|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|>"
               "<|tool_calls_section_end|>")
            .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})", "functions.special_function:0" },
                { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", "functions.special_function_with_opt:1" },
            })
            .run();

        // Multiple tool calls with reasoning
        tst.test(
               "<think>I need to call two functions</think>"
               "<|tool_calls_section_begin|>"
               "<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>"
               "<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}<|tool_call_end|>"
               "<|tool_calls_section_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({
                special_function_tool, python_tool
        })
            .expect_reasoning("I need to call two functions")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "functions.special_function:0" },
                { "python", "{\"code\": \"print('hey')\"}", "functions.python:1" },
            })
            .run();

        // Python tool with multiline code
        tst.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.python:0<|tool_call_argument_begin|>"
               "{\"code\": \"def hello():\\n    print(\\\"Hello, world!\\\")\\n\\nhello()\"}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ python_tool })
            .expect_tool_calls({
                { "python", "{\"code\": \"def hello():\\n    print(\\\"Hello, world!\\\")\\n\\nhello()\"}", "functions.python:0" },
            })
            .run();

        // Tool call with empty arguments
        tst.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.empty_args:0<|tool_call_argument_begin|>"
               "{}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ empty_args_tool })
            .expect(simple_assist_msg("", "", "empty_args", "{}", "functions.empty_args:0"))
            .run();

        // Partial tool call (streaming)
        tst.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": ")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .is_partial(true)
            .expect(simple_assist_msg("", "", "special_function", "{\"arg1\": ", "functions.special_function:0"))
            .run();

        // Three tool calls to verify counter continues incrementing
        tst.test(
               "<|tool_calls_section_begin|>"
               "<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>"
               "<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print(1)\"}<|tool_call_end|>"
               "<|tool_call_begin|>functions.html:2<|tool_call_argument_begin|>{\"markup\": \"<p>test</p>\"}<|tool_call_end|>"
               "<|tool_calls_section_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({
                special_function_tool, python_tool, html_tool
        })
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "functions.special_function:0" },
                { "python", "{\"code\": \"print(1)\"}", "functions.python:1" },
                { "html", "{\"markup\": \"<p>test</p>\"}", "functions.html:2" },
            })
            .run();

        // Multiple tool calls with reasoning, call *inside thinking block*
        tst.test(
               "<think>I need to call two functions"
               "<|tool_calls_section_begin|>"
               "<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>"
               "<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}<|tool_call_end|>"
               "<|tool_calls_section_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({
                special_function_tool, python_tool
        })
            .expect_reasoning("I need to call two functions")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "functions.special_function:0" },
                { "python", "{\"code\": \"print('hey')\"}", "functions.python:1" },
            })
            .run();

        // Multiple tool calls with reasoning, call *inside thinking block* and *without section markers or end markers
        tst.test(
               "<think>I need to call two functions"
               "<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}"
               "<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({
                special_function_tool, python_tool
        })
            .expect_reasoning("I need to call two functions")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "functions.special_function:0" },
                { "python", "{\"code\": \"print('hey')\"}", "functions.python:1" },
            })
            .run();

        // Real life test - execute_command
        tst.test("<|tool_call_begin|>functions.execute_command:0<|tool_call_argument_begin|>{\"command\": \"ls -lah\""
            ", \"cwd\": \"/home/jarvis/development/exllamav3\", \"timeout\": 10}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({
                {
                    /* .name = */ "execute_command",
                    /* .description = */ "Execute shell command",
                    /* .parameters = */ R"({
                        "type": "object",
                        "properties": {
                            "command": {
                                "type": "string",
                                "description": "Shell command to execute"
                            },
                            "cwd": {
                                "type": "string",
                                "description": "Working directory"
                            },
                            "timeout": {
                                "type": "integer",
                                "description": "The timeout in seconds"
                            }
                        },
                        "required": ["command"]
                    })"
                }
            }).
            expect_tool_calls({
                {
                    "execute_command",
                    R"({"command": "ls -lah", "cwd": "/home/jarvis/development/exllamav3", "timeout": 10})",
                    "functions.execute_command:0"
                }
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        auto kimi_id_special_func_tool_call =
            simple_assist_msg("", "", "special_function", "{\"arg1\": 1}", "functions.special_function:0");

        // Kimi-K2 old template
        auto tst = peg_tester("models/templates/moonshotai-Kimi-K2.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ special_function_tool })
            .expect(kimi_id_special_func_tool_call)
            .expect_reconstruction()
            .run();

        // Kimi-K2-Instruct
        auto tst2 = peg_tester("models/templates/Kimi-K2-Instruct.jinja", detailed_debug);
        tst2.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst2.test(
               "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
               "{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>")
            .tools({ special_function_tool })
            .expect(kimi_id_special_func_tool_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    for (const char * tmpl : {
             "models/templates/LFM2-8B-A1B.jinja",
             "models/templates/LFM2.5-Instruct.jinja",
             "models/templates/LFM2.5-8B-A1B.jinja",
         }) {
        test_lfm2_parser(tmpl, detailed_debug);
    }

    // Thinking cases only apply to LFM2.5-8B-A1B, the one LFM2 template that emits <think>
    {
        auto tst = peg_tester("models/templates/LFM2.5-8B-A1B.jinja", detailed_debug);

        // Reasoning is parsed independent of enable_thinking

        // Tool call with reasoning
        tst.test("<think>I'm\nthinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        // Tool call with reasoning and content
        tst.test("<think>I need to call a function</think>"
                 "Let me check the time.<|tool_call_start|>[get_time(city=\"Paris\")]<|tool_call_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ get_time_tool })
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "I need to call a function", "Let me check the time.", { { "get_time", "{\"city\":\"Paris\"}" } }
            ))
            .run();

        // Fake tool call marker inside reasoning is not parsed as a call
        tst.test("<think>Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm</think>"
                 "<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect_reasoning("Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .run();

        // enable_thinking=false still captures emitted reasoning
        tst.test("<think>I'm\nthinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        tst.test("<think>I'm\nthinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        // Continuation: prefill content
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        // Continuation: prefill reasoning
        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Reka-Edge tests - uses native JSON format with per-call wrapper
    {
        auto tst = peg_tester("models/templates/Reka-Edge.jinja", detailed_debug);

        // Basic content only
        tst.test("Hello, world!\nWhat's up?").enable_thinking(false).expect(message_assist).run();

        // Single tool call without reasoning
        tst.test("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>")
            .enable_thinking(false)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Tool call with string argument
        tst.test("<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>")
            .enable_thinking(false)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls("get_time", "{\"city\":\"XYZCITY\"}"))
            .run();

        // Tool call with reasoning (enable_thinking=true)
        tst.test("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        // Multiple tool calls (parallel)
        tst.test(
            "<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>"
            "<tool_call>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1, \"arg2\": 2}}</tool_call>"
        )
            .enable_thinking(false)
            .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();

        // Tool call with reasoning and content
        tst.test("I need to call a function\n</think>\n\n"
                 "Let me check the time.<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ get_time_tool })
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "I need to call a function", "Let me check the time.", { { "get_time", "{\"city\":\"XYZCITY\"}" } }
            ))
            .run();

        // Partial tool call (streaming)
        tst.test("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\":")
            .tools({ special_function_tool })
            .enable_thinking(false)
            .is_partial(true)
            .expect(simple_assist_msg("", "", "special_function", "{\"arg1\": "))
            .run();

        // Tool call with empty arguments
        tst.test("<tool_call>\n{\"name\": \"empty_args\", \"arguments\": {}}</tool_call>")
            .enable_thinking(false)
            .tools({ empty_args_tool })
            .expect(simple_assist_msg("", "", "empty_args", "{}"))
            .run();

        // fake tool call marker in reasoning
        tst.test(
               "Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm\n</think>\n\n"
               "<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect_reasoning("Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\n\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }


    // Apertus-8B-Instruct tests - FUNC_NAME_AS_KEY format
    // Format: <|tools_prefix|>[{"function_name": {...arguments...}}]<|tools_suffix|>
    {
        auto tst = peg_tester("models/templates/Apertus-8B-Instruct.jinja", detailed_debug);
        tst.test("<|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // MiniMax-M2 tests - XML invoke format with parameter tags
    // Format: <minimax:tool_call><invoke name="func"><parameter name="key">value</parameter></invoke></minimax:tool_call>
    {
        auto tst = peg_tester("models/templates/MiniMax-M2.jinja", detailed_debug);
        tst.test("\n</think>\n\nHello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist).run();

        tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?").enable_thinking(true).reasoning_format(COMMON_REASONING_FORMAT_AUTO).expect(message_assist_thoughts).run();

        tst.test("Let's call a tool:\n</think>\n\n<minimax:tool_call>\n<invoke name=\"empty_args\">\n</invoke>\n</minimax:tool_call>").
            enable_thinking(true).
            reasoning_format(COMMON_REASONING_FORMAT_AUTO).
            tools({ empty_args_tool }).
            expect(message_with_reasoning_and_tool_call("Let's call a tool:", "empty_args", "{}")).
            run();

        tst.test(
               "\n</think>\n\n<minimax:tool_call>\n<invoke name=\"special_function\">\n<parameter "
               "name=\"arg1\">1</parameter>\n</invoke>\n</minimax:tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\n\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // MiniMax-M3 tests - namespaced XML invoke format, the parameter name is the tag
    // Format:
    //   ]<]minimax[>[<tool_call>
    //   ]<]minimax[>[<invoke name="get_time">]<]minimax[>[<city>Tokyo]<]minimax[>[</city>]<]minimax[>[</invoke>
    //   ]<]minimax[>[</tool_call>
    // Reasoning uses <mm:think>...</mm:think>. The generation prompt is only "]~b]ai\n", so the model
    // opens the thinking block itself; a turn without reasoning is prefixed with a bare </mm:think>.
    {
        auto tst = peg_tester("models/templates/MiniMax-M3.jinja", detailed_debug);

        // Content only (bare </mm:think> prefix)
        tst.test("</mm:think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .expect_reconstruction()
            .run();

        // Thinking + content
        tst.test("<mm:think>I'm\nthinking</mm:think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .expect_reconstruction()
            .run();

        // Thinking + tool call (single, string param)
        tst.test(
               "<mm:think>Let me check the time</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"get_time\">"
               "]<]minimax[>[<city>Tokyo]<]minimax[>[</city>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls_and_reasoning("get_time", R"({"city": "Tokyo"})", "Let me check the time"))
            .expect_reconstruction()
            .run();

        // Tool call without reasoning, integer param
        tst.test(
               "</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"special_function\">"
               "]<]minimax[>[<arg1>1]<]minimax[>[</arg1>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Tool call with no parameters
        tst.test(
               "<mm:think>Let's call a tool:</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"empty_args\">"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ empty_args_tool })
            .expect(message_with_reasoning_and_tool_call("Let's call a tool:", "empty_args", "{}"))
            .expect_reconstruction()
            .run();

        // Multiple parallel tool calls in one block
        tst.test(
               "<mm:think>Calling both</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"get_time\">"
               "]<]minimax[>[<city>Paris]<]minimax[>[</city>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[<invoke name=\"get_weather\">"
               "]<]minimax[>[<city>Paris]<]minimax[>[</city>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({ get_time_tool, get_weather_tool })
            .expect(message_with_reasoning_content_and_multiple_tool_calls(
                "Calling both", "",
                { { "get_time", R"({"city": "Paris"})" }, { "get_weather", R"({"city": "Paris"})" } }))
            .expect_reconstruction()
            .run();

        // Content before the tool call block
        tst.test(
               "<mm:think>Thinking about it</mm:think>"
               "Let me call the function."
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"special_function\">"
               "]<]minimax[>[<arg1>1]<]minimax[>[</arg1>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect_reasoning("Thinking about it")
            .expect_content("Let me call the function.")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", {} },
            })
            .expect_reconstruction()
            .run();

        // Negative number
        tst.test(
               "<mm:think>Test negative</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"magic_int\">"
               "]<]minimax[>[<ref>-14]<]minimax[>[</ref>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ magic_int_tool })
            .expect_reasoning("Test negative")
            .expect_tool_calls({
                { "magic_int", R"({"ref": -14})", {} },
            })
            .expect_reconstruction()
            .run();

        // Decimal number
        tst.test(
               "<mm:think>Test decimal</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"amount\">"
               "]<]minimax[>[<orig>3.14]<]minimax[>[</orig>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ amount_tool })
            .expect_reasoning("Test decimal")
            .expect_tool_calls({
                { "amount", R"({"orig": 3.14})", {} },
            })
            .expect_reconstruction()
            .run();

        // Boolean
        tst.test(
               "<mm:think>Test boolean</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"toggle\">"
               "]<]minimax[>[<enabled>true]<]minimax[>[</enabled>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ toggle_tool })
            .expect_reasoning("Test boolean")
            .expect_tool_calls({
                { "toggle", R"({"enabled": true})", {} },
            })
            .expect_reconstruction()
            .run();

        // Multiple params of mixed types (required int first, then optional string)
        tst.test(
               "<mm:think>Multi-arg call</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"magic_int\">"
               "]<]minimax[>[<ref>42]<]minimax[>[</ref>"
               "]<]minimax[>[<name>foo bar]<]minimax[>[</name>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ magic_int_tool })
            .expect_reasoning("Multi-arg call")
            .expect_tool_calls({
                { "magic_int", R"({"ref": 42, "name": "foo bar"})", {} },
            })
            .expect_reconstruction()
            .run();

        // Nested object param, expanded into one element per key
        tst.test(
               "<mm:think>Nested object</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"imaginary_number\">"
               "]<]minimax[>[<number>"
               "]<]minimax[>[<real>1.5]<]minimax[>[</real>"
               "]<]minimax[>[<imaginary>-2.5]<]minimax[>[</imaginary>"
               "]<]minimax[>[</number>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ imaginary_number_tool })
            .expect_reasoning("Nested object")
            .expect_tool_calls({
                { "imaginary_number", R"({"number": {"real": 1.5, "imaginary": -2.5}})", {} },
            })
            .expect_reconstruction()
            .run();

        // Array params, expanded into <item> elements (of scalars and of objects)
        tst.test(
               "<mm:think>Nested arrays</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"nested_args\">"
               "]<]minimax[>[<tags>"
               "]<]minimax[>[<item>alpha]<]minimax[>[</item>"
               "]<]minimax[>[<item>beta]<]minimax[>[</item>"
               "]<]minimax[>[</tags>"
               "]<]minimax[>[<entries>"
               "]<]minimax[>[<item>"
               "]<]minimax[>[<id>1]<]minimax[>[</id>"
               "]<]minimax[>[<label>one]<]minimax[>[</label>"
               "]<]minimax[>[</item>"
               "]<]minimax[>[</entries>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ nested_args_tool })
            .expect_reasoning("Nested arrays")
            .expect_tool_calls({
                { "nested_args", R"({"tags": ["alpha", "beta"], "entries": [{"id": 1, "label": "one"}]})", {} },
            })
            .expect_reconstruction()
            .run();

        // Union params (anyOf/oneOf), expanded as a choice of the alternatives
        tst.test(
               "<mm:think>Union array</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"union_args\">"
               "]<]minimax[>[<filter>"
               "]<]minimax[>[<item>alpha]<]minimax[>[</item>"
               "]<]minimax[>[<item>beta]<]minimax[>[</item>"
               "]<]minimax[>[</filter>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ union_args_tool })
            .expect_reasoning("Union array")
            .expect_tool_calls({
                { "union_args", R"({"filter": ["alpha", "beta"]})", {} },
            })
            .expect_reconstruction()
            .run();

        // oneOf between a scalar and an object
        tst.test(
               "<mm:think>Union scalar</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"union_args\">"
               "]<]minimax[>[<limit>5]<]minimax[>[</limit>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ union_args_tool })
            .expect_reasoning("Union scalar")
            .expect_tool_calls({
                { "union_args", R"({"limit": 5})", {} },
            })
            .expect_reconstruction()
            .run();

        tst.test(
               "<mm:think>Union nested</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"union_args\">"
               "]<]minimax[>[<limit>"
               "]<]minimax[>[<max>10]<]minimax[>[</max>"
               "]<]minimax[>[</limit>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ union_args_tool })
            .expect_reasoning("Union nested")
            .expect_tool_calls({
                { "union_args", R"({"limit": {"max": 10}})", {} },
            })
            .expect_reconstruction()
            .run();

        // A union with a string alternative is a string
        tst.test(
               "<mm:think>Union string</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"union_args\">"
               "]<]minimax[>[<label>hi]<]minimax[>[</label>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ union_args_tool })
            .expect_reasoning("Union string")
            .expect_tool_calls({
                { "union_args", R"({"label": "hi"})", {} },
            })
            .expect_reconstruction()
            .run();

        // ... even when the value looks structured
        tst.test(
               "<mm:think>Union string</mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"union_args\">"
               "]<]minimax[>[<label>"
               "]<]minimax[>[<text>hi]<]minimax[>[</text>"
               "]<]minimax[>[</label>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ union_args_tool })
            .expect_reasoning("Union string")
            .expect_tool_calls({
                { "union_args", R"({"label": "]<]minimax[>[<text>hi]<]minimax[>[</text>"})", {} },
            })
            .expect_reconstruction()
            .run();

        // Edge case: empty reasoning followed by a tool call
        tst.test(
               "<mm:think></mm:think>"
               "]<]minimax[>[<tool_call>\n"
               "]<]minimax[>[<invoke name=\"get_time\">"
               "]<]minimax[>[<city>XYZCITY]<]minimax[>[</city>"
               "]<]minimax[>[</invoke>\n"
               "]<]minimax[>[</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ get_time_tool })
            .expect(message_with_tool_calls("get_time", R"({"city": "XYZCITY"})"))
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</mm:think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // NVIDIA-Nemotron-Nano-v2 tests - <TOOLCALL>...</TOOLCALL> format
    // Format: <TOOLCALL>[{"name": "func", "arguments": {...}}]</TOOLCALL>
    {
        auto tst = peg_tester("models/templates/NVIDIA-Nemotron-Nano-v2.jinja", detailed_debug);
        tst.test("I'm\nthinking\n</think>\n<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        tst.test("I'm\nthinking\n</think>\n\n<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>\n")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // CohereForAI-c4ai-command-r7b (uses START_RESPONSE/END_RESPONSE, START_THINKING/END_THINKING, START_ACTION/END_ACTION)
    {
        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_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();
    }
    // CohereForAI-c4ai-command-r-plus (uses markdown code block format)
    {
        auto tst = peg_tester("models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja", detailed_debug);
        tst.test("<|CHATBOT_TOKEN|>Hello, world!\nWhat's up?<|END_OF_TURN_TOKEN|>").expect(message_assist).run();
        // Tool calls: Action: followed by JSON code block
        tst.test(
               "Action:\n"
               "```json\n"
               "[{\"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}]\n"
               "```")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // mistralai-Mistral-Nemo-Instruct-2407.jinja
    {
        auto tst = peg_tester("models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("[TOOL_CALLS][{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}, \"id\": \"123456789\"}]")
            .tools({ special_function_tool })
            .expect(message_assist_call_id)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    {
        auto tst = peg_tester("models/templates/meetkai-functionary-medium-v3.1.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("<function=special_function>{\"arg1\": 1}</function>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    // Functionary v3.2 - recipient-based format: >>>recipient\n{content}
    {
        auto tst = peg_tester("models/templates/meetkai-functionary-medium-v3.2.jinja", detailed_debug);
        tst.test("all\nHello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("special_function\n{\"arg1\": 1}")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // FireFunction
    {
        auto tst = peg_tester("models/templates/fireworks-ai-llama-3-firefunction-v2.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test(" functools[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // DeepSeek R1 Distill Llama 8B - reasoning tests only (forced open thinking)
    // Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag
    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja", detailed_debug);
        tst.test("</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist)
            .run();
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    // llama-cpp DeepSeek R1 template (always forced-open thinking)
    {
        auto tst = peg_tester("models/templates/llama-cpp-deepseek-r1.jinja", detailed_debug);
        tst.test("</think>Hello, world!\nWhat's up?").expect(message_assist).reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK).run();
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();
        tst.test(
               "</think><｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>special_function\n"
               "```json\n{\"arg1\": 1}```<｜tool▁call▁end｜><｜tool▁calls▁end｜>")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .parallel_tool_calls(true)
            .expect(message_assist_call)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    // DeepSeek R1 Distill Qwen 32B - reasoning tests only (forced open thinking)
    // Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag
    {
        auto tst = peg_tester("models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja", detailed_debug);
        tst.test("</think>Hello, world!\nWhat's up?").enable_thinking(true).
            reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK).
            expect(message_assist).run();
        tst.test("I'm\nthinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();
        tst.test(
               "<｜tool▁calls▁begin｜><｜tool▁call▁begin｜>function<｜tool▁sep｜>special_function\n"
               "```json\n{\"arg1\": 1}```<｜tool▁call▁end｜><｜tool▁calls▁end｜>")
            .tools({ special_function_tool })
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_call)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // MiMo-VL / Hermes 3 / Qwen 2.5 (Common <tool_call> JSON format)
    for (const auto & path :
         { "models/templates/MiMo-VL.jinja", "models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja",
           "models/templates/Qwen-Qwen2.5-7B-Instruct.jinja" }) {
        auto tst = peg_tester(path, detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Reka Edge
    {
        auto tst = peg_tester("models/templates/Reka-Edge.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .expect(message_assist)
            .run();
        tst.test("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .expect(message_assist_thoughts)
            .run();
        tst.test("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>")
            .enable_thinking(false)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();
        tst.test("Hello, world!\nWhat's up?\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>")
            .enable_thinking(false)
            .tools({ special_function_tool })
            .expect(message_assist_call_content)
            .run();
        tst.test("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();
        tst.test("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>\n<tool_call>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1, \"arg2\": 2}}\n</tool_call>")
            .enable_thinking(false)
            .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("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg")
            .enable_thinking(false)
            .tools({ special_function_tool })
            .is_partial(true)
            .expect(message_assist_call_cutoff_args)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\n\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Apriel 1.5
    {
        auto tst = peg_tester("models/templates/unsloth-Apriel-1.5.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).run();
        tst.test("<tool_calls>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</tool_calls>")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Apriel 1.6 Thinker (reasoning-only support)
    {
        auto tst = peg_tester("models/templates/Apriel-1.6-15b-Thinker-fixed.jinja", detailed_debug);

        // Implicit reasoning start (forced open)
        tst.test("I'm\nthinking\n[BEGIN FINAL RESPONSE]\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .expect(simple_assist_msg("Hello, world!\nWhat's up?", "Here are my reasoning steps:\nI'm\nthinking"))
            .expect_reconstruction()
            .run();

        // Reasoning + Tool calls
        tst.test(
               "I'm\nthinking\n[BEGIN FINAL RESPONSE]\n<tool_calls>[{\"name\": \"special_function\", \"arguments\": "
               "{\"arg1\": 1}}]</tool_calls>")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .tools({ special_function_tool })
            .expect(simple_assist_msg("", "Here are my reasoning steps:\nI'm\nthinking", "special_function", "{\"arg1\":1}"))
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n[BEGIN FINAL RESPONSE]\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // Mistral Small 3.2 - FUNC_BRACKET_TAG format: [TOOL_CALLS]func_name[CALL_ID]id[ARGS]{...}
    {
        auto tst = peg_tester("models/templates/Mistral-Small-3.2-24B-Instruct-2506.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("[TOOL_CALLS]special_function[CALL_ID]123456789[ARGS]{\"arg1\": 1}")
            .tools({ special_function_tool })
            .expect(message_assist_call_id)
            .expect_reconstruction()
            .run();

        tst.test("[TOOL_CALLS]special_function[CALL_ID]000000001[ARGS]{\"arg1\": 1}"
            "[TOOL_CALLS]special_function_with_opt[CALL_ID]000000002[ARGS]{\"arg1\": 1, \"arg2\": 2}")
            .parallel_tool_calls(true)
            .tools({
                special_function_tool, special_function_tool_with_optional_param
            })
            .expect_tool_calls({
                { "special_function", R"({"arg1": 1})", "000000001" },
                { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", "000000002" },
            })
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
    // Devstral
    {
        auto tst = peg_tester("models/templates/unsloth-mistral-Devstral-Small-2507.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("[TOOL_CALLS]special_function[ARGS]{\"arg1\": 1}")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();
        tst.test("Hello, world!\nWhat's up?[TOOL_CALLS]special_function[ARGS]{\"arg1\": 1}")
            .tools({ special_function_tool })
            .expect(message_assist_call_content)
            .expect_reconstruction()
            .run();
    }

    {
        // Llama 3.1
        auto tst = peg_tester("models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").tools({ special_function_tool }).expect(message_assist).expect_reconstruction().run();

        tst.test(
             "```json\n\"42\"\n```")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .json_schema(const_schema)
            .expect_content(R"("42")")
            .run();

        tst.test(
             "\"42\"\n")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .json_schema(const_schema)
            .expect_content(R"("42")")
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // Llama 3.2
        auto tst = peg_tester("models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").tools({ special_function_tool }).expect(message_assist).expect_reconstruction().run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        // Llama 3.3
        auto tst = peg_tester("models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").tools({ python_tool }).expect(message_assist).expect_reconstruction().run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // GPT-OSS format tests
    {
        auto tst = peg_tester("models/templates/openai-gpt-oss-120b.jinja", detailed_debug);

        // Basic content only - final channel
        tst.test("<|channel|>final<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();

        // Basic content only - commentary channel
        tst.test("<|channel|>commentary<|message|>Hello, world!\nWhat's up?").expect(message_assist).run();

        // Analysis channel (reasoning) with final channel (content)
        tst.test(
               "<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's "
               "up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        // Analysis channel (reasoning) with final channel (content) with reasoning_format = none
        tst.test(
               "<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's "
               "up?")
            .reasoning_format(COMMON_REASONING_FORMAT_NONE)
            .expect_content("<|channel|>analysis<|message|>I'm\nthinking<|end|>Hello, world!\nWhat's up?")
            .run();

        // Analysis channel only (partial) - still works when reasoning format is set
        tst.test("<|channel|>analysis<|message|>I'm\nthinking")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .is_partial(true)
            .expect_reasoning("I'm\nthinking")
            .run();

        // Tool call with recipient in role header: " to=functions.NAME<|channel|>analysis<|message|>JSON"
        tst.test(" to=functions.special_function<|channel|>analysis<|message|>{\"arg1\": 1}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Tool call with recipient in channel header: "<|channel|>analysis to=functions.NAME<|message|>JSON"
        tst.test("<|channel|>analysis to=functions.special_function<|message|>{\"arg1\": 1}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Tool call with constraint: " to=functions.NAME<|channel|>analysis <|constrain|>json<|message|>JSON"
        tst.test(" to=functions.special_function<|channel|>analysis <|constrain|>json<|message|>{\"arg1\": 1}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Tool call in commentary channel (channel header variant)
        tst.test("<|channel|>commentary to=functions.special_function<|message|>{\"arg1\": 1}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .run();

        // Tool call with reasoning + content (analysis first, then tool call)
        tst.test(
               "<|channel|>analysis<|message|>I'm\nthinking<|end|>"
               "<|start|>assistant to=functions.special_function<|channel|>analysis<|message|>{\"arg1\": 1}")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ special_function_tool })
            .expect(message_assist_call_thoughts)
            .run();

        // Complex tool calling
        tst.test(
            "<|channel|>analysis<|message|>Thinking about edit...<|end|>"
            "<|start|>assistant<|channel|>commentary to=functions.edit <|constrain|>json"
            "<|message|>{\"oldString\": \"if (part < railCount - 1) {\", \"newString\": \"if (part < 4) {\", \"replaceAll\": false}"
            )
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({
                {
                    /* .name = */ "edit",
                    /* .description = */ "Edit a file",
                    /* .parameters = */ R"({
                        "type": "object",
                        "properties": {
                            "oldString": {
                                "type": "string",
                                "description": "Old string to replace."
                            },
                            "newString": {
                                "type": "string",
                                "description": "New replacement string."
                            },
                            "replaceAll": {
                                "type": "boolean",
                                "description": "Whether to replace all occurrences."
                            }
                        },
                        "required": ["oldString", "newString"]
                    })",
                }
            })
            .expect_reasoning("Thinking about edit...")
            .expect_tool_calls({
                { "edit", R"({"oldString": "if (part < railCount - 1) {", "newString": "if (part < 4) {", "replaceAll": false})", {} }
            })
            .run();

        // Structured output
        tst.test(
            "<|channel|>analysis<|message|>I need to output the invoice details in JSON<|end|>"
            "<|start|>assistant<|channel|>final <|constrain|>json"
            "<|message|>"
            R"({"amount": 123.45, "date": "2025-12-03"})"
            )
            .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();


        // Unsolicited tool calls. There is no good way to handle these, so we return empty content.

        // Builtin function - recipient in role
        tst.test(
               "<|channel|>analysis<|message|>I will execute python to say hello<|end|>"
               "<|start|>assistant to=container.exec<|channel|>commentary<|message|>python3 -c 'print(\"hello\")'")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect_reasoning("I will execute python to say hello")
            .expect_content("")
            .run();

        // Builtin function - recipient in channel
        tst.test(
               "<|channel|>analysis<|message|>I will execute python to say hello<|end|>"
               "<|start|>assistant<|channel|>commentary to=python <|constrain|>code<|message|>print(\"hello\")")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect_reasoning("I will execute python to say hello")
            .expect_content("")
            .run();

        // Edge cases

        // "<|channel|>commentary to=assistant" before reasoning
        tst.test(
               "<|channel|>commentary to=assistant<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's "
               "up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        // "<|channel|>commentary to=assistant" before final message
        tst.test(
               "<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>commentary to=assistant<|channel|>final<|message|>Hello, world!\nWhat's "
               "up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist_thoughts)
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    {
        auto tst = peg_tester("models/templates/StepFun3.5-Flash.jinja", detailed_debug);

        tst.test("I was thinking\n</think>\nNow I'm not.").
            enable_thinking(true).
            reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK).
            expect_reasoning("I was thinking").
            expect_content("Now I'm not.")
        .run();

        // Test that numeric-looking string values are coerced to strings per the schema
        tst.test(
               "Let me call the magic tool\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=magic>\n"
               "<parameter=name>\nfooBar\n</parameter>\n"
               "<parameter=ref>\n5123123\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_tool })
            .expect_reasoning("Let me call the magic tool")
            .expect_tool_calls({
                { "magic", R"({"name": "fooBar", "ref": "5123123"})", {} },
            })
            .run();

        // Test that numeric values are correctly interpreted as numbers when schema calls for number
        tst.test(
               "Let me call the special function\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=special_function>\n"
               "<parameter=arg1>\n42555916\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool })
            .expect_reasoning("Let me call the special function")
            .expect_tool_calls({
                { "special_function", R"({"arg1": 42555916})", {} },
            })
            .run();

        tst.test(
               "Let me call the special function with opt\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=special_function_with_opt>\n"
               "<parameter=arg1>\n42555916\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ special_function_tool_with_optional_param })
            .expect_reasoning("Let me call the special function with opt")
            .expect_tool_calls({
                { "special_function_with_opt", R"({"arg1": 42555916})", {} },
            })
            .run();

        tst.test(
               "Let me call the magic_int function\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=magic_int>\n"
               "<parameter=ref>\n42555916\n</parameter>\n"
               "<parameter=name>\nbaz\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_int_tool })
            .expect_reasoning("Let me call the magic_int function")
            .expect_tool_calls({
                { "magic_int", R"({"ref": 42555916, "name": "baz"})", {} },
            })
            .run();

        tst.test(
               "Call string_param with empty text\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=string_param>\n"
               "<parameter=text>\n\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ string_param_tool })
            .expect_reasoning("Call string_param with empty text")
            .expect_tool_calls({
                { "string_param", R"({"text": ""})", {} },
            })
            .run();

        tst.test(
               "Test simple quoted unquoted\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=quoted_unquoted>\n"
               "<parameter=quoted>\n\"foo\"\n</parameter>\n"
               "<parameter=unquoted>\nfoo\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ quoted_unquoted_tool })
            .expect_reasoning("Test simple quoted unquoted")
            .expect_tool_calls({
                { "quoted_unquoted", R"({"quoted": "\"foo\"", "unquoted": "foo"})", {} },
            })
            .run();

        tst.test(
               "Test complex quoted unquoted\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=quoted_unquoted>\n"
               "<parameter=quoted>\n\"printf(\\\"foo\\\");\"\n</parameter>\n"
               "<parameter=unquoted>\nprintf(\"foo\");\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ quoted_unquoted_tool })
            .expect_reasoning("Test complex quoted unquoted")
            .expect_tool_calls({
                { "quoted_unquoted", R"({ "quoted" : "\"printf(\\\"foo\\\");\"", "unquoted": "printf(\"foo\");" })", {} }
            })
            .run();

            tst.test(
               "Test negative number\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=magic_int>\n"
               "<parameter=ref>\n-14\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ magic_int_tool })
            .expect_reasoning("Test negative number")
            .expect_tool_calls({
                { "magic_int", R"({ "ref" : -14 })", {} }
            })
            .run();

            tst.test(
               "Test decimal number\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=amount>\n"
               "<parameter=orig>\n3.14\n</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ amount_tool })
            .expect_reasoning("Test decimal number")
            .expect_tool_calls({
                { "amount", R"({ "orig" : 3.14 })", {} }
            })
            .run();

            tst.test(
               "Test imaginary number\n"
               "</think>\n"
               "<tool_call>\n"
               "<function=imaginary_number>\n"
               "<parameter=number>\n"
               "{ \"real\": 3.14, \"imaginary\": 2.71 }\n"
               "</parameter>\n"
               "</function>\n"
               "</tool_call>")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .tools({ imaginary_number_tool })
            .expect_reasoning("Test imaginary number")
            .expect_tool_calls({
                { "imaginary_number", R"({ "number" : {"real":3.14,"imaginary":2.71 } })", {} }
            })
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();

        tst.test(" thinking\n</think>\nHello, world!\nWhat's up?")
            .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
            .enable_thinking(true)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // GigaChat V3
    {
        auto tst = peg_tester("models/templates/GigaChat3-10B-A1.8B.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        tst.test(
            "Hello, world!\nWhat's up?"
            "<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}"
        )
            .tools({ special_function_tool })
            .expect(message_assist_call_content)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // GigaChat V3.1
    {
        auto tst = peg_tester("models/templates/GigaChat3.1-10B-A1.8B.jinja", detailed_debug);
        tst.test("Hello, world!\nWhat's up?").expect(message_assist).expect_reconstruction().run();
        tst.test("<|function_call|>{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}")
            .tools({ special_function_tool })
            .expect(message_assist_call)
            .expect_reconstruction()
            .run();

        tst.test(
            "Hello, world!\nWhat's up?"
            "<|function_call|>{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}"
        )
            .tools({ special_function_tool })
            .expect(message_assist_call_content)
            .expect_reconstruction()
            .run();

        // Continuation tests
        tst.test("world!\nWhat's up?")
            .messages({ message_user, message_assist_prefill_content })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_CONTENT)
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }

    // MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function>
    {
        auto tst = peg_tester("models/templates/openbmb-MiniCPM5-1B.jinja", detailed_debug);

        tst.test("Hello, world!\nWhat's up?")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .expect(message_assist)
            .run();

        tst.test(R"(<function name="python"><param name="code">print('Hello, World!')</param></function>)")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ python_tool })
            .expect_tool_calls({ { "python", R"#({"code": "print('Hello, World!')"})#", {} } })
            .run();

        tst.test(R"(<function name="empty_args"></function>)")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ empty_args_tool })
            .expect(simple_assist_msg("", "", "empty_args", "{}"))
            .run();

        tst.test(R"(<function name="python"><param name="code">print('x')</param></function>)")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({ python_tool })
            .expect_tool_calls({ { "python", R"#({"code": "print('x')"})#", {} } })
            .run();

        // CDATA lets a string value carry characters that would otherwise close the tag.
        tst.test(R"(<function name="html"><param name="markup"><![CDATA[<a href="/x">hi</a> </param>]]></param></function>)")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ html_tool })
            .expect_tool_calls({ { "html", R"#({"markup": "<a href=\"/x\">hi</a> </param>"})#", {} } })
            .run();

        tst.test(R"(I'm thinking</think><function name="python"><param name="code">print('hey')</param></function>)")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .tools({ python_tool })
            .expect_reasoning("I'm thinking")
            .expect_tool_calls({ { "python", R"#({"code": "print('hey')"})#", {} } })
            .run();

        tst.test(R"(<function name="python"><param name="code">print('x')</param></function>
<function name="python"><param name="code">print('y')</param></function>)")
            .enable_thinking(false)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .parallel_tool_calls(true)
            .tools({ python_tool })
            .expect_tool_calls({
                { "python", R"#({"code": "print('x')"})#", {} },
                { "python", R"#({"code": "print('y')"})#", {} },
            })
            .run();

        tst.test(" thinking</think>Hello, world!\nWhat's up?")
            .enable_thinking(true)
            .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
            .messages({ message_user, message_assist_prefill_reasoning })
            .add_generation_prompt(false)
            .continue_final_message(COMMON_CHAT_CONTINUATION_REASONING)
            .expect_reasoning("I'm thinking")
            .expect_content("Hello, world!\nWhat's up?")
            .run();
    }
}

static void test_template_generation_prompt() {
    common_chat_msg system_msg;
    system_msg.role = "system";
    system_msg.content ="You are a helpful assistant.";

    common_chat_msg tool_call_msg = simple_assist_msg("", "", "special_function", "{\"arg1\": 1}");

    common_chat_msg tool_msg;
    tool_msg.role = "tool";
    tool_msg.tool_name = "special_function";
    tool_msg.tool_call_id = "call0";
    tool_msg.content = "Sunny";

    struct test_case_options {
        std::vector<common_chat_msg> messages;
        bool                         add_generation_prompt  = true;
        common_chat_continuation     continue_final_message = COMMON_CHAT_CONTINUATION_NONE;
    };

    auto basic = [&]() {
        test_case_options opts;
        opts.messages = { system_msg, message_user };
        return opts;
    };

    auto continuation_content = [&]() {
        test_case_options opts;
        opts.messages               = { system_msg, message_user, message_assist_prefill_content };
        opts.add_generation_prompt  = false;
        opts.continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT;
        return opts;
    };

    auto continuation_reasoning = [&]() {
        test_case_options opts;
        opts.messages               = { system_msg, message_user, message_assist_prefill_reasoning };
        opts.add_generation_prompt  = false;
        opts.continue_final_message = COMMON_CHAT_CONTINUATION_REASONING;
        return opts;
    };

    auto check = [&](const common_chat_templates_ptr & tmpls,
                     const test_case_options & opts,
                     const std::string & expected_generation_prompt) {
        common_chat_templates_inputs inputs;
        inputs.messages               = opts.messages;
        inputs.add_generation_prompt  = opts.add_generation_prompt;
        inputs.continue_final_message = opts.continue_final_message;

        auto params = common_chat_templates_apply(tmpls.get(), inputs);

        assert_contains(params.prompt, system_msg.content);
        assert_contains(params.prompt, message_user.content);
        assert_equals(expected_generation_prompt, params.generation_prompt);
        assert_ends_with(params.prompt, expected_generation_prompt);
    };

    {
        auto tmpls = read_templates("models/templates/Qwen3.5-4B.jinja");
        check(tmpls, basic(),                  "<|im_start|>assistant\n<think>\n");
        check(tmpls, continuation_content(),   "<|im_start|>assistant\n<think>\nI'm thinking\n</think>\n\nHello, ");
        check(tmpls, continuation_reasoning(), "<|im_start|>assistant\n<think>\nI'm");
    }

    {
        auto tmpls = read_templates("models/templates/openai-gpt-oss-120b.jinja");
        check(tmpls, basic(),                  "<|start|>assistant");
        check(tmpls, continuation_content(),   "<|start|>assistant<|channel|>analysis<|message|>I'm thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, ");
        check(tmpls, continuation_reasoning(), "<|start|>assistant<|channel|>analysis<|message|>I'm");
    }

    {
        auto tmpls = read_templates("models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja");
        check(tmpls, basic(),                  "");
        check(tmpls, continuation_content(),   "[THINK]I'm thinking[/THINK]Hello, ");
        check(tmpls, continuation_reasoning(), "[THINK]I'm");
    }

    {
        auto tmpls = read_templates("models/templates/google-gemma-4-31B-it.jinja");
        check(tmpls, basic(),                  "<|turn>model\n");
        check(tmpls, continuation_content(),   "<|turn>model\n<|channel>thought\nI'm thinking<channel|>Hello, ");
        check(tmpls, continuation_reasoning(), "<|turn>model\n<|channel>thought\nI'm");

        // Special case when last message is a tool response
        test_case_options after_tool_call = continuation_reasoning();
        after_tool_call.messages          = { system_msg, message_user, tool_call_msg, tool_msg, message_assist_prefill_reasoning };
        check(tmpls, after_tool_call, "<|channel>thought\nI'm");
    }

    {
        auto tmpls = read_templates("models/templates/meetkai-functionary-medium-v3.2.jinja");
        check(tmpls, basic(),                  "<|start_header_id|>assistant<|end_header_id|>\n\n>>>");
        check(tmpls, continuation_content(),   "<|start_header_id|>assistant<|end_header_id|>\n\n>>>all\nHello, ");
        check(tmpls, continuation_reasoning(), "<|start_header_id|>assistant<|end_header_id|>\n\n>>>all\n");
    }

    {
        auto tmpls = read_templates("models/templates/Reka-Edge.jinja");
        check(tmpls, basic(),                  "assistant: <think>\n");
        check(tmpls, continuation_content(),   "assistant: <think>\nI'm thinking\n</think>\n\nHello, ");
        check(tmpls, continuation_reasoning(), "assistant: <think>\nI'm");
    }

    {
        auto tmpls = read_templates("models/templates/moonshotai-Kimi-K2.jinja");
        check(tmpls, basic(),                  "<|im_assistant|>assistant<|im_middle|>");
        check(tmpls, continuation_content(),   "<|im_assistant|>assistant<|im_middle|><think>I'm thinking</think>Hello, ");
        check(tmpls, continuation_reasoning(), "<|im_assistant|>assistant<|im_middle|><think>I'm");
    }

    for (const char * tmpl : {
             "models/templates/LFM2-8B-A1B.jinja",
             "models/templates/LFM2.5-Instruct.jinja",
             "models/templates/LFM2.5-8B-A1B.jinja",
         }) {
        auto tmpls = read_templates(tmpl);
        check(tmpls, basic(),                  "<|im_start|>assistant\n");
        check(tmpls, continuation_content(),   "<|im_start|>assistant\n<think>I'm thinking</think>Hello, ");
        check(tmpls, continuation_reasoning(), "<|im_start|>assistant\n<think>I'm");
    }

    {
        // 8B-A1B renders prior-turn reasoning via the "thinking" field
        auto tmpls = read_templates("models/templates/LFM2.5-8B-A1B.jinja");
        common_chat_templates_inputs inputs;
        inputs.messages              = { message_user, message_assist_call_thoughts, tool_msg };
        inputs.add_generation_prompt = true;
        auto params = common_chat_templates_apply(tmpls.get(), inputs);
        assert_contains(params.prompt, "<think>I'm\nthinking</think>");
    }

    {
        auto tmpls = read_templates("models/templates/GigaChat3-10B-A1.8B.jinja");
        check(tmpls, basic(),                  "assistant<|role_sep|>\n");
        check(tmpls, continuation_content(),   "assistant<|role_sep|>\nHello, ");
        check(tmpls, continuation_reasoning(), "assistant<|role_sep|>\n");
    }

    {
        auto tmpls = read_templates("models/templates/deepseek-ai-DeepSeek-V3.2.jinja");
        check(tmpls, basic(),                  "<｜Assistant｜><think>");
        check(tmpls, continuation_content(),   "<｜Assistant｜><think>I'm thinking</think>Hello, ");
        check(tmpls, continuation_reasoning(), "<｜Assistant｜><think>I'm");
    }

    {
        auto tmpls = read_templates("models/templates/openbmb-MiniCPM5-1B.jinja");
        check(tmpls, basic(),                  "<|im_start|>assistant\n<think>\n");
        check(tmpls, continuation_content(),   "<|im_start|>assistant\n<think>\nI'm thinking\n</think>\n\nHello, ");
        check(tmpls, continuation_reasoning(), "<|im_start|>assistant\n<think>\nI'm");
    }
}

// Test the developer role to system workaround with a simple mock template
static void test_developer_role_to_system_workaround() {
    LOG_DBG("%s\n", __func__);

    // Simple mock template that supports system role
    const std::string mock_template =
        "{%- for message in messages -%}\n"
        "  {{- '<|' + message.role + '|>' + message.content + '<|end|>' -}}\n"
        "{%- endfor -%}\n"
        "{%- if add_generation_prompt -%}\n"
        "  {{- '<|assistant|>' -}}\n"
        "{%- endif -%}";

    auto tmpls = common_chat_templates_ptr(common_chat_templates_init(/* model= */ nullptr, mock_template));

    // Test case 1: Developer message - should be changed to system
    // After simplification we only test this case
    {
        common_chat_templates_inputs inputs;
        common_chat_msg developer_msg;
        developer_msg.role = "developer";
        developer_msg.content = "You are a helpful developer assistant.";
        inputs.messages = { developer_msg };
        inputs.add_generation_prompt = false;

        auto params = common_chat_templates_apply(tmpls.get(), inputs);

        // The developer role should have been changed to system
        if (params.prompt.find("<|developer|>") != std::string::npos) {
            throw std::runtime_error("Test failed: developer role was not changed to system");
        }
        if (params.prompt.find("<|system|>You are a helpful developer assistant.<|end|>") == std::string::npos) {
            throw std::runtime_error("Test failed: system message not found in output");
        }
        LOG_ERR("Test 1 passed: developer role changed to system\n");
    }
}

// Verify reasoning-trace retention rules in the DeepSeek-V4 template:
// all traces are retained unless drop_thinking is true AND the conversation
// has no tool calls, in which case only the last (after-final-user) trace is
// kept and earlier ones are dropped.
static void test_deepseek_v4_thinking_retention() {
    LOG_DBG("%s\n", __func__);

    auto tmpls = read_templates("models/templates/deepseek-ai-DeepSeek-V4.jinja");

    common_chat_msg user_q1; user_q1.role = "user"; user_q1.content = "Question 1";
    common_chat_msg user_q2; user_q2.role = "user"; user_q2.content = "Question 2";
    common_chat_msg asst_a1 = simple_assist_msg("Answer 1", "thinking A1");
    common_chat_msg asst_a2 = simple_assist_msg("Answer 2", "thinking A2");

    common_chat_msg tool_assist = message_with_tool_calls("special_function", "{\"arg1\": 1}");
    common_chat_msg tool_result; tool_result.role = "tool";
    tool_result.tool_name = "special_function"; tool_result.tool_call_id = "0"; tool_result.content = "result";

    // The template uses U+FF5C as the role separator and literal think tags
    // for the reasoning block.
    const std::string asst_marker   = "<\xef\xbd\x9c" "Assistant" "\xef\xbd\x9c>";
    // Built via concatenation so the thinking tokens are not interpreted by
    // tooling processing this source file.
    const std::string think_start   = "<" "think" ">";
    const std::string think_end     = "</" "think" ">";

    const std::string think_a1      = asst_marker + think_start + "thinking A1" + think_end;
    const std::string think_a2      = asst_marker + think_start + "thinking A2" + think_end;
    const std::string asst_no_think = asst_marker + think_end;

    auto render = [&](const std::vector<common_chat_msg> & messages, bool drop_thinking) {
        common_chat_templates_inputs inputs;
        inputs.messages = messages;
        inputs.add_generation_prompt = false;
        inputs.chat_template_kwargs["thinking"]     = "true";
        inputs.chat_template_kwargs["drop_thinking"] = drop_thinking ? "true" : "false";
        return common_chat_templates_apply(tmpls.get(), inputs).prompt;
    };

    // No tools, drop_thinking=false: all reasoning is retained.
    {
        auto prompt = render({ user_q1, asst_a1, user_q2, asst_a2 }, /* drop_thinking = */ false);
        assert_contains(prompt, think_a1);
        assert_contains(prompt, think_a2);
    }

    // No tools, drop_thinking=true: only the last reasoning trace is kept,
    // earlier ones are dropped (the assistant block emits just the end token).
    {
        auto prompt = render({ user_q1, asst_a1, user_q2, asst_a2 }, /* drop_thinking = */ true);
        assert_not_contains(prompt, think_a1);
        assert_contains(prompt, think_a2);
        // The dropped assistant turn still opens with the marker + bare end token.
        assert_contains(prompt, asst_no_think + "Answer 1");
    }

    // Single assistant turn, drop_thinking=true: the only trace is the last
    // one, so it must be retained even with drop_thinking set.
    {
        auto prompt = render({ user_q1, asst_a1 }, /* drop_thinking = */ true);
        assert_contains(prompt, think_a1);
    }

    // Single assistant turn, drop_thinking=false: reasoning is retained.
    {
        auto prompt = render({ user_q1, asst_a1 }, /* drop_thinking = */ false);
        assert_contains(prompt, think_a1);
    }

    // With tool calls, drop_thinking=true: tool presence forces all reasoning
    // to be retained, including the pre-tool-call trace.
    {
        auto prompt = render({ user_q1, asst_a1, user_q2, tool_assist, tool_result, asst_a2 },
                             /* drop_thinking = */ true);
        assert_contains(prompt, think_a1);
        assert_contains(prompt, think_a2);
    }

    // With tool calls, drop_thinking=false: all reasoning retained.
    {
        auto prompt = render({ user_q1, asst_a1, user_q2, tool_assist, tool_result, asst_a2 },
                             /* drop_thinking = */ false);
        assert_contains(prompt, think_a1);
        assert_contains(prompt, think_a2);
    }
}

// Verify that consecutive tool results are rendered in the tool call order of the
// preceding assistant message (matched by tool call id), as required by the reference
// DeepSeek-V4 implementation.
static void test_deepseek_v4_tool_result_ordering() {
    LOG_DBG("%s\n", __func__);

    auto tmpls = read_templates("models/templates/deepseek-ai-DeepSeek-V4.jinja");

    common_chat_msg user_q; user_q.role = "user"; user_q.content = "Question";

    common_chat_msg assist_calls;
    assist_calls.role = "assistant";
    assist_calls.tool_calls.push_back({ "get_time",    "{\"city\": \"Paris\"}", "call_1" });
    assist_calls.tool_calls.push_back({ "get_weather", "{\"city\": \"Paris\"}", "call_2" });

    common_chat_msg time_result; time_result.role = "tool";
    time_result.tool_name = "get_time"; time_result.tool_call_id = "call_1"; time_result.content = "12:00";
    common_chat_msg weather_result; weather_result.role = "tool";
    weather_result.tool_name = "get_weather"; weather_result.tool_call_id = "call_2"; weather_result.content = "sunny";

    auto render = [&](const std::vector<common_chat_msg> & messages) {
        common_chat_templates_inputs inputs;
        inputs.messages = messages;
        inputs.add_generation_prompt = false;
        return common_chat_templates_apply(tmpls.get(), inputs).prompt;
    };

    // Results sent out of order are reordered to match the tool call order.
    {
        auto prompt = render({ user_q, assist_calls, weather_result, time_result });
        assert_contains(prompt, "<tool_result>12:00</tool_result>\n\n<tool_result>sunny</tool_result>");
    }

    // Results already in call order stay put.
    {
        auto prompt = render({ user_q, assist_calls, time_result, weather_result });
        assert_contains(prompt, "<tool_result>12:00</tool_result>\n\n<tool_result>sunny</tool_result>");
    }

    // Without tool call ids there is nothing to match against; order is preserved.
    {
        auto no_id_calls = assist_calls;
        no_id_calls.tool_calls[0].id = "";
        no_id_calls.tool_calls[1].id = "";
        auto no_id_weather = weather_result; no_id_weather.tool_call_id = "";
        auto no_id_time    = time_result;    no_id_time.tool_call_id = "";
        auto prompt = render({ user_q, no_id_calls, no_id_weather, no_id_time });
        assert_contains(prompt, "<tool_result>sunny</tool_result>\n\n<tool_result>12:00</tool_result>");
    }
}

static void test_reasoning_budget_tokens_per_request() {
    LOG_DBG("%s\n", __func__);
    // Use Qwen3 template which has <think>...</think> reasoning markers.
    // The autoparser detects them and sets thinking_start/end_tag, which enables
    // the reasoning-budget code path in oaicompat_chat_params_parse.
    auto tmpls = read_templates("models/templates/Qwen-Qwen3-0.6B.jinja");

    server_chat_params opt;
    opt.tmpls            = std::move(tmpls);
    opt.use_jinja        = true;
    opt.enable_thinking  = true;
    opt.reasoning_budget = -1;
    opt.reasoning_format = COMMON_REASONING_FORMAT_NONE;

    // Body with per-request reasoning_budget_tokens=0 (suppress thinking).
    json body = {
        {"messages", json::array({json{{"role", "user"}, {"content", "hello"}}})},
        {"reasoning_budget_tokens", 0},
    };
    std::vector<raw_buffer> out_files;
    auto llama_params = oaicompat_chat_params_parse(body, opt, out_files);

    // The per-request value must win over the server default (-1).
    if (!llama_params.contains("reasoning_budget_tokens")) {
        throw std::runtime_error("reasoning_budget_tokens missing from llama_params (thinking_end_tag may be empty for this template)");
    }
    int got = llama_params["reasoning_budget_tokens"].get<int>();
    if (got != 0) {
        throw std::runtime_error(std::string("Expected reasoning_budget_tokens=0, got ") + std::to_string(got));
    }
}

static void test_reasoning_budget_message_per_request() {
    LOG_DBG("%s\n", __func__);
    // Same code path as test_reasoning_budget_tokens_per_request: the Qwen3 template's
    // <think>...</think> markers enable the reasoning-budget block in oaicompat_chat_params_parse.
    auto tmpls = read_templates("models/templates/Qwen-Qwen3-0.6B.jinja");

    server_chat_params opt;
    opt.tmpls                   = std::move(tmpls);
    opt.use_jinja               = true;
    opt.enable_thinking         = true;
    opt.reasoning_budget        = -1;
    opt.reasoning_format        = COMMON_REASONING_FORMAT_NONE;
    opt.reasoning_budget_message = "server default";

    // Body with a per-request reasoning_budget_message override.
    const std::string per_request_message = "per-request message";
    json body = {
        {"messages", json::array({json{{"role", "user"}, {"content", "hello"}}})},
        {"reasoning_budget_message", per_request_message},
    };
    std::vector<raw_buffer> out_files;
    auto llama_params = oaicompat_chat_params_parse(body, opt, out_files);

    // The per-request value must win over the server default.
    if (!llama_params.contains("reasoning_budget_message")) {
        throw std::runtime_error("reasoning_budget_message missing from llama_params (thinking_end_tag may be empty for this template)");
    }
    std::string got = llama_params["reasoning_budget_message"].get<std::string>();
    if (got != per_request_message) {
        throw std::runtime_error("Expected reasoning_budget_message='" + per_request_message + "', got '" + got + "'");
    }
}

static void test_msg_diffs_compute() {
    LOG_DBG("%s\n", __func__);
    {
        common_chat_msg msg1;

        common_chat_msg msg2;
        msg2.content = "Hello, world!";

        common_chat_msg_diff diff;
        diff.content_delta = "Hello, world!";

        assert_equals({ diff }, common_chat_msg_diff::compute_diffs(msg1, msg2));
    }
    {
        common_chat_msg msg1;
        msg1.content = "Hello,";

        common_chat_msg msg2;
        msg2.content = "Hello, world!";

        common_chat_msg_diff diff;
        diff.content_delta = " world!";

        assert_equals({ diff }, common_chat_msg_diff::compute_diffs(msg1, msg2));
    }
    {
        common_chat_msg msg0;

        common_chat_msg msg1;
        msg1.tool_calls = {
            { "special_function", "{\"ar", /* .id = */ "123" }
        };

        common_chat_msg msg2;
        msg2.tool_calls = {
            { "special_function", "{\"arg1\": 1}", /* .id = */ "123" }
        };

        common_chat_msg_diff diff01;
        diff01.tool_call_index           = 0;
        diff01.tool_call_delta.name      = "special_function";
        diff01.tool_call_delta.id        = "123";
        diff01.tool_call_delta.arguments = "{\"ar";

        assert_equals({ diff01 }, common_chat_msg_diff::compute_diffs(msg0, msg1));

        common_chat_msg_diff diff12;
        diff12.tool_call_index           = 0;
        // Note: neither id nor name change here.
        diff12.tool_call_delta.arguments = "g1\": 1}";

        assert_equals({ diff12 }, common_chat_msg_diff::compute_diffs(msg1, msg2));
    }
    {
        common_chat_msg msg0;

        common_chat_msg msg2;
        msg2.tool_calls = {
            { "f1", "{\"arg1\": 1}", /* .id = */ "123" },
            { "f2", "{\"arg2\": 2}", /* .id = */ "222" },
        };

        common_chat_msg_diff diff1;
        diff1.tool_call_index           = 0;
        diff1.tool_call_delta.name      = "f1";
        diff1.tool_call_delta.id        = "123";
        diff1.tool_call_delta.arguments = "{\"arg1\": 1}";

        common_chat_msg_diff diff2;
        diff2.tool_call_index           = 1;
        diff2.tool_call_delta.name      = "f2";
        diff2.tool_call_delta.id        = "222";
        diff2.tool_call_delta.arguments = "{\"arg2\": 2}";

        assert_equals({ diff1, diff2 }, common_chat_msg_diff::compute_diffs(msg0, msg2));
    }
}

int main(int argc, char ** argv) {
    bool detailed_debug    = false;
    bool only_run_filtered = false;

    // Check for --template and --detailed flags
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--template" && i + 1 < argc) {
            g_template_filter = argv[++i];
            // Only run PEG parser tests with the filter
            only_run_filtered = true;
        }
        if (arg == "--detailed") {
            detailed_debug = true;
            common_log_set_verbosity_thold(999);
        }
        if (arg == "--force-reconstruction-test") {
            g_force_reconstruction_test = true;
            only_run_filtered          = true;
        }
    }

    if (only_run_filtered) {
        test_template_output_peg_parsers(detailed_debug);
        std::cout << "\n[chat] All template tests passed!" << '\n';
        return 0;
    }

#ifndef _WIN32
    // Check if any argument is a .jinja file (for template format detection mode)
    bool has_jinja_files = false;
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--detailed") {
            continue;
        }
        if (arg.size() >= 6 && arg.rfind(".jinja") == arg.size() - 6) {
            has_jinja_files = true;
            break;
        }
    }

    if (has_jinja_files) {
        common_chat_templates_inputs inputs;
        common_chat_msg              msg;
        msg.role        = "user";
        msg.content     = "Hey";
        inputs.messages = { msg };
        inputs.tools    = { special_function_tool };

        std::cout << "| Template | Format |\n";
        std::cout << "|----------|--------|\n";

        for (int i = 1; i < argc; i++) {
            try {
                std::string path = argv[i];
                if (path.rfind(".jinja") != path.size() - 6) {
                    std::cerr << "Skipping non-jinja file: " << path << '\n';
                    continue;
                }
                auto         tmpls  = read_templates(path);
                auto         parts  = string_split(path, "/");
                const auto & name   = parts[parts.size() - 1];
                const auto * format = common_chat_format_name(common_chat_templates_apply(tmpls.get(), inputs).format);
                std::cout << "| " << name << " | " << format << " |\n";
            } catch (const std::exception & e) {
                std::cerr << "Failed to process " << argv[i] << ": " << e.what() << '\n';
            }
        }
    } else
#endif
    {
        test_msg_diffs_compute();
        test_msgs_oaicompat_json_conversion();
        test_msg_token_delimiters_split();
        test_tools_oaicompat_json_conversion();
        test_convert_responses_to_chatcmpl();
        test_developer_role_to_system_workaround();
        test_deepseek_v4_thinking_retention();
        test_deepseek_v4_tool_result_ordering();
        test_template_generation_prompt();
        test_reasoning_budget_tokens_per_request();
        test_reasoning_budget_message_per_request();
        test_template_output_peg_parsers(detailed_debug);
        std::cout << "\n[chat] All tests passed!" << '\n';
    }
    return 0;
}
