mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-15 00:45:56 +02:00
99f3dc3229
* server: honour per-request reasoning_budget_tokens in chat completions The reasoning-budget block in oaicompat_chat_params_parse read only the server-level default (opt.reasoning_budget, typically -1) and the Anthropic-style alias thinking_budget_tokens, but never the canonical reasoning_budget_tokens field from the request body. Because the key was then written into llama_params before the generic body-copy loop ran, the copy loop found the key already present and silently skipped the caller-supplied value. Any per-request override (e.g. 0 to suppress thinking entirely) was therefore discarded. Fix: read reasoning_budget_tokens from the request body first, so the value that reaches the sampling layer is the one the caller intended. Add a unit test in test-chat.cpp that exercises this path via oaicompat_chat_params_parse with a Qwen3 template (which the autoparser detects as a thinking-capable model) and asserts the returned llama_params carries reasoning_budget_tokens == 0. * server: honour per-request reasoning_budget_message in chat completions The reasoning-budget block in oaicompat_chat_params_parse wrote reasoning_budget_message into llama_params straight from the server-level default (opt.reasoning_budget_message) and never read the canonical reasoning_budget_message field from the request body. Because the key was written before the generic body-copy loop ran, that loop found the key already present and silently skipped the caller-supplied value. Any per-request override of the message injected before the end tag when the budget is exhausted was therefore discarded, even though server-task.cpp already reads reasoning_budget_message from that data. This mirrors the reasoning_budget_tokens bug fixed in the previous commit. Fix: read reasoning_budget_message from the request body first, falling back to the server default, so the value that reaches the sampling layer is the one the caller intended. While here, collapse the adjacent reasoning_budget_tokens override to a single json_value() call; json_value already falls back to the default on a missing/null/wrong-type key, so the explicit body.contains() guard was redundant. No behavioral change. Add a unit test in test-chat.cpp that exercises this path via oaicompat_chat_params_parse with a Qwen3 template (which the autoparser detects as a thinking-capable model) and asserts the returned llama_params carries the per-request reasoning_budget_message rather than the server default. * cleanup --------- Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
6143 lines
249 KiB
C++
6143 lines
249 KiB
C++
// 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_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 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_tag.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_end = parser.params_.thinking_end_tag;
|
||
|
||
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 && full_input.compare(i, think_end.size(), think_end) == 0) {
|
||
in_thinking = false;
|
||
i += think_end.size() - 1;
|
||
continue;
|
||
}
|
||
if (in_thinking) {
|
||
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();
|
||
|
||
// 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();
|
||
}
|
||
|
||
// 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();
|
||
}
|
||
|
||
// 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("<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>")
|
||
.tools({ special_function_tool })
|
||
.expect(message_assist_call)
|
||
.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");
|
||
}
|
||
}
|
||
|
||
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_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;
|
||
}
|