Brunobkr/llama.cpp_AlgMor24_github
ΩFFFΣLLIa • llama.cpp • AlgMor24 ██████╗ ███████╗███████╗███████╗██╗ ██╗ ██╗ █████╗ ██╔═══██╗██╔════╝██╔════╝██╔════╝██║ ██║ ██║██╔══██╗ ██║ ██║█████╗ █████╗ █████╗ ██║ ██║ ██║███████║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██║ ██║ ██║██╔══██║ ╚██████╔╝██║ ██║ ███████╗███████╗███████╗██║██║ ██║ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚═╝╚═╝ ╚═╝ High-Performance LLM / VLM Inference & Autonomous Agentic Ecosystem… See the full description on the dataset page: https://huggingface.co/datasets/Brunobkr/llama.cpp_AlgMor24_github.
03k
1// Tests chat handling, including grammar genration and parsing for tool calling, for various templates.2//3// Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates,4// e.g. given Minja (http://github.com/google/minja) checked out in parent dir:5//6// cmake -B build && cmake --build build --parallel && ./build/bin/test-chat ../minja/build/tests/*.jinja 2>/dev/null7//8#include "../src/llama-grammar.h"9#include "../src/unicode.h"10#include "../tools/server/server-chat.h"11#include "chat-auto-parser.h"12#include "chat.h"13#include "common.h"14#include "ggml.h"15#include "log.h"16 17#include <algorithm>18#include <exception>19#include <fstream>20#include <functional>21#include <iostream>22#include <nlohmann/json.hpp>23#include <set>24#include <stdexcept>25#include <string>26 27using json = nlohmann::ordered_json;28 29static std::ostream & operator<<(std::ostream & os, const common_chat_msg_diff & diff) {30 os << "{ content_delta: " << diff.content_delta << "; ";31 os << "reasoning_content_delta: " << diff.reasoning_content_delta << "; ";32 if (diff.tool_call_index != std::string::npos) {33 os << "tool_call_index: " << diff.tool_call_index << "; ";34 os << "tool_call_delta.name: " << diff.tool_call_delta.name << "; ";35 os << "tool_call_delta.id: " << diff.tool_call_delta.id << "; ";36 os << "tool_call_delta.arguments: " << diff.tool_call_delta.arguments << "; ";37 }38 os << "}";39 return os;40}41 42// operator<< for vector<common_chat_msg_diff>:43static std::ostream & operator<<(std::ostream & os, const std::vector<common_chat_msg_diff> & diffs) {44 os << "[\n";45 for (const auto & diff : diffs) {46 os << " " << diff << ",\n";47 }48 os << "]";49 return os;50}51 52static std::ostream & operator<<(std::ostream & os, const common_chat_msg & msg) {53 os << "{ role: " << msg.role << "; ";54 os << "content: " << msg.content << "; ";55 os << "content_parts: [\n";56 for (const auto & part : msg.content_parts) {57 os << " { type: " << part.type << "; text: " << part.text << " },\n";58 }59 os << "]; ";60 os << "reasoning_content: " << msg.reasoning_content << "; ";61 os << "tool_calls: [\n";62 for (const auto & tool_call : msg.tool_calls) {63 os << " { name: " << tool_call.name << "; arguments: " << tool_call.arguments << "; id: " << tool_call.id64 << " },\n";65 }66 os << "]";67 os << "}";68 return os;69}70 71template <class T> static bool equals(const T & expected, const T & actual) {72 return expected == actual;73}74 75static common_chat_msg normalize(const common_chat_msg & msg) {76 common_chat_msg normalized = msg;77 for (auto & tool_call : normalized.tool_calls) {78 try {79 tool_call.arguments = json::parse(tool_call.arguments).dump();80 } catch (const std::exception &) {81 }82 }83 return normalized;84}85 86template <> bool equals(const common_chat_msg & expected, const common_chat_msg & actual) {87 return normalize(expected) == normalize(actual);88}89 90template <class T> static void assert_equals(const T & expected, const T & actual) {91 if (!equals(expected, actual)) {92 std::ostringstream oss_expected;93 oss_expected << expected;94 std::ostringstream oss_actual;95 oss_actual << actual;96 LOG_ERR("Expected: %s\n", oss_expected.str().c_str());97 LOG_ERR("Actual: %s\n", oss_actual.str().c_str());98 common_log_flush(common_log_main());99 throw std::runtime_error("Test failed");100 }101}102 103static void assert_contains(const std::string & haystack, const std::string & needle) {104 if (haystack.find(needle) == std::string::npos) {105 LOG_ERR("Expected to contain: %s\n", needle.c_str());106 LOG_ERR("Actual: %s\n", haystack.c_str());107 common_log_flush(common_log_main());108 throw std::runtime_error("Test failed");109 }110}111 112static void assert_not_contains(const std::string & haystack, const std::string & needle) {113 if (haystack.find(needle) != std::string::npos) {114 LOG_ERR("Expected NOT to contain: %s\n", needle.c_str());115 LOG_ERR("Actual: %s\n", haystack.c_str());116 common_log_flush(common_log_main());117 throw std::runtime_error("Test failed");118 }119}120 121static void assert_ends_with(const std::string & str, const std::string & suffix) {122 if (str.size() < suffix.size() ||123 str.compare(str.size() - suffix.size(), suffix.size(), suffix) != 0) {124 LOG_ERR("Expected to end with: %s\n", suffix.c_str());125 LOG_ERR("Actual: %s\n", str.c_str());126 common_log_flush(common_log_main());127 throw std::runtime_error("Test failed");128 }129}130 131static std::string read_file(const std::string & path) {132 std::ifstream fs(path, std::ios_base::binary);133 if (!fs.is_open()) {134 fs = std::ifstream("../" + path, std::ios_base::binary);135 if (!fs.is_open()) {136 throw std::runtime_error("Failed to open file: " + path);137 }138 }139 fs.seekg(0, std::ios_base::end);140 auto size = fs.tellg();141 fs.seekg(0);142 std::string out;143 out.resize(static_cast<size_t>(size));144 fs.read(out.data(), static_cast<std::streamsize>(size));145 return out;146}147 148static common_chat_templates_ptr read_templates(const std::string & path) {149 return common_chat_templates_ptr(common_chat_templates_init(/* model= */ nullptr, read_file(path)));150}151 152static std::unique_ptr<llama_grammar> build_grammar(const std::string & grammar_str) {153 return std::unique_ptr<llama_grammar>(154 llama_grammar_init_impl(nullptr, grammar_str.c_str(), "root", false, nullptr, 0, nullptr, 0));155}156 157// Helper to format a code point as a readable string158static std::string format_codepoint(uint32_t cp) {159 if (cp >= 32 && cp < 127) {160 return std::string("'") + static_cast<char>(cp) + "'";161 } else if (cp == '\n') {162 return "'\\n'";163 } else if (cp == '\r') {164 return "'\\r'";165 } else if (cp == '\t') {166 return "'\\t'";167 } else {168 return "U+" + std::to_string(cp);169 }170}171 172// Helper to format expected element from grammar stack173static std::string format_expected_element(const llama_grammar_rules & /* rules*/, const llama_grammar_element * elem) {174 if (!elem) {175 return "<end>";176 }177 178 switch (elem->type) {179 case LLAMA_GRETYPE_END:180 return "<end of rule>";181 case LLAMA_GRETYPE_ALT:182 return "<alternative>";183 case LLAMA_GRETYPE_RULE_REF:184 {185 // Find rule name - just show rule ID for now186 return "<rule-" + std::to_string(elem->value) + ">";187 }188 case LLAMA_GRETYPE_CHAR:189 {190 std::string result;191 const llama_grammar_element * pos = elem;192 bool first = true;193 194 do {195 if (!first) {196 result += " | ";197 }198 first = false;199 200 if (pos[1].type == LLAMA_GRETYPE_CHAR_RNG_UPPER) {201 // Range like [a-z]202 result += "[" + format_codepoint(pos->value) + "-" + format_codepoint(pos[1].value) + "]";203 pos += 2;204 } else {205 result += format_codepoint(pos->value);206 pos += 1;207 }208 } while (pos->type == LLAMA_GRETYPE_CHAR_ALT);209 210 return result;211 }212 case LLAMA_GRETYPE_CHAR_NOT:213 {214 std::string result = "[^";215 const llama_grammar_element * pos = elem;216 bool first = true;217 218 do {219 if (!first) {220 result += " ";221 }222 first = false;223 224 if (pos[1].type == LLAMA_GRETYPE_CHAR_RNG_UPPER) {225 result += format_codepoint(pos->value) + "-" + format_codepoint(pos[1].value);226 pos += 2;227 } else {228 result += format_codepoint(pos->value);229 pos += 1;230 }231 } while (pos->type == LLAMA_GRETYPE_CHAR_ALT);232 233 return result + "]";234 }235 case LLAMA_GRETYPE_CHAR_ANY:236 return "<any char>";237 case LLAMA_GRETYPE_TOKEN:238 return "<token-" + std::to_string(elem->value) + ">";239 case LLAMA_GRETYPE_TOKEN_NOT:240 return "<not-token-" + std::to_string(elem->value) + ">";241 default:242 return "<unknown>";243 }244}245 246// Get description of what the grammar expects at current position247static std::string get_expected_description(const llama_grammar_rules & rules, const llama_grammar_stacks & stacks) {248 if (stacks.empty()) {249 return "<no valid continuations>";250 }251 252 std::string result;253 std::set<std::string> seen;254 255 for (const auto & stack : stacks) {256 if (stack.empty()) {257 if (seen.insert("<end>").second) {258 if (!result.empty()) {259 result += " OR ";260 }261 result += "<end>";262 }263 continue;264 }265 266 const llama_grammar_element * elem = stack.back();267 std::string desc = format_expected_element(rules, elem);268 if (seen.insert(desc).second) {269 if (!result.empty()) {270 result += " OR ";271 }272 result += desc;273 }274 }275 276 return result;277}278 279// Result of a detailed grammar match attempt280struct grammar_match_result {281 bool success = false; // Did the string fully match the grammar?282 size_t matched_bytes = 0; // Bytes successfully matched before failure283 size_t matched_codepoints = 0; // Codepoints successfully matched before failure284 size_t total_bytes = 0; // Total bytes in input285 size_t total_codepoints = 0; // Total codepoints in input286 std::string matched_prefix; // The portion that was successfully matched287 std::string failing_char; // The character that caused failure (if any)288 std::string expected_description; // What the grammar expected at failure point289 bool incomplete = false; // True if matched all input but grammar expects more290};291 292// Detailed version of match_string that returns failure information293static grammar_match_result match_string_detailed(const std::string & input, llama_grammar * grammar) {294 grammar_match_result result;295 result.total_bytes = input.size();296 297 const auto cpts = unicode_cpts_from_utf8(input);298 result.total_codepoints = cpts.size();299 300 auto & stacks_cur = llama_grammar_get_stacks(grammar);301 const auto & rules = llama_grammar_get_rules(grammar);302 303 size_t byte_pos = 0;304 305 for (size_t i = 0; i < cpts.size(); i++) {306 const auto & cpt = cpts[i];307 308 // Get expected before accepting (for error reporting)309 std::string expected_before = get_expected_description(rules, stacks_cur);310 311 llama_grammar_accept(grammar, cpt);312 313 // Calculate byte position for this codepoint314 size_t cpt_bytes = 0;315 if (cpt < 0x80) {316 cpt_bytes = 1;317 } else if (cpt < 0x800) {318 cpt_bytes = 2;319 } else if (cpt < 0x10000) {320 cpt_bytes = 3;321 } else {322 cpt_bytes = 4;323 }324 325 if (stacks_cur.empty()) {326 // Grammar failed to match at this point327 result.matched_bytes = byte_pos;328 result.matched_codepoints = i;329 result.matched_prefix = input.substr(0, byte_pos);330 result.failing_char = format_codepoint(cpt);331 result.expected_description = expected_before;332 result.incomplete = false;333 return result;334 }335 336 byte_pos += cpt_bytes;337 }338 339 // All input matched - check if grammar is complete340 result.matched_bytes = input.size();341 result.matched_codepoints = cpts.size();342 result.matched_prefix = input;343 344 if (std::any_of(stacks_cur.begin(), stacks_cur.end(), [](const auto & stack) { return stack.empty(); })) {345 // An empty stack means that the grammar has been completed346 result.success = true;347 result.incomplete = false;348 } else {349 // Grammar expects more input350 result.success = false;351 result.incomplete = true;352 result.expected_description = get_expected_description(rules, stacks_cur);353 }354 355 return result;356}357 358// TODO: extract to common helper (copied from test-grammar-integration.cpp)359static bool match_string(const std::string & input, llama_grammar * grammar) {360 const auto cpts = unicode_cpts_from_utf8(input);361 362 auto & stacks_cur = llama_grammar_get_stacks(grammar);363 364 for (const auto & cpt : cpts) {365 llama_grammar_accept(grammar, cpt);366 367 if (stacks_cur.empty()) {368 // no stacks means that the grammar failed to match at this point369 return false;370 }371 }372 373 if (std::any_of(stacks_cur.begin(), stacks_cur.end(), [](const auto & stack) { return stack.empty(); })) {374 // An empty stack means that the grammar has been completed375 return true;376 }377 378 return false;379}380 381static std::string renormalize_json(const std::string & json_str) {382 try {383 auto json_obj = json::parse(json_str);384 return json_obj.dump();385 } catch (const std::exception & e) {386 return ""; // ignore parial JSON contents for comparison purposes387 }388}389 390static void assert_msg_equals(const common_chat_msg & expected,391 const common_chat_msg & actual,392 bool ignore_whitespace_differences = false) {393 assert_equals(expected.role, actual.role);394 if (ignore_whitespace_differences) {395 assert_equals(string_strip(expected.content), string_strip(actual.content));396 } else {397 assert_equals(expected.content, actual.content);398 }399 assert_equals(expected.content_parts.size(), actual.content_parts.size());400 for (size_t i = 0; i < expected.content_parts.size(); i++) {401 const auto & expected_part = expected.content_parts[i];402 const auto & actual_part = actual.content_parts[i];403 assert_equals(expected_part.type, actual_part.type);404 if (ignore_whitespace_differences) {405 assert_equals(string_strip(expected_part.text), string_strip(actual_part.text));406 } else {407 assert_equals(expected_part.text, actual_part.text);408 }409 }410 if (ignore_whitespace_differences) {411 assert_equals(string_strip(expected.reasoning_content), string_strip(actual.reasoning_content));412 } else {413 assert_equals(expected.reasoning_content, actual.reasoning_content);414 }415 assert_equals(expected.tool_calls.size(), actual.tool_calls.size());416 for (size_t i = 0; i < expected.tool_calls.size(); i++) {417 const auto & expected_tool_call = expected.tool_calls[i];418 const auto & actual_tool_call = actual.tool_calls[i];419 assert_equals(expected_tool_call.name, actual_tool_call.name);420 assert_equals(renormalize_json(expected_tool_call.arguments), renormalize_json(actual_tool_call.arguments));421 assert_equals(expected_tool_call.id, actual_tool_call.id);422 }423}424 425static common_chat_tool special_function_tool{426 /* .name = */ "special_function",427 /* .description = */ "I'm special",428 /* .parameters = */ R"({429 "type": "object",430 "properties": {431 "arg1": {432 "type": "integer",433 "description": "The arg."434 }435 },436 "required": ["arg1"]437 })",438};439static common_chat_tool special_function_tool_with_optional_param{440 /* .name = */ "special_function_with_opt",441 /* .description = */ "I'm special but have optional stuff",442 /* .parameters = */ R"({443 "type": "object",444 "properties": {445 "arg1": {446 "type": "integer",447 "description": "The arg."448 },449 "arg2": {450 "type": "integer",451 "description": "The optional arg."452 }453 },454 "required": ["arg1"]455 })",456};457 458static common_chat_tool empty_args_tool{459 /* .name = */ "empty_args",460 /* .description = */ "A tool that takes no arguments",461 /* .parameters = */ R"({462 "type": "object",463 "properties": {}464 })",465};466 467static common_chat_tool empty_args_tool_no_properties{468 /* .name = */ "empty_args_no_props",469 /* .description = */ "A tool that takes no arguments and has no properties",470 /* .parameters = */ R"({471 "type": "object"472 })",473};474 475static common_chat_tool python_tool{476 /* .name = */ "python",477 /* .description = */ "an ipython interpreter",478 /* .parameters = */ R"({479 "type": "object",480 "properties": {481 "code": {482 "type": "string",483 "description": "Python code to execute."484 }485 },486 "required": ["code"]487 })",488};489 490static common_chat_tool html_tool{491 /* .name = */ "html",492 /* .description = */ "an html validator",493 /* .parameters = */ R"({494 "type": "object",495 "properties": {496 "markup": {497 "type": "string",498 "description": "HTML markup to validate."499 }500 },501 "required": ["markup"]502 })",503};504 505static common_chat_tool get_time_tool{506 /* .name = */ "get_time",507 /* .description = */ "Get the current time in a city",508 /* .parameters = */ R"({509 "type": "object",510 "properties": {511 "city": {512 "type": "string",513 "description": "City name"514 }515 },516 "required": ["city"]517 })",518};519 520static common_chat_tool get_weather_tool{521 /* .name = */ "get_weather",522 /* .description = */ "Get the current weather in a city",523 /* .parameters = */ R"({524 "type": "object",525 "properties": {526 "city": {527 "type": "string",528 "description": "City name"529 }530 },531 "required": ["city"]532 })",533};534 535static common_chat_tool todo_list{536 /* .name = */ "todo_list",537 /* .description = */ "Create or update the todo list",538 /* .parameters = */ R"({539 "type": "object",540 "properties": {541 "todos": {542 "type": "array",543 "description": "List of TODO list items"544 }545 },546 "required": ["todos"]547 })",548};549 550static common_chat_tool edit_tool{551 /* .name = */ "edit",552 /* .description = */ "Edit file",553 /* .parameters = */ R"({554 "type": "object",555 "properties": {556 "filename": {557 "type": "string",558 "description": "Path of file to edit"559 },560 "oldString": {561 "type": "string",562 "description": "String to replace"563 },564 "newString": {565 "type": "string",566 "description": "New (replacement) value"567 }568 },569 "required": ["filename", "oldString", "newString"]570 })",571};572 573static common_chat_tool manage_todo_list_tool{574 /* .name = */ "manage_todo_list",575 /* .description = */ "Create or update the todo list",576 /* .parameters = */ R"({577 "type": "object",578 "properties": {579 "todos": {580 "type": "array",581 "description": "List of TODO list items"582 }583 },584 "required": ["todos"]585 })",586};587 588static common_chat_tool run_in_terminal_tool{589 /* .name = */ "run_in_terminal",590 /* .description = */ "Run a shell command.",591 /* .parameters = */ R"({592 "type": "object",593 "properties": {594 "command": {595 "type": "string",596 "description": "Shell command to run"597 }598 },599 "required": ["command"]600 })",601};602 603static common_chat_tool magic_tool{604 /* .name = */ "magic",605 /* .description = */ "Magic tool that takes a hash",606 /* .parameters = */ R"({607 "type": "object",608 "properties": {609 "name": {610 "type": "string"611 },612 "ref": {613 "type": "string"614 }615 },616 "required": ["name", "ref"]617 })",618};619 620static common_chat_tool magic_int_tool{621 /* .name = */ "magic_int",622 /* .description = */ "Magic tool that takes a hash",623 /* .parameters = */ R"({624 "type": "object",625 "properties": {626 "ref": {627 "type": "integer"628 },629 "name": {630 "type": "string"631 }632 },633 "required": ["ref"]634 })",635};636 637static common_chat_tool amount_tool{638 /* .name = */ "amount",639 /* .description = */ "Amount converter",640 /* .parameters = */ R"({641 "type": "object",642 "properties": {643 "orig": {644 "type": "number"645 }646 },647 "required": ["orig"]648 })",649};650 651static common_chat_tool toggle_tool{652 /* .name = */ "toggle",653 /* .description = */ "Toggle a feature",654 /* .parameters = */ R"({655 "type": "object",656 "properties": {657 "enabled": {658 "type": "boolean",659 "description": "Whether to enable the feature"660 }661 },662 "required": ["enabled"]663 })",664};665 666static common_chat_tool nullable_tool{667 /* .name = */ "set_nullable",668 /* .description = */ "Set a nullable value",669 /* .parameters = */ R"({670 "type": "object",671 "properties": {672 "value": {673 "type": "null",674 "description": "A null value"675 }676 },677 "required": ["value"]678 })",679};680 681static common_chat_tool config_tool{682 /* .name = */ "set_config",683 /* .description = */ "Set configuration",684 /* .parameters = */ R"({685 "type": "object",686 "properties": {687 "config": {688 "type": "object",689 "description": "Configuration dict"690 }691 },692 "required": ["config"]693 })",694};695 696static common_chat_tool calendar_create_event_tool{697 /* .name = */ "Calendar.create_event",698 /* .description = */ "Create a calendar event",699 /* .parameters = */ R"({700 "type": "object",701 "properties": {702 "title": { "type": "string" },703 "participants": { "type": "array", "items": { "type": "string" } },704 "metadata": { "type": "object" }705 },706 "required": ["title", "participants", "metadata"]707 })",708};709 710static common_chat_tool imaginary_number_tool{711 /* .name = */ "imaginary_number",712 /* .description = */ "Imaginary number converter",713 /* .parameters = */ R"({714 "type": "object",715 "properties": {716 "number": {717 "type": "object",718 "properties": {719 "real": {720 "type": "number"721 },722 "imaginary": {723 "type": "number"724 }725 },726 "required": ["real", "imaginary"]727 }728 },729 "required": ["number"]730 })",731};732 733static common_chat_tool nested_args_tool{734 /* .name = */ "nested_args",735 /* .description = */ "Tool with nested array arguments",736 /* .parameters = */ R"({737 "type": "object",738 "properties": {739 "tags": {740 "type": "array",741 "items": { "type": "string" }742 },743 "entries": {744 "type": "array",745 "items": {746 "type": "object",747 "properties": {748 "id": { "type": "integer" },749 "label": { "type": "string" }750 },751 "required": ["id", "label"]752 }753 }754 },755 "required": ["tags", "entries"]756 })",757};758 759static common_chat_tool union_args_tool{760 /* .name = */ "union_args",761 /* .description = */ "Tool with union arguments",762 /* .parameters = */ R"({763 "type": "object",764 "properties": {765 "filter": {766 "anyOf": [767 { "type": "array", "items": { "type": "string" } },768 {769 "type": "object",770 "properties": {771 "field": { "type": "string" },772 "op": { "type": "string" }773 },774 "required": ["field", "op"]775 }776 ]777 },778 "label": {779 "oneOf": [780 { "type": "string" },781 { "type": "object", "properties": { "text": { "type": "string" } } }782 ]783 },784 "limit": {785 "oneOf": [786 { "type": "integer" },787 {788 "type": "object",789 "properties": { "max": { "type": "integer" } },790 "required": ["max"]791 }792 ]793 }794 }795 })",796};797 798static common_chat_tool nullable_string_tool{799 /* .name = */ "set_nullable_str",800 /* .description = */ "Set a nullable string value",801 /* .parameters = */ R"({802 "type": "object",803 "properties": {804 "name": {805 "type": ["string", "null"],806 "description": "A nullable string"807 }808 },809 "required": ["name"]810 })",811};812 813static common_chat_tool nullable_string_null_first_tool{814 /* .name = */ "set_nullable_str_nf",815 /* .description = */ "Set a nullable string value with null first in type array",816 /* .parameters = */ R"({817 "type": "object",818 "properties": {819 "name": {820 "type": ["null", "string"],821 "description": "A nullable string with null first"822 }823 },824 "required": ["name"]825 })",826};827 828static common_chat_tool nullable_int_tool{829 /* .name = */ "set_nullable_int",830 /* .description = */ "Set a nullable integer value",831 /* .parameters = */ R"({832 "type": "object",833 "properties": {834 "count": {835 "type": ["integer", "null"],836 "description": "A nullable integer"837 }838 },839 "required": ["count"]840 })",841};842 843static common_chat_tool enum_no_type_tool{844 /* .name = */ "set_unit",845 /* .description = */ "Set a temperature unit",846 /* .parameters = */ R"({847 "type": "object",848 "properties": {849 "unit": {850 "enum": ["celsius", "fahrenheit"],851 "description": "Temperature unit"852 }853 },854 "required": ["unit"]855 })",856};857 858static common_chat_tool string_param_tool{859 /* .name = */ "string_param",860 /* .description = */ "Tool with string parameter for testing",861 /* .parameters = */ R"({862 "type": "object",863 "properties": {864 "text": {865 "type": "string",866 "description": "A text parameter"867 }868 },869 "required": []870 })",871};872 873static common_chat_tool quoted_unquoted_tool{874 /* .name = */ "quoted_unquoted",875 /* .description = */ "Tool with two string parameters, one for quoted string, one for unquoted",876 /* .parameters = */ R"({877 "type": "object",878 "properties": {879 "quoted": {880 "type": "string",881 "description": "Quoted value"882 },883 "unquoted": {884 "type": "string",885 "description": "Unquoted value"886 }887 },888 "required": ["quoted", "unquoted"]889 })",890};891 892 893static common_chat_tool tool_2req_4opt{894 /* .name = */ "tool_2req_4opt",895 /* .description = */ "Tool with 2 required and 4 optional params",896 /* .parameters = */ R"({897 "type": "object",898 "properties": {899 "req1": { "type": "string", "description": "Required string" },900 "req2": { "type": "integer", "description": "Required int" },901 "opt1": { "type": "string", "description": "Optional string 1" },902 "opt2": { "type": "integer", "description": "Optional int 1" },903 "opt3": { "type": "string", "description": "Optional string 2" },904 "opt4": { "type": "integer", "description": "Optional int 2" }905 },906 "required": ["req1", "req2"]907 })",908};909 910static common_chat_tool tool_2req_5opt{911 /* .name = */ "tool_2req_5opt",912 /* .description = */ "Tool with 2 required and 5 optional params",913 /* .parameters = */ R"({914 "type": "object",915 "properties": {916 "req1": { "type": "string", "description": "Required string" },917 "req2": { "type": "integer", "description": "Required int" },918 "opt1": { "type": "string", "description": "Optional string 1" },919 "opt2": { "type": "integer", "description": "Optional int 1" },920 "opt3": { "type": "string", "description": "Optional string 2" },921 "opt4": { "type": "integer", "description": "Optional int 2" },922 "opt5": { "type": "string", "description": "Optional string 3" }923 },924 "required": ["req1", "req2"]925 })",926};927 928static std::vector<common_chat_tool> tools{ special_function_tool, special_function_tool_with_optional_param,929 python_tool, html_tool, todo_list };930 931const common_chat_msg message_user{932 "user",933 "Hey there!",934 /* .content_parts = */ {},935 /* .tool_calls = */ {},936 /* .reasoning_content = */ "",937 /* .tool_name = */ "",938 /* .tool_call_id = */ "",939};940 941const common_chat_msg message_user_parts{942 "user",943 /* .content = */ "",944 /* .content_parts = */945 {946 { "text", "Hey" },947 { "text", "there" },948 },949 /* .tool_calls = */950 { },951 /* .reasoning_content = */952 "",953 /* .tool_name = */ "",954 /* .tool_call_id = */ "",955};956 957static common_chat_msg simple_assist_msg(const std::string & content,958 const std::string & reasoning_content = "",959 const std::string & tool_name = "",960 const std::string & arguments = "",961 const std::string & id = "") {962 common_chat_msg msg;963 msg.role = "assistant";964 msg.content = content;965 msg.reasoning_content = reasoning_content;966 if (!tool_name.empty() || !id.empty()) {967 msg.tool_calls.push_back({ tool_name, arguments, id });968 }969 return msg;970}971 972static common_chat_msg message_with_tool_calls(const std::string & tool_name, const std::string & arguments) {973 return simple_assist_msg("", "", tool_name, arguments);974}975 976static common_chat_msg message_with_tool_calls_and_reasoning(const std::string & tool_name,977 const std::string & arguments,978 const std::string & reasoning) {979 return simple_assist_msg("", reasoning, tool_name, arguments);980}981 982static common_chat_msg message_with_reasoning_content_and_multiple_tool_calls(983 const std::string & reasoning,984 const std::string & content,985 const std::vector<std::pair<std::string, std::string>> & tool_calls) {986 common_chat_msg msg;987 msg.role = "assistant";988 msg.content = content;989 msg.reasoning_content = reasoning;990 for (const auto & [name, args] : tool_calls) {991 msg.tool_calls.push_back({ name, args, "" });992 }993 return msg;994}995 996static common_chat_msg message_with_content_and_tool_call(const std::string & content,997 const std::string & tool_name,998 const std::string & arguments) {999 return simple_assist_msg(content, "", tool_name, arguments);1000}1001 1002static common_chat_msg message_with_reasoning_and_tool_call(const std::string & reasoning,1003 const std::string & tool_name,1004 const std::string & arguments) {1005 return simple_assist_msg("", reasoning, tool_name, arguments);1006}1007 1008const common_chat_msg message_assist = simple_assist_msg("Hello, world!\nWhat's up?");1009const common_chat_msg message_assist_empty = simple_assist_msg("");1010const common_chat_msg message_assist_thoughts_unparsed_deepseek =1011 simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?");1012const common_chat_msg message_assist_thoughts_unparsed_md =1013 simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```");1014const common_chat_msg message_assist_thoughts_unparsed_md_partial =1015 simple_assist_msg("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}");1016 1017const common_chat_msg message_assist_thoughts_unparsed_r7b =1018 simple_assist_msg("<|START_THINKING|>I'm\nthinking<|END_THINKING|>Hello, world!\nWhat's up?");1019const common_chat_msg message_assist_thoughts_unparsed_magistral =1020 simple_assist_msg("[THINK]raisonnement[/THINK]Réponse");1021const common_chat_msg message_assist_thoughts = simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking");1022const common_chat_msg message_assist_thoughts_unopened_unparsed =1023 simple_assist_msg("I'm\nthinking</think>Hello, world!\nWhat's up?");1024const common_chat_msg message_assist_thoughts_no_content = simple_assist_msg("", "I'm\nthinking");1025const common_chat_msg message_assist_call = simple_assist_msg("", "", "special_function", "{\"arg1\": 1}");1026const common_chat_msg message_assist_call_noopt =1027 simple_assist_msg("", "", "special_function_with_opt", "{\"arg1\": 1}");1028const common_chat_msg message_assist_call_withopt =1029 simple_assist_msg("", "", "special_function_with_opt", "{\"arg1\": 1, \"arg2\": 2}");1030const common_chat_msg message_assist_call_content =1031 simple_assist_msg("Hello, world!\nWhat's up?", "", "special_function", "{\"arg1\":1}");1032const common_chat_msg message_assist_call_empty_args = simple_assist_msg("", "", "special_function");1033const common_chat_msg message_assist_call_cutoff_args = simple_assist_msg("", "", "special_function", "{\"arg");1034const common_chat_msg message_assist_call_thoughts =1035 simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1\":1}");1036const common_chat_msg message_assist_call_thoughts_unparsed =1037 simple_assist_msg("<think>I'm\nthinking</think>\n\n", "", "special_function", "{\"arg1\": 1}");1038const common_chat_msg message_assist_call_thoughts_content =1039 simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": 1}");1040const common_chat_msg message_assist_call_id =1041 simple_assist_msg("", "", "special_function", "{\"arg1\":1}", /* .id = */ "123456789");1042const common_chat_msg message_assist_call_idx =1043 simple_assist_msg("", "", "special_function", "{\"arg1\":1}", /* .id = */ "0");1044const common_chat_msg message_assist_thoughts_call_idx =1045 simple_assist_msg("", "I'm\nthinking", "special_function", "{\"arg1\": 1}", /* id = */ "0");1046const common_chat_msg message_assist_thoughts_partial_call =1047 simple_assist_msg("", "I'm\nthinking", "special_function", "", /* id = */ "0");1048const common_chat_msg message_assist_call_python = simple_assist_msg("", "", "python", "{\"code\":\"print('hey')\"}");1049const common_chat_msg message_assist_call_python_lines =1050 simple_assist_msg("", "", "python", "{\"code\":\"# This is a program:\\nprint('hey')\"}");1051const common_chat_msg message_assist_call_python_lines_unclosed =1052 simple_assist_msg("", "", "python", "{\"code\":\"# This is a program:\\nprint('hey')");1053const common_chat_msg message_assist_json_content =1054 simple_assist_msg("{\n \"response\": \"Hello, world!\\nWhat's up?\"\n}");1055const common_chat_msg message_assist_prefill_content = simple_assist_msg("Hello, ", "I'm thinking");1056const common_chat_msg message_assist_prefill_reasoning = simple_assist_msg("", "I'm");1057 1058// Use for PEG parser implementations1059struct peg_test_case {1060 common_chat_templates_inputs params;1061 std::string input;1062 common_chat_msg expect;1063 bool is_partial = false;1064 bool expect_reconstruction = false;1065};1066 1067struct make_peg_parser {1068 common_chat_params params_;1069 common_peg_arena arena_;1070 bool detailed_debug_;1071 1072 make_peg_parser(common_chat_templates * tmpls,1073 const common_chat_templates_inputs & inputs,1074 bool detailed_debug = false) {1075 detailed_debug_ = detailed_debug;1076 params_ = common_chat_templates_apply(tmpls, inputs);1077 arena_.load(params_.parser);1078 }1079 1080 common_chat_msg parse(const std::string & msg, bool is_partial) const {1081 common_chat_parser_params parser_params(params_);1082 parser_params.debug = detailed_debug_;1083 return common_chat_peg_parse(arena_, msg, is_partial, parser_params);1084 }1085};1086 1087// Global template filter for --template flag1088static std::string g_template_filter;1089 1090// When true, run reconstruction test on every non-partial test and report results1091static bool g_force_reconstruction_test = false;1092 1093static void test_peg_parser(common_chat_templates * tmpls,1094 const std::function<void(peg_test_case &)> & init,1095 bool detailed_debug) {1096 // UTF-8-safe truncation helper (same as in test_parser_with_streaming)1097 constexpr auto utf8_truncate_safe_len = [](const std::string_view s) -> size_t {1098 auto len = s.size();1099 if (len == 0) {1100 return 0;1101 }1102 auto i = len;1103 for (size_t back = 0; back < 4 && i > 0; ++back) {1104 --i;1105 unsigned char c = s[i];1106 if ((c & 0x80) == 0) {1107 return len;1108 }1109 if ((c & 0xC0) == 0xC0) {1110 size_t expected_len = 0;1111 if ((c & 0xE0) == 0xC0) {1112 expected_len = 2;1113 } else if ((c & 0xF0) == 0xE0) {1114 expected_len = 3;1115 } else if ((c & 0xF8) == 0xF0) {1116 expected_len = 4;1117 } else {1118 return i;1119 }1120 if (len - i >= expected_len) {1121 return len;1122 }1123 return i;1124 }1125 }1126 return len - std::min(len, size_t(3));1127 };1128 1129 peg_test_case tc;1130 init(tc);1131 if (tc.params.messages.empty()) {1132 tc.params.messages = { message_user };1133 }1134 if (tc.expect.role.empty()) {1135 tc.expect.role = "assistant";1136 }1137 1138 auto parser = make_peg_parser(tmpls, tc.params, detailed_debug);1139 if (detailed_debug) {1140 LOG_DBG("Using parser: \n%s\n", parser.arena_.dump(parser.arena_.root()).c_str());1141 LOG_DBG("Generation prompt: '%s'\n", parser.params_.generation_prompt.c_str());1142 }1143 1144 common_chat_msg msg_accum;1145 common_chat_msg msg_prev;1146 msg_accum.role = msg_prev.role = "assistant";1147 1148 for (size_t i = 1; i <= tc.input.size(); ++i) {1149 auto is_partial = i < tc.input.size() || tc.is_partial;1150 // Use UTF-8 safe truncation to avoid corrupting multi-byte characters1151 size_t safe_len = utf8_truncate_safe_len(std::string_view(tc.input).substr(0, i));1152 std::string prefix = tc.input.substr(0, safe_len);1153 common_chat_msg msg_current = parser.parse(prefix, is_partial);1154 1155 for (const auto & diff : common_chat_msg_diff::compute_diffs(msg_prev, msg_current)) {1156 if (!diff.reasoning_content_delta.empty()) {1157 msg_accum.reasoning_content += diff.reasoning_content_delta;1158 }1159 if (!diff.content_delta.empty()) {1160 msg_accum.content += diff.content_delta;1161 }1162 if (diff.tool_call_index != std::string::npos) {1163 // During partial parsing, a new tool call may appear with empty name initially1164 // The name gets filled in as more input is parsed1165 while (msg_accum.tool_calls.size() <= diff.tool_call_index) {1166 msg_accum.tool_calls.push_back({ "", "", "" });1167 }1168 // Always update name and id from diff (may change during incremental parsing), but only if the delta1169 // actually contains them1170 if (!diff.tool_call_delta.name.empty()) {1171 msg_accum.tool_calls[diff.tool_call_index].name = diff.tool_call_delta.name;1172 }1173 if (!diff.tool_call_delta.id.empty()) {1174 msg_accum.tool_calls[diff.tool_call_index].id = diff.tool_call_delta.id;1175 }1176 if (!diff.tool_call_delta.arguments.empty()) {1177 msg_accum.tool_calls[diff.tool_call_index].arguments += diff.tool_call_delta.arguments;1178 }1179 }1180 }1181 try {1182 assert_msg_equals(msg_current, msg_accum, true);1183 } catch (std::exception & e) {1184 throw std::runtime_error((std::string("Error comparing accumulated message to current: ") + e.what()).c_str());1185 }1186 1187 msg_prev = msg_current;1188 }1189 1190 if (!tc.is_partial) {1191 assert_msg_equals(tc.expect, parser.parse(tc.input, false), true);1192 }1193 assert_msg_equals(tc.expect, msg_accum, true);1194 1195 // Test grammar if present in params1196 if (!parser.params_.grammar.empty()) {1197 auto grammar = build_grammar(parser.params_.grammar);1198 if (!grammar) {1199 throw std::runtime_error("Failed to build grammar: " + parser.params_.grammar);1200 }