char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
790cf51aa
char/aliceai-t5-moe
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/null 7// 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 "json.h" 23#include <set> 24#include <stdexcept> 25#include <string> 26 27using json = common_json ; 28 29static std::ostream & operator<<(std::ostream & os ,const common_chat_msg_diff & diff ) { 30os <<"{ content_delta: " <<diff .content_delta <<"; " ; 31os <<"reasoning_content_delta: " <<diff .reasoning_content_delta <<"; " ; 32if (diff .tool_call_index != std::string::npos ) { 33os <<"tool_call_index: " <<diff .tool_call_index <<"; " ; 34os <<"tool_call_delta.name: " <<diff .tool_call_delta .name <<"; " ; 35os <<"tool_call_delta.id: " <<diff .tool_call_delta .id <<"; " ; 36os <<"tool_call_delta.arguments: " <<diff .tool_call_delta .arguments <<"; " ; 37 } 38os <<"}" ; 39return 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 ) { 44os <<"[\n" ; 45for (const auto & diff :diffs ) { 46os <<" " <<diff <<",\n" ; 47 } 48os <<"]" ; 49return os ; 50} 51 52static std::ostream & operator<<(std::ostream & os ,const common_chat_msg & msg ) { 53os <<"{ role: " <<msg .role <<"; " ; 54os <<"content: " <<msg .content <<"; " ; 55os <<"content_parts: [\n" ; 56for (const auto & part :msg .content_parts ) { 57os <<" { type: " <<part .type <<"; text: " <<part .text <<" },\n" ; 58 } 59os <<"]; " ; 60os <<"reasoning_content: " <<msg .reasoning_content <<"; " ; 61os <<"tool_calls: [\n" ; 62for (const auto & tool_call :msg .tool_calls ) { 63os <<" { name: " <<tool_call .name <<"; arguments: " <<tool_call .arguments <<"; id: " <<tool_call .id 64 <<" },\n" ; 65 } 66os <<"]" ; 67os <<"}" ; 68return os ; 69} 70 71template < class T > static bool equals (const T & expected ,const T & actual ) { 72return expected == actual ; 73} 74 75static common_chat_msg normalize (const common_chat_msg & msg ) { 76common_chat_msg normalized = msg ; 77for (auto & tool_call :normalized .tool_calls ) { 78try { 79tool_call .arguments = json::parse (tool_call .arguments ).dump (); 80 }catch (const std::exception & ) { 81 } 82 } 83return normalized ; 84} 85 86template <> bool equals (const common_chat_msg & expected ,const common_chat_msg & actual ) { 87return normalize (expected )== normalize (actual ); 88} 89 90template < class T > static void assert_equals (const T & expected ,const T & actual ) { 91if (!equals (expected ,actual )) { 92 std::ostringstream oss_expected ; 93oss_expected <<expected ; 94 std::ostringstream oss_actual ; 95oss_actual <<actual ; 96LOG_ERR ("Expected: %s\n" ,oss_expected .str ().c_str ()); 97LOG_ERR ("Actual: %s\n" ,oss_actual .str ().c_str ()); 98common_log_flush (common_log_main ()); 99throw std::runtime_error ("Test failed" ); 100 } 101} 102 103static void assert_contains (const std::string & haystack ,const std::string & needle ) { 104if (haystack .find (needle )== std::string::npos ) { 105LOG_ERR ("Expected to contain: %s\n" ,needle .c_str ()); 106LOG_ERR ("Actual: %s\n" ,haystack .c_str ()); 107common_log_flush (common_log_main ()); 108throw std::runtime_error ("Test failed" ); 109 } 110} 111 112static void assert_not_contains (const std::string & haystack ,const std::string & needle ) { 113if (haystack .find (needle )!= std::string::npos ) { 114LOG_ERR ("Expected NOT to contain: %s\n" ,needle .c_str ()); 115LOG_ERR ("Actual: %s\n" ,haystack .c_str ()); 116common_log_flush (common_log_main ()); 117throw std::runtime_error ("Test failed" ); 118 } 119} 120 121static void assert_ends_with (const std::string & str ,const std::string & suffix ) { 122if (str .size ()< suffix .size ()|| 123str .compare (str .size ()- suffix .size (),suffix .size (),suffix )!= 0 ) { 124LOG_ERR ("Expected to end with: %s\n" ,suffix .c_str ()); 125LOG_ERR ("Actual: %s\n" ,str .c_str ()); 126common_log_flush (common_log_main ()); 127throw 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 ); 133if (!fs .is_open ()) { 134fs = std::ifstream ("../" + path , std::ios_base::binary ); 135if (!fs .is_open ()) { 136throw std::runtime_error ("Failed to open file: " + path ); 137 } 138 } 139fs .seekg (0 , std::ios_base::end ); 140auto size = fs .tellg (); 141fs .seekg (0 ); 142 std::string out ; 143out .resize (static_cast < size_t > (size )); 144fs .read (out .data (),static_cast < std::streamsize > (size )); 145return out ; 146} 147 148static common_chat_templates_ptr read_templates (const std::string & path ) { 149return 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 ) { 153return std::unique_ptr < llama_grammar > ( 154llama_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 string 158static std::string format_codepoint (uint32_t cp ) { 159if (cp >=32 && cp < 127 ) { 160return std::string ("'" )+ static_cast < char > (cp )+ "'" ; 161 }else if (cp == '\n' ) { 162return "'\\n'" ; 163 }else if (cp == '\r' ) { 164return "'\\r'" ; 165 }else if (cp == '\t' ) { 166return "'\\t'" ; 167 }else { 168return "U+" + std::to_string (cp ); 169 } 170} 171 172// Helper to format expected element from grammar stack 173static std::string format_expected_element (const llama_grammar_rules & /* rules*/ ,const llama_grammar_element * elem ) { 174if (!elem ) { 175return "<end>" ; 176 } 177 178switch (elem -> type ) { 179case LLAMA_GRETYPE_END : 180return "<end of rule>" ; 181case LLAMA_GRETYPE_ALT : 182return "<alternative>" ; 183case LLAMA_GRETYPE_RULE_REF : 184 { 185// Find rule name - just show rule ID for now 186return "<rule-" + std::to_string (elem -> value )+ ">" ; 187 } 188case LLAMA_GRETYPE_CHAR : 189 { 190 std::string result ; 191const llama_grammar_element * pos = elem ; 192bool first = true; 193 194do { 195if (!first ) { 196result += " | " ; 197 } 198first = false; 199 200if (pos [1 ].type == LLAMA_GRETYPE_CHAR_RNG_UPPER ) { 201// Range like [a-z] 202result += "[" + format_codepoint (pos -> value )+ "-" + format_codepoint (pos [1 ].value )+ "]" ; 203pos += 2 ; 204 }else { 205result += format_codepoint (pos -> value ); 206pos += 1 ; 207 } 208 }while (pos -> type == LLAMA_GRETYPE_CHAR_ALT ); 209 210return result ; 211 } 212case LLAMA_GRETYPE_CHAR_NOT : 213 { 214 std::string result = "[^" ; 215const llama_grammar_element * pos = elem ; 216bool first = true; 217 218do { 219if (!first ) { 220result += " " ; 221 } 222first = false; 223 224if (pos [1 ].type == LLAMA_GRETYPE_CHAR_RNG_UPPER ) { 225result += format_codepoint (pos -> value )+ "-" + format_codepoint (pos [1 ].value ); 226pos += 2 ; 227 }else { 228result += format_codepoint (pos -> value ); 229pos += 1 ; 230 } 231 }while (pos -> type == LLAMA_GRETYPE_CHAR_ALT ); 232 233return result + "]" ; 234 } 235case LLAMA_GRETYPE_CHAR_ANY : 236return "<any char>" ; 237case LLAMA_GRETYPE_TOKEN : 238return "<token-" + std::to_string (elem -> value )+ ">" ; 239case LLAMA_GRETYPE_TOKEN_NOT : 240return "<not-token-" + std::to_string (elem -> value )+ ">" ; 241default : 242return "<unknown>" ; 243 } 244} 245 246// Get description of what the grammar expects at current position 247static std::string get_expected_description (const llama_grammar_rules & rules ,const llama_grammar_stacks & stacks ) { 248if (stacks .empty ()) { 249return "<no valid continuations>" ; 250 } 251 252 std::string result ; 253 std::set < std::string > seen ; 254 255for (const auto & stack :stacks ) { 256if (stack .empty ()) { 257if (seen .insert ("<end>" ).second ) { 258if (!result .empty ()) { 259result += " OR " ; 260 } 261result += "<end>" ; 262 } 263continue ; 264 } 265 266const llama_grammar_element * elem = stack .back (); 267 std::string desc = format_expected_element (rules ,elem ); 268if (seen .insert (desc ).second ) { 269if (!result .empty ()) { 270result += " OR " ; 271 } 272result += desc ; 273 } 274 } 275 276return result ; 277} 278 279// Result of a detailed grammar match attempt 280struct grammar_match_result { 281bool success = false;// Did the string fully match the grammar? 282size_t matched_bytes = 0 ;// Bytes successfully matched before failure 283size_t matched_codepoints = 0 ;// Codepoints successfully matched before failure 284size_t total_bytes = 0 ;// Total bytes in input 285size_t total_codepoints = 0 ;// Total codepoints in input 286 std::string matched_prefix ;// The portion that was successfully matched 287 std::string failing_char ;// The character that caused failure (if any) 288 std::string expected_description ;// What the grammar expected at failure point 289bool incomplete = false;// True if matched all input but grammar expects more 290}; 291 292// Detailed version of match_string that returns failure information 293static grammar_match_result match_string_detailed (const std::string & input ,llama_grammar * grammar ) { 294grammar_match_result result ; 295result .total_bytes = input .size (); 296 297const auto cpts = unicode_cpts_from_utf8 (input ); 298result .total_codepoints = cpts .size (); 299 300auto & stacks_cur = llama_grammar_get_stacks (grammar ); 301const auto & rules = llama_grammar_get_rules (grammar ); 302 303size_t byte_pos = 0 ; 304 305for (size_t i = 0 ;i < cpts .size ();i ++ ) { 306const 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 311llama_grammar_accept (grammar ,cpt ); 312 313// Calculate byte position for this codepoint 314size_t cpt_bytes = 0 ; 315if (cpt < 0x80 ) { 316cpt_bytes = 1 ; 317 }else if (cpt < 0x800 ) { 318cpt_bytes = 2 ; 319 }else if (cpt < 0x10000 ) { 320cpt_bytes = 3 ; 321 }else { 322cpt_bytes = 4 ; 323 } 324 325if (stacks_cur .empty ()) { 326// Grammar failed to match at this point 327result .matched_bytes = byte_pos ; 328result .matched_codepoints = i ; 329result .matched_prefix = input .substr (0 ,byte_pos ); 330result .failing_char = format_codepoint (cpt ); 331result .expected_description = expected_before ; 332result .incomplete = false; 333return result ; 334 } 335 336byte_pos += cpt_bytes ; 337 } 338 339// All input matched - check if grammar is complete 340result .matched_bytes = input .size (); 341result .matched_codepoints = cpts .size (); 342result .matched_prefix = input ; 343 344if (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 completed 346result .success = true; 347result .incomplete = false; 348 }else { 349// Grammar expects more input 350result .success = false; 351result .incomplete = true; 352result .expected_description = get_expected_description (rules ,stacks_cur ); 353 } 354 355return 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 ) { 360const auto cpts = unicode_cpts_from_utf8 (input ); 361 362auto & stacks_cur = llama_grammar_get_stacks (grammar ); 363 364for (const auto & cpt :cpts ) { 365llama_grammar_accept (grammar ,cpt ); 366 367if (stacks_cur .empty ()) { 368// no stacks means that the grammar failed to match at this point 369return false; 370 } 371 } 372 373if (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 completed 375return true; 376 } 377 378return false; 379} 380 381static std::string renormalize_json (const std::string & json_str ) { 382try { 383auto json_obj = json::parse (json_str ); 384return json_obj .dump (); 385 }catch (const std::exception & e ) { 386return "" ;// ignore parial JSON contents for comparison purposes 387 } 388} 389 390static void assert_msg_equals (const common_chat_msg & expected , 391const common_chat_msg & actual , 392bool ignore_whitespace_differences = false) { 393assert_equals (expected .role ,actual .role ); 394if (ignore_whitespace_differences ) { 395assert_equals (string_strip (expected .content ),string_strip (actual .content )); 396 }else { 397assert_equals (expected .content ,actual .content ); 398 } 399assert_equals (expected .content_parts .size (),actual .content_parts .size ()); 400for (size_t i = 0 ;i < expected .content_parts .size ();i ++ ) { 401const auto & expected_part = expected .content_parts [i ]; 402const auto & actual_part = actual .content_parts [i ]; 403assert_equals (expected_part .type ,actual_part .type ); 404if (ignore_whitespace_differences ) { 405assert_equals (string_strip (expected_part .text ),string_strip (actual_part .text )); 406 }else { 407assert_equals (expected_part .text ,actual_part .text ); 408 } 409 } 410if (ignore_whitespace_differences ) { 411assert_equals (string_strip (expected .reasoning_content ),string_strip (actual .reasoning_content )); 412 }else { 413assert_equals (expected .reasoning_content ,actual .reasoning_content ); 414 } 415assert_equals (expected .tool_calls .size (),actual .tool_calls .size ()); 416for (size_t i = 0 ;i < expected .tool_calls .size ();i ++ ) { 417const auto & expected_tool_call = expected .tool_calls [i ]; 418const auto & actual_tool_call = actual .tool_calls [i ]; 419assert_equals (expected_tool_call .name ,actual_tool_call .name ); 420assert_equals (renormalize_json (expected_tool_call .arguments ),renormalize_json (actual_tool_call .arguments )); 421assert_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 empty_args_tool_no_schema { 476/* .name = */ "empty_args_no_schema" , 477/* .description = */ "A tool that takes no arguments and has no parameters schema" , 478/* .parameters = */ "{}" , 479}; 480 481static common_chat_tool python_tool { 482/* .name = */ "python" , 483/* .description = */ "an ipython interpreter" , 484/* .parameters = */ R"({ 485"type": "object", 486"properties": { 487"code": { 488"type": "string", 489"description": "Python code to execute." 490} 491}, 492"required": ["code"] 493})" , 494}; 495 496static common_chat_tool html_tool { 497/* .name = */ "html" , 498/* .description = */ "an html validator" , 499/* .parameters = */ R"({ 500"type": "object", 501"properties": { 502"markup": { 503"type": "string", 504"description": "HTML markup to validate." 505} 506}, 507"required": ["markup"] 508})" , 509}; 510 511static common_chat_tool get_time_tool { 512/* .name = */ "get_time" , 513/* .description = */ "Get the current time in a city" , 514/* .parameters = */ R"({ 515"type": "object", 516"properties": { 517"city": { 518"type": "string", 519"description": "City name" 520} 521}, 522"required": ["city"] 523})" , 524}; 525 526static common_chat_tool get_weather_tool { 527/* .name = */ "get_weather" , 528/* .description = */ "Get the current weather in a city" , 529/* .parameters = */ R"({ 530"type": "object", 531"properties": { 532"city": { 533"type": "string", 534"description": "City name" 535} 536}, 537"required": ["city"] 538})" , 539}; 540 541static common_chat_tool todo_list { 542/* .name = */ "todo_list" , 543/* .description = */ "Create or update the todo list" , 544/* .parameters = */ R"({ 545"type": "object", 546"properties": { 547"todos": { 548"type": "array", 549"description": "List of TODO list items" 550} 551}, 552"required": ["todos"] 553})" , 554}; 555 556static common_chat_tool edit_tool { 557/* .name = */ "edit" , 558/* .description = */ "Edit file" , 559/* .parameters = */ R"({ 560"type": "object", 561"properties": { 562"filename": { 563"type": "string", 564"description": "Path of file to edit" 565}, 566"oldString": { 567"type": "string", 568"description": "String to replace" 569}, 570"newString": { 571"type": "string", 572"description": "New (replacement) value" 573} 574}, 575"required": ["filename", "oldString", "newString"] 576})" , 577}; 578 579static common_chat_tool manage_todo_list_tool { 580/* .name = */ "manage_todo_list" , 581/* .description = */ "Create or update the todo list" , 582/* .parameters = */ R"({ 583"type": "object", 584"properties": { 585"todos": { 586"type": "array", 587"description": "List of TODO list items" 588} 589}, 590"required": ["todos"] 591})" , 592}; 593 594static common_chat_tool run_in_terminal_tool { 595/* .name = */ "run_in_terminal" , 596/* .description = */ "Run a shell command." , 597/* .parameters = */ R"({ 598"type": "object", 599"properties": { 600"command": { 601"type": "string", 602"description": "Shell command to run" 603} 604}, 605"required": ["command"] 606})" , 607}; 608 609static common_chat_tool magic_tool { 610/* .name = */ "magic" , 611/* .description = */ "Magic tool that takes a hash" , 612/* .parameters = */ R"({ 613"type": "object", 614"properties": { 615"name": { 616"type": "string" 617}, 618"ref": { 619"type": "string" 620} 621}, 622"required": ["name", "ref"] 623})" , 624}; 625 626static common_chat_tool magic_int_tool { 627/* .name = */ "magic_int" , 628/* .description = */ "Magic tool that takes a hash" , 629/* .parameters = */ R"({ 630"type": "object", 631"properties": { 632"ref": { 633"type": "integer" 634}, 635"name": { 636"type": "string" 637} 638}, 639"required": ["ref"] 640})" , 641}; 642 643static common_chat_tool amount_tool { 644/* .name = */ "amount" , 645/* .description = */ "Amount converter" , 646/* .parameters = */ R"({ 647"type": "object", 648"properties": { 649"orig": { 650"type": "number" 651} 652}, 653"required": ["orig"] 654})" , 655}; 656 657static common_chat_tool toggle_tool { 658/* .name = */ "toggle" , 659/* .description = */ "Toggle a feature" , 660/* .parameters = */ R"({ 661"type": "object", 662"properties": { 663"enabled": { 664"type": "boolean", 665"description": "Whether to enable the feature" 666} 667}, 668"required": ["enabled"] 669})" , 670}; 671 672static common_chat_tool nullable_tool { 673/* .name = */ "set_nullable" , 674/* .description = */ "Set a nullable value" , 675/* .parameters = */ R"({ 676"type": "object", 677"properties": { 678"value": { 679"type": "null", 680"description": "A null value" 681} 682}, 683"required": ["value"] 684})" , 685}; 686 687static common_chat_tool config_tool { 688/* .name = */ "set_config" , 689/* .description = */ "Set configuration" , 690/* .parameters = */ R"({ 691"type": "object", 692"properties": { 693"config": { 694"type": "object", 695"description": "Configuration dict" 696} 697}, 698"required": ["config"] 699})" , 700}; 701 702static common_chat_tool calendar_create_event_tool { 703/* .name = */ "Calendar.create_event" , 704/* .description = */ "Create a calendar event" , 705/* .parameters = */ R"({ 706"type": "object", 707"properties": { 708"title": { "type": "string" }, 709"participants": { "type": "array", "items": { "type": "string" } }, 710"metadata": { "type": "object" } 711}, 712"required": ["title", "participants", "metadata"] 713})" , 714}; 715 716static common_chat_tool imaginary_number_tool { 717/* .name = */ "imaginary_number" , 718/* .description = */ "Imaginary number converter" , 719/* .parameters = */ R"({ 720"type": "object", 721"properties": { 722"number": { 723"type": "object", 724"properties": { 725"real": { 726"type": "number" 727}, 728"imaginary": { 729"type": "number" 730} 731}, 732"required": ["real", "imaginary"] 733} 734}, 735"required": ["number"] 736})" , 737}; 738 739static common_chat_tool nested_args_tool { 740/* .name = */ "nested_args" , 741/* .description = */ "Tool with nested array arguments" , 742/* .parameters = */ R"({ 743"type": "object", 744"properties": { 745"tags": { 746"type": "array", 747"items": { "type": "string" } 748}, 749"entries": { 750"type": "array", 751"items": { 752"type": "object", 753"properties": { 754"id": { "type": "integer" }, 755"label": { "type": "string" } 756}, 757"required": ["id", "label"] 758} 759} 760}, 761"required": ["tags", "entries"] 762})" , 763}; 764 765static common_chat_tool union_args_tool { 766/* .name = */ "union_args" , 767/* .description = */ "Tool with union arguments" , 768/* .parameters = */ R"({ 769"type": "object", 770"properties": { 771"filter": { 772"anyOf": [ 773{ "type": "array", "items": { "type": "string" } }, 774{ 775"type": "object", 776"properties": { 777"field": { "type": "string" }, 778"op": { "type": "string" } 779}, 780"required": ["field", "op"] 781} 782] 783}, 784"label": { 785"oneOf": [ 786{ "type": "string" }, 787{ "type": "object", "properties": { "text": { "type": "string" } } } 788] 789}, 790"limit": { 791"oneOf": [ 792{ "type": "integer" }, 793{ 794"type": "object", 795"properties": { "max": { "type": "integer" } }, 796"required": ["max"] 797} 798] 799} 800} 801})" , 802}; 803 804static common_chat_tool nullable_string_tool { 805/* .name = */ "set_nullable_str" , 806/* .description = */ "Set a nullable string value" , 807/* .parameters = */ R"({ 808"type": "object", 809"properties": { 810"name": { 811"type": ["string", "null"], 812"description": "A nullable string" 813} 814}, 815"required": ["name"] 816})" , 817}; 818 819static common_chat_tool nullable_string_null_first_tool { 820/* .name = */ "set_nullable_str_nf" , 821/* .description = */ "Set a nullable string value with null first in type array" , 822/* .parameters = */ R"({ 823"type": "object", 824"properties": { 825"name": { 826"type": ["null", "string"], 827"description": "A nullable string with null first" 828} 829}, 830"required": ["name"] 831})" , 832}; 833 834static common_chat_tool nullable_int_tool { 835/* .name = */ "set_nullable_int" , 836/* .description = */ "Set a nullable integer value" , 837/* .parameters = */ R"({ 838"type": "object", 839"properties": { 840"count": { 841"type": ["integer", "null"], 842"description": "A nullable integer" 843} 844}, 845"required": ["count"] 846})" , 847}; 848 849static common_chat_tool string_union_tool { 850/* .name = */ "set_union" , 851/* .description = */ "Set values whose types are unions with string" , 852/* .parameters = */ R"({ 853"type": "object", 854"properties": { 855"value": { 856"type": ["string", "object"], 857"description": "A string or object value" 858}, 859"amount": { 860"type": ["string", "integer"], 861"description": "A string or integer value" 862} 863}, 864"required": ["value", "amount"] 865})" , 866}; 867 868static common_chat_tool enum_no_type_tool { 869/* .name = */ "set_unit" , 870/* .description = */ "Set a temperature unit" , 871/* .parameters = */ R"({ 872"type": "object", 873"properties": { 874"unit": { 875"enum": ["celsius", "fahrenheit"], 876"description": "Temperature unit" 877} 878}, 879"required": ["unit"] 880})" , 881}; 882 883static common_chat_tool string_param_tool { 884/* .name = */ "string_param" , 885/* .description = */ "Tool with string parameter for testing" , 886/* .parameters = */ R"({ 887"type": "object", 888"properties": { 889"text": { 890"type": "string", 891"description": "A text parameter" 892} 893}, 894"required": [] 895})" , 896}; 897 898static common_chat_tool quoted_unquoted_tool { 899/* .name = */ "quoted_unquoted" , 900/* .description = */ "Tool with two string parameters, one for quoted string, one for unquoted" , 901/* .parameters = */ R"({ 902"type": "object", 903"properties": { 904"quoted": { 905"type": "string", 906"description": "Quoted value" 907}, 908"unquoted": { 909"type": "string", 910"description": "Unquoted value" 911} 912}, 913"required": ["quoted", "unquoted"] 914})" , 915}; 916 917 918static common_chat_tool tool_2req_4opt { 919/* .name = */ "tool_2req_4opt" , 920/* .description = */ "Tool with 2 required and 4 optional params" , 921/* .parameters = */ R"({ 922"type": "object", 923"properties": { 924"req1": { "type": "string", "description": "Required string" }, 925"req2": { "type": "integer", "description": "Required int" }, 926"opt1": { "type": "string", "description": "Optional string 1" }, 927"opt2": { "type": "integer", "description": "Optional int 1" }, 928"opt3": { "type": "string", "description": "Optional string 2" }, 929"opt4": { "type": "integer", "description": "Optional int 2" } 930}, 931"required": ["req1", "req2"] 932})" , 933}; 934 935static common_chat_tool tool_2req_5opt { 936/* .name = */ "tool_2req_5opt" , 937/* .description = */ "Tool with 2 required and 5 optional params" , 938/* .parameters = */ R"({ 939"type": "object", 940"properties": { 941"req1": { "type": "string", "description": "Required string" }, 942"req2": { "type": "integer", "description": "Required int" }, 943"opt1": { "type": "string", "description": "Optional string 1" }, 944"opt2": { "type": "integer", "description": "Optional int 1" }, 945"opt3": { "type": "string", "description": "Optional string 2" }, 946"opt4": { "type": "integer", "description": "Optional int 2" }, 947"opt5": { "type": "string", "description": "Optional string 3" } 948}, 949"required": ["req1", "req2"] 950})" , 951}; 952 953static std::vector < common_chat_tool > tools {special_function_tool ,special_function_tool_with_optional_param , 954python_tool ,html_tool ,todo_list }; 955 956const common_chat_msg message_user { 957"user" , 958"Hey there!" , 959/* .content_parts = */ {}, 960/* .tool_calls = */ {}, 961/* .reasoning_content = */ "" , 962/* .tool_name = */ "" , 963/* .tool_call_id = */ "" , 964}; 965 966const common_chat_msg message_user_parts { 967"user" , 968/* .content = */ "" , 969/* .content_parts = */ 970 { 971 {"text" ,"Hey" }, 972 {"text" ,"there" }, 973 }, 974/* .tool_calls = */ 975 { }, 976/* .reasoning_content = */ 977"" , 978/* .tool_name = */ "" , 979/* .tool_call_id = */ "" , 980}; 981 982static common_chat_msg simple_assist_msg (const std::string & content , 983const std::string & reasoning_content = "" , 984const std::string & tool_name = "" , 985const std::string & arguments = "" , 986const std::string & id = "" ) { 987common_chat_msg msg ; 988msg .role = "assistant" ; 989msg .content = content ; 990msg .reasoning_content = reasoning_content ; 991if (!tool_name .empty ()|| !id .empty ()) { 992msg .tool_calls .push_back ({tool_name ,arguments ,id }); 993 } 994return msg ; 995} 996 997static common_chat_msg message_with_tool_calls (const std::string & tool_name ,const std::string & arguments ) { 998return simple_assist_msg ("" ,"" ,tool_name ,arguments ); 999} 1000 1001static common_chat_msg message_with_tool_calls_and_reasoning (const std::string & tool_name , 1002const std::string & arguments , 1003const std::string & reasoning ) { 1004return simple_assist_msg ("" ,reasoning ,tool_name ,arguments ); 1005} 1006 1007static common_chat_msg message_with_reasoning_content_and_multiple_tool_calls ( 1008const std::string & reasoning , 1009const std::string & content , 1010const std::vector < std::pair < std::string , std::string >> & tool_calls ) { 1011common_chat_msg msg ; 1012msg .role = "assistant" ; 1013msg .content = content ; 1014msg .reasoning_content = reasoning ; 1015for (const auto & [name ,args ] :tool_calls ) { 1016msg .tool_calls .push_back ({name ,args ,"" }); 1017 } 1018return msg ; 1019} 1020 1021static common_chat_msg message_with_content_and_tool_call (const std::string & content , 1022const std::string & tool_name , 1023const std::string & arguments ) { 1024return simple_assist_msg (content ,"" ,tool_name ,arguments ); 1025} 1026 1027static common_chat_msg message_with_reasoning_and_tool_call (const std::string & reasoning , 1028const std::string & tool_name , 1029const std::string & arguments ) { 1030return simple_assist_msg ("" ,reasoning ,tool_name ,arguments ); 1031} 1032 1033const common_chat_msg message_assist = simple_assist_msg ("Hello, world!\nWhat's up?" ); 1034const common_chat_msg message_assist_empty = simple_assist_msg ("" ); 1035const common_chat_msg message_assist_thoughts_unparsed_deepseek = 1036simple_assist_msg ("<think>I'm\nthinking</think>Hello, world!\nWhat's up?" ); 1037const common_chat_msg message_assist_thoughts_unparsed_md = 1038simple_assist_msg ("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}```" ); 1039const common_chat_msg message_assist_thoughts_unparsed_md_partial = 1040simple_assist_msg ("<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n```json\n{}" ); 1041 1042const common_chat_msg message_assist_thoughts_unparsed_r7b = 1043simple_assist_msg ("<|START_THINKING|>I'm\nthinking<|END_THINKING|>Hello, world!\nWhat's up?" ); 1044const common_chat_msg message_assist_thoughts_unparsed_magistral = 1045simple_assist_msg ("[THINK]raisonnement[/THINK]Réponse" ); 1046const common_chat_msg message_assist_thoughts = simple_assist_msg ("Hello, world!\nWhat's up?" ,"I'm\nthinking" ); 1047const common_chat_msg message_assist_thoughts_unopened_unparsed = 1048simple_assist_msg ("I'm\nthinking</think>Hello, world!\nWhat's up?" ); 1049const common_chat_msg message_assist_thoughts_no_content = simple_assist_msg ("" ,"I'm\nthinking" ); 1050const common_chat_msg message_assist_call = simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": 1}" ); 1051const common_chat_msg message_assist_call_noopt = 1052simple_assist_msg ("" ,"" ,"special_function_with_opt" ,"{\"arg1\": 1}" ); 1053const common_chat_msg message_assist_call_withopt = 1054simple_assist_msg ("" ,"" ,"special_function_with_opt" ,"{\"arg1\": 1, \"arg2\": 2}" ); 1055const common_chat_msg message_assist_call_content = 1056simple_assist_msg ("Hello, world!\nWhat's up?" ,"" ,"special_function" ,"{\"arg1\":1}" ); 1057const common_chat_msg message_assist_call_empty_args = simple_assist_msg ("" ,"" ,"special_function" ); 1058const common_chat_msg message_assist_call_cutoff_args = simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg" ); 1059const common_chat_msg message_assist_call_thoughts = 1060simple_assist_msg ("" ,"I'm\nthinking" ,"special_function" ,"{\"arg1\":1}" ); 1061const common_chat_msg message_assist_call_thoughts_unparsed = 1062simple_assist_msg ("<think>I'm\nthinking</think>\n\n" ,"" ,"special_function" ,"{\"arg1\": 1}" ); 1063const common_chat_msg message_assist_call_thoughts_content = 1064simple_assist_msg ("Hello, world!\nWhat's up?" ,"I'm\nthinking" ,"special_function" ,"{\"arg1\": 1}" ); 1065const common_chat_msg message_assist_call_id = 1066simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\":1}" ,/* .id = */ "123456789" ); 1067const common_chat_msg message_assist_call_idx = 1068simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\":1}" ,/* .id = */ "0" ); 1069const common_chat_msg message_assist_thoughts_call_idx = 1070simple_assist_msg ("" ,"I'm\nthinking" ,"special_function" ,"{\"arg1\": 1}" ,/* id = */ "0" ); 1071const common_chat_msg message_assist_thoughts_partial_call = 1072simple_assist_msg ("" ,"I'm\nthinking" ,"special_function" ,"" ,/* id = */ "0" ); 1073const common_chat_msg message_assist_call_python = simple_assist_msg ("" ,"" ,"python" ,"{\"code\":\"print('hey')\"}" ); 1074const common_chat_msg message_assist_call_python_lines = 1075simple_assist_msg ("" ,"" ,"python" ,"{\"code\":\"# This is a program:\\nprint('hey')\"}" ); 1076const common_chat_msg message_assist_call_python_lines_unclosed = 1077simple_assist_msg ("" ,"" ,"python" ,"{\"code\":\"# This is a program:\\nprint('hey')" ); 1078const common_chat_msg message_assist_json_content = 1079simple_assist_msg ("{\n \"response\": \"Hello, world!\\nWhat's up?\"\n}" ); 1080const common_chat_msg message_assist_prefill_content = simple_assist_msg ("Hello, " ,"I'm thinking" ); 1081const common_chat_msg message_assist_prefill_reasoning = simple_assist_msg ("" ,"I'm" ); 1082 1083// Use for PEG parser implementations 1084struct peg_test_case { 1085common_chat_templates_inputs params ; 1086 std::string input ; 1087common_chat_msg expect ; 1088bool is_partial = false; 1089bool expect_reconstruction = false; 1090}; 1091 1092struct make_peg_parser { 1093common_chat_params params_ ; 1094common_peg_arena arena_ ; 1095bool detailed_debug_ ; 1096 1097make_peg_parser (common_chat_templates * tmpls , 1098const common_chat_templates_inputs & inputs , 1099bool detailed_debug = false) { 1100detailed_debug_ = detailed_debug ; 1101params_ = common_chat_templates_apply (tmpls ,inputs ); 1102arena_ .load (params_ .parser ); 1103 } 1104 1105common_chat_msg parse (const std::string & msg ,bool is_partial )const { 1106common_chat_parser_params parser_params (params_ ); 1107parser_params .debug = detailed_debug_ ; 1108return common_chat_peg_parse (arena_ ,msg ,is_partial ,parser_params ); 1109 } 1110}; 1111 1112// Global template filter for --template flag 1113static std::string g_template_filter ; 1114 1115// When true, run reconstruction test on every non-partial test and report results 1116static bool g_force_reconstruction_test = false; 1117 1118static void test_peg_parser (common_chat_templates * tmpls , 1119const std::function < void (peg_test_case & )> & init , 1120bool detailed_debug ) { 1121// UTF-8-safe truncation helper (same as in test_parser_with_streaming) 1122constexpr auto utf8_truncate_safe_len = [](const std::string_view s )-> size_t { 1123auto len = s .size (); 1124if (len == 0 ) { 1125return 0 ; 1126 } 1127auto i = len ; 1128for (size_t back = 0 ;back < 4 && i > 0 ;++ back ) { 1129-- i ; 1130unsigned char c = s [i ]; 1131if ((c & 0x80 )== 0 ) { 1132return len ; 1133 } 1134if ((c & 0xC0 )== 0xC0 ) { 1135size_t expected_len = 0 ; 1136if ((c & 0xE0 )== 0xC0 ) { 1137expected_len = 2 ; 1138 }else if ((c & 0xF0 )== 0xE0 ) { 1139expected_len = 3 ; 1140 }else if ((c & 0xF8 )== 0xF0 ) { 1141expected_len = 4 ; 1142 }else { 1143return i ; 1144 } 1145if (len - i >=expected_len ) { 1146return len ; 1147 } 1148return i ; 1149 } 1150 } 1151return len - std::min (len ,size_t (3 )); 1152 }; 1153 1154peg_test_case tc ; 1155init (tc ); 1156if (tc .params .messages .empty ()) { 1157tc .params .messages = {message_user }; 1158 } 1159if (tc .expect .role .empty ()) { 1160tc .expect .role = "assistant" ; 1161 } 1162 1163auto parser = make_peg_parser (tmpls ,tc .params ,detailed_debug ); 1164if (detailed_debug ) { 1165LOG_DBG ("Using parser: \n%s\n" ,parser .arena_ .dump (parser .arena_ .root ()).c_str ()); 1166LOG_DBG ("Generation prompt: '%s'\n" ,parser .params_ .generation_prompt .c_str ()); 1167 } 1168 1169common_chat_msg msg_accum ; 1170common_chat_msg msg_prev ; 1171msg_accum .role = msg_prev .role = "assistant" ; 1172 1173for (size_t i = 1 ;i <=tc .input .size ();++ i ) { 1174auto is_partial = i < tc .input .size ()|| tc .is_partial ; 1175// Use UTF-8 safe truncation to avoid corrupting multi-byte characters 1176size_t safe_len = utf8_truncate_safe_len (std::string_view (tc .input ).substr (0 ,i )); 1177 std::string prefix = tc .input .substr (0 ,safe_len ); 1178common_chat_msg msg_current = parser .parse (prefix ,is_partial ); 1179 1180for (const auto & diff : common_chat_msg_diff::compute_diffs (msg_prev ,msg_current )) { 1181if (!diff .reasoning_content_delta .empty ()) { 1182msg_accum .reasoning_content += diff .reasoning_content_delta ; 1183 } 1184if (!diff .content_delta .empty ()) { 1185msg_accum .content += diff .content_delta ; 1186 } 1187if (diff .tool_call_index != std::string::npos ) { 1188// During partial parsing, a new tool call may appear with empty name initially 1189// The name gets filled in as more input is parsed 1190while (msg_accum .tool_calls .size () <=diff .tool_call_index ) { 1191msg_accum .tool_calls .push_back ({"" ,"" ,"" }); 1192 } 1193// Always update name and id from diff (may change during incremental parsing), but only if the delta 1194// actually contains them 1195if (!diff .tool_call_delta .name .empty ()) { 1196msg_accum .tool_calls [diff .tool_call_index ].name = diff .tool_call_delta .name ; 1197 } 1198if (!diff .tool_call_delta .id .empty ()) { 1199msg_accum .tool_calls [diff .tool_call_index ].id = diff .tool_call_delta .id ; 1200 } 1201if (!diff .tool_call_delta .arguments .empty ()) { 1202msg_accum .tool_calls [diff .tool_call_index ].arguments += diff .tool_call_delta .arguments ; 1203 } 1204 } 1205 } 1206try { 1207assert_msg_equals (msg_current ,msg_accum , true); 1208 }catch (std::exception & e ) { 1209throw std::runtime_error ((std::string ("Error comparing accumulated message to current: " )+ e .what ()).c_str ()); 1210 } 1211 1212msg_prev = msg_current ; 1213 } 1214 1215if (!tc .is_partial ) { 1216assert_msg_equals (tc .expect ,parser .parse (tc .input , false), true); 1217 } 1218assert_msg_equals (tc .expect ,msg_accum , true); 1219 1220// Test grammar if present in params 1221if (!parser .params_ .grammar .empty ()) { 1222auto grammar = build_grammar (parser .params_ .grammar ); 1223if (!grammar ) { 1224throw std::runtime_error ("Failed to build grammar: " + parser .params_ .grammar ); 1225 } 1226 1227// In production, grammar triggers match against the full generated text 1228// including the generation prompt. All positions are in full_input coordinates. 1229const auto & gen_prompt = parser .params_ .generation_prompt ; 1230 std::string full_input = gen_prompt + tc .input ; 1231 1232// Determine whether the reasoning-budget sampler path applies: tool-call grammar 1233// with all WORD triggers and thinking tags present. In production, the reasoning 1234// budget sampler inhibits grammar application while inside thinking blocks — 1235// triggers inside <think>...</think> are suppressed. 1236bool use_reasoning_budget_path = false; 1237if (parser .params_ .grammar_lazy && !parser .params_ .thinking_end_tags .empty ()) { 1238use_reasoning_budget_path = true; 1239for (const auto & trigger :parser .params_ .grammar_triggers ) { 1240if (trigger .type != COMMON_GRAMMAR_TRIGGER_TYPE_WORD ) { 1241use_reasoning_budget_path = false; 1242break ; 1243 } 1244 } 1245 } 1246 1247// Find the earliest trigger position to determine the constrained portion 1248auto earliest_trigger_pos = std::string::npos ; 1249 1250if (use_reasoning_budget_path ) { 1251// Reasoning-budget path: simulate thinking-aware trigger detection. 1252// Walk through full_input tracking thinking state; only match triggers 1253// when outside thinking blocks. 1254const auto & think_start = parser .params_ .thinking_start_tag ; 1255const auto & think_ends = parser .params_ .thinking_end_tags ; 1256 1257bool in_thinking = false; 1258for (size_t i = 0 ;i < full_input .size ();++ i ) { 1259if (!in_thinking && !think_start .empty () 1260&& full_input .compare (i ,think_start .size (),think_start )== 0 ) { 1261in_thinking = true; 1262i += think_start .size ()- 1 ; 1263continue ; 1264 } 1265if (in_thinking ) { 1266for (const auto & think_end :think_ends ) { 1267if (full_input .compare (i ,think_end .size (),think_end )== 0 ) { 1268in_thinking = false; 1269i += think_end .size ()- 1 ; 1270break ; 1271 } 1272 } 1273continue ; 1274 } 1275// Outside thinking — check if any trigger word starts here 1276for (const auto & trigger :parser .params_ .grammar_triggers ) { 1277if (full_input .compare (i ,trigger .value .size (),trigger .value )== 0 ) { 1278if (earliest_trigger_pos == std::string::npos || i < earliest_trigger_pos ) { 1279earliest_trigger_pos = i ; 1280 } 1281 } 1282 } 1283if (earliest_trigger_pos != std::string::npos ) { 1284break ;// found the earliest 1285 } 1286 } 1287 1288// If the reasoning-budget path found no trigger outside thinking but the test 1289// expects tool calls, this template nests tool calls inside thinking 1290// blocks (e.g. Kimi). Fall back to the legacy path for this case. 1291if (earliest_trigger_pos == std::string::npos && !tc .expect .tool_calls .empty ()) { 1292use_reasoning_budget_path = false; 1293 } 1294 } 1295 1296if (!use_reasoning_budget_path ) { 1297// Legacy path: find triggers without thinking-awareness 1298for (const auto & trigger :parser .params_ .grammar_triggers ) { 1299size_t pos = std::string::npos ; 1300 std::smatch match ; 1301switch (trigger .type ) { 1302case COMMON_GRAMMAR_TRIGGER_TYPE_WORD : 1303 { 1304const auto & word = trigger .value ; 1305pos = full_input .find (word ); 1306break ; 1307 } 1308case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN : 1309 { 1310const auto & compiled = std::regex (trigger .value ); 1311if (std::regex_search (full_input ,match ,compiled )) { 1312pos = match .position (compiled .mark_count ()); 1313 } 1314break ; 1315 } 1316case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL : 1317 { 1318// In production, PATTERN_FULL triggers are checked against 1319// the text generated so far, growing token by token. Simulate 1320// by trying every prefix of full_input. 1321const auto & compiled = std::regex (trigger .value ); 1322for (size_t end = gen_prompt .size ();end <=full_input .size ();++ end ) { 1323 std::string prefix = full_input .substr (0 ,end ); 1324if (std::regex_match (prefix ,match ,compiled )) { 1325pos = std::string::npos ; 1326for (size_t gi = 1 ;gi < match .size ();++ gi ) { 1327if (match [gi ].length ()> 0 ) { 1328pos = match .position (gi ); 1329break ; 1330 } 1331 } 1332if (pos == std::string::npos ) { 1333pos = match .position (0 ); 1334 } 1335break ; 1336 } 1337 } 1338break ; 1339 } 1340default : 1341throw std::runtime_error ("Unknown trigger type" ); 1342 } 1343if (pos != std::string::npos ) { 1344if (earliest_trigger_pos == std::string::npos || pos < earliest_trigger_pos ) { 1345earliest_trigger_pos = pos ; 1346 } 1347 } 1348 } 1349 } 1350 1351// If the test expects tool calls and the grammar is lazy, the trigger must fire. 1352// Otherwise the grammar would never activate in production and tool calls wouldn't 1353// be constrained. A silent skip here would hide broken triggers. 1354if (parser .params_ .grammar_lazy && !tc .expect .tool_calls .empty ()&& !tc .is_partial 1355&& earliest_trigger_pos == std::string::npos ) { 1356 std::string trigger_desc ; 1357for (const auto & trigger :parser .params_ .grammar_triggers ) { 1358trigger_desc += "\n [type=" + std::to_string (trigger .type )+ "] " + trigger .value ; 1359 } 1360throw std::runtime_error ( 1361"Grammar trigger did not fire, but test expects tool calls (lazy grammar).\n" 1362">>> Input: " + full_input + "\n" 1363">>> Triggers (" + std::to_string (parser .params_ .grammar_triggers .size ())+ "):" + trigger_desc ); 1364 } 1365 1366// Determine the constrained portion of input to test against grammar. 1367// If the trigger position falls inside the generation prompt, the grammar 1368// sampler was already active before model output began — constrain from the 1369// start of the model output (i.e. tc.input). 1370 std::string constrained = full_input ; 1371bool grammar_triggered = false; 1372if (earliest_trigger_pos != std::string::npos ) { 1373auto constrain_from = std::max (earliest_trigger_pos ,gen_prompt .size ()); 1374constrained = full_input .substr (constrain_from ); 1375grammar_triggered = true; 1376 }else if (!parser .params_ .grammar_lazy ) { 1377// For non-lazy grammars, the entire input should match 1378grammar_triggered = true; 1379 } 1380 1381// Test the constrained portion against the grammar 1382if (grammar_triggered && !tc .is_partial ) { 1383auto result = match_string_detailed (constrained ,grammar .get ()); 1384if (!result .success ) { 1385 std::string error_msg ; 1386if (result .incomplete ) { 1387error_msg = 1388"Grammar matched all input but expects more:\n\n" 1389">>> Input: " + tc .input + 1390"\n\n>>> Constrained: " + constrained + 1391"\n\n>>> Matched prefix (" + std::to_string (result .matched_bytes )+ " bytes, " + 1392 std::to_string (result .matched_codepoints )+ " codepoints): " + 1393 (result .matched_prefix .size ()> 100 ?result .matched_prefix .substr (0 ,100 )+ "..." :result .matched_prefix )+ 1394"\n\n>>> Expected next: " + result .expected_description + 1395"\n\n>>> Grammar: " + parser .params_ .grammar ; 1396 }else { 1397error_msg = 1398"Grammar match failed:\n\n" 1399">>> Input: " + tc .input + 1400"\n\n>>> Constrained: " + constrained + 1401"\n\n>>> Matched prefix (" + std::to_string (result .matched_bytes )+ " bytes, " + 1402 std::to_string (result .matched_codepoints )+ " codepoints): " + 1403 (result .matched_prefix .size ()> 100 ?result .matched_prefix .substr (0 ,100 )+ "..." :result .matched_prefix )+ 1404"\n\n>>> Failing character: " + result .failing_char + 1405"\n\n>>> Expected: " + result .expected_description + 1406"\n\n>>> Grammar: " + parser .params_ .grammar ; 1407 } 1408throw std::runtime_error (error_msg ); 1409 } 1410 } 1411 } 1412 1413// Reconstruction test: verify that appending the parsed message to the original 1414// messages and re-rendering the template (without generation prompt) reproduces 1415// the original prompt + input exactly, or as a proper prefix (the template may 1416// append end-of-turn tokens after the assistant message). 1417if ((tc .expect_reconstruction || g_force_reconstruction_test )&& !tc .is_partial ) { 1418// Start from tc.expect but copy tool call arguments from the actual parser 1419// output, which preserves original JSON formatting (e.g. {"arg1":1} vs {"arg1": 1}). 1420auto reconstruction_msg = tc .expect ; 1421auto parsed_msg = parser .parse (tc .input , false); 1422for (size_t i = 0 ;i < reconstruction_msg .tool_calls .size ()&& i < parsed_msg .tool_calls .size ();i ++ ) { 1423reconstruction_msg .tool_calls [i ].arguments = parsed_msg .tool_calls [i ].arguments ; 1424 } 1425common_chat_templates_inputs reconstruction_inputs = tc .params ; 1426reconstruction_inputs .messages .push_back (reconstruction_msg ); 1427reconstruction_inputs .add_generation_prompt = false; 1428 1429auto reconstruction_params = common_chat_templates_apply (tmpls ,reconstruction_inputs ); 1430 std::string expected_text = parser .params_ .prompt + tc .input ; 1431bool match = reconstruction_params .prompt == expected_text || 1432 (reconstruction_params .prompt .size ()> expected_text .size ()&& 1433reconstruction_params .prompt .compare (0 ,expected_text .size (),expected_text )== 0 ); 1434if (!match && g_force_reconstruction_test && !tc .expect_reconstruction ) { 1435// In forced mode, report mismatch but don't fail 1436// Find the first difference position 1437size_t diff_pos = 0 ; 1438size_t min_len = std::min (expected_text .size (),reconstruction_params .prompt .size ()); 1439while (diff_pos < min_len && expected_text [diff_pos ]== reconstruction_params .prompt [diff_pos ]) { 1440diff_pos ++ ; 1441 } 1442size_t ctx_start = diff_pos > 60 ?diff_pos - 60 :0 ; 1443size_t ctx_end_e = std::min (expected_text .size (),diff_pos + 40 ); 1444size_t ctx_end_r = std::min (reconstruction_params .prompt .size (),diff_pos + 40 ); 1445LOG_ERR ("\x1b[31m[RECONSTRUCTION FAIL]\x1b[0m " 1446"first diff at byte %zu (expected len=%zu, reconstructed len=%zu)\n" 1447" expected: ...%s...\n" 1448" reconstructed: ...%s...\n" , 1449diff_pos ,expected_text .size (),reconstruction_params .prompt .size (), 1450expected_text .substr (ctx_start ,ctx_end_e - ctx_start ).c_str (), 1451reconstruction_params .prompt .substr (ctx_start ,ctx_end_r - ctx_start ).c_str ()); 1452 }else if (!match ) { 1453 std::string error_msg = 1454"Reconstruction mismatch:\n\n" 1455">>> Expected (prompt + input):\n" + expected_text + 1456"\n\n>>> Reconstructed:\n" + reconstruction_params .prompt ; 1457throw std::runtime_error (error_msg ); 1458 }else if (g_force_reconstruction_test ) { 1459LOG_INF ("\x1b[32m[RECONSTRUCTION OK]\x1b[0m\n" ); 1460 } 1461 } 1462} 1463 1464// Fluent builder for PEG parser tests 1465class peg_test_builder ; 1466 1467class peg_tester { 1468common_chat_templates_ptr tmpls_ ; 1469 std::string template_path_ ; 1470bool detailed_debug_ ; 1471friend class peg_test_builder ; 1472 1473public : 1474explicit peg_tester (const std::string & template_path ,const bool detailed_debug = false) : 1475tmpls_ (read_templates (template_path )), 1476template_path_ (template_path ), 1477detailed_debug_ (detailed_debug ) {} 1478 1479const std::string & template_path ()const {return template_path_ ; } 1480 1481peg_test_builder test (const std::string & input ); 1482}; 1483 1484class peg_test_builder { 1485peg_tester & tester_ ; 1486peg_test_case tc_ ; 1487 1488public : 1489peg_test_builder (peg_tester & tester ,const std::string & input ) :tester_ (tester ) { 1490tc_ .input = input ; 1491tc_ .params .add_generation_prompt = true; 1492 } 1493 1494// Parameter setters 1495peg_test_builder & reasoning_format (common_reasoning_format fmt ) { 1496tc_ .params .reasoning_format = fmt ; 1497return * this ; 1498 } 1499 1500peg_test_builder & tools (std::vector < common_chat_tool > tools ) { 1501tc_ .params .tools = std::move (tools ); 1502return * this ; 1503 } 1504 1505peg_test_builder & enable_thinking (bool val ) { 1506tc_ .params .enable_thinking = val ; 1507return * this ; 1508 } 1509 1510peg_test_builder & parallel_tool_calls (bool val ) { 1511tc_ .params .parallel_tool_calls = val ; 1512return * this ; 1513 } 1514 1515peg_test_builder & add_generation_prompt (bool val ) { 1516tc_ .params .add_generation_prompt = val ; 1517return * this ; 1518 } 1519 1520peg_test_builder & continue_final_message (common_chat_continuation cont ) { 1521tc_ .params .continue_final_message = cont ; 1522return * this ; 1523 } 1524 1525peg_test_builder & json_schema (const std::string & schema ) { 1526tc_ .params .json_schema = schema ; 1527return * this ; 1528 } 1529 1530peg_test_builder & is_partial (bool val ) { 1531tc_ .is_partial = val ; 1532return * this ; 1533 } 1534 1535peg_test_builder & expect_reconstruction (bool val = true) { 1536tc_ .expect_reconstruction = val ; 1537return * this ; 1538 } 1539 1540// Expect setters 1541peg_test_builder & expect (const common_chat_msg & msg ) { 1542tc_ .expect = msg ; 1543return * this ; 1544 } 1545 1546peg_test_builder & expect_content (const std::string & content ) { 1547tc_ .expect .content = content ; 1548return * this ; 1549 } 1550 1551peg_test_builder & expect_reasoning (const std::string & reasoning ) { 1552tc_ .expect .reasoning_content = reasoning ; 1553return * this ; 1554 } 1555 1556peg_test_builder & expect_tool_calls (std::vector < common_chat_tool_call > calls ) { 1557tc_ .expect .tool_calls = std::move (calls ); 1558return * this ; 1559 } 1560 1561peg_test_builder & tool_choice (common_chat_tool_choice choice ) { 1562tc_ .params .tool_choice = choice ; 1563return * this ; 1564 } 1565 1566peg_test_builder & messages (std::vector < common_chat_msg > messages ) { 1567tc_ .params .messages = std::move (messages ); 1568return * this ; 1569 } 1570 1571// Execute the test 1572void run () { 1573// Check template filter 1574if (!g_template_filter .empty ()) { 1575// Case-insensitive substring match 1576 std::string template_path_lower = tester_ .template_path (); 1577 std::string filter_lower = g_template_filter ; 1578 std::transform (template_path_lower .begin (),template_path_lower .end (),template_path_lower .begin (), 1579 ::tolower ); 1580 std::transform (filter_lower .begin (),filter_lower .end (),filter_lower .begin (), ::tolower ); 1581if (template_path_lower .find (filter_lower )== std::string::npos ) { 1582// Skip this test 1583return ; 1584 } 1585 } 1586LOG_INF ("\n\x1b[38;5;126m[%s]\x1b[0m\n%s\n\n" ,tester_ .template_path ().c_str (),tc_ .input .c_str ()); 1587test_peg_parser (tester_ .tmpls_ .get (), [this ](peg_test_case & t ) {t = tc_ ; },tester_ .detailed_debug_ ); 1588 } 1589}; 1590 1591peg_test_builder peg_tester::test (const std::string & input ) { 1592return peg_test_builder (* this ,input ); 1593} 1594 1595static void test_msgs_oaicompat_json_conversion () { 1596LOG_DBG ("%s\n" ,__func__ ); 1597 std::vector < common_chat_msg > msgs { 1598message_user , 1599message_user_parts , 1600message_assist_call , 1601message_assist_call_thoughts , 1602message_assist_call_thoughts_unparsed , 1603message_assist_call_thoughts_content , 1604message_assist_call_id , 1605message_assist_call_idx , 1606message_assist_call_python , 1607 }; 1608for (const auto & msg :msgs ) { 1609auto oai_json = common_chat_msgs_to_json_oaicompat ({msg }); 1610auto msgs2 = common_chat_msgs_parse_oaicompat (oai_json ); 1611assert_equals ((size_t )1 ,msgs2 .size ()); 1612const auto & msg2 = msgs2 [0 ]; 1613assert_msg_equals (msg ,msg2 ); 1614 } 1615assert_equals (std::string ("[\n" 1616" {\n" 1617" \"role\": \"user\",\n" 1618" \"content\": [\n" 1619" {\n" 1620" \"type\": \"text\",\n" 1621" \"text\": \"Hey\"\n" 1622" },\n" 1623" {\n" 1624" \"type\": \"text\",\n" 1625" \"text\": \"there\"\n" 1626" }\n" 1627" ]\n" 1628" }\n" 1629"]" ), 1630common_chat_msgs_to_json_oaicompat ({message_user_parts }).dump (2 )); 1631 1632// Note: content is "" instead of null due to workaround for templates that render null as "None" 1633assert_equals (std::string ("[\n" 1634" {\n" 1635" \"role\": \"assistant\",\n" 1636" \"content\": \"\",\n" 1637" \"tool_calls\": [\n" 1638" {\n" 1639" \"type\": \"function\",\n" 1640" \"function\": {\n" 1641" \"name\": \"python\",\n" 1642" \"arguments\": \"{\\\"code\\\":\\\"print('hey')\\\"}\"\n" 1643" }\n" 1644" }\n" 1645" ]\n" 1646" }\n" 1647"]" ), 1648common_chat_msgs_to_json_oaicompat ({message_assist_call_python }).dump (2 )); 1649 1650auto res = common_chat_msgs_parse_oaicompat (json::parse ("[{\"role\": \"assistant\", \"tool_calls\": []}]" )); 1651assert_equals < size_t > (1 ,res .size ()); 1652assert_equals < std::string > (res [0 ].role ,"assistant" ); 1653assert_equals (true,res [0 ].content .empty ()); 1654assert_equals (true,res [0 ].tool_calls .empty ()); 1655 1656try { 1657common_chat_msgs_parse_oaicompat (json::parse ("[{\"role\": \"assistant\"}]" )); 1658throw std::runtime_error ("Expected exception" ); 1659 }catch (const std::exception & e ) { 1660if (std::string (e .what ()).find ("'content'" )== std::string::npos ) { 1661throw std::runtime_error ("Expected exception about missing 'content'" ); 1662 } 1663 } 1664} 1665 1666static void test_msg_token_delimiters_split () { 1667LOG_DBG ("%s\n" ,__func__ ); 1668 1669// Delimiters that share a leading token, distinguished by the second token, 1670// to exercise the per-position token matching. 1671const common_chat_msg_delimiters delims = { 1672 { {COMMON_CHAT_ROLE_USER ,"" , {10 ,11 } }, 1673 {COMMON_CHAT_ROLE_ASSISTANT ,"" , {10 ,12 } } } 1674 }; 1675 1676// Empty inputs 1677assert_equals < size_t > (0 ,common_chat_msg_delimiters {}.split ({}).spans .size ()); 1678assert_equals < size_t > (0 ,common_chat_msg_delimiters {}.split ({10 ,11 }).spans .size ()); 1679assert_equals < size_t > (0 ,delims .split ({}).spans .size ()); 1680 1681// No delimiters match -> no spans 1682assert_equals < size_t > (0 ,delims .split ({100 ,101 ,102 }).spans .size ()); 1683 1684// Multi-role conversation: <user>Hi<assistant>Hello<user>Bye 1685 { 1686const llama_tokens tokens = { 168710 ,11 ,// <user> 1688100 ,101 ,// Hi 168910 ,12 ,// <assistant> 1690200 ,201 ,202 ,// Hello 169110 ,11 ,// <user> 1692300 ,301 ,// Bye 1693 }; 1694 1695const auto result = delims .split (tokens ); 1696const auto & spans = result .spans ; 1697assert_equals < size_t > (3 ,spans .size ()); 1698 1699assert_equals (COMMON_CHAT_ROLE_USER ,spans [0 ].role ); 1700assert_equals < size_t > (0 ,spans [0 ].pos ); 1701assert_equals < size_t > (4 ,spans [0 ].len ); 1702 1703assert_equals (COMMON_CHAT_ROLE_ASSISTANT ,spans [1 ].role ); 1704assert_equals < size_t > (4 ,spans [1 ].pos ); 1705assert_equals < size_t > (5 ,spans [1 ].len ); 1706 1707assert_equals (COMMON_CHAT_ROLE_USER ,spans [2 ].role ); 1708assert_equals < size_t > (9 ,spans [2 ].pos ); 1709assert_equals < size_t > (4 ,spans [2 ].len ); 1710 1711// is_user_start() is true at the token position where a user span begins 1712assert_equals (true,result .is_user_start (0 )); 1713assert_equals (false,result .is_user_start (4 ));// assistant span 1714assert_equals (true,result .is_user_start (9 )); 1715 } 1716 1717// Content before the first delimiter is not captured as a span 1718 { 1719const llama_tokens tokens = { 1720500 ,501 ,// leading content (dropped) 172110 ,11 ,// <user> 1722100 ,// Hi 1723 }; 1724 1725const auto spans = delims .split (tokens ).spans ; 1726assert_equals < size_t > (1 ,spans .size ()); 1727assert_equals (COMMON_CHAT_ROLE_USER ,spans [0 ].role ); 1728assert_equals < size_t > (2 ,spans [0 ].pos ); 1729assert_equals < size_t > (3 ,spans [0 ].len ); 1730 } 1731 1732// Skipped regions (media chunks) are jumped over but still count as span content 1733 { 1734const llama_tokens tokens = { 173510 ,11 ,// <user> 1736LLAMA_TOKEN_NULL ,// media chunk (3 tokens) 1737LLAMA_TOKEN_NULL , 1738LLAMA_TOKEN_NULL , 1739100 ,// Hi 174010 ,12 ,// <assistant> 1741 }; 1742 1743const std::map < size_t ,size_t > skips = { {2 ,3 } }; 1744 1745const auto spans = delims .split (tokens ,skips ).spans ; 1746assert_equals < size_t > (2 ,spans .size ()); 1747 1748assert_equals (COMMON_CHAT_ROLE_USER ,spans [0 ].role ); 1749assert_equals < size_t > (0 ,spans [0 ].pos ); 1750assert_equals < size_t > (6 ,spans [0 ].len ); 1751 1752assert_equals (COMMON_CHAT_ROLE_ASSISTANT ,spans [1 ].role ); 1753assert_equals < size_t > (6 ,spans [1 ].pos ); 1754assert_equals < size_t > (2 ,spans [1 ].len ); 1755 } 1756 1757// A delimiter sequence inside a skipped region is not matched 1758 { 1759const llama_tokens tokens = { 176010 ,11 ,// <user> 176110 ,12 ,// skipped region that happens to contain delimiter tokens 1762100 ,// Hi 1763 }; 1764 1765const std::map < size_t ,size_t > skips = { {2 ,2 } }; 1766 1767const auto spans = delims .split (tokens ,skips ).spans ; 1768assert_equals < size_t > (1 ,spans .size ()); 1769assert_equals (COMMON_CHAT_ROLE_USER ,spans [0 ].role ); 1770assert_equals < size_t > (0 ,spans [0 ].pos ); 1771assert_equals < size_t > (5 ,spans [0 ].len ); 1772 } 1773} 1774 1775static void test_tools_oaicompat_json_conversion () { 1776LOG_DBG ("%s\n" ,__func__ ); 1777 std::vector < common_chat_tool > tools { 1778special_function_tool , 1779python_tool , 1780 }; 1781 1782for (const auto & tool :tools ) { 1783auto oai_json = common_chat_tools_to_json_oaicompat ({tool }); 1784auto tools2 = common_chat_tools_parse_oaicompat (oai_json ); 1785assert_equals ((size_t )1 ,tools2 .size ()); 1786auto tool2 = tools2 [0 ]; 1787assert_equals (tool .name ,tool2 .name ); 1788assert_equals (tool .description ,tool2 .description ); 1789assert_equals (json::parse (tool .parameters ).dump (2 ), json::parse (tool2 .parameters ).dump (2 )); 1790 } 1791 1792assert_equals (std::string ("[\n" 1793" {\n" 1794" \"type\": \"function\",\n" 1795" \"function\": {\n" 1796" \"name\": \"special_function\",\n" 1797" \"description\": \"I'm special\",\n" 1798" \"parameters\": {\n" 1799" \"type\": \"object\",\n" 1800" \"properties\": {\n" 1801" \"arg1\": {\n" 1802" \"type\": \"integer\",\n" 1803" \"description\": \"The arg.\"\n" 1804" }\n" 1805" },\n" 1806" \"required\": [\n" 1807" \"arg1\"\n" 1808" ]\n" 1809" }\n" 1810" }\n" 1811" }\n" 1812"]" ), 1813common_chat_tools_to_json_oaicompat ({special_function_tool }).dump (2 )); 1814} 1815 1816static void test_convert_responses_to_chatcmpl () { 1817LOG_DBG ("%s\n" ,__func__ ); 1818 1819// Test basic conversion with input messages (user/assistant alternating) 1820 { 1821json input = json::parse (R"({ 1822"input": [ 1823{ 1824"type": "message", 1825"role": "user", 1826"content": "hi wassup" 1827}, 1828{ 1829"type": "message", 1830"role": "assistant", 1831"content": "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?" 1832}, 1833{ 1834"type": "message", 1835"role": "user", 1836"content": "hi" 1837} 1838], 1839"model": "gpt-5-mini", 1840"stream": false, 1841"text": {}, 1842"reasoning": { 1843"effort": "medium" 1844} 1845})" ); 1846 1847json result = server_chat_convert_responses_to_chatcmpl (input ); 1848 1849// Verify messages were converted correctly 1850assert_equals (true,result .contains ("messages" )); 1851assert_equals (true,result .at ("messages" ).is_array ()); 1852assert_equals ((size_t )3 ,result .at ("messages" ).size ()); 1853 1854// Check first message (user) 1855const auto & msg0 = result .at ("messages" )[0 ]; 1856assert_equals (std::string ("user" ),msg0 .at ("role" ).get < std::string > ()); 1857assert_equals (true,msg0 .at ("content" ).is_array ()); 1858assert_equals (std::string ("text" ),msg0 .at ("content" )[0 ].at ("type" ).get < std::string > ()); 1859assert_equals (std::string ("hi wassup" ),msg0 .at ("content" )[0 ].at ("text" ).get < std::string > ()); 1860 1861// Check second message (assistant) 1862const auto & msg1 = result .at ("messages" )[1 ]; 1863assert_equals (std::string ("assistant" ),msg1 .at ("role" ).get < std::string > ()); 1864assert_equals (true,msg1 .at ("content" ).is_array ()); 1865assert_equals (std::string ("text" ),msg1 .at ("content" )[0 ].at ("type" ).get < std::string > ()); 1866assert_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 > ()); 1867 1868// Check third message (user) 1869const auto & msg2 = result .at ("messages" )[2 ]; 1870assert_equals (std::string ("user" ),msg2 .at ("role" ).get < std::string > ()); 1871assert_equals (true,msg2 .at ("content" ).is_array ()); 1872assert_equals (std::string ("text" ),msg2 .at ("content" )[0 ].at ("type" ).get < std::string > ()); 1873assert_equals (std::string ("hi" ),msg2 .at ("content" )[0 ].at ("text" ).get < std::string > ()); 1874 1875// Verify other fields preserved 1876assert_equals (std::string ("gpt-5-mini" ),result .at ("model" ).get < std::string > ()); 1877assert_equals (false,result .at ("stream" ).get < bool > ()); 1878 } 1879 1880// Test string input 1881 { 1882json input = json::parse (R"({ 1883"input": "Hello, world!", 1884"model": "test-model" 1885})" ); 1886 1887json result = server_chat_convert_responses_to_chatcmpl (input ); 1888 1889assert_equals ((size_t )1 ,result .at ("messages" ).size ()); 1890const auto & msg = result .at ("messages" )[0 ]; 1891assert_equals (std::string ("user" ),msg .at ("role" ).get < std::string > ()); 1892assert_equals (std::string ("Hello, world!" ),msg .at ("content" ).get < std::string > ()); 1893 } 1894 1895// Test with instructions (system message) 1896 { 1897json input = json::parse (R"({ 1898"input": "Hello", 1899"instructions": "You are a helpful assistant.", 1900"model": "test-model" 1901})" ); 1902 1903json result = server_chat_convert_responses_to_chatcmpl (input ); 1904 1905assert_equals ((size_t )2 ,result .at ("messages" ).size ()); 1906const auto & sys_msg = result .at ("messages" )[0 ]; 1907assert_equals (std::string ("system" ),sys_msg .at ("role" ).get < std::string > ()); 1908assert_equals (std::string ("You are a helpful assistant." ),sys_msg .at ("content" ).get < std::string > ()); 1909 } 1910 1911// Test with max_output_tokens conversion 1912 { 1913json input = json::parse (R"({ 1914"input": "Hello", 1915"model": "test-model", 1916"max_output_tokens": 100 1917})" ); 1918 1919json result = server_chat_convert_responses_to_chatcmpl (input ); 1920 1921assert_equals (true,result .contains ("max_tokens" )); 1922assert_equals (false,result .contains ("max_output_tokens" )); 1923assert_equals (100 ,result .at ("max_tokens" ).get < int > ()); 1924 } 1925 1926// Test mixed Responses tools: convert only function tools 1927 { 1928json input = json::parse (R"({ 1929"input": "Hello", 1930"model": "test-model", 1931"tools": [ 1932{ 1933"type": "web_search" 1934}, 1935{ 1936"type": "function", 1937"name": "get_weather", 1938"description": "Get weather for a location", 1939"parameters": { 1940"type": "object", 1941"properties": { 1942"location": { 1943"type": "string" 1944} 1945}, 1946"required": ["location"] 1947} 1948}, 1949{ 1950"type": "image_generation" 1951}, 1952{ 1953"type": "mcp", 1954"server_label": "test-server" 1955}, 1956{ 1957"type": "namespace", 1958"name": "browser" 1959} 1960] 1961})" ); 1962 1963json result = server_chat_convert_responses_to_chatcmpl (input ); 1964 1965assert_equals (true,result .contains ("tools" )); 1966assert_equals (true,result .at ("tools" ).is_array ()); 1967assert_equals ((size_t )1 ,result .at ("tools" ).size ()); 1968 1969const auto & tool = result .at ("tools" )[0 ]; 1970assert_equals (std::string ("function" ),tool .at ("type" ).get < std::string > ()); 1971assert_equals (std::string ("get_weather" ),tool .at ("function" ).at ("name" ).get < std::string > ()); 1972assert_equals (true,tool .at ("function" ).at ("strict" ).get < bool > ()); 1973 } 1974 1975// Test non-function Responses tools are ignored 1976 { 1977json input = json::parse (R"({ 1978"input": "Hello", 1979"model": "test-model", 1980"tools": [ 1981{ 1982"type": "web_search" 1983}, 1984{ 1985"type": "image_generation" 1986}, 1987{ 1988"type": "mcp", 1989"server_label": "test-server" 1990}, 1991{ 1992"type": "namespace", 1993"name": "browser" 1994} 1995] 1996})" ); 1997 1998json result = server_chat_convert_responses_to_chatcmpl (input ); 1999 2000assert_equals (false,result .contains ("tools" )); 2001 } 2002} 2003 2004// Shared LFM2 parser cases - all variants use one output format and parser 2005static void test_lfm2_parser (const std::string & template_path ,bool detailed_debug ) { 2006auto tst = peg_tester (template_path ,detailed_debug ); 2007 2008// Basic content only 2009tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 2010 2011// Single tool call without reasoning 2012tst .test ("<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" ) 2013 .tools ({special_function_tool }) 2014 .expect (message_assist_call ) 2015 .run (); 2016 2017// Tool call with string argument 2018tst .test ("<|tool_call_start|>[get_time(city=\"XYZCITY\")]<|tool_call_end|>" ) 2019 .tools ({get_time_tool }) 2020 .expect (message_with_tool_calls ("get_time" ,"{\"city\":\"XYZCITY\"}" )) 2021 .run (); 2022 2023// Python literals become JSON 2024tst .test ("<|tool_call_start|>[toggle(enabled=True)]<|tool_call_end|>" ) 2025 .tools ({toggle_tool }) 2026 .expect (message_with_tool_calls ("toggle" ,R"({"enabled": true})" )) 2027 .run (); 2028 2029tst .test ("<|tool_call_start|>[set_nullable(value=None)]<|tool_call_end|>" ) 2030 .tools ({nullable_tool }) 2031 .expect (message_with_tool_calls ("set_nullable" ,R"({"value": null})" )) 2032 .run (); 2033 2034// Nested Python literal 2035tst .test ("<|tool_call_start|>[set_config(config={\"enabled\": True, \"count\": 3})]<|tool_call_end|>" ) 2036 .tools ({config_tool }) 2037 .expect (message_with_tool_calls ("set_config" ,R"({"config": {"enabled": true, "count": 3}})" )) 2038 .run (); 2039 2040// JSON literals are accepted too 2041tst .test ("<|tool_call_start|>[set_config(config={\"enabled\": true, \"note\": null})]<|tool_call_end|>" ) 2042 .tools ({config_tool }) 2043 .expect (message_with_tool_calls ("set_config" ,R"({"config": {"enabled": true, "note": null}})" )) 2044 .run (); 2045 2046// Dotted function name with structured args 2047tst .test ("<|tool_call_start|>[Calendar.create_event(title=\"demo\", participants=[\"Alice\", \"Bob\"], " 2048"metadata={\"priority\": \"high\", \"reminder\": true})]<|tool_call_end|>" ) 2049 .tools ({calendar_create_event_tool }) 2050 .expect (message_with_tool_calls ( 2051"Calendar.create_event" , 2052R"({"title": "demo", "participants": ["Alice", "Bob"], "metadata": {"priority": "high", "reminder": true}})" )) 2053 .run (); 2054 2055// Markdown links stay content 2056tst .test ("Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org)." ) 2057 .tools ({get_time_tool }) 2058 .expect (simple_assist_msg ("Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org)." )) 2059 .run (); 2060 2061// Python tool with multiline code in string: the \n in the literal decodes to a real 2062// newline, emitted as a JSON \n escape (not a doubled backslash). 2063tst .test ("<|tool_call_start|>[python(code=\"def hello():\\n print('hey')\")]<|tool_call_end|>" ) 2064 .tools ({python_tool }) 2065 .expect_tool_calls ({ 2066 {"python" ,R"#({"code": "def hello():\n print('hey')"})#" ,"" } 2067 }) 2068 .run (); 2069 2070// String escape sequences decode to their actual characters (newline + tab here), 2071// so a "write a two line file" style call produces real line breaks, not literal "\n". 2072tst .test ("<|tool_call_start|>[python(code=\"First line\\nSecond line\\tindented\")]<|tool_call_end|>" ) 2073 .tools ({python_tool }) 2074 .expect_tool_calls ({ 2075 {"python" ,R"#({"code": "First line\nSecond line\tindented"})#" ,"" } 2076 }) 2077 .run (); 2078 2079// Escaped quotes inside a string argument survive the round-trip. 2080tst .test ("<|tool_call_start|>[python(code=\"print(\\\"hi\\\")\")]<|tool_call_end|>" ) 2081 .tools ({python_tool }) 2082 .expect_tool_calls ({ 2083 {"python" ,R"#({"code": "print(\"hi\")"})#" ,"" } 2084 }) 2085 .run (); 2086 2087// Content before tool call (no reasoning) 2088tst .test ("Let me check the time.<|tool_call_start|>[get_time(city=\"Paris\")]<|tool_call_end|>" ) 2089 .tools ({get_time_tool }) 2090 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 2091"" ,"Let me check the time." , { {"get_time" ,"{\"city\":\"Paris\"}" } } 2092 )) 2093 .run (); 2094 2095// Multiple tool calls (parallel) 2096tst .test ("<|tool_call_start|>[special_function(arg1=1), special_function_with_opt(arg1=1, arg2=2)]<|tool_call_end|>" ) 2097 .parallel_tool_calls (true) 2098 .tools ({special_function_tool ,special_function_tool_with_optional_param }) 2099 .expect_tool_calls ({ 2100 {"special_function" ,R"({"arg1": 1})" , {} }, 2101 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 2102 }) 2103 .run (); 2104 2105// Partial tool call (streaming) 2106tst .test ("<|tool_call_start|>[special_function(arg1=" ) 2107 .tools ({special_function_tool }) 2108 .is_partial (true) 2109 .expect (simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": " )) 2110 .run (); 2111 2112// Tool call with empty arguments 2113tst .test ("<|tool_call_start|>[empty_args()]<|tool_call_end|>" ) 2114 .tools ({empty_args_tool }) 2115 .expect (simple_assist_msg ("" ,"" ,"empty_args" ,"{}" )) 2116 .run (); 2117 2118} 2119 2120static void test_template_output_peg_parsers (bool detailed_debug ) { 2121LOG_DBG ("%s\n" ,__func__ ); 2122 2123// JSON schemas 2124const char * invoice_schema = R"({ 2125"type": "object", 2126"properties": { 2127"amount": {"type": "number"}, 2128"date": {"type": "string"} 2129} 2130})" ; 2131 2132const char * const_schema = R"({ 2133"const": "42" 2134})" ; 2135 2136 { 2137// Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case) 2138auto tst = peg_tester ("models/templates/Qwen3.5-4B.jinja" ,detailed_debug ); 2139 2140tst .test ("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?" ) 2141 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2142 .enable_thinking (true) 2143 .expect (message_assist_thoughts ) 2144 .run (); 2145 2146tst .test ("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?" ) 2147 .enable_thinking (true) 2148 .reasoning_format (COMMON_REASONING_FORMAT_NONE ) 2149 .expect_content ("<think>\nI'm\nthinking\n</think>\n\nHello, world!\nWhat's up?" ) 2150 .run (); 2151 2152tst .test ( 2153"<tool_call>\n" 2154"<function=special_function>\n" 2155"<parameter=arg1>\n1\n</parameter>\n" 2156"</function>\n" 2157"</tool_call>" ) 2158 .enable_thinking (false) 2159 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2160 .tools ({special_function_tool }) 2161 .expect (message_assist_call ) 2162 .run (); 2163 2164tst .test ( 2165"I'm\nthinking\n</think>\n\n" 2166"<tool_call>\n" 2167"<function=special_function>\n" 2168"<parameter=arg1>\n1\n</parameter>\n" 2169"</function>\n" 2170"</tool_call>" ) 2171 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2172 .tools ({special_function_tool }) 2173 .expect (message_assist_call_thoughts ) 2174 .run (); 2175 2176tst .test ( 2177"<tool_call>\n" 2178"<function=special_function>\n" 2179"<parameter=arg1>\n1\n</parameter>\n" 2180"</function>\n" 2181"</tool_call>\n" 2182"<tool_call>\n" 2183"<function=special_function_with_opt>\n" 2184"<parameter=arg1>\n1\n</parameter>\n" 2185"<parameter=arg2>\n2\n</parameter>\n" 2186"</function>\n" 2187"</tool_call>" ) 2188 .enable_thinking (false) 2189 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2190 .parallel_tool_calls (true) 2191 .tools ({ 2192special_function_tool ,special_function_tool_with_optional_param 2193 }) 2194 .expect_tool_calls ({ 2195 {"special_function" ,R"({"arg1": 1})" , {} }, 2196 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 2197 }) 2198 .run (); 2199 2200tst .test ( 2201"<tool_call>\n" 2202"<function=python>\n" 2203"<parameter=code>\n" 2204"def hello():\n" 2205" print(\"Hello, world!\")\n" 2206"\n" 2207"hello()\n" 2208"</parameter>\n" 2209"</function>\n" 2210"</tool_call>" ) 2211 .enable_thinking (false) 2212 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2213 .tools ({ 2214python_tool 2215 }) 2216 .expect_tool_calls ({ 2217 {"python" ,"{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}" , {} }, 2218 }) 2219 .run (); 2220 2221tst .test ( 2222"<tool_call>\n" 2223"<function=edit>\n" 2224"<parameter=filename>\n" 2225"foo.c\n" 2226"</parameter>\n" 2227"<parameter=oldString>\n" 2228"#iclunde\n" 2229"</parameter>\n" 2230"<parameter=newString>\n" 2231"#include\n" 2232"</parameter>\n" 2233"</function>\n" 2234"</tool_call>" ) 2235 .enable_thinking (false) 2236 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2237 .tools ({ 2238edit_tool 2239 }) 2240 .expect_tool_calls ({ 2241 {"edit" ,"{\"filename\": \"foo.c\", \"oldString\": \"#iclunde\", \"newString\": \"#include\"}" , {} }, 2242 }) 2243 .run (); 2244 2245// a parameter value that itself ends in a newline (e.g. a source file with a 2246// trailing newline). The structural delimiter is "\n</parameter>\n", so the value 2247// "#include\n" renders as "...#include\n\n</parameter>\n". The trailing newline must 2248// be preserved faithfully (no stripping), and the generated grammar must admit a 2249// value ending on a delimiter prefix. Regression test for gbnf_excluding_pattern. 2250tst .test ( 2251"<tool_call>\n" 2252"<function=edit>\n" 2253"<parameter=filename>\n" 2254"foo.c\n" 2255"</parameter>\n" 2256"<parameter=oldString>\n" 2257"#iclunde\n" 2258"</parameter>\n" 2259"<parameter=newString>\n" 2260"#include\n" 2261"\n" 2262"</parameter>\n" 2263"</function>\n" 2264"</tool_call>" ) 2265 .enable_thinking (false) 2266 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2267 .tools ({ 2268edit_tool 2269 }) 2270 .expect_tool_calls ({ 2271 {"edit" ,"{\"filename\": \"foo.c\", \"oldString\": \"#iclunde\", \"newString\": \"#include\\n\"}" , {} }, 2272 }) 2273 .run (); 2274 2275 2276// test code that starts with indent 2277tst .test ( 2278"<tool_call>\n" 2279"<function=python>\n" 2280"<parameter=code>\n" 2281" print(\"Hello, world!\")\n" 2282"</parameter>\n" 2283"</function>\n" 2284"</tool_call>" ) 2285 .enable_thinking (false) 2286 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2287 .tools ({ 2288python_tool 2289 }) 2290 .expect_tool_calls ({ 2291 {"python" ,"{\"code\": \" print(\\\"Hello, world!\\\")\"}" , {} }, 2292 }) 2293 .run (); 2294 2295tst .test ( 2296"I need to output the invoice details in JSON\n" 2297"</think>\n\n" 2298R"({"amount": 123.45, "date": "2025-12-03"})" ) 2299 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2300 .enable_thinking (true) 2301 .json_schema (invoice_schema ) 2302 .expect_reasoning ("I need to output the invoice details in JSON" ) 2303 .expect_content (R"({"amount": 123.45, "date": "2025-12-03"})" ) 2304 .run (); 2305 2306// a tool call ends the prefilled thinking block, with or without a closing </think> 2307tst .test ( 2308"<tool_call>\n" 2309"<function=run_in_terminal>\n" 2310"<parameter=command>\n" 2311"pwd\n" 2312"</parameter>\n" 2313"</function>\n" 2314"</tool_call>" ) 2315 .enable_thinking (true) 2316 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2317 .tools ({run_in_terminal_tool }) 2318 .expect_tool_calls ({ 2319 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2320 }) 2321 .run (); 2322 2323// ...including after the model has thought about it 2324tst .test ( 2325"Need to inspect the current directory.\n" 2326"<tool_call>\n" 2327"<function=run_in_terminal>\n" 2328"<parameter=command>\n" 2329"pwd\n" 2330"</parameter>\n" 2331"</function>\n" 2332"</tool_call>" ) 2333 .enable_thinking (true) 2334 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2335 .tools ({run_in_terminal_tool }) 2336 .expect_reasoning ("Need to inspect the current directory." ) 2337 .expect_tool_calls ({ 2338 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2339 }) 2340 .run (); 2341 2342// No args tool 2343tst .test ( 2344"<tool_call>\n" 2345"<function=empty_args>\n" 2346"</function>\n" 2347"</tool_call>" ) 2348 .enable_thinking (false) 2349 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2350 .tools ({empty_args_tool }) 2351 .expect (message_with_tool_calls ("empty_args" ,"{}" )) 2352 .run (); 2353 2354// No args tool with no properties defined 2355tst .test ( 2356"<tool_call>\n" 2357"<function=empty_args_no_props>\n" 2358"</function>\n" 2359"</tool_call>" ) 2360 .enable_thinking (false) 2361 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2362 .tools ({empty_args_tool_no_properties }) 2363 .expect (message_with_tool_calls ("empty_args_no_props" ,"{}" )) 2364 .run (); 2365 2366// Edge cases when reasoning traces are not sent 2367tst .test ( 2368"<think>\n\n</think>\n\n" 2369"<tool_call>\n" 2370"<function=special_function>\n" 2371"<parameter=arg1>\n1\n</parameter>\n" 2372"</function>\n" 2373"</tool_call>" ) 2374 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2375 .tools ({ 2376special_function_tool 2377 }) 2378 .expect_reasoning ("<think>\n\n" ) 2379 .expect_tool_calls ({ {"special_function" ,"{\"arg1\": 1}" ,"" } }) 2380 .run (); 2381 2382tst .test ( 2383"</think>\n\n" 2384"<tool_call>\n" 2385"<function=special_function>\n" 2386"<parameter=arg1>\n1\n</parameter>\n" 2387"</function>\n" 2388"</tool_call>" ) 2389 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2390 .tools ({ 2391special_function_tool 2392 }) 2393 .expect_reasoning ("" ) 2394 .expect_tool_calls ({ {"special_function" ,"{\"arg1\": 1}" ,"" } }) 2395 .run (); 2396 2397tst .test ( 2398"</think>\n\n" 2399"<tool_call>\n" 2400"<function=run_in_terminal>\n" 2401"<parameter=command>\n" 2402"pwd\n" 2403"</parameter>\n" 2404"</function>\n" 2405"</tool_call>" ) 2406 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2407 .enable_thinking (true) 2408 .tools ({ 2409run_in_terminal_tool 2410 }) 2411 .expect_tool_calls ({ 2412 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2413 }) 2414 .run (); 2415 2416tst .test ( 2417"</think>\n\n" 2418"Let me inspect the current directory.\n" 2419"<tool_call>\n" 2420"<function=run_in_terminal>\n" 2421"<parameter=command>\n" 2422"pwd\n" 2423"</parameter>\n" 2424"</function>\n" 2425"</tool_call>" ) 2426 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2427 .enable_thinking (true) 2428 .tools ({ 2429run_in_terminal_tool 2430 }) 2431 .expect_content ("Let me inspect the current directory.\n" ) 2432 .expect_tool_calls ({ 2433 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2434 }) 2435 .run (); 2436 2437tst .test ( 2438"</think>\n\n" 2439"Let me inspect the current directory.\n" 2440"<tool_call>\n" 2441"<function=run_in_terminal>\n" 2442"<parameter=command>\n" 2443"pwd\n" 2444"</parameter>\n" 2445"</function>\n" 2446"</tool_call>" ) 2447 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2448 .enable_thinking (true) 2449 .tools ({ 2450run_in_terminal_tool 2451 }) 2452 .tool_choice (COMMON_CHAT_TOOL_CHOICE_REQUIRED ) 2453 .expect_content ("Let me inspect the current directory.\n" ) 2454 .expect_tool_calls ({ 2455 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2456 }) 2457 .run (); 2458 2459tst .test ( 2460"I should inspect the directory.\n" 2461"</think>\n\n" 2462"Let me inspect it now.\n" 2463"<tool_call>\n" 2464"<function=run_in_terminal>\n" 2465"<parameter=command>\n" 2466"pwd\n" 2467"</parameter>\n" 2468"</function>\n" 2469"</tool_call>" ) 2470 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2471 .enable_thinking (true) 2472 .tools ({ 2473run_in_terminal_tool 2474 }) 2475 .expect_reasoning ("I should inspect the directory." ) 2476 .expect_content ("Let me inspect it now.\n" ) 2477 .expect_tool_calls ({ 2478 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2479 }) 2480 .run (); 2481 2482// Continuation tests 2483tst .test ("world!\nWhat's up?" ) 2484 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2485 .enable_thinking (true) 2486 .messages ({message_user ,message_assist_prefill_content }) 2487 .add_generation_prompt (false) 2488 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 2489 .expect_reasoning ("I'm thinking" ) 2490 .expect_content ("Hello, world!\nWhat's up?" ) 2491 .run (); 2492 2493tst .test (" thinking\n</think>\n\nHello, world!\nWhat's up?" ) 2494 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2495 .enable_thinking (true) 2496 .messages ({message_user ,message_assist_prefill_reasoning }) 2497 .add_generation_prompt (false) 2498 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 2499 .expect_reasoning ("I'm thinking" ) 2500 .expect_content ("Hello, world!\nWhat's up?" ) 2501 .run (); 2502 2503 { 2504common_chat_msg user_start ; 2505user_start .role = "user" ; 2506user_start .content = "Create a todo list, then inspect the repository." ; 2507 2508common_chat_msg assistant_todos = 2509simple_assist_msg ("" ,"" ,"manage_todo_list" , 2510R"({"todos":[{"item":"Inspect repository","selected":false}]})" ,"call_todos" ); 2511 2512common_chat_msg tool_result ; 2513tool_result .role = "tool" ; 2514tool_result .content = "Successfully wrote todo list" ; 2515tool_result .tool_call_id = "call_todos" ; 2516 2517common_chat_msg user_continue ; 2518user_continue .role = "user" ; 2519user_continue .content = "Proceed." ; 2520 2521tst .test ( 2522"I need to run a terminal command.\n" 2523"</think>\n\n" 2524"<tool_call>\n" 2525"<function=run_in_terminal>\n" 2526"<parameter=command>\n" 2527"pwd\n" 2528"</parameter>\n" 2529"</function>\n" 2530"</tool_call>" ) 2531 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2532 .enable_thinking (true) 2533 .tools ({ 2534manage_todo_list_tool ,run_in_terminal_tool 2535 }) 2536 .messages ({user_start ,assistant_todos ,tool_result ,user_continue }) 2537 .expect_reasoning ("I need to run a terminal command." ) 2538 .expect_tool_calls ({ 2539 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2540 }) 2541 .run (); 2542 2543tst .test ( 2544"I need to run a terminal command.\n" 2545"</think>\n\n" 2546"Let me inspect the current directory.\n" 2547"<tool_call>\n" 2548"<function=run_in_terminal>\n" 2549"<parameter=command>\n" 2550"pwd\n" 2551"</parameter>\n" 2552"</function>\n" 2553"</tool_call>" ) 2554 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2555 .enable_thinking (true) 2556 .tools ({ 2557manage_todo_list_tool ,run_in_terminal_tool 2558 }) 2559 .tool_choice (COMMON_CHAT_TOOL_CHOICE_REQUIRED ) 2560 .messages ({user_start ,assistant_todos ,tool_result ,user_continue }) 2561 .expect_reasoning ("I need to run a terminal command." ) 2562 .expect_content ("Let me inspect the current directory.\n" ) 2563 .expect_tool_calls ({ 2564 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2565 }) 2566 .run (); 2567 2568tst .test ( 2569"</think>\n\n" 2570"<tool_call>\n" 2571"<function=run_in_terminal>\n" 2572"<parameter=command>\n" 2573"pwd\n" 2574"</parameter>\n" 2575"</function>\n" 2576"</tool_call>" ) 2577 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2578 .enable_thinking (true) 2579 .tools ({ 2580manage_todo_list_tool ,run_in_terminal_tool 2581 }) 2582 .messages ({user_start ,assistant_todos ,tool_result ,user_continue }) 2583 .expect_tool_calls ({ 2584 {"run_in_terminal" ,R"({"command": "pwd"})" , {} }, 2585 }) 2586 .run (); 2587 } 2588 } 2589 2590 { 2591// Ministral-3-14B-Reasoning-2512 2592auto tst = peg_tester ("models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja" ,detailed_debug ); 2593 2594tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 2595 2596tst .test ("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?" ) 2597 .expect_content ("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?" ) 2598 .expect_reconstruction () 2599 .run (); 2600 2601tst .test ("[THINK]I'm\nthinking[/THINK]Hello, world!\nWhat's up?" ) 2602 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2603 .enable_thinking (true) 2604 .expect (message_assist_thoughts ) 2605 .expect_reconstruction () 2606 .run (); 2607 2608tst .test (R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})" ) 2609 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2610 .enable_thinking (true) 2611 .tools ({special_function_tool }) 2612 .expect (message_assist_call ) 2613 .run (); 2614 2615tst .test ( 2616"[THINK]I'm\nthinking[/THINK]" 2617R"([TOOL_CALLS]special_function[ARGS]{"arg1":1})" ) 2618 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2619 .enable_thinking (true) 2620 .tools ({special_function_tool }) 2621 .expect (message_assist_call_thoughts ) 2622 .run (); 2623 2624tst .test (R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})" 2625R"([TOOL_CALLS]special_function_with_opt[ARGS]{"arg1": 1, "arg2": 2})" ) 2626 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2627 .parallel_tool_calls (true) 2628 .tools ({ 2629special_function_tool ,special_function_tool_with_optional_param 2630 }) 2631 .expect_tool_calls ({ 2632 {"special_function" ,R"({"arg1": 1})" , {} }, 2633 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 2634 }) 2635 .expect_reconstruction () 2636 .run (); 2637 2638tst .test ( 2639"[THINK]I need to output the invoice details in JSON[/THINK]" 2640"```json\n" 2641R"({"amount": 123.45, "date": "2025-12-03"})" 2642"\n```" ) 2643 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2644 .json_schema (invoice_schema ) 2645 .expect_reasoning ("I need to output the invoice details in JSON" ) 2646 .expect_content (R"({"amount": 123.45, "date": "2025-12-03"})" ) 2647 .run (); 2648 2649// fake tool call marker in reasoning 2650tst .test ( 2651"[THINK]Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more[/THINK]" 2652R"([TOOL_CALLS]special_function[ARGS]{"arg1": 1})" ) 2653 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2654 .enable_thinking (true) 2655 .tools ({special_function_tool }) 2656 .expect_reasoning ("Let me think about [TOOL_CALLS]special_function[ARGS]{\"arg1\":1} and more" ) 2657 .expect_tool_calls ({ 2658 {"special_function" ,R"({"arg1": 1})" , {} }, 2659 }) 2660 .expect_reconstruction () 2661 .run (); 2662 2663// Continuation tests 2664tst .test ("world!\nWhat's up?" ) 2665 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2666 .enable_thinking (true) 2667 .messages ({message_user ,message_assist_prefill_content }) 2668 .add_generation_prompt (false) 2669 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 2670 .expect_reasoning ("I'm thinking" ) 2671 .expect_content ("Hello, world!\nWhat's up?" ) 2672 .run (); 2673 2674tst .test (" thinking[/THINK]Hello, world!\nWhat's up?" ) 2675 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2676 .enable_thinking (true) 2677 .messages ({message_user ,message_assist_prefill_reasoning }) 2678 .add_generation_prompt (false) 2679 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 2680 .expect_reasoning ("I'm thinking" ) 2681 .expect_content ("Hello, world!\nWhat's up?" ) 2682 .run (); 2683 } 2684 2685 { 2686// NVIDIA Nemotron-3 Nano 2687auto tst = peg_tester ("models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja" ,detailed_debug ); 2688 2689tst .test ("Hello, world!\nWhat's up?" ). 2690enable_thinking (false). 2691reasoning_format (COMMON_REASONING_FORMAT_AUTO ). 2692expect (message_assist ).run (); 2693 2694tst .test ("I'm\nthinking\n</think>\nHello, world!\nWhat's up?" ) 2695 .enable_thinking (true) 2696 .reasoning_format (COMMON_REASONING_FORMAT_NONE ) 2697 .expect_content ("<think>\nI'm\nthinking\n</think>\nHello, world!\nWhat's up?" ) 2698 .run (); 2699 2700tst .test ("I'm\nthinking\n</think>\nHello, world!\nWhat's up?" ) 2701 .enable_thinking (true) 2702 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2703 .expect (message_assist_thoughts ) 2704 .run (); 2705 2706tst .test ( 2707"<tool_call>\n" 2708"<function=special_function>\n" 2709"<parameter=arg1>\n1\n</parameter>\n" 2710"</function>\n" 2711"</tool_call>\n" ) 2712 .enable_thinking (false) 2713 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2714 .tools ({special_function_tool }) 2715 .expect (message_assist_call ) 2716 .run (); 2717 2718tst .test ( 2719"I'm\nthinking\n</think>\n" 2720"<tool_call>\n" 2721"<function=special_function>\n" 2722"<parameter=arg1>\n1\n</parameter>\n" 2723"</function>\n" 2724"</tool_call>\n" ) 2725 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2726 .tools ({special_function_tool }) 2727 .expect (message_assist_call_thoughts ) 2728 .run (); 2729 2730tst .test ( 2731"<tool_call>\n" 2732"<function=special_function>\n" 2733"<parameter=arg1>\n1\n</parameter>\n" 2734"</function>\n" 2735"</tool_call>\n" 2736"<tool_call>\n" 2737"<function=special_function_with_opt>\n" 2738"<parameter=arg1>\n1\n</parameter>\n" 2739"<parameter=arg2>\n2\n</parameter>\n" 2740"</function>\n" 2741"</tool_call>\n" ) 2742 .enable_thinking (false) 2743 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2744 .parallel_tool_calls (true) 2745 .tools ({ 2746special_function_tool ,special_function_tool_with_optional_param 2747 }) 2748 .expect_tool_calls ({ 2749 {"special_function" ,R"({"arg1": 1})" , {} }, 2750 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 2751 }) 2752 .run (); 2753 2754tst .test ( 2755"<tool_call>\n" 2756"<function=python>\n" 2757"<parameter=code>\n" 2758"def hello():\n" 2759" print(\"Hello, world!\")\n" 2760"\n" 2761"hello()\n" 2762"</parameter>\n" 2763"</function>\n" 2764"</tool_call>\n" ) 2765 .enable_thinking (false) 2766 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2767 .tools ({ 2768python_tool 2769 }) 2770 .expect_tool_calls ({ 2771 {"python" ,"{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}" , {} }, 2772 }) 2773 .run (); 2774 2775tst .test ( 2776"I need to output the invoice details in JSON\n" 2777"</think>\n" 2778R"({"amount": 123.45, "date": "2025-12-03"})" ) 2779 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2780 .enable_thinking (true) 2781 .json_schema (invoice_schema ) 2782 .expect_reasoning ("I need to output the invoice details in JSON" ) 2783 .expect_content (R"({"amount": 123.45, "date": "2025-12-03"})" ) 2784 .run (); 2785 2786// Continuation tests 2787tst .test ("world!\nWhat's up?" ) 2788 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2789 .enable_thinking (true) 2790 .messages ({message_user ,message_assist_prefill_content }) 2791 .add_generation_prompt (false) 2792 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 2793 .expect_reasoning ("I'm thinking" ) 2794 .expect_content ("Hello, world!\nWhat's up?" ) 2795 .run (); 2796 2797tst .test (" thinking\n</think>\nHello, world!\nWhat's up?" ) 2798 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2799 .enable_thinking (true) 2800 .messages ({message_user ,message_assist_prefill_reasoning }) 2801 .add_generation_prompt (false) 2802 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 2803 .expect_reasoning ("I'm thinking" ) 2804 .expect_content ("Hello, world!\nWhat's up?" ) 2805 .run (); 2806 } 2807 2808 { 2809// CohereForAI Command-R 7B (2024-tool_use) 2810auto tst = peg_tester ("models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" ,detailed_debug ); 2811 2812tst .test ("<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>" ).expect (message_assist ).run (); 2813 2814tst .test ( 2815"<|START_THINKING|>I'm\nthinking<|END_THINKING|>" 2816"<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>" ) 2817 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2818 .expect (message_assist_thoughts ) 2819 .run (); 2820 2821tst .test ( 2822"<|START_THINKING|>I'm\nthinking<|END_THINKING|>" 2823"<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>" ) 2824 .expect (message_assist_thoughts_unparsed_r7b ) 2825 .run (); 2826 2827tst .test ( 2828"<|START_THINKING|>I'm\nthinking<|END_THINKING|>" 2829"<|START_ACTION|>[\n" 2830" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" 2831"]<|END_ACTION|>" ) 2832 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2833 .tools ({special_function_tool }) 2834 .expect (message_assist_thoughts_call_idx ) 2835 .run (); 2836 2837tst .test ( 2838"<|START_THINKING|>I'm\nthinking<|END_THINKING|>" 2839"<|START_ACTION|>[\n" 2840" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", " ) 2841 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2842 .tools ({special_function_tool }) 2843 .is_partial (true) 2844 .expect (message_assist_thoughts_partial_call ) 2845 .run (); 2846 2847tst .test ( 2848"<|START_THINKING|><|END_THINKING|>" 2849"<|START_ACTION|>[\n" 2850" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" 2851"]<|END_ACTION|>" ) 2852 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2853 .tools ({special_function_tool }) 2854 .expect (message_assist_call_idx ) 2855 .run (); 2856 } 2857 2858 { 2859// Cohere2 MoE (North Code) - dedicated parser. 2860// Marker-wrapped format: <|START_THINKING|>...<|END_THINKING|> then either 2861// <|START_TEXT|>...<|END_TEXT|> (content) or <|START_ACTION|>[json]<|END_ACTION|> (tools). 2862// The generation prompt forces a leading <|START_THINKING|>, so model output begins inside 2863// the thinking block: test inputs start with the reasoning body, not the <|START_THINKING|> tag. 2864auto tst = peg_tester ("models/templates/Cohere2MoE.jinja" ,detailed_debug ); 2865 2866// Content with reasoning, extracted. 2867tst .test ("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>" ) 2868 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2869 .expect (message_assist_thoughts ) 2870 .run (); 2871 2872// Content with reasoning, reasoning_format=NONE -> thinking kept inline in content (markers preserved). 2873tst .test ("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>" ) 2874 .expect (message_assist_thoughts_unparsed_r7b ) 2875 .run (); 2876 2877// Content with empty thinking block. 2878tst .test ("<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>" ) 2879 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2880 .expect (message_assist ) 2881 .run (); 2882 2883// JSON output schema 2884tst .test ( 2885"I need to output the invoice details in JSON<|END_THINKING|>" 2886"<|START_TEXT|>{\"amount\": 123.45, \"date\": \"2025-12-03\"}<|END_TEXT|>" ) 2887 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2888 .json_schema (invoice_schema ) 2889 .tools ({special_function_tool }) 2890 .expect_reasoning ("I need to output the invoice details in JSON" ) 2891 .expect_content (R"({"amount": 123.45, "date": "2025-12-03"})" ) 2892 .run (); 2893 2894// Single tool call with reasoning. 2895tst .test ( 2896"I'm\nthinking<|END_THINKING|>" 2897"<|START_ACTION|>[\n" 2898" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" 2899"]<|END_ACTION|>" ) 2900 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2901 .tools ({special_function_tool }) 2902 .expect (message_assist_thoughts_call_idx ) 2903 .run (); 2904 2905// Single tool call, empty thinking block (no reasoning content). 2906tst .test ( 2907"<|END_THINKING|>" 2908"<|START_ACTION|>[\n" 2909" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" 2910"]<|END_ACTION|>" ) 2911 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2912 .tools ({special_function_tool }) 2913 .expect (message_assist_call_idx ) 2914 .run (); 2915 2916// Tool call with an array argument (todo_list). 2917tst .test ( 2918"<|END_THINKING|>" 2919"<|START_ACTION|>[\n" 2920" {\"tool_call_id\": \"0\", \"tool_name\": \"todo_list\", \"parameters\": {\"todos\": [\"buy milk\", \"walk dog\"]}}\n" 2921"]<|END_ACTION|>" ) 2922 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2923 .tools ({todo_list }) 2924 .expect (simple_assist_msg ("" ,"" ,"todo_list" ,"{\"todos\": [\"buy milk\", \"walk dog\"]}" ,"0" )) 2925 .run (); 2926 2927// Parallel tool calls with reasoning. 2928tst .test ( 2929"I'm\nthinking<|END_THINKING|>" 2930"<|START_ACTION|>[\n" 2931" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}},\n" 2932" {\"tool_call_id\": \"1\", \"tool_name\": \"python\", \"parameters\": {\"code\": \"print('hey')\"}}\n" 2933"]<|END_ACTION|>" ) 2934 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2935 .parallel_tool_calls (true) 2936 .tools ({special_function_tool ,python_tool }) 2937 .expect_reasoning ("I'm\nthinking" ) 2938 .expect_tool_calls ({ 2939 {"special_function" ,R"({"arg1": 1})" ,"0" }, 2940 {"python" ,"{\"code\": \"print('hey')\"}" ,"1" }, 2941 }) 2942 .run (); 2943 2944// Tools available but the model answers with content instead of calling a tool. 2945tst .test ("I'm\nthinking<|END_THINKING|><|START_TEXT|>Hello, world!\nWhat's up?<|END_TEXT|>" ) 2946 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2947 .tools ({special_function_tool }) 2948 .expect (message_assist_thoughts ) 2949 .run (); 2950 2951// Partial tool call (streaming): name/id resolved before arguments arrive. 2952tst .test ( 2953"I'm\nthinking<|END_THINKING|>" 2954"<|START_ACTION|>[\n" 2955" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", " ) 2956 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 2957 .tools ({special_function_tool }) 2958 .is_partial (true) 2959 .expect (message_assist_thoughts_partial_call ) 2960 .run (); 2961 } 2962 2963 { 2964// Google Gemma 2 2B - does not support tool calling 2965auto tst = peg_tester ("models/templates/google-gemma-2-2b-it.jinja" ); 2966 2967tst .test ("Hello, world!" ).expect (simple_assist_msg ("Hello, world!" )).expect_reconstruction ().run (); 2968 2969tst .test ("Line 1\nLine 2\nLine 3" ).expect (simple_assist_msg ("Line 1\nLine 2\nLine 3" )).expect_reconstruction ().run (); 2970 2971// Continuation tests 2972tst .test ("world!\nWhat's up?" ) 2973 .messages ({message_user ,message_assist_prefill_content }) 2974 .add_generation_prompt (false) 2975 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 2976 .expect_content ("Hello, world!\nWhat's up?" ) 2977 .run (); 2978 } 2979 2980 { 2981// Google Gemma 4 (tool calling with Gemma4 dict format) 2982auto tst = peg_tester ("models/templates/google-gemma-4-31B-it.jinja" ); 2983 2984tst .test ("Hello, world!" ).expect (simple_assist_msg ("Hello, world!" )).run (); 2985 2986// Reasoning and content 2987tst .test ( 2988"<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?" ) 2989 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2990 .expect (message_assist_thoughts ) 2991 .run (); 2992 2993// Empty reasoning (budget=0: sampler forces end tag before newline) 2994tst .test ( 2995"<|channel>thought<channel|>Hello, world!\nWhat's up?" ) 2996 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 2997 .expect (simple_assist_msg ("Hello, world!\nWhat's up?" ,"" )) 2998 .run (); 2999 3000// Reasoning and content with reasoning_format = none 3001tst .test ( 3002"<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?" ) 3003 .reasoning_format (COMMON_REASONING_FORMAT_NONE ) 3004 .expect_content ("<|channel>thought\nI'm\nthinking<channel|>Hello, world!\nWhat's up?" ) 3005 .run (); 3006 3007// Simple tool call with string argument 3008tst .test ( 3009"<|tool_call>call:get_time{city:<|\"|>London<|\"|>}<tool_call|>" ) 3010 .tools ({get_time_tool }) 3011 .expect (message_with_tool_calls ("get_time" ,R"({"city": "London"})" )) 3012 .run (); 3013 3014// Tool call with string argument containing special chars 3015tst .test ( 3016"<|tool_call>call:get_time{city:<|\"|>San Francisco<|\"|>}<tool_call|>" ) 3017 .tools ({get_time_tool }) 3018 .expect (message_with_tool_calls ("get_time" ,R"({"city": "San Francisco"})" )) 3019 .run (); 3020 3021// Tool call with empty args 3022tst .test ( 3023"<|tool_call>call:empty_args{}<tool_call|>" ) 3024 .tools ({empty_args_tool }) 3025 .expect (message_with_tool_calls ("empty_args" ,"{}" )) 3026 .run (); 3027 3028// Tool call with string and content 3029tst .test ( 3030"Hello, world!\nWhat's up?<|tool_call>call:get_time{city:<|\"|>Paris<|\"|>}<tool_call|>" ) 3031 .tools ({get_time_tool }) 3032 .expect (message_with_content_and_tool_call ("Hello, world!\nWhat's up?" ,"get_time" ,R"({"city": "Paris"})" )) 3033 .run (); 3034 3035// Parallel tool calls 3036tst .test ( 3037"<|tool_call>call:get_time{city:<|\"|>London<|\"|>}<tool_call|>" 3038"<|tool_call>call:get_weather{city:<|\"|>Paris<|\"|>}<tool_call|>" ) 3039 .tools ({get_time_tool ,get_weather_tool }) 3040 .parallel_tool_calls (true) 3041 .expect_tool_calls ({ 3042 {"get_time" ,R"({"city": "London"})" ,"" }, 3043 {"get_weather" ,R"({"city": "Paris"})" ,"" }, 3044 }) 3045 .run (); 3046 3047// Tool call with integer argument (number type) 3048tst .test ( 3049"<|tool_call>call:special_function{arg1:42}<tool_call|>" ) 3050 .tools ({special_function_tool }) 3051 .expect (message_with_tool_calls ("special_function" ,R"({"arg1": 42})" )) 3052 .run (); 3053 3054// Tool call with negative number argument 3055tst .test ( 3056"<|tool_call>call:special_function{arg1:-7}<tool_call|>" ) 3057 .tools ({special_function_tool }) 3058 .expect (message_with_tool_calls ("special_function" ,R"({"arg1": -7})" )) 3059 .run (); 3060 3061// Tool call with decimal number argument 3062tst .test ( 3063"<|tool_call>call:amount{orig:3.14}<tool_call|>" ) 3064 .tools ({amount_tool }) 3065 .expect (message_with_tool_calls ("amount" ,R"({"orig": 3.14})" )) 3066 .run (); 3067 3068// Tool call with boolean argument (true) 3069tst .test ( 3070"<|tool_call>call:toggle{enabled:true}<tool_call|>" ) 3071 .tools ({toggle_tool }) 3072 .expect (message_with_tool_calls ("toggle" ,R"({"enabled": true})" )) 3073 .run (); 3074 3075// Tool call with boolean argument (false) 3076tst .test ( 3077"<|tool_call>call:toggle{enabled:false}<tool_call|>" ) 3078 .tools ({toggle_tool }) 3079 .expect (message_with_tool_calls ("toggle" ,R"({"enabled": false})" )) 3080 .run (); 3081 3082// Tool call with null argument 3083tst .test ( 3084"<|tool_call>call:set_nullable{value:null}<tool_call|>" ) 3085 .tools ({nullable_tool }) 3086 .expect (message_with_tool_calls ("set_nullable" ,R"({"value": null})" )) 3087 .run (); 3088 3089// Tool call with array argument (todo list) 3090tst .test ( 3091"<|tool_call>call:todo_list{todos:[<|\"|>buy milk<|\"|>,<|\"|>walk dog<|\"|>]}<tool_call|>" ) 3092 .tools ({todo_list }) 3093 .expect (message_with_tool_calls ("todo_list" ,R"({"todos":["buy milk","walk dog"]})" )) 3094 .run (); 3095 3096// Tool call with object/dict argument 3097tst .test ( 3098"<|tool_call>call:set_config{config:{theme:<|\"|>dark<|\"|>,count:3}}<tool_call|>" ) 3099 .tools ({config_tool }) 3100 .expect (message_with_tool_calls ("set_config" ,R"({"config":{"theme":"dark","count":3}})" )) 3101 .run (); 3102 3103// Tool call with empty array 3104tst .test ( 3105"<|tool_call>call:todo_list{todos:[]}<tool_call|>" ) 3106 .tools ({todo_list }) 3107 .expect (message_with_tool_calls ("todo_list" ,R"({"todos":[]})" )) 3108 .run (); 3109 3110// Tool call with empty dict 3111tst .test ( 3112"<|tool_call>call:set_config{config:{}}<tool_call|>" ) 3113 .tools ({config_tool }) 3114 .expect (message_with_tool_calls ("set_config" ,R"({"config":{}})" )) 3115 .run (); 3116 3117// Tool call with scientific notation number 3118tst .test ( 3119"<|tool_call>call:amount{orig:1.5e10}<tool_call|>" ) 3120 .tools ({amount_tool }) 3121 .expect (message_with_tool_calls ("amount" ,R"({"orig": 1.5e10})" )) 3122 .run (); 3123 3124// Edge cases 3125tst .test ( 3126"<|channel>thought\n<channel|>Hello, world!\nWhat's up?<channel|>" ) 3127 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3128 .expect (message_assist ) 3129 .run (); 3130 3131tst .test ( 3132"<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|>" ) 3133 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3134 .expect (message_assist ) 3135 .run (); 3136 3137tst .test ( 3138"<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|><channel|>" ) 3139 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3140 .expect (message_assist ) 3141 .run (); 3142 3143tst .test ( 3144"<|channel><|channel>thought\n<channel|>Hello, world!\nWhat's up?" ) 3145 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3146 .expect (message_assist ) 3147 .run (); 3148 3149// Continuation tests 3150tst .test ("world!\nWhat's up?" ) 3151 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3152 .enable_thinking (true) 3153 .messages ({message_user ,message_assist_prefill_content }) 3154 .add_generation_prompt (false) 3155 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3156 .expect_reasoning ("I'm thinking" ) 3157 .expect_content ("Hello, world!\nWhat's up?" ) 3158 .run (); 3159 3160tst .test (" thinking<channel|>Hello, world!\nWhat's up?" ) 3161 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3162 .enable_thinking (true) 3163 .messages ({message_user ,message_assist_prefill_reasoning }) 3164 .add_generation_prompt (false) 3165 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 3166 .expect_reasoning ("I'm thinking" ) 3167 .expect_content ("Hello, world!\nWhat's up?" ) 3168 .run (); 3169 3170 { 3171// additional tests for https://github.com/ggml-org/llama.cpp/pull/21760 3172auto tmpls = read_templates ("models/templates/google-gemma-4-31B-it.jinja" ); 3173 3174common_chat_msg tool_call_msg = simple_assist_msg ( 3175"Let me check." ,"" ,"special_function" ,"{\"arg1\": 1}" ,"c0" ); 3176 3177common_chat_msg tool_msg ; 3178tool_msg .role = "tool" ; 3179tool_msg .tool_name = "special_function" ; 3180tool_msg .tool_call_id = "c0" ; 3181tool_msg .content = "{\"r\":\"ok\"}" ; 3182 3183 { 3184common_chat_templates_inputs inputs ; 3185inputs .messages = {message_user ,tool_call_msg ,tool_msg }; 3186inputs .tools = {special_function_tool }; 3187inputs .add_generation_prompt = true; 3188 3189auto params = common_chat_templates_apply (tmpls .get (),inputs ); 3190 3191if (!string_ends_with (params .prompt ,"<turn|>\n<|turn>model\n" )) { 3192throw std::runtime_error ("Missing generation prompt for Gemma 4" ); 3193 } 3194 } 3195 3196 { 3197common_chat_templates_inputs inputs ; 3198inputs .messages = {message_user ,tool_call_msg ,tool_msg }; 3199inputs .tools = {special_function_tool }; 3200inputs .add_generation_prompt = false; 3201 3202auto params = common_chat_templates_apply (tmpls .get (),inputs ); 3203 3204if (string_ends_with (params .prompt ,"<|turn>model\n" )) { 3205throw std::runtime_error ("Gemma 4: generation prompt was modified despite add_generation_prompt=false" ); 3206 } 3207 } 3208 } 3209 3210 { 3211// StepFun trimming regression test (see https://github.com/ggml-org/llama.cpp/pull/25238) 3212auto tmpls = read_templates ("models/templates/StepFun3.5-Flash.jinja" ); 3213 3214common_chat_msg message_chatbot = simple_assist_msg ("Let me check.\n\n" ,"I am thinking.\n\n" ); 3215 3216 { 3217common_chat_templates_inputs inputs ; 3218inputs .messages = {message_chatbot }; 3219inputs .add_generation_prompt = true; 3220 3221auto params = common_chat_templates_apply (tmpls .get (),inputs ); 3222 3223if (params .prompt .find ("Let me check.\n\n" )!= std::string::npos ) { 3224throw std::runtime_error ("StepFun 3.5: content not trimmed" ); 3225 } 3226 3227if (params .prompt .find ("I am thinking.\n\n" )!= std::string::npos ) { 3228throw std::runtime_error ("StepFun 3.5: reasoning_content not trimmed" ); 3229 } 3230 } 3231 3232 { 3233// Trimming must also reach typed (text) content parts, not just string content 3234// (see https://github.com/ggml-org/llama.cpp/pull/25238) 3235common_chat_msg message_parts ; 3236message_parts .role = "user" ; 3237message_parts .content_parts = { 3238 {/* .type = */ "text" ,/* .text = */ "First part.\n\n" }, 3239 {/* .type = */ "media_marker" ,/* .text = */ "<__media__>" }, 3240 {/* .type = */ "text" ,/* .text = */ "Second part.\n\n" }, 3241 }; 3242 3243common_chat_templates_inputs inputs ; 3244inputs .messages = {message_parts }; 3245inputs .add_generation_prompt = true; 3246 3247auto params = common_chat_templates_apply (tmpls .get (),inputs ); 3248 3249if (params .prompt .find ("First part.\n\n" )!= std::string::npos || 3250params .prompt .find ("Second part.\n\n" )!= std::string::npos ) { 3251throw std::runtime_error ("StepFun 3.5: text content parts not trimmed" ); 3252 } 3253 3254// the trimmed text itself must still be present 3255if (params .prompt .find ("First part." )== std::string::npos || 3256params .prompt .find ("Second part." )== std::string::npos ) { 3257throw std::runtime_error ("StepFun 3.5: text content parts missing after trim" ); 3258 } 3259 } 3260 } 3261 3262 } 3263 3264 { 3265// Qwen-QwQ-32B (reasoning model) 3266auto tst = peg_tester ("models/templates/Qwen-QwQ-32B.jinja" ); 3267 3268// QwQ always has thinking forced open - input starts after the <think>\n in the prompt 3269tst .test ("Let me think about this...\n</think>\nThe answer is 42." ) 3270 .enable_thinking (true) 3271 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3272 .expect (simple_assist_msg ("The answer is 42." ,"Let me think about this..." )) 3273 .run (); 3274 3275tst .test ("</think>Hello, world!" ).reasoning_format (COMMON_REASONING_FORMAT_AUTO ).expect (simple_assist_msg ("Hello, world!" )).run (); 3276 3277// Continuation tests 3278tst .test ("world!\nWhat's up?" ) 3279 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3280 .enable_thinking (true) 3281 .messages ({message_user ,message_assist_prefill_content }) 3282 .add_generation_prompt (false) 3283 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3284 .expect_reasoning ("I'm thinking" ) 3285 .expect_content ("Hello, world!\nWhat's up?" ) 3286 .run (); 3287 3288tst .test (" thinking\n</think>\nHello, world!\nWhat's up?" ) 3289 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 3290 .enable_thinking (true) 3291 .messages ({message_user ,message_assist_prefill_reasoning }) 3292 .add_generation_prompt (false) 3293 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 3294 .expect_reasoning ("I'm thinking" ) 3295 .expect_content ("Hello, world!\nWhat's up?" ) 3296 .run (); 3297 } 3298 { 3299// NousResearch-Hermes-2-Pro and Hermes-3 (tool calling models) 3300auto tst = peg_tester ("models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja" ,detailed_debug ); 3301 3302tst .test ( 3303"<tool_call>\n" 3304"{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" 3305"</tool_call>" ) 3306 .tools ({special_function_tool }) 3307 .expect (message_assist_call ) 3308 .run (); 3309 3310tst .test ( 3311"Hello, world!\nWhat's up?<tool_call>\n" 3312"{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" 3313"</tool_call>" ) 3314 .tools ({special_function_tool }) 3315 .expect (message_assist_call_content ) 3316 .run (); 3317 3318// Note: Hermes template doesn't support thinking/reasoning natively 3319// Note: We only support one tool calling format per template, no alternate formats 3320 3321// Continuation tests 3322tst .test ("world!\nWhat's up?" ) 3323 .messages ({message_user ,message_assist_prefill_content }) 3324 .add_generation_prompt (false) 3325 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3326 .expect_content ("Hello, world!\nWhat's up?" ) 3327 .run (); 3328 } 3329 { 3330// Test simple content-only template 3331auto tst = peg_tester ("models/templates/google-gemma-2-2b-it.jinja" ,detailed_debug ); 3332 3333tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 3334 } 3335 { 3336// IBM Granite (reasoning and tool calling model) 3337auto tst = peg_tester ("models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja" ,detailed_debug ); 3338 3339tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 3340 3341tst .test ("<think>I'm\nthinking</think>Hello, world!\nWhat's up?" ) 3342 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3343 .expect (message_assist_thoughts ) 3344 .run (); 3345 3346// TODO: pending support for WRAPPED_WITH_REASONING 3347// tst.test("<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>") 3348// .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK) 3349// .expect(message_assist_thoughts) 3350// .run(); 3351 3352// Continuation tests 3353tst .test ("world!\nWhat's up?" ) 3354 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3355 .enable_thinking (true) 3356 .messages ({message_user ,message_assist_prefill_content }) 3357 .add_generation_prompt (false) 3358 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3359 .expect_reasoning ("I'm thinking" ) 3360 .expect_content ("Hello, world!\nWhat's up?" ) 3361 .run (); 3362 3363tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 3364 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3365 .enable_thinking (true) 3366 .messages ({message_user ,message_assist_prefill_reasoning }) 3367 .add_generation_prompt (false) 3368 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 3369 .expect_reasoning ("I'm thinking" ) 3370 .expect_content ("Hello, world!\nWhat's up?" ) 3371 .run (); 3372 } 3373 3374 { 3375// IBM Granite 4.0 (production template shared by h-tiny, h-small, micro) 3376// Uses <tool_call> XML tags for tool calls, tools in system message 3377auto tst = peg_tester ("models/templates/ibm-granite-granite-4.0.jinja" ,detailed_debug ); 3378 3379tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 3380 3381tst .test ( 3382"<tool_call>\n" 3383"{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n" 3384"</tool_call>" ) 3385 .tools ({special_function_tool }) 3386 .expect (message_assist_call ) 3387 .run (); 3388 3389// Continuation tests 3390tst .test ("world!\nWhat's up?" ) 3391 .messages ({message_user ,message_assist_prefill_content }) 3392 .add_generation_prompt (false) 3393 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3394 .expect_content ("Hello, world!\nWhat's up?" ) 3395 .run (); 3396 } 3397 3398 { 3399// IBM Granite 4.1 (same format as 4.0) 3400auto tst = peg_tester ("models/templates/ibm-granite-granite-4.1.jinja" ,detailed_debug ); 3401 3402tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 3403 3404tst .test ( 3405"<tool_call>\n" 3406"{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n" 3407"</tool_call>" ) 3408 .tools ({special_function_tool }) 3409 .expect (message_assist_call ) 3410 .run (); 3411 } 3412 3413 { 3414// ByteDance-Seed-OSS (reasoning and tool calling model) 3415auto tst = peg_tester ("models/templates/ByteDance-Seed-OSS.jinja" ,detailed_debug ); 3416 3417tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 3418 3419tst .test ("<seed:think>I'm thinking about the answer</seed:think>\nHello, world!" ) 3420 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3421 .expect (simple_assist_msg ("Hello, world!" ,"I'm thinking about the answer" )) 3422 .run (); 3423 3424tst .test ( 3425"<seed:tool_call>\n" 3426"<function=special_function>\n" 3427"<parameter=arg1>1</parameter>\n" 3428"</function>\n" 3429"</seed:tool_call>" ) 3430 .tools ({special_function_tool }) 3431 .expect (message_assist_call ) 3432 .run (); 3433 3434tst .test ( 3435"<seed:tool_call>\n" 3436"<function=special_function>\n" 3437"<parameter=arg1>1</parameter>\n" 3438"</function>\n" 3439"</seed:tool_call>\n" 3440"<seed:tool_call>\n" 3441"<function=special_function_with_opt>\n" 3442"<parameter=arg1>1</parameter>\n" 3443"<parameter=arg2>2</parameter>\n" 3444"</function>\n" 3445"</seed:tool_call>" ) 3446 .parallel_tool_calls (true) 3447 .tools ({ 3448special_function_tool ,special_function_tool_with_optional_param 3449 }) 3450 .expect_tool_calls ({ 3451 {"special_function" ,R"({"arg1": 1})" , {} }, 3452 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 3453 }) 3454 .run (); 3455 3456tst .test ( 3457"<seed:tool_call>\n" 3458"<function=todo_list>\n" 3459"<parameter=todos>[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]</parameter>\n" 3460"</function>\n" 3461"</seed:tool_call>" ) 3462 .tools ({ 3463todo_list 3464 }) 3465 .expect_tool_calls ({ 3466 {"todo_list" ,"{\"todos\": [{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]}" , {} }, 3467 }) 3468 .run (); 3469 3470// tool call with inside quotes 3471tst .test ( 3472"<seed:tool_call>\n" 3473"<function=edit>\n" 3474"<parameter=filename>" 3475"foo.cpp" 3476"</parameter>\n" 3477"<parameter=oldString>" 3478"def foo(arg = \"14\"):\n" 3479" return arg + \"bar\"\n" 3480"</parameter>\n" 3481"<parameter=newString>" 3482"def foo(arg = \"15\"):\n" 3483" pass\n" 3484"</parameter>\n" 3485"</function>\n" 3486"</seed:tool_call>" ) 3487 .tools ({ 3488edit_tool 3489 }) 3490 .expect_tool_calls ({ 3491 {"edit" ,"{\"filename\": \"foo.cpp\", " 3492"\"oldString\": \"def foo(arg = \\\"14\\\"):\\n return arg + \\\"bar\\\"\\n\", " 3493"\"newString\": \"def foo(arg = \\\"15\\\"):\\n pass\\n\"}" , {} 3494 } 3495 }) 3496 .run (); 3497 3498// Continuation tests 3499tst .test ("world!\nWhat's up?" ) 3500 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3501 .enable_thinking (true) 3502 .messages ({message_user ,message_assist_prefill_content }) 3503 .add_generation_prompt (false) 3504 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3505 .expect_reasoning ("I'm thinking" ) 3506 .expect_content ("Hello, world!\nWhat's up?" ) 3507 .run (); 3508 3509tst .test (" thinking</seed:think>\nHello, world!\nWhat's up?" ) 3510 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3511 .enable_thinking (true) 3512 .messages ({message_user ,message_assist_prefill_reasoning }) 3513 .add_generation_prompt (false) 3514 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 3515 .expect_reasoning ("I'm thinking" ) 3516 .expect_content ("Hello, world!\nWhat's up?" ) 3517 .run (); 3518 } 3519 3520 { 3521// Qwen3-Coder (tool calling with XML-style format) 3522auto tst = peg_tester ("models/templates/Qwen3-Coder.jinja" ,detailed_debug ); 3523 3524tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 3525 3526tst .test ( 3527"<tool_call>\n" 3528"<function=special_function>\n" 3529"<parameter=arg1>\n" 3530"1\n" 3531"</parameter>\n" 3532"</function>\n" 3533"</tool_call>" ) 3534 .tools ({special_function_tool }) 3535 .expect (message_assist_call ) 3536 .expect_reconstruction () 3537 .run (); 3538 3539// Some models skip the opening <tool_call> and go straight to <function=> 3540tst .test ( 3541"<function=special_function>\n" 3542"<parameter=arg1>\n" 3543"1\n" 3544"</parameter>\n" 3545"</function>\n" 3546"</tool_call>" ) 3547 .tools ({special_function_tool }) 3548 .expect (message_assist_call ) 3549 .run (); 3550 3551tst .test ( 3552"Let me call it.\n" 3553"<function=special_function>\n" 3554"<parameter=arg1>\n" 3555"1\n" 3556"</parameter>\n" 3557"</function>\n" 3558"</tool_call>" ) 3559 .tools ({special_function_tool }) 3560 .expect_content ("Let me call it.\n" ) 3561 .expect_tool_calls ({ 3562 {"special_function" ,R"({"arg1": 1})" , {} }, 3563 }) 3564 .run (); 3565 3566// Only the first call may omit it, the rest keep the </tool_call>\n<tool_call> separator 3567tst .test ( 3568"<function=special_function>\n" 3569"<parameter=arg1>\n" 3570"1\n" 3571"</parameter>\n" 3572"</function>\n" 3573"</tool_call>\n" 3574"<tool_call>\n" 3575"<function=special_function_with_opt>\n" 3576"<parameter=arg1>\n" 3577"1\n" 3578"</parameter>\n" 3579"<parameter=arg2>\n" 3580"2\n" 3581"</parameter>\n" 3582"</function>\n" 3583"</tool_call>" ) 3584 .parallel_tool_calls (true) 3585 .tools ({ 3586special_function_tool ,special_function_tool_with_optional_param 3587 }) 3588 .expect_tool_calls ({ 3589 {"special_function" ,R"({"arg1": 1})" , {} }, 3590 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 3591 }) 3592 .run (); 3593 3594tst .test ( 3595"<tool_call>\n" 3596"<function=special_function>\n" 3597"<parameter=arg1>\n" 3598"1\n" 3599"</parameter>\n" 3600"</function>\n" 3601"</tool_call>\n" 3602"<tool_call>\n" 3603"<function=special_function_with_opt>\n" 3604"<parameter=arg1>\n" 3605"1\n" 3606"</parameter>\n" 3607"<parameter=arg2>\n" 3608"2\n" 3609"</parameter>\n" 3610"</function>\n" 3611"</tool_call>" ) 3612 .parallel_tool_calls (true) 3613 .tools ({ 3614special_function_tool ,special_function_tool_with_optional_param 3615 }) 3616 .expect_tool_calls ({ 3617 {"special_function" ,R"({"arg1": 1})" , {} }, 3618 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 3619 }) 3620 .expect_reconstruction () 3621 .run (); 3622 3623// Test with code content (multiline) 3624tst .test ( 3625"<tool_call>\n" 3626"<function=python>\n" 3627"<parameter=code>\n" 3628"def hello():\n" 3629" print(\"Hello, world!\")\n" 3630"\n" 3631"hello()\n" 3632"</parameter>\n" 3633"</function>\n" 3634"</tool_call>" ) 3635 .tools ({ 3636python_tool 3637 }) 3638 .expect_tool_calls ({ 3639 {"python" ,"{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}" , {} }, 3640 }) 3641 .expect_reconstruction () 3642 .run (); 3643 3644// Test with code content (asian unicode chars) 3645tst .test ( 3646"<tool_call>\n" 3647"<function=python>\n" 3648"<parameter=code>\n" 3649"格\n" 3650"</parameter>\n" 3651"</function>\n" 3652"</tool_call>" ) 3653 .tools ({ 3654python_tool 3655 }) 3656 .expect_tool_calls ({ 3657 {"python" ,"{\"code\": \"格\"}" , {} }, 3658 }) 3659 .expect_reconstruction () 3660 .run (); 3661 3662// Test with HTML tag content 3663tst .test ( 3664"<tool_call>\n" 3665"<function=html>\n" 3666"<parameter=markup>\n" 3667"<html>\n" 3668" <head>\n" 3669" <title>Hello!</title>\n" 3670" </head>\n" 3671"</html>\n" 3672"</parameter>\n" 3673"</function>\n" 3674"</tool_call>" ) 3675 .tools ({ 3676html_tool 3677 }) 3678 .expect_tool_calls ({ 3679 {"html" ,"{\"markup\": \"<html>\\n <head>\\n <title>Hello!</title>\\n </head>\\n</html>\"}" , {} }, 3680 }) 3681 .expect_reconstruction () 3682 .run (); 3683 3684// Test with TODO list (array of objects) 3685tst .test ( 3686"<tool_call>\n" 3687"<function=todo_list>\n" 3688"<parameter=todos>\n" 3689"[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]\n" 3690"</parameter>\n" 3691"</function>\n" 3692"</tool_call>" ) 3693 .tools ({ 3694todo_list 3695 }) 3696 .expect_tool_calls ({ 3697 {"todo_list" ,"{\"todos\": [{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]}" , {} }, 3698 }) 3699 .expect_reconstruction () 3700 .run (); 3701 3702// Test flexible required argument ordering (required args still come first, in any order) 3703tst .test ( 3704"<tool_call>\n" 3705"<function=edit>\n" 3706"<parameter=newString>\n#include\n</parameter>\n" 3707"<parameter=filename>\nfoo.c\n</parameter>\n" 3708"<parameter=oldString>\n#iclunde\n</parameter>\n" 3709"</function>\n" 3710"</tool_call>" ) 3711 .tools ({edit_tool }) 3712 .expect_tool_calls ({ 3713 {"edit" ,R"({"newString": "#include", "filename": "foo.c", "oldString": "#iclunde"})" , {} }, 3714 }) 3715 .expect_reconstruction () 3716 .run (); 3717 3718tst .test ( 3719"<tool_call>\n" 3720"<function=tool_2req_4opt>\n" 3721"<parameter=req2>\n42\n</parameter>\n" 3722"<parameter=req1>\nhello\n</parameter>\n" 3723"<parameter=opt2>\n200\n</parameter>\n" 3724"</function>\n" 3725"</tool_call>" ) 3726 .tools ({tool_2req_4opt }) 3727 .expect_tool_calls ({ 3728 {"tool_2req_4opt" ,R"({"req2": 42, "req1": "hello", "opt2": 200})" , {} }, 3729 }) 3730 .expect_reconstruction () 3731 .run (); 3732 3733// Test flexible optional argument ordering (2 required + 4 optional, reversed optional order) 3734tst .test ( 3735"<tool_call>\n" 3736"<function=tool_2req_4opt>\n" 3737"<parameter=req1>\nhello\n</parameter>\n" 3738"<parameter=req2>\n42\n</parameter>\n" 3739"<parameter=opt4>\n100\n</parameter>\n" 3740"<parameter=opt2>\n200\n</parameter>\n" 3741"</function>\n" 3742"</tool_call>" ) 3743 .tools ({tool_2req_4opt }) 3744 .expect_tool_calls ({ 3745 {"tool_2req_4opt" ,R"({"req1": "hello", "req2": 42, "opt4": 100, "opt2": 200})" , {} }, 3746 }) 3747 .expect_reconstruction () 3748 .run (); 3749 3750// Test flexible optional argument ordering (2 required + 5 optional, reversed optional order) 3751tst .test ( 3752"<tool_call>\n" 3753"<function=tool_2req_5opt>\n" 3754"<parameter=req1>\nworld\n</parameter>\n" 3755"<parameter=req2>\n7\n</parameter>\n" 3756"<parameter=opt5>\nlast\n</parameter>\n" 3757"<parameter=opt3>\nmiddle\n</parameter>\n" 3758"<parameter=opt1>\nfirst\n</parameter>\n" 3759"</function>\n" 3760"</tool_call>" ) 3761 .tools ({tool_2req_5opt }) 3762 .expect_tool_calls ({ 3763 {"tool_2req_5opt" ,R"({"req1": "world", "req2": 7, "opt5": "last", "opt3": "middle", "opt1": "first"})" , {} }, 3764 }) 3765 .expect_reconstruction () 3766 .run (); 3767 3768// Test flexible optional argument ordering (2 required + 5 optional, all 5 in shuffled order) 3769tst .test ( 3770"<tool_call>\n" 3771"<function=tool_2req_5opt>\n" 3772"<parameter=req1>\ntest\n</parameter>\n" 3773"<parameter=req2>\n99\n</parameter>\n" 3774"<parameter=opt3>\nc\n</parameter>\n" 3775"<parameter=opt1>\na\n</parameter>\n" 3776"<parameter=opt5>\ne\n</parameter>\n" 3777"<parameter=opt4>\n4\n</parameter>\n" 3778"<parameter=opt2>\n2\n</parameter>\n" 3779"</function>\n" 3780"</tool_call>" ) 3781 .tools ({tool_2req_5opt }) 3782 .expect_tool_calls ({ 3783 {"tool_2req_5opt" ,R"({"req1": "test", "req2": 99, "opt3": "c", "opt1": "a", "opt5": "e", "opt4": 4, "opt2": 2})" , {} }, 3784 }) 3785 .expect_reconstruction () 3786 .run (); 3787 3788// nullable string type ["string", "null"] 3789tst .test ( 3790"<tool_call>\n" 3791"<function=set_nullable_str>\n" 3792"<parameter=name>\nhello world\n</parameter>\n" 3793"</function>\n" 3794"</tool_call>" ) 3795 .tools ({nullable_string_tool }) 3796 .expect_tool_calls ({ 3797 {"set_nullable_str" ,R"({"name": "hello world"})" , {} }, 3798 }) 3799 .run (); 3800 3801// nullable string with null first in type array ["null", "string"] 3802tst .test ( 3803"<tool_call>\n" 3804"<function=set_nullable_str_nf>\n" 3805"<parameter=name>\nhello world\n</parameter>\n" 3806"</function>\n" 3807"</tool_call>" ) 3808 .tools ({nullable_string_null_first_tool }) 3809 .expect_tool_calls ({ 3810 {"set_nullable_str_nf" ,R"({"name": "hello world"})" , {} }, 3811 }) 3812 .run (); 3813 3814// nullable integer type ["integer", "null"] - should use JSON value path, not string 3815tst .test ( 3816"<tool_call>\n" 3817"<function=set_nullable_int>\n" 3818"<parameter=count>\n42\n</parameter>\n" 3819"</function>\n" 3820"</tool_call>" ) 3821 .tools ({nullable_int_tool }) 3822 .expect_tool_calls ({ 3823 {"set_nullable_int" ,R"({"count": 42})" , {} }, 3824 }) 3825 .run (); 3826 3827// nullable string given null - parses as JSON null, not the string "null" 3828tst .test ( 3829"<tool_call>\n" 3830"<function=set_nullable_str>\n" 3831"<parameter=name>\nnull\n</parameter>\n" 3832"</function>\n" 3833"</tool_call>" ) 3834 .tools ({nullable_string_tool }) 3835 .expect_tool_calls ({ 3836 {"set_nullable_str" ,R"({"name": null})" , {} }, 3837 }) 3838 .run (); 3839 3840// unions with string - JSON values of the other types are typed, everything else is a string 3841tst .test ( 3842"<tool_call>\n" 3843"<function=set_union>\n" 3844"<parameter=value>\n{\"a\": 1}\n</parameter>\n" 3845"<parameter=amount>\n2 dollars\n</parameter>\n" 3846"</function>\n" 3847"</tool_call>" ) 3848 .tools ({string_union_tool }) 3849 .expect_tool_calls ({ 3850 {"set_union" ,R"({"value": {"a": 1}, "amount": "2 dollars"})" , {} }, 3851 }) 3852 .run (); 3853 3854tst .test ( 3855"<tool_call>\n" 3856"<function=set_union>\n" 3857"<parameter=value>\n{not valid json\n</parameter>\n" 3858"<parameter=amount>\n42\n</parameter>\n" 3859"</function>\n" 3860"</tool_call>" ) 3861 .tools ({string_union_tool }) 3862 .expect_tool_calls ({ 3863 {"set_union" ,R"({"value": "{not valid json", "amount": 42})" , {} }, 3864 }) 3865 .run (); 3866 3867// enum without explicit type key - should infer string from enum values 3868tst .test ( 3869"<tool_call>\n" 3870"<function=set_unit>\n" 3871"<parameter=unit>\ncelsius\n</parameter>\n" 3872"</function>\n" 3873"</tool_call>" ) 3874 .tools ({enum_no_type_tool }) 3875 .expect_tool_calls ({ 3876 {"set_unit" ,R"({"unit": "celsius"})" , {} }, 3877 }) 3878 .run (); 3879 3880// Continuation tests 3881tst .test ("world!\nWhat's up?" ) 3882 .messages ({message_user ,message_assist_prefill_content }) 3883 .add_generation_prompt (false) 3884 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3885 .expect_content ("Hello, world!\nWhat's up?" ) 3886 .run (); 3887 } 3888 { 3889auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.1.jinja" ,detailed_debug ); 3890tst .test ( 3891"<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": " 3892"\"XYZCITY\"}<|tool▁call▁end|><|tool▁calls▁end|>" ) 3893 .tools ({get_time_tool }) 3894 .enable_thinking (false) 3895 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3896 .expect (message_with_tool_calls ("get_time" ,"{\"city\":\"XYZCITY\"}" )) 3897 .run (); 3898 3899// Continuation tests 3900tst .test ("world!\nWhat's up?" ) 3901 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3902 .enable_thinking (true) 3903 .messages ({message_user ,message_assist_prefill_content }) 3904 .add_generation_prompt (false) 3905 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 3906 .expect_reasoning ("I'm thinking" ) 3907 .expect_content ("Hello, world!\nWhat's up?" ) 3908 .run (); 3909 3910tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 3911 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3912 .enable_thinking (true) 3913 .messages ({message_user ,message_assist_prefill_reasoning }) 3914 .add_generation_prompt (false) 3915 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 3916 .expect_reasoning ("I'm thinking" ) 3917 .expect_content ("Hello, world!\nWhat's up?" ) 3918 .run (); 3919 } 3920 3921 { 3922auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.1.jinja" ,detailed_debug ); 3923tst .test ( 3924"REASONING</think><|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": " 3925"\"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>" ) 3926 .enable_thinking (true) 3927 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3928 .tools ({get_time_tool }) 3929 .expect (message_with_tool_calls_and_reasoning ("get_time" ,"{\"city\":\"Tokyo\"}" ,"REASONING" )) 3930 .run (); 3931 } 3932 3933 { 3934auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.1.jinja" ,detailed_debug ); 3935tst .test ( 3936"REASONING</think>CONTENT<|tool▁calls▁begin|><|tool▁call▁begin|>get_time<|tool▁sep|>{\"city\": " 3937"\"Paris\"}<|tool▁call▁end|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"city\": " 3938"\"Paris\"}<|tool▁call▁end|><|tool▁calls▁end|>" ) 3939 .tools ({ 3940get_time_tool ,get_weather_tool 3941 }) 3942 .enable_thinking (true) 3943 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3944 .parallel_tool_calls (true) 3945 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 3946"REASONING" ,"CONTENT" , 3947 { {"get_time" ,"{\"city\":\"Paris\"}" }, {"get_weather" ,"{\"city\":\"Paris\"}" } })) 3948 .run (); 3949 } 3950 3951 { 3952auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.1.jinja" ,detailed_debug ); 3953tst .test ("REASONING</think>\nCONTENT" ) 3954 .enable_thinking (true) 3955 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3956 .expect (simple_assist_msg ("CONTENT" ,"REASONING\n" )) 3957 .run (); 3958 } 3959 3960 { 3961auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.1.jinja" ,detailed_debug ); 3962tst .test ("CONTENT" ).enable_thinking (false).reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ). 3963expect (simple_assist_msg ("CONTENT" ,"" )).run (); 3964 } 3965 3966// DeepSeek V3.2 tests - format uses DSML markup: 3967// <|DSML|function_calls> 3968// <|DSML|invoke name="foo"> 3969// <|DSML|parameter name="bar" string="true|false">value</|DSML|parameter> 3970// </|DSML|invoke> 3971// </|DSML|function_calls> 3972// Reasoning uses <think>...</think>. The generation prompt ends in <think> (thinking mode) 3973// or <think></think> (non-thinking mode). 3974 { 3975auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V3.2.jinja" ,detailed_debug ); 3976 3977// Pure content (non-thinking mode) 3978tst .test ("Hello, world!\nWhat's up?" ) 3979 .enable_thinking (false) 3980 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3981 .expect (message_assist ) 3982 .run (); 3983 3984// Thinking + content 3985tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 3986 .enable_thinking (true) 3987 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 3988 .expect (message_assist_thoughts ) 3989 .run (); 3990 3991// Thinking + tool call (single, string param) 3992tst .test ( 3993"Let me check the time</think>\n\n" 3994"<|DSML|function_calls>\n" 3995"<|DSML|invoke name=\"get_time\">\n" 3996"<|DSML|parameter name=\"city\" string=\"true\">Tokyo</|DSML|parameter>\n" 3997"</|DSML|invoke>\n" 3998"</|DSML|function_calls>" ) 3999 .enable_thinking (true) 4000 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4001 .tools ({get_time_tool }) 4002 .expect (message_with_tool_calls_and_reasoning ("get_time" ,R"({"city": "Tokyo"})" ,"Let me check the time" )) 4003 .run (); 4004 4005// Tool call without reasoning (non-thinking mode), integer param (string="false") 4006tst .test ( 4007"<|DSML|function_calls>\n" 4008"<|DSML|invoke name=\"special_function\">\n" 4009"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n" 4010"</|DSML|invoke>\n" 4011"</|DSML|function_calls>" ) 4012 .enable_thinking (false) 4013 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4014 .tools ({special_function_tool }) 4015 .expect (message_assist_call ) 4016 .run (); 4017 4018// Multiple parallel tool calls with reasoning 4019tst .test ( 4020"Calling both</think>\n\n" 4021"<|DSML|function_calls>\n" 4022"<|DSML|invoke name=\"get_time\">\n" 4023"<|DSML|parameter name=\"city\" string=\"true\">Paris</|DSML|parameter>\n" 4024"</|DSML|invoke>\n" 4025"<|DSML|invoke name=\"get_weather\">\n" 4026"<|DSML|parameter name=\"city\" string=\"true\">Paris</|DSML|parameter>\n" 4027"</|DSML|invoke>\n" 4028"</|DSML|function_calls>" ) 4029 .enable_thinking (true) 4030 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4031 .parallel_tool_calls (true) 4032 .tools ({get_time_tool ,get_weather_tool }) 4033 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 4034"Calling both" ,"" , 4035 { {"get_time" ,R"({"city": "Paris"})" }, {"get_weather" ,R"({"city": "Paris"})" } })) 4036 .run (); 4037 4038// Tool call with content before tool calls 4039tst .test ( 4040"Thinking about it</think>" 4041"Let me call the function.\n\n" 4042"<|DSML|function_calls>\n" 4043"<|DSML|invoke name=\"special_function\">\n" 4044"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n" 4045"</|DSML|invoke>\n" 4046"</|DSML|function_calls>" ) 4047 .enable_thinking (true) 4048 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4049 .tools ({special_function_tool }) 4050 .expect_reasoning ("Thinking about it" ) 4051 .expect_content ("Let me call the function." ) 4052 .expect_tool_calls ({ 4053 {"special_function" ,R"({"arg1": 1})" , {} }, 4054 }) 4055 .expect_reconstruction () 4056 .run (); 4057 4058// Tool call with negative number 4059tst .test ( 4060"Test negative</think>\n\n" 4061"<|DSML|function_calls>\n" 4062"<|DSML|invoke name=\"magic_int\">\n" 4063"<|DSML|parameter name=\"ref\" string=\"false\">-14</|DSML|parameter>\n" 4064"</|DSML|invoke>\n" 4065"</|DSML|function_calls>" ) 4066 .enable_thinking (true) 4067 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4068 .tools ({magic_int_tool }) 4069 .expect_reasoning ("Test negative" ) 4070 .expect_tool_calls ({ 4071 {"magic_int" ,R"({"ref": -14})" , {} }, 4072 }) 4073 .run (); 4074 4075// Tool call with decimal number 4076tst .test ( 4077"Test decimal</think>\n\n" 4078"<|DSML|function_calls>\n" 4079"<|DSML|invoke name=\"amount\">\n" 4080"<|DSML|parameter name=\"orig\" string=\"false\">3.14</|DSML|parameter>\n" 4081"</|DSML|invoke>\n" 4082"</|DSML|function_calls>" ) 4083 .enable_thinking (true) 4084 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4085 .tools ({amount_tool }) 4086 .expect_reasoning ("Test decimal" ) 4087 .expect_tool_calls ({ 4088 {"amount" ,R"({"orig": 3.14})" , {} }, 4089 }) 4090 .run (); 4091 4092// Tool call with boolean 4093tst .test ( 4094"Test boolean</think>\n\n" 4095"<|DSML|function_calls>\n" 4096"<|DSML|invoke name=\"toggle\">\n" 4097"<|DSML|parameter name=\"enabled\" string=\"false\">true</|DSML|parameter>\n" 4098"</|DSML|invoke>\n" 4099"</|DSML|function_calls>" ) 4100 .enable_thinking (true) 4101 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4102 .tools ({toggle_tool }) 4103 .expect_reasoning ("Test boolean" ) 4104 .expect_tool_calls ({ 4105 {"toggle" ,R"({"enabled": true})" , {} }, 4106 }) 4107 .run (); 4108 4109// Tool call with array parameter (JSON-formatted) 4110tst .test ( 4111"Test array</think>\n\n" 4112"<|DSML|function_calls>\n" 4113"<|DSML|invoke name=\"todo_list\">\n" 4114"<|DSML|parameter name=\"todos\" string=\"false\">[\"buy milk\",\"walk dog\"]</|DSML|parameter>\n" 4115"</|DSML|invoke>\n" 4116"</|DSML|function_calls>" ) 4117 .enable_thinking (true) 4118 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4119 .tools ({todo_list }) 4120 .expect_reasoning ("Test array" ) 4121 .expect_tool_calls ({ 4122 {"todo_list" ,R"({"todos": ["buy milk", "walk dog"]})" , {} }, 4123 }) 4124 .run (); 4125 4126// Tool call with object parameter (JSON-formatted) 4127tst .test ( 4128"Test object</think>\n\n" 4129"<|DSML|function_calls>\n" 4130"<|DSML|invoke name=\"set_config\">\n" 4131"<|DSML|parameter name=\"config\" string=\"false\">{\"theme\":\"dark\",\"level\":2}</|DSML|parameter>\n" 4132"</|DSML|invoke>\n" 4133"</|DSML|function_calls>" ) 4134 .enable_thinking (true) 4135 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4136 .tools ({config_tool }) 4137 .expect_reasoning ("Test object" ) 4138 .expect_tool_calls ({ 4139 {"set_config" ,R"({"config": {"theme": "dark", "level": 2}})" , {} }, 4140 }) 4141 .run (); 4142 4143// Edge case: empty reasoning 4144tst .test ( 4145"</think>\n\n" 4146"<|DSML|function_calls>\n" 4147"<|DSML|invoke name=\"get_time\">\n" 4148"<|DSML|parameter name=\"city\" string=\"true\">XYZCITY</|DSML|parameter>\n" 4149"</|DSML|invoke>\n" 4150"</|DSML|function_calls>" ) 4151 .enable_thinking (true) 4152 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4153 .tools ({get_time_tool }) 4154 .expect (message_with_tool_calls ("get_time" ,R"({"city": "XYZCITY"})" )) 4155 .run (); 4156 4157// Edge case: tool call with multiple params (mixed types, string first) 4158tst .test ( 4159"Multi-arg call</think>\n\n" 4160"<|DSML|function_calls>\n" 4161"<|DSML|invoke name=\"magic_int\">\n" 4162"<|DSML|parameter name=\"ref\" string=\"false\">42</|DSML|parameter>\n" 4163"<|DSML|parameter name=\"name\" string=\"true\">foo bar</|DSML|parameter>\n" 4164"</|DSML|invoke>\n" 4165"</|DSML|function_calls>" ) 4166 .enable_thinking (true) 4167 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4168 .tools ({magic_int_tool }) 4169 .expect_reasoning ("Multi-arg call" ) 4170 .expect_tool_calls ({ 4171 {"magic_int" ,R"({"ref": 42, "name": "foo bar"})" , {} }, 4172 }) 4173 .run (); 4174 4175// Continuation tests 4176tst .test ("world!\nWhat's up?" ) 4177 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4178 .enable_thinking (true) 4179 .messages ({message_user ,message_assist_prefill_content }) 4180 .add_generation_prompt (false) 4181 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 4182 .expect_reasoning ("I'm thinking" ) 4183 .expect_content ("Hello, world!\nWhat's up?" ) 4184 .run (); 4185 4186tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 4187 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4188 .enable_thinking (true) 4189 .messages ({message_user ,message_assist_prefill_reasoning }) 4190 .add_generation_prompt (false) 4191 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 4192 .expect_reasoning ("I'm thinking" ) 4193 .expect_content ("Hello, world!\nWhat's up?" ) 4194 .run (); 4195 } 4196 4197// DeepSeek V4 tests - same DSML markup as V3.2, but the tool call block is named 4198// "tool_calls" and the non-thinking generation prompt ends in a bare </think> 4199// instead of an empty <think></think> pair. 4200 { 4201auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V4.jinja" ,detailed_debug ); 4202 4203// Pure content (non-thinking mode; generation prompt ends with </think>) 4204tst .test ("Hello, world!\nWhat's up?" ) 4205 .enable_thinking (false) 4206 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4207 .expect (message_assist ) 4208 .run (); 4209 4210// Thinking + content 4211tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 4212 .enable_thinking (true) 4213 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4214 .expect (message_assist_thoughts ) 4215 .run (); 4216 4217// Thinking + tool call (single, string param) 4218tst .test ( 4219"Let me check the time</think>\n\n" 4220"<|DSML|tool_calls>\n" 4221"<|DSML|invoke name=\"get_time\">\n" 4222"<|DSML|parameter name=\"city\" string=\"true\">Tokyo</|DSML|parameter>\n" 4223"</|DSML|invoke>\n" 4224"</|DSML|tool_calls>" ) 4225 .enable_thinking (true) 4226 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4227 .tools ({get_time_tool }) 4228 .expect (message_with_tool_calls_and_reasoning ("get_time" ,R"({"city": "Tokyo"})" ,"Let me check the time" )) 4229 .run (); 4230 4231// Tool call without reasoning (non-thinking mode), integer param (string="false") 4232tst .test ( 4233"<|DSML|tool_calls>\n" 4234"<|DSML|invoke name=\"special_function\">\n" 4235"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n" 4236"</|DSML|invoke>\n" 4237"</|DSML|tool_calls>" ) 4238 .enable_thinking (false) 4239 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4240 .tools ({special_function_tool }) 4241 .expect (message_assist_call ) 4242 .run (); 4243 4244// Multiple parallel tool calls with reasoning 4245tst .test ( 4246"Calling both</think>\n\n" 4247"<|DSML|tool_calls>\n" 4248"<|DSML|invoke name=\"get_time\">\n" 4249"<|DSML|parameter name=\"city\" string=\"true\">Paris</|DSML|parameter>\n" 4250"</|DSML|invoke>\n" 4251"<|DSML|invoke name=\"get_weather\">\n" 4252"<|DSML|parameter name=\"city\" string=\"true\">Paris</|DSML|parameter>\n" 4253"</|DSML|invoke>\n" 4254"</|DSML|tool_calls>" ) 4255 .enable_thinking (true) 4256 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4257 .parallel_tool_calls (true) 4258 .tools ({get_time_tool ,get_weather_tool }) 4259 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 4260"Calling both" ,"" , 4261 { {"get_time" ,R"({"city": "Paris"})" }, {"get_weather" ,R"({"city": "Paris"})" } })) 4262 .run (); 4263 4264// Tool call with content before tool calls 4265tst .test ( 4266"Thinking about it</think>" 4267"Let me call the function.\n\n" 4268"<|DSML|tool_calls>\n" 4269"<|DSML|invoke name=\"special_function\">\n" 4270"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n" 4271"</|DSML|invoke>\n" 4272"</|DSML|tool_calls>" ) 4273 .enable_thinking (true) 4274 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4275 .tools ({special_function_tool }) 4276 .expect_reasoning ("Thinking about it" ) 4277 .expect_content ("Let me call the function." ) 4278 .expect_tool_calls ({ 4279 {"special_function" ,R"({"arg1": 1})" , {} }, 4280 }) 4281 .expect_reconstruction () 4282 .run (); 4283 4284// Tool call with multiple params (mixed types) 4285tst .test ( 4286"Multi-arg call</think>\n\n" 4287"<|DSML|tool_calls>\n" 4288"<|DSML|invoke name=\"magic_int\">\n" 4289"<|DSML|parameter name=\"ref\" string=\"false\">42</|DSML|parameter>\n" 4290"<|DSML|parameter name=\"name\" string=\"true\">foo bar</|DSML|parameter>\n" 4291"</|DSML|invoke>\n" 4292"</|DSML|tool_calls>" ) 4293 .enable_thinking (true) 4294 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4295 .tools ({magic_int_tool }) 4296 .expect_reasoning ("Multi-arg call" ) 4297 .expect_tool_calls ({ 4298 {"magic_int" ,R"({"ref": 42, "name": "foo bar"})" , {} }, 4299 }) 4300 .run (); 4301 4302// Continuation tests 4303tst .test ("world!\nWhat's up?" ) 4304 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4305 .enable_thinking (true) 4306 .messages ({message_user ,message_assist_prefill_content }) 4307 .add_generation_prompt (false) 4308 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 4309 .expect_reasoning ("I'm thinking" ) 4310 .expect_content ("Hello, world!\nWhat's up?" ) 4311 .run (); 4312 4313tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 4314 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4315 .enable_thinking (true) 4316 .messages ({message_user ,message_assist_prefill_reasoning }) 4317 .add_generation_prompt (false) 4318 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 4319 .expect_reasoning ("I'm thinking" ) 4320 .expect_content ("Hello, world!\nWhat's up?" ) 4321 .run (); 4322 4323tst .test ( 4324"Let me check the time\n\n" 4325"<|DSML|tool_calls>\n" 4326"<|DSML|invoke name=\"get_time\">\n" 4327"<|DSML|parameter name=\"city\" string=\"true\">Tokyo</|DSML|parameter>\n" 4328"</|DSML|invoke>\n" 4329"</|DSML|tool_calls>" )// no </think> after the TC close because the grammar will immediately constrain it to end 4330 .enable_thinking (true) 4331 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4332 .tools ({get_time_tool }) 4333 .expect_reasoning ("Let me check the time" ) 4334 .expect_tool_calls ({ {"get_time" ,R"({"city": "Tokyo"})" , {} } }) 4335 .run (); 4336 } 4337 4338 { 4339// The DSML separator belongs to the tool call block, not assistant content. 4340auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja" ,detailed_debug ); 4341tst .test ( 4342"\n\n" 4343"<|DSML|tool_calls>\n" 4344"<|DSML|invoke name=\"special_function\">\n" 4345"<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n" 4346"</|DSML|invoke>\n" 4347"</|DSML|tool_calls>" ) 4348 .enable_thinking (false) 4349 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4350 .tools ({special_function_tool }) 4351 .expect (message_assist_call ) 4352 .expect_reconstruction () 4353 .run (); 4354 } 4355 4356// GLM-4.6 tests - format: <tool_call>function_name\n<arg_key>...</arg_key>\n<arg_value>...</arg_value>\n</tool_call> 4357 { 4358auto tst = peg_tester ("models/templates/GLM-4.6.jinja" ,detailed_debug ); 4359tst .test ( 4360"<tool_call>special_function\n" 4361"<arg_key>arg1</arg_key>\n<arg_value>1</arg_value>\n" 4362"</tool_call>" ) 4363 .tools ({special_function_tool }) 4364 .expect (message_assist_call ) 4365 .run (); 4366 } 4367 4368// GLM-4.7-Flash tests - format: <tool_call>function_name<arg_key>...</arg_key><arg_value>...</arg_value></tool_call> 4369// Note: Template uses forced-open thinking mode (prompt ends with <think>) 4370 { 4371auto tst = peg_tester ("models/templates/GLM-4.7-Flash.jinja" ,detailed_debug ); 4372 4373// Pure content (no reasoning) 4374tst .test ("Hello, world!\nWhat's up?" ) 4375 .enable_thinking (false) 4376 .expect (message_assist ) 4377 .expect_reconstruction () 4378 .run (); 4379 4380// Reasoning with content (forced-open mode - input starts after <think>) 4381tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 4382 .enable_thinking (true) 4383 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4384 .expect (message_assist_thoughts ) 4385 .expect_reconstruction () 4386 .run (); 4387 4388// Tool call without reasoning 4389tst .test ( 4390"<tool_call>special_function" 4391"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4392"</tool_call>" ) 4393 .enable_thinking (false) 4394 .tools ({special_function_tool }) 4395 .expect (message_assist_call ) 4396 .expect_reconstruction () 4397 .run (); 4398 4399// Tool call with reasoning (forced-open mode) 4400tst .test ( 4401"I'm\nthinking</think>" 4402"<tool_call>special_function" 4403"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4404"</tool_call>" ) 4405 .enable_thinking (true) 4406 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4407 .tools ({special_function_tool }) 4408 .expect (message_assist_call_thoughts ) 4409 .expect_reconstruction () 4410 .run (); 4411 4412tst .test ( 4413"<tool_call>special_function" 4414"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4415"</tool_call>" 4416"<tool_call>special_function_with_opt" 4417"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4418"<arg_key>arg2</arg_key><arg_value>2</arg_value>" 4419"</tool_call>" ) 4420 .enable_thinking (false) 4421 .parallel_tool_calls (true) 4422 .tools ({ 4423special_function_tool ,special_function_tool_with_optional_param 4424 }) 4425 .expect_tool_calls ({ 4426 {"special_function" ,R"({"arg1": 1})" , {} }, 4427 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 4428 }) 4429 .expect_reconstruction () 4430 .run (); 4431 4432// #20650: tool with no required args, model emits <tool_call>name</tool_call> with no arg tags. 4433 { 4434static common_chat_tool no_args_tool { 4435"read_file_diff_md" ,"Reads a file diff" , 4436R"({"type":"object","properties":{"review_id":{"type":"string"},"file_id":{"type":"string"}}})" , 4437 }; 4438tst .test ( 4439"Let me read the diff content." 4440"</think>" 4441"<tool_call>read_file_diff_md</tool_call>" ) 4442 .enable_thinking (true) 4443 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4444 .tools ({no_args_tool }) 4445 .expect_reasoning ("Let me read the diff content." ) 4446 .expect_tool_calls ({{"read_file_diff_md" ,"{}" , {} }}) 4447 .expect_reconstruction () 4448 .run (); 4449 } 4450 4451// Continuation tests 4452tst .test ("world!\nWhat's up?" ) 4453 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4454 .enable_thinking (true) 4455 .messages ({message_user ,message_assist_prefill_content }) 4456 .add_generation_prompt (false) 4457 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 4458 .expect_reasoning ("I'm thinking" ) 4459 .expect_content ("Hello, world!\nWhat's up?" ) 4460 .run (); 4461 4462tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 4463 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4464 .enable_thinking (true) 4465 .messages ({message_user ,message_assist_prefill_reasoning }) 4466 .add_generation_prompt (false) 4467 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 4468 .expect_reasoning ("I'm thinking" ) 4469 .expect_content ("Hello, world!\nWhat's up?" ) 4470 .run (); 4471 } 4472 4473// Spark2.5 uses tagged arguments with forced-open thinking. 4474 { 4475auto tst = peg_tester ("models/templates/Spark2.5.jinja" ,detailed_debug ); 4476 4477tst .test ("Hello, world!\nWhat's up?" ) 4478 .enable_thinking (false) 4479 .expect (message_assist ) 4480 .expect_reconstruction () 4481 .run (); 4482 4483tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 4484 .enable_thinking (true) 4485 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4486 .expect (message_assist_thoughts ) 4487 .expect_reconstruction () 4488 .run (); 4489 4490tst .test ( 4491"<tool_call>special_function" 4492"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4493"</tool_call>" ) 4494 .enable_thinking (false) 4495 .tools ({special_function_tool }) 4496 .expect (message_assist_call ) 4497 .expect_reconstruction () 4498 .run (); 4499 4500tst .test ( 4501"I'm\nthinking</think>" 4502"<tool_call>special_function" 4503"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4504"</tool_call>" ) 4505 .enable_thinking (true) 4506 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 4507 .tools ({special_function_tool }) 4508 .expect (message_assist_call_thoughts ) 4509 .expect_reconstruction () 4510 .run (); 4511 4512tst .test ( 4513"<tool_call>special_function" 4514"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4515"</tool_call>" 4516"<tool_call>special_function_with_opt" 4517"<arg_key>arg1</arg_key><arg_value>1</arg_value>" 4518"<arg_key>arg2</arg_key><arg_value>2</arg_value>" 4519"</tool_call>" ) 4520 .enable_thinking (false) 4521 .parallel_tool_calls (true) 4522 .tools ({special_function_tool ,special_function_tool_with_optional_param }) 4523 .expect_tool_calls ({ 4524 {"special_function" ,R"({"arg1": 1})" , {} }, 4525 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 4526 }) 4527 .expect_reconstruction () 4528 .run (); 4529 4530tst .test ( 4531"Preparing updates." 4532"<tool_call>magic_int" 4533"<arg_key>ref</arg_key><arg_value>42</arg_value>" 4534"<arg_key>name</arg_key><arg_value>上海</arg_value>" 4535"</tool_call>" 4536"<tool_call>amount" 4537"<arg_key>orig</arg_key><arg_value>2.5</arg_value>" 4538"</tool_call>" 4539"<tool_call>toggle" 4540"<arg_key>enabled</arg_key><arg_value>true</arg_value>" 4541"</tool_call>" 4542"<tool_call>set_config" 4543"<arg_key>config</arg_key><arg_value>{\"source\": \"spark\", \"options\": {\"strict\": true}}</arg_value>" 4544"</tool_call>" 4545"<tool_call>nested_args" 4546"<arg_key>tags</arg_key><arg_value>[\"alpha\", \"测试\"]</arg_value>" 4547"<arg_key>entries</arg_key><arg_value>[{\"id\": 1, \"label\": \"first\"}, {\"id\": 2, \"label\": \"第二\"}]</arg_value>" 4548"</tool_call>" 4549"<tool_call>empty_args" 4550"</tool_call>" ) 4551 .enable_thinking (false) 4552 .parallel_tool_calls (true) 4553 .tools ({magic_int_tool ,amount_tool ,toggle_tool ,config_tool ,nested_args_tool ,empty_args_tool }) 4554 .expect_content ("Preparing updates." ) 4555 .expect_tool_calls ({ 4556 {"magic_int" ,R"({"ref": 42, "name": "上海"})" , {} }, 4557 {"amount" ,R"({"orig": 2.5})" , {} }, 4558 {"toggle" ,R"({"enabled": true})" , {} }, 4559 {"set_config" ,R"({"config": {"source": "spark", "options": {"strict": true}}})" , {} }, 4560 {"nested_args" ,R"({"tags": ["alpha", "测试"], "entries": [{"id": 1, "label": "first"}, {"id": 2, "label": "第二"}]})" , {} }, 4561 {"empty_args" ,"{}" , {} }, 4562 }) 4563 .expect_reconstruction () 4564 .run (); 4565 } 4566 4567// Verify the throw path produces a readable error message, not std::out_of_range. 4568// #20424 introduced effective_input = generation_prompt + input, but the throw 4569// uses input.substr(result.end) where result.end is in effective_input space. 4570 { 4571if (!g_template_filter .empty ()&& std::string ("models/templates/GLM-4.7-Flash.jinja" ).find (g_template_filter )!= std::string::npos ) { 4572auto tmpls = common_chat_templates_ptr ( 4573common_chat_templates_init (nullptr ,read_file ("models/templates/GLM-4.7-Flash.jinja" ))); 4574 4575static common_chat_tool weather_tool { 4576"get_weather" ,"Get weather" , 4577R"({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})" , 4578 }; 4579 4580common_chat_templates_inputs inputs ; 4581inputs .tools = {weather_tool }; 4582inputs .enable_thinking = true; 4583inputs .reasoning_format = COMMON_REASONING_FORMAT_AUTO ; 4584inputs .add_generation_prompt = true; 4585inputs .use_jinja = true; 4586common_chat_msg msg ; 4587msg .role = "user" ; 4588msg .content = "get_weather" ; 4589inputs .messages = {msg }; 4590 4591auto params = common_chat_templates_apply (tmpls .get (),inputs ); 4592common_peg_arena arena ; 4593arena .load (params .parser ); 4594common_chat_parser_params pp (params ); 4595 4596// generation_prompt is non-empty for thinking models, so result.end 4597// will be offset by generation_prompt.size() into effective_input space. 4598assert (!pp .generation_prompt .empty ()); 4599 4600 std::string bad_input = 4601"Thinking.\n" 4602"</think>" 4603"<tool_call>get_weather" 4604"<arg_key>city</arg_key><arg_value>Tokyo</arg_value>" 4605"</tool_call>\n" ; 4606 4607bool got_runtime_error = false; 4608bool got_out_of_range = false; 4609 std::string error_msg ; 4610try { 4611common_chat_peg_parse (arena ,bad_input ,/*is_partial=*/ false,pp ); 4612 }catch (const std::out_of_range & e ) { 4613got_out_of_range = true; 4614error_msg = e .what (); 4615 }catch (const std::runtime_error & e ) { 4616got_runtime_error = true; 4617error_msg = e .what (); 4618 } 4619GGML_ASSERT (!got_out_of_range && "throw path crashed with out_of_range (input.substr in effective_input space)" ); 4620GGML_ASSERT (got_runtime_error && "throw path should produce std::runtime_error with parse position" ); 4621 } 4622 } 4623 4624// Kimi-K3 tests - custom parser 4625// Unique feature: XTML tags built from <|open|>/<|close|>/<|sep|>, and a 4626// generation prompt that leaves the think section already open. 4627 { 4628auto tst = peg_tester ("models/templates/Kimi-K3.jinja" ,detailed_debug ); 4629 4630// Content only. The response section is explicit even with no reasoning. 4631tst .test ("<|open|>response<|sep|>Hello, world!\nWhat's up?<|close|>response<|sep|>" 4632"<|close|>message<|sep|>" ) 4633 .expect (message_assist ) 4634 .run (); 4635 4636// Reasoning with no opening tag - the generation prompt already opened it 4637tst .test ("I'm thinking about this<|close|>think<|sep|>" 4638"<|open|>response<|sep|>Hello, world!\nWhat's up?<|close|>response<|sep|>" 4639"<|close|>message<|sep|>" ) 4640 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4641 .expect (simple_assist_msg ("Hello, world!\nWhat's up?" ,"I'm thinking about this" )) 4642 .run (); 4643 4644// Prose that mentions the tag names must survive intact. 4645tst .test ("<|open|>response<|sep|>Use the response tag, then message the handler." 4646"<|close|>response<|sep|><|close|>message<|sep|>" ) 4647 .expect (simple_assist_msg ("Use the response tag, then message the handler." )) 4648 .run (); 4649 4650// Truncated mid-reasoning (hit the token budget): keep the reasoning. 4651tst .test ("I was still thinking when the budget ran out" ) 4652 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4653 .expect_reasoning ("I was still thinking when the budget ran out" ) 4654 .run (); 4655 4656// Single tool call, one argument. 4657tst .test ("<|open|>response<|sep|><|close|>response<|sep|>" 4658"<|open|>tools<|sep|>" 4659"<|open|>call tool=\"special_function\" index=\"1\"<|sep|>" 4660"<|open|>argument key=\"arg1\" type=\"number\"<|sep|>1<|close|>argument<|sep|>" 4661"<|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>" ) 4662 .tools ({special_function_tool }) 4663 .expect_tool_calls ({ 4664 {"special_function" ,R"({"arg1":1})" ,"" }, 4665 }) 4666 .run (); 4667 4668// Tool call preceded by reasoning (no opening think tag) and content. 4669tst .test ("I should call it<|close|>think<|sep|>" 4670"<|open|>response<|sep|>On it.<|close|>response<|sep|>" 4671"<|open|>tools<|sep|>" 4672"<|open|>call tool=\"special_function\" index=\"1\"<|sep|>" 4673"<|open|>argument key=\"arg1\" type=\"number\"<|sep|>1<|close|>argument<|sep|>" 4674"<|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>" ) 4675 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4676 .tools ({special_function_tool }) 4677 .expect (simple_assist_msg ("On it." ,"I should call it" ,"special_function" , 4678R"({"arg1":1})" ,"" )) 4679 .run (); 4680 4681// Multiple typed arguments: values must come back as JSON numbers, not strings 4682tst .test ("<|open|>response<|sep|><|close|>response<|sep|>" 4683"<|open|>tools<|sep|>" 4684"<|open|>call tool=\"special_function_with_opt\" index=\"1\"<|sep|>" 4685"<|open|>argument key=\"arg1\" type=\"number\"<|sep|>1<|close|>argument<|sep|>" 4686"<|open|>argument key=\"arg2\" type=\"number\"<|sep|>2<|close|>argument<|sep|>" 4687"<|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>" ) 4688 .tools ({special_function_tool_with_optional_param }) 4689 .expect_tool_calls ({ 4690 {"special_function_with_opt" ,R"({"arg1":1,"arg2":2})" ,"" }, 4691 }) 4692 .run (); 4693 4694// Parallel tool calls in one <|open|>tools<|sep|> section. 4695tst .test ("<|open|>response<|sep|><|close|>response<|sep|>" 4696"<|open|>tools<|sep|>" 4697"<|open|>call tool=\"special_function\" index=\"1\"<|sep|>" 4698"<|open|>argument key=\"arg1\" type=\"number\"<|sep|>1<|close|>argument<|sep|>" 4699"<|close|>call<|sep|>" 4700"<|open|>call tool=\"special_function_with_opt\" index=\"2\"<|sep|>" 4701"<|open|>argument key=\"arg1\" type=\"number\"<|sep|>1<|close|>argument<|sep|>" 4702"<|open|>argument key=\"arg2\" type=\"number\"<|sep|>2<|close|>argument<|sep|>" 4703"<|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>" ) 4704 .parallel_tool_calls (true) 4705 .tools ({special_function_tool ,special_function_tool_with_optional_param }) 4706 .expect_tool_calls ({ 4707 {"special_function" ,R"({"arg1":1})" ,"" }, 4708 {"special_function_with_opt" ,R"({"arg1":1,"arg2":2})" ,"" }, 4709 }) 4710 .run (); 4711 4712// String-typed argument keeps its literal text (no JSON coercion). 4713tst .test ("<|open|>response<|sep|><|close|>response<|sep|>" 4714"<|open|>tools<|sep|>" 4715"<|open|>call tool=\"python\" index=\"1\"<|sep|>" 4716"<|open|>argument key=\"code\" type=\"string\"<|sep|>print('hey')" 4717"<|close|>argument<|sep|>" 4718"<|close|>call<|sep|><|close|>tools<|sep|><|close|>message<|sep|>" ) 4719 .tools ({python_tool }) 4720 .expect_tool_calls ({ 4721// custom delimiter: the payload itself contains )" 4722 {"python" ,R"JSON({"code":"print('hey')"})JSON" ,"" }, 4723 }) 4724 .run (); 4725 } 4726 4727// Kimi-K2-Thinking tests - custom parser 4728// Unique feature: tool call ID embeds function name as functions.<name>:<counter> 4729 { 4730auto tst = peg_tester ("models/templates/Kimi-K2-Thinking.jinja" ,detailed_debug ); 4731 4732// Basic content only 4733tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 4734 4735// Single tool call 4736tst .test ( 4737"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4738"{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>" ) 4739 .tools ({special_function_tool }) 4740 .expect (simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": 1}" ,"functions.special_function:0" )) 4741 .run (); 4742 4743// Single tool call with reasoning 4744tst .test ( 4745"<think>I'm thinking about this</think>" 4746"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4747"{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>" ) 4748 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4749 .tools ({special_function_tool }) 4750 .expect (simple_assist_msg ("" ,"I'm thinking about this" ,"special_function" ,"{\"arg1\": 1}" ,"functions.special_function:0" )) 4751 .run (); 4752 4753// Tool call with content 4754tst .test ( 4755"Hello, world!\nWhat's up?" 4756"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4757"{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>" ) 4758 .tools ({special_function_tool }) 4759 .expect (simple_assist_msg ("Hello, world!\nWhat's up?" ,"" ,"special_function" ,"{\"arg1\": 1}" ,"functions.special_function:0" )) 4760 .run (); 4761 4762// Multiple tool calls (parallel) - tests the indexing behavior 4763tst .test ( 4764"<|tool_calls_section_begin|>" 4765"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>" 4766"<|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|>" 4767"<|tool_calls_section_end|>" ) 4768 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4769 .parallel_tool_calls (true) 4770 .tools ({ 4771special_function_tool ,special_function_tool_with_optional_param 4772 }) 4773 .expect_tool_calls ({ 4774 {"special_function" ,R"({"arg1": 1})" ,"functions.special_function:0" }, 4775 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" ,"functions.special_function_with_opt:1" }, 4776 }) 4777 .run (); 4778 4779// Multiple tool calls with reasoning 4780tst .test ( 4781"<think>I need to call two functions</think>" 4782"<|tool_calls_section_begin|>" 4783"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>" 4784"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}<|tool_call_end|>" 4785"<|tool_calls_section_end|>" ) 4786 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4787 .parallel_tool_calls (true) 4788 .tools ({ 4789special_function_tool ,python_tool 4790 }) 4791 .expect_reasoning ("I need to call two functions" ) 4792 .expect_tool_calls ({ 4793 {"special_function" ,R"({"arg1": 1})" ,"functions.special_function:0" }, 4794 {"python" ,"{\"code\": \"print('hey')\"}" ,"functions.python:1" }, 4795 }) 4796 .run (); 4797 4798// Python tool with multiline code 4799tst .test ( 4800"<|tool_calls_section_begin|><|tool_call_begin|>functions.python:0<|tool_call_argument_begin|>" 4801"{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}<|tool_call_end|><|tool_calls_section_end|>" ) 4802 .tools ({python_tool }) 4803 .expect_tool_calls ({ 4804 {"python" ,"{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}" ,"functions.python:0" }, 4805 }) 4806 .run (); 4807 4808// Tool call with empty arguments 4809tst .test ( 4810"<|tool_calls_section_begin|><|tool_call_begin|>functions.empty_args:0<|tool_call_argument_begin|>" 4811"{}<|tool_call_end|><|tool_calls_section_end|>" ) 4812 .tools ({empty_args_tool }) 4813 .expect (simple_assist_msg ("" ,"" ,"empty_args" ,"{}" ,"functions.empty_args:0" )) 4814 .run (); 4815 4816// Partial tool call (streaming) 4817tst .test ( 4818"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4819"{\"arg1\": " ) 4820 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4821 .tools ({special_function_tool }) 4822 .is_partial (true) 4823 .expect (simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": " ,"functions.special_function:0" )) 4824 .run (); 4825 4826// Three tool calls to verify counter continues incrementing 4827tst .test ( 4828"<|tool_calls_section_begin|>" 4829"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>" 4830"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print(1)\"}<|tool_call_end|>" 4831"<|tool_call_begin|>functions.html:2<|tool_call_argument_begin|>{\"markup\": \"<p>test</p>\"}<|tool_call_end|>" 4832"<|tool_calls_section_end|>" ) 4833 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4834 .parallel_tool_calls (true) 4835 .tools ({ 4836special_function_tool ,python_tool ,html_tool 4837 }) 4838 .expect_tool_calls ({ 4839 {"special_function" ,R"({"arg1": 1})" ,"functions.special_function:0" }, 4840 {"python" ,"{\"code\": \"print(1)\"}" ,"functions.python:1" }, 4841 {"html" ,"{\"markup\": \"<p>test</p>\"}" ,"functions.html:2" }, 4842 }) 4843 .run (); 4844 4845// Multiple tool calls with reasoning, call *inside thinking block* 4846tst .test ( 4847"<think>I need to call two functions" 4848"<|tool_calls_section_begin|>" 4849"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|>" 4850"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}<|tool_call_end|>" 4851"<|tool_calls_section_end|>" ) 4852 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4853 .parallel_tool_calls (true) 4854 .tools ({ 4855special_function_tool ,python_tool 4856 }) 4857 .expect_reasoning ("I need to call two functions" ) 4858 .expect_tool_calls ({ 4859 {"special_function" ,R"({"arg1": 1})" ,"functions.special_function:0" }, 4860 {"python" ,"{\"code\": \"print('hey')\"}" ,"functions.python:1" }, 4861 }) 4862 .run (); 4863 4864// Multiple tool calls with reasoning, call *inside thinking block* and *without section markers or end markers 4865tst .test ( 4866"<think>I need to call two functions" 4867"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}" 4868"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{\"code\": \"print('hey')\"}" ) 4869 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4870 .parallel_tool_calls (true) 4871 .tools ({ 4872special_function_tool ,python_tool 4873 }) 4874 .expect_reasoning ("I need to call two functions" ) 4875 .expect_tool_calls ({ 4876 {"special_function" ,R"({"arg1": 1})" ,"functions.special_function:0" }, 4877 {"python" ,"{\"code\": \"print('hey')\"}" ,"functions.python:1" }, 4878 }) 4879 .run (); 4880 4881// Real life test - execute_command 4882tst .test ("<|tool_call_begin|>functions.execute_command:0<|tool_call_argument_begin|>{\"command\": \"ls -lah\"" 4883", \"cwd\": \"/home/user/development/exllamav3\", \"timeout\": 10}" ) 4884 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4885 .parallel_tool_calls (true) 4886 .tools ({ 4887 { 4888/* .name = */ "execute_command" , 4889/* .description = */ "Execute shell command" , 4890/* .parameters = */ R"({ 4891"type": "object", 4892"properties": { 4893"command": { 4894"type": "string", 4895"description": "Shell command to execute" 4896}, 4897"cwd": { 4898"type": "string", 4899"description": "Working directory" 4900}, 4901"timeout": { 4902"type": "integer", 4903"description": "The timeout in seconds" 4904} 4905}, 4906"required": ["command"] 4907})" 4908 } 4909 }). 4910expect_tool_calls ({ 4911 { 4912"execute_command" , 4913R"({"command": "ls -lah", "cwd": "/home/user/development/exllamav3", "timeout": 10})" , 4914"functions.execute_command:0" 4915 } 4916 }) 4917 .run (); 4918 4919// Continuation tests 4920tst .test ("world!\nWhat's up?" ) 4921 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4922 .enable_thinking (true) 4923 .messages ({message_user ,message_assist_prefill_content }) 4924 .add_generation_prompt (false) 4925 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 4926 .expect_reasoning ("I'm thinking" ) 4927 .expect_content ("Hello, world!\nWhat's up?" ) 4928 .run (); 4929 4930tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 4931 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4932 .enable_thinking (true) 4933 .messages ({message_user ,message_assist_prefill_reasoning }) 4934 .add_generation_prompt (false) 4935 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 4936 .expect_reasoning ("I'm thinking" ) 4937 .expect_content ("Hello, world!\nWhat's up?" ) 4938 .run (); 4939 } 4940 4941 { 4942auto kimi_id_special_func_tool_call = 4943simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": 1}" ,"functions.special_function:0" ); 4944 4945// Kimi-K2 old template 4946auto tst = peg_tester ("models/templates/moonshotai-Kimi-K2.jinja" ,detailed_debug ); 4947tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 4948tst .test ( 4949"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4950"{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>" ) 4951 .tools ({special_function_tool }) 4952 .expect (kimi_id_special_func_tool_call ) 4953 .expect_reconstruction () 4954 .run (); 4955 4956// Kimi-K2-Instruct 4957auto tst2 = peg_tester ("models/templates/Kimi-K2-Instruct.jinja" ,detailed_debug ); 4958tst2 .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 4959tst2 .test ( 4960"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>" 4961"{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>" ) 4962 .tools ({special_function_tool }) 4963 .expect (kimi_id_special_func_tool_call ) 4964 .expect_reconstruction () 4965 .run (); 4966 4967// Continuation tests 4968tst .test ("world!\nWhat's up?" ) 4969 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4970 .enable_thinking (true) 4971 .messages ({message_user ,message_assist_prefill_content }) 4972 .add_generation_prompt (false) 4973 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 4974 .expect_reasoning ("I'm thinking" ) 4975 .expect_content ("Hello, world!\nWhat's up?" ) 4976 .run (); 4977 4978tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 4979 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 4980 .enable_thinking (true) 4981 .messages ({message_user ,message_assist_prefill_reasoning }) 4982 .add_generation_prompt (false) 4983 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 4984 .expect_reasoning ("I'm thinking" ) 4985 .expect_content ("Hello, world!\nWhat's up?" ) 4986 .run (); 4987 } 4988 4989for (const char * tmpl : { 4990"models/templates/LFM2-8B-A1B.jinja" , 4991"models/templates/LFM2.5-Instruct.jinja" , 4992"models/templates/LFM2.5-8B-A1B.jinja" , 4993 }) { 4994test_lfm2_parser (tmpl ,detailed_debug ); 4995 } 4996 4997// Thinking cases only apply to LFM2.5-8B-A1B, the one LFM2 template that emits <think> 4998 { 4999auto tst = peg_tester ("models/templates/LFM2.5-8B-A1B.jinja" ,detailed_debug ); 5000 5001// Reasoning is parsed independent of enable_thinking 5002 5003// Tool call with reasoning 5004tst .test ("<think>I'm\nthinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" ) 5005 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5006 .tools ({special_function_tool }) 5007 .expect (message_assist_call_thoughts ) 5008 .run (); 5009 5010// Tool call with reasoning and content 5011tst .test ("<think>I need to call a function</think>" 5012"Let me check the time.<|tool_call_start|>[get_time(city=\"Paris\")]<|tool_call_end|>" ) 5013 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5014 .tools ({get_time_tool }) 5015 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 5016"I need to call a function" ,"Let me check the time." , { {"get_time" ,"{\"city\":\"Paris\"}" } } 5017 )) 5018 .run (); 5019 5020// Fake tool call marker inside reasoning is not parsed as a call 5021tst .test ("<think>Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm</think>" 5022"<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" ) 5023 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5024 .tools ({special_function_tool }) 5025 .expect_reasoning ("Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm" ) 5026 .expect_tool_calls ({ 5027 {"special_function" ,R"({"arg1": 1})" , {} }, 5028 }) 5029 .run (); 5030 5031// enable_thinking=false still captures emitted reasoning 5032tst .test ("<think>I'm\nthinking</think>Hello, world!\nWhat's up?" ) 5033 .enable_thinking (false) 5034 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5035 .expect (message_assist_thoughts ) 5036 .run (); 5037 5038tst .test ("<think>I'm\nthinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" ) 5039 .enable_thinking (false) 5040 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5041 .tools ({special_function_tool }) 5042 .expect (message_assist_call_thoughts ) 5043 .run (); 5044 5045// Continuation: prefill content 5046tst .test ("world!\nWhat's up?" ) 5047 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5048 .enable_thinking (true) 5049 .messages ({message_user ,message_assist_prefill_content }) 5050 .add_generation_prompt (false) 5051 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5052 .expect_reasoning ("I'm thinking" ) 5053 .expect_content ("Hello, world!\nWhat's up?" ) 5054 .run (); 5055 5056// Continuation: prefill reasoning 5057tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 5058 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5059 .enable_thinking (true) 5060 .messages ({message_user ,message_assist_prefill_reasoning }) 5061 .add_generation_prompt (false) 5062 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5063 .expect_reasoning ("I'm thinking" ) 5064 .expect_content ("Hello, world!\nWhat's up?" ) 5065 .run (); 5066 } 5067 5068// Reka-Edge tests - uses native JSON format with per-call wrapper 5069 { 5070auto tst = peg_tester ("models/templates/Reka-Edge.jinja" ,detailed_debug ); 5071 5072// Basic content only 5073tst .test ("Hello, world!\nWhat's up?" ).enable_thinking (false).expect (message_assist ).run (); 5074 5075// Single tool call without reasoning 5076tst .test ("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>" ) 5077 .enable_thinking (false) 5078 .tools ({special_function_tool }) 5079 .expect (message_assist_call ) 5080 .run (); 5081 5082// Tool call with string argument 5083tst .test ("<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>" ) 5084 .enable_thinking (false) 5085 .tools ({get_time_tool }) 5086 .expect (message_with_tool_calls ("get_time" ,"{\"city\":\"XYZCITY\"}" )) 5087 .run (); 5088 5089// Tool call with reasoning (enable_thinking=true) 5090tst .test ("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>" ) 5091 .enable_thinking (true) 5092 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5093 .tools ({special_function_tool }) 5094 .expect (message_assist_call_thoughts ) 5095 .run (); 5096 5097// Multiple tool calls (parallel) 5098tst .test ( 5099"<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>" 5100"<tool_call>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1, \"arg2\": 2}}</tool_call>" 5101 ) 5102 .enable_thinking (false) 5103 .parallel_tool_calls (true) 5104 .tools ({ 5105special_function_tool ,special_function_tool_with_optional_param 5106 }) 5107 .expect_tool_calls ({ 5108 {"special_function" ,R"({"arg1": 1})" , {} }, 5109 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 5110 }) 5111 .run (); 5112 5113// Tool call with reasoning and content 5114tst .test ("I need to call a function\n</think>\n\n" 5115"Let me check the time.<tool_call>\n{\"name\": \"get_time\", \"arguments\": {\"city\": \"XYZCITY\"}}</tool_call>" ) 5116 .enable_thinking (true) 5117 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5118 .tools ({get_time_tool }) 5119 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 5120"I need to call a function" ,"Let me check the time." , { {"get_time" ,"{\"city\":\"XYZCITY\"}" } } 5121 )) 5122 .run (); 5123 5124// Partial tool call (streaming) 5125tst .test ("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\":" ) 5126 .tools ({special_function_tool }) 5127 .enable_thinking (false) 5128 .is_partial (true) 5129 .expect (simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": " )) 5130 .run (); 5131 5132// Tool call with empty arguments 5133tst .test ("<tool_call>\n{\"name\": \"empty_args\", \"arguments\": {}}</tool_call>" ) 5134 .enable_thinking (false) 5135 .tools ({empty_args_tool }) 5136 .expect (simple_assist_msg ("" ,"" ,"empty_args" ,"{}" )) 5137 .run (); 5138 5139// Tool call with no parameters schema, {} means no arguments 5140tst .test ("<tool_call>\n{\"name\": \"empty_args_no_schema\", \"arguments\": {}}</tool_call>" ) 5141 .enable_thinking (false) 5142 .tools ({empty_args_tool_no_schema }) 5143 .expect (simple_assist_msg ("" ,"" ,"empty_args_no_schema" ,"{}" )) 5144 .run (); 5145 5146// fake tool call marker in reasoning 5147tst .test ( 5148"Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm\n</think>\n\n" 5149"<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}</tool_call>" ) 5150 .enable_thinking (true) 5151 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5152 .tools ({special_function_tool }) 5153 .expect_reasoning ("Let me think about <tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 2}}</tool_call> hmm" ) 5154 .expect_tool_calls ({ 5155 {"special_function" ,R"({"arg1": 1})" , {} }, 5156 }) 5157 .run (); 5158 5159// Continuation tests 5160tst .test ("world!\nWhat's up?" ) 5161 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5162 .enable_thinking (true) 5163 .messages ({message_user ,message_assist_prefill_content }) 5164 .add_generation_prompt (false) 5165 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5166 .expect_reasoning ("I'm thinking" ) 5167 .expect_content ("Hello, world!\nWhat's up?" ) 5168 .run (); 5169 5170tst .test (" thinking\n</think>\n\nHello, world!\nWhat's up?" ) 5171 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5172 .enable_thinking (true) 5173 .messages ({message_user ,message_assist_prefill_reasoning }) 5174 .add_generation_prompt (false) 5175 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5176 .expect_reasoning ("I'm thinking" ) 5177 .expect_content ("Hello, world!\nWhat's up?" ) 5178 .run (); 5179 } 5180 5181 5182// Apertus-8B-Instruct tests - FUNC_NAME_AS_KEY format 5183// Format: <|tools_prefix|>[{"function_name": {...arguments...}}]<|tools_suffix|> 5184 { 5185auto tst = peg_tester ("models/templates/Apertus-8B-Instruct.jinja" ,detailed_debug ); 5186tst .test ("<|tools_prefix|>[{\"special_function\": {\"arg1\": 1}}]<|tools_suffix|>" ) 5187 .tools ({special_function_tool }) 5188 .expect (message_assist_call ) 5189 .expect_reconstruction () 5190 .run (); 5191 5192// Continuation tests 5193tst .test ("world!\nWhat's up?" ) 5194 .messages ({message_user ,message_assist_prefill_content }) 5195 .add_generation_prompt (false) 5196 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5197 .expect_content ("Hello, world!\nWhat's up?" ) 5198 .run (); 5199 } 5200 5201// MiniMax-M2 tests - XML invoke format with parameter tags 5202// Format: <minimax:tool_call><invoke name="func"><parameter name="key">value</parameter></invoke></minimax:tool_call> 5203 { 5204auto tst = peg_tester ("models/templates/MiniMax-M2.jinja" ,detailed_debug ); 5205tst .test ("\n</think>\n\nHello, world!\nWhat's up?" ).enable_thinking (true).reasoning_format (COMMON_REASONING_FORMAT_AUTO ).expect (message_assist ).run (); 5206 5207tst .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 (); 5208 5209tst .test ("Let's call a tool:\n</think>\n\n<minimax:tool_call>\n<invoke name=\"empty_args\">\n</invoke>\n</minimax:tool_call>" ). 5210enable_thinking (true). 5211reasoning_format (COMMON_REASONING_FORMAT_AUTO ). 5212tools ({empty_args_tool }). 5213expect (message_with_reasoning_and_tool_call ("Let's call a tool:" ,"empty_args" ,"{}" )). 5214run (); 5215 5216tst .test ( 5217"\n</think>\n\n<minimax:tool_call>\n<invoke name=\"special_function\">\n<parameter " 5218"name=\"arg1\">1</parameter>\n</invoke>\n</minimax:tool_call>" ) 5219 .tools ({special_function_tool }) 5220 .expect (message_assist_call ) 5221 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5222 .run (); 5223 5224// Continuation tests 5225tst .test ("world!\nWhat's up?" ) 5226 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5227 .enable_thinking (true) 5228 .messages ({message_user ,message_assist_prefill_content }) 5229 .add_generation_prompt (false) 5230 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5231 .expect_reasoning ("I'm thinking" ) 5232 .expect_content ("Hello, world!\nWhat's up?" ) 5233 .run (); 5234 5235tst .test (" thinking\n</think>\n\nHello, world!\nWhat's up?" ) 5236 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5237 .enable_thinking (true) 5238 .messages ({message_user ,message_assist_prefill_reasoning }) 5239 .add_generation_prompt (false) 5240 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5241 .expect_reasoning ("I'm thinking" ) 5242 .expect_content ("Hello, world!\nWhat's up?" ) 5243 .run (); 5244 } 5245 5246// MiniMax-M3 tests - namespaced XML invoke format, the parameter name is the tag 5247// Format: 5248// ]<]minimax[>[<tool_call> 5249// ]<]minimax[>[<invoke name="get_time">]<]minimax[>[<city>Tokyo]<]minimax[>[</city>]<]minimax[>[</invoke> 5250// ]<]minimax[>[</tool_call> 5251// Reasoning uses <mm:think>...</mm:think>. The generation prompt is only "]~b]ai\n", so the model 5252// opens the thinking block itself; a turn without reasoning is prefixed with a bare </mm:think>. 5253 { 5254auto tst = peg_tester ("models/templates/MiniMax-M3.jinja" ,detailed_debug ); 5255 5256// Content only (bare </mm:think> prefix) 5257tst .test ("</mm:think>Hello, world!\nWhat's up?" ) 5258 .enable_thinking (true) 5259 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5260 .expect (message_assist ) 5261 .expect_reconstruction () 5262 .run (); 5263 5264// Thinking + content 5265tst .test ("<mm:think>I'm\nthinking</mm:think>Hello, world!\nWhat's up?" ) 5266 .enable_thinking (true) 5267 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5268 .expect (message_assist_thoughts ) 5269 .expect_reconstruction () 5270 .run (); 5271 5272// Thinking + tool call (single, string param) 5273tst .test ( 5274"<mm:think>Let me check the time</mm:think>" 5275"]<]minimax[>[<tool_call>\n" 5276"]<]minimax[>[<invoke name=\"get_time\">" 5277"]<]minimax[>[<city>Tokyo]<]minimax[>[</city>" 5278"]<]minimax[>[</invoke>\n" 5279"]<]minimax[>[</tool_call>" ) 5280 .enable_thinking (true) 5281 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5282 .tools ({get_time_tool }) 5283 .expect (message_with_tool_calls_and_reasoning ("get_time" ,R"({"city": "Tokyo"})" ,"Let me check the time" )) 5284 .expect_reconstruction () 5285 .run (); 5286 5287// Tool call without reasoning, integer param 5288tst .test ( 5289"</mm:think>" 5290"]<]minimax[>[<tool_call>\n" 5291"]<]minimax[>[<invoke name=\"special_function\">" 5292"]<]minimax[>[<arg1>1]<]minimax[>[</arg1>" 5293"]<]minimax[>[</invoke>\n" 5294"]<]minimax[>[</tool_call>" ) 5295 .enable_thinking (true) 5296 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5297 .tools ({special_function_tool }) 5298 .expect (message_assist_call ) 5299 .expect_reconstruction () 5300 .run (); 5301 5302// Tool call with no parameters 5303tst .test ( 5304"<mm:think>Let's call a tool:</mm:think>" 5305"]<]minimax[>[<tool_call>\n" 5306"]<]minimax[>[<invoke name=\"empty_args\">" 5307"]<]minimax[>[</invoke>\n" 5308"]<]minimax[>[</tool_call>" ) 5309 .enable_thinking (true) 5310 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5311 .tools ({empty_args_tool }) 5312 .expect (message_with_reasoning_and_tool_call ("Let's call a tool:" ,"empty_args" ,"{}" )) 5313 .expect_reconstruction () 5314 .run (); 5315 5316// Multiple parallel tool calls in one block 5317tst .test ( 5318"<mm:think>Calling both</mm:think>" 5319"]<]minimax[>[<tool_call>\n" 5320"]<]minimax[>[<invoke name=\"get_time\">" 5321"]<]minimax[>[<city>Paris]<]minimax[>[</city>" 5322"]<]minimax[>[</invoke>\n" 5323"]<]minimax[>[<invoke name=\"get_weather\">" 5324"]<]minimax[>[<city>Paris]<]minimax[>[</city>" 5325"]<]minimax[>[</invoke>\n" 5326"]<]minimax[>[</tool_call>" ) 5327 .enable_thinking (true) 5328 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5329 .parallel_tool_calls (true) 5330 .tools ({get_time_tool ,get_weather_tool }) 5331 .expect (message_with_reasoning_content_and_multiple_tool_calls ( 5332"Calling both" ,"" , 5333 { {"get_time" ,R"({"city": "Paris"})" }, {"get_weather" ,R"({"city": "Paris"})" } })) 5334 .expect_reconstruction () 5335 .run (); 5336 5337// Content before the tool call block 5338tst .test ( 5339"<mm:think>Thinking about it</mm:think>" 5340"Let me call the function." 5341"]<]minimax[>[<tool_call>\n" 5342"]<]minimax[>[<invoke name=\"special_function\">" 5343"]<]minimax[>[<arg1>1]<]minimax[>[</arg1>" 5344"]<]minimax[>[</invoke>\n" 5345"]<]minimax[>[</tool_call>" ) 5346 .enable_thinking (true) 5347 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5348 .tools ({special_function_tool }) 5349 .expect_reasoning ("Thinking about it" ) 5350 .expect_content ("Let me call the function." ) 5351 .expect_tool_calls ({ 5352 {"special_function" ,R"({"arg1": 1})" , {} }, 5353 }) 5354 .expect_reconstruction () 5355 .run (); 5356 5357// Negative number 5358tst .test ( 5359"<mm:think>Test negative</mm:think>" 5360"]<]minimax[>[<tool_call>\n" 5361"]<]minimax[>[<invoke name=\"magic_int\">" 5362"]<]minimax[>[<ref>-14]<]minimax[>[</ref>" 5363"]<]minimax[>[</invoke>\n" 5364"]<]minimax[>[</tool_call>" ) 5365 .enable_thinking (true) 5366 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5367 .tools ({magic_int_tool }) 5368 .expect_reasoning ("Test negative" ) 5369 .expect_tool_calls ({ 5370 {"magic_int" ,R"({"ref": -14})" , {} }, 5371 }) 5372 .expect_reconstruction () 5373 .run (); 5374 5375// Decimal number 5376tst .test ( 5377"<mm:think>Test decimal</mm:think>" 5378"]<]minimax[>[<tool_call>\n" 5379"]<]minimax[>[<invoke name=\"amount\">" 5380"]<]minimax[>[<orig>3.14]<]minimax[>[</orig>" 5381"]<]minimax[>[</invoke>\n" 5382"]<]minimax[>[</tool_call>" ) 5383 .enable_thinking (true) 5384 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5385 .tools ({amount_tool }) 5386 .expect_reasoning ("Test decimal" ) 5387 .expect_tool_calls ({ 5388 {"amount" ,R"({"orig": 3.14})" , {} }, 5389 }) 5390 .expect_reconstruction () 5391 .run (); 5392 5393// Boolean 5394tst .test ( 5395"<mm:think>Test boolean</mm:think>" 5396"]<]minimax[>[<tool_call>\n" 5397"]<]minimax[>[<invoke name=\"toggle\">" 5398"]<]minimax[>[<enabled>true]<]minimax[>[</enabled>" 5399"]<]minimax[>[</invoke>\n" 5400"]<]minimax[>[</tool_call>" ) 5401 .enable_thinking (true) 5402 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5403 .tools ({toggle_tool }) 5404 .expect_reasoning ("Test boolean" ) 5405 .expect_tool_calls ({ 5406 {"toggle" ,R"({"enabled": true})" , {} }, 5407 }) 5408 .expect_reconstruction () 5409 .run (); 5410 5411// Multiple params of mixed types (required int first, then optional string) 5412tst .test ( 5413"<mm:think>Multi-arg call</mm:think>" 5414"]<]minimax[>[<tool_call>\n" 5415"]<]minimax[>[<invoke name=\"magic_int\">" 5416"]<]minimax[>[<ref>42]<]minimax[>[</ref>" 5417"]<]minimax[>[<name>foo bar]<]minimax[>[</name>" 5418"]<]minimax[>[</invoke>\n" 5419"]<]minimax[>[</tool_call>" ) 5420 .enable_thinking (true) 5421 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5422 .tools ({magic_int_tool }) 5423 .expect_reasoning ("Multi-arg call" ) 5424 .expect_tool_calls ({ 5425 {"magic_int" ,R"({"ref": 42, "name": "foo bar"})" , {} }, 5426 }) 5427 .expect_reconstruction () 5428 .run (); 5429 5430// Nested object param, expanded into one element per key 5431tst .test ( 5432"<mm:think>Nested object</mm:think>" 5433"]<]minimax[>[<tool_call>\n" 5434"]<]minimax[>[<invoke name=\"imaginary_number\">" 5435"]<]minimax[>[<number>" 5436"]<]minimax[>[<real>1.5]<]minimax[>[</real>" 5437"]<]minimax[>[<imaginary>-2.5]<]minimax[>[</imaginary>" 5438"]<]minimax[>[</number>" 5439"]<]minimax[>[</invoke>\n" 5440"]<]minimax[>[</tool_call>" ) 5441 .enable_thinking (true) 5442 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5443 .tools ({imaginary_number_tool }) 5444 .expect_reasoning ("Nested object" ) 5445 .expect_tool_calls ({ 5446 {"imaginary_number" ,R"({"number": {"real": 1.5, "imaginary": -2.5}})" , {} }, 5447 }) 5448 .expect_reconstruction () 5449 .run (); 5450 5451// Array params, expanded into <item> elements (of scalars and of objects) 5452tst .test ( 5453"<mm:think>Nested arrays</mm:think>" 5454"]<]minimax[>[<tool_call>\n" 5455"]<]minimax[>[<invoke name=\"nested_args\">" 5456"]<]minimax[>[<tags>" 5457"]<]minimax[>[<item>alpha]<]minimax[>[</item>" 5458"]<]minimax[>[<item>beta]<]minimax[>[</item>" 5459"]<]minimax[>[</tags>" 5460"]<]minimax[>[<entries>" 5461"]<]minimax[>[<item>" 5462"]<]minimax[>[<id>1]<]minimax[>[</id>" 5463"]<]minimax[>[<label>one]<]minimax[>[</label>" 5464"]<]minimax[>[</item>" 5465"]<]minimax[>[</entries>" 5466"]<]minimax[>[</invoke>\n" 5467"]<]minimax[>[</tool_call>" ) 5468 .enable_thinking (true) 5469 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5470 .tools ({nested_args_tool }) 5471 .expect_reasoning ("Nested arrays" ) 5472 .expect_tool_calls ({ 5473 {"nested_args" ,R"({"tags": ["alpha", "beta"], "entries": [{"id": 1, "label": "one"}]})" , {} }, 5474 }) 5475 .expect_reconstruction () 5476 .run (); 5477 5478// Union params (anyOf/oneOf), expanded as a choice of the alternatives 5479tst .test ( 5480"<mm:think>Union array</mm:think>" 5481"]<]minimax[>[<tool_call>\n" 5482"]<]minimax[>[<invoke name=\"union_args\">" 5483"]<]minimax[>[<filter>" 5484"]<]minimax[>[<item>alpha]<]minimax[>[</item>" 5485"]<]minimax[>[<item>beta]<]minimax[>[</item>" 5486"]<]minimax[>[</filter>" 5487"]<]minimax[>[</invoke>\n" 5488"]<]minimax[>[</tool_call>" ) 5489 .enable_thinking (true) 5490 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5491 .tools ({union_args_tool }) 5492 .expect_reasoning ("Union array" ) 5493 .expect_tool_calls ({ 5494 {"union_args" ,R"({"filter": ["alpha", "beta"]})" , {} }, 5495 }) 5496 .expect_reconstruction () 5497 .run (); 5498 5499// oneOf between a scalar and an object 5500tst .test ( 5501"<mm:think>Union scalar</mm:think>" 5502"]<]minimax[>[<tool_call>\n" 5503"]<]minimax[>[<invoke name=\"union_args\">" 5504"]<]minimax[>[<limit>5]<]minimax[>[</limit>" 5505"]<]minimax[>[</invoke>\n" 5506"]<]minimax[>[</tool_call>" ) 5507 .enable_thinking (true) 5508 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5509 .tools ({union_args_tool }) 5510 .expect_reasoning ("Union scalar" ) 5511 .expect_tool_calls ({ 5512 {"union_args" ,R"({"limit": 5})" , {} }, 5513 }) 5514 .expect_reconstruction () 5515 .run (); 5516 5517tst .test ( 5518"<mm:think>Union nested</mm:think>" 5519"]<]minimax[>[<tool_call>\n" 5520"]<]minimax[>[<invoke name=\"union_args\">" 5521"]<]minimax[>[<limit>" 5522"]<]minimax[>[<max>10]<]minimax[>[</max>" 5523"]<]minimax[>[</limit>" 5524"]<]minimax[>[</invoke>\n" 5525"]<]minimax[>[</tool_call>" ) 5526 .enable_thinking (true) 5527 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5528 .tools ({union_args_tool }) 5529 .expect_reasoning ("Union nested" ) 5530 .expect_tool_calls ({ 5531 {"union_args" ,R"({"limit": {"max": 10}})" , {} }, 5532 }) 5533 .expect_reconstruction () 5534 .run (); 5535 5536// A union with a string alternative is a string 5537tst .test ( 5538"<mm:think>Union string</mm:think>" 5539"]<]minimax[>[<tool_call>\n" 5540"]<]minimax[>[<invoke name=\"union_args\">" 5541"]<]minimax[>[<label>hi]<]minimax[>[</label>" 5542"]<]minimax[>[</invoke>\n" 5543"]<]minimax[>[</tool_call>" ) 5544 .enable_thinking (true) 5545 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5546 .tools ({union_args_tool }) 5547 .expect_reasoning ("Union string" ) 5548 .expect_tool_calls ({ 5549 {"union_args" ,R"({"label": "hi"})" , {} }, 5550 }) 5551 .expect_reconstruction () 5552 .run (); 5553 5554// ... even when the value looks structured 5555tst .test ( 5556"<mm:think>Union string</mm:think>" 5557"]<]minimax[>[<tool_call>\n" 5558"]<]minimax[>[<invoke name=\"union_args\">" 5559"]<]minimax[>[<label>" 5560"]<]minimax[>[<text>hi]<]minimax[>[</text>" 5561"]<]minimax[>[</label>" 5562"]<]minimax[>[</invoke>\n" 5563"]<]minimax[>[</tool_call>" ) 5564 .enable_thinking (true) 5565 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5566 .tools ({union_args_tool }) 5567 .expect_reasoning ("Union string" ) 5568 .expect_tool_calls ({ 5569 {"union_args" ,R"({"label": "]<]minimax[>[<text>hi]<]minimax[>[</text>"})" , {} }, 5570 }) 5571 .expect_reconstruction () 5572 .run (); 5573 5574// Edge case: empty reasoning followed by a tool call 5575tst .test ( 5576"<mm:think></mm:think>" 5577"]<]minimax[>[<tool_call>\n" 5578"]<]minimax[>[<invoke name=\"get_time\">" 5579"]<]minimax[>[<city>XYZCITY]<]minimax[>[</city>" 5580"]<]minimax[>[</invoke>\n" 5581"]<]minimax[>[</tool_call>" ) 5582 .enable_thinking (true) 5583 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5584 .tools ({get_time_tool }) 5585 .expect (message_with_tool_calls ("get_time" ,R"({"city": "XYZCITY"})" )) 5586 .run (); 5587 5588// Continuation tests 5589tst .test ("world!\nWhat's up?" ) 5590 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5591 .enable_thinking (true) 5592 .messages ({message_user ,message_assist_prefill_content }) 5593 .add_generation_prompt (false) 5594 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5595 .expect_reasoning ("I'm thinking" ) 5596 .expect_content ("Hello, world!\nWhat's up?" ) 5597 .run (); 5598 5599tst .test (" thinking</mm:think>Hello, world!\nWhat's up?" ) 5600 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5601 .enable_thinking (true) 5602 .messages ({message_user ,message_assist_prefill_reasoning }) 5603 .add_generation_prompt (false) 5604 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5605 .expect_reasoning ("I'm thinking" ) 5606 .expect_content ("Hello, world!\nWhat's up?" ) 5607 .run (); 5608 } 5609 5610// NVIDIA-Nemotron-Nano-v2 tests - <TOOLCALL>...</TOOLCALL> format 5611// Format: <TOOLCALL>[{"name": "func", "arguments": {...}}]</TOOLCALL> 5612 { 5613auto tst = peg_tester ("models/templates/NVIDIA-Nemotron-Nano-v2.jinja" ,detailed_debug ); 5614tst .test ("I'm\nthinking\n</think>\n<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>" ) 5615 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5616 .tools ({special_function_tool }) 5617 .expect (message_assist_call_thoughts ) 5618 .run (); 5619 5620tst .test ("I'm\nthinking\n</think>\n\n<TOOLCALL>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</TOOLCALL>\n" ) 5621 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5622 .tools ({special_function_tool }) 5623 .expect (message_assist_call_thoughts ) 5624 .run (); 5625 5626// Continuation tests 5627tst .test ("world!\nWhat's up?" ) 5628 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5629 .enable_thinking (true) 5630 .messages ({message_user ,message_assist_prefill_content }) 5631 .add_generation_prompt (false) 5632 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5633 .expect_reasoning ("I'm thinking" ) 5634 .expect_content ("Hello, world!\nWhat's up?" ) 5635 .run (); 5636 } 5637 5638// CohereForAI-c4ai-command-r7b (uses START_RESPONSE/END_RESPONSE, START_THINKING/END_THINKING, START_ACTION/END_ACTION) 5639 { 5640auto tst = peg_tester ("models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" ,detailed_debug ); 5641tst .test ("<|START_RESPONSE|>Hello, world!\nWhat's up?<|END_RESPONSE|>" ).expect (message_assist ).run (); 5642tst .test ( 5643"<|START_THINKING|>I'm\nthinking<|END_THINKING|>" 5644"<|START_ACTION|>[\n" 5645" {\"tool_call_id\": \"0\", \"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}\n" 5646"]<|END_ACTION|>" ) 5647 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5648 .tools ({special_function_tool }) 5649 .expect (message_assist_thoughts_call_idx ) 5650 .run (); 5651 } 5652// CohereForAI-c4ai-command-r-plus (uses markdown code block format) 5653 { 5654auto tst = peg_tester ("models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja" ,detailed_debug ); 5655tst .test ("<|CHATBOT_TOKEN|>Hello, world!\nWhat's up?<|END_OF_TURN_TOKEN|>" ).expect (message_assist ).run (); 5656// Tool calls: Action: followed by JSON code block 5657tst .test ( 5658"Action:\n" 5659"```json\n" 5660"[{\"tool_name\": \"special_function\", \"parameters\": {\"arg1\": 1}}]\n" 5661"```" ) 5662 .tools ({special_function_tool }) 5663 .expect (message_assist_call ) 5664 .run (); 5665 5666// Continuation tests 5667tst .test ("world!\nWhat's up?" ) 5668 .messages ({message_user ,message_assist_prefill_content }) 5669 .add_generation_prompt (false) 5670 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5671 .expect_content ("Hello, world!\nWhat's up?" ) 5672 .run (); 5673 } 5674 5675// mistralai-Mistral-Nemo-Instruct-2407.jinja 5676 { 5677auto tst = peg_tester ("models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja" ,detailed_debug ); 5678tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 5679tst .test ("[TOOL_CALLS][{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}, \"id\": \"123456789\"}]" ) 5680 .tools ({special_function_tool }) 5681 .expect (message_assist_call_id ) 5682 .expect_reconstruction () 5683 .run (); 5684 5685// Continuation tests 5686tst .test ("world!\nWhat's up?" ) 5687 .messages ({message_user ,message_assist_prefill_content }) 5688 .add_generation_prompt (false) 5689 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5690 .expect_content ("Hello, world!\nWhat's up?" ) 5691 .run (); 5692 } 5693 { 5694auto tst = peg_tester ("models/templates/meetkai-functionary-medium-v3.1.jinja" ,detailed_debug ); 5695tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 5696tst .test ("<function=special_function>{\"arg1\": 1}</function>" ) 5697 .tools ({special_function_tool }) 5698 .expect (message_assist_call ) 5699 .expect_reconstruction () 5700 .run (); 5701 5702// Continuation tests 5703tst .test ("world!\nWhat's up?" ) 5704 .messages ({message_user ,message_assist_prefill_content }) 5705 .add_generation_prompt (false) 5706 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5707 .expect_content ("Hello, world!\nWhat's up?" ) 5708 .run (); 5709 } 5710// Functionary v3.2 - recipient-based format: >>>recipient\n{content} 5711 { 5712auto tst = peg_tester ("models/templates/meetkai-functionary-medium-v3.2.jinja" ,detailed_debug ); 5713tst .test ("all\nHello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 5714tst .test ("special_function\n{\"arg1\": 1}" ) 5715 .tools ({special_function_tool }) 5716 .expect (message_assist_call ) 5717 .expect_reconstruction () 5718 .run (); 5719 5720// Continuation tests 5721tst .test ("world!\nWhat's up?" ) 5722 .messages ({message_user ,message_assist_prefill_content }) 5723 .add_generation_prompt (false) 5724 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5725 .expect_content ("Hello, world!\nWhat's up?" ) 5726 .run (); 5727 } 5728 5729// FireFunction 5730 { 5731auto tst = peg_tester ("models/templates/fireworks-ai-llama-3-firefunction-v2.jinja" ,detailed_debug ); 5732tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 5733tst .test (" functools[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]" ) 5734 .tools ({special_function_tool }) 5735 .expect (message_assist_call ) 5736 .expect_reconstruction () 5737 .run (); 5738 5739// Continuation tests 5740tst .test ("world!\nWhat's up?" ) 5741 .messages ({message_user ,message_assist_prefill_content }) 5742 .add_generation_prompt (false) 5743 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5744 .expect_content ("Hello, world!\nWhat's up?" ) 5745 .run (); 5746 } 5747 5748// DeepSeek R1 Distill Llama 8B - reasoning tests only (forced open thinking) 5749// Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag 5750 { 5751auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja" ,detailed_debug ); 5752tst .test ("</think>Hello, world!\nWhat's up?" ) 5753 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5754 .expect (message_assist ) 5755 .run (); 5756tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 5757 .enable_thinking (true) 5758 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5759 .expect (message_assist_thoughts ) 5760 .run (); 5761 5762// Continuation tests 5763tst .test ("world!\nWhat's up?" ) 5764 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5765 .enable_thinking (true) 5766 .messages ({message_user ,message_assist_prefill_content }) 5767 .add_generation_prompt (false) 5768 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5769 .expect_reasoning ("I'm thinking" ) 5770 .expect_content ("Hello, world!\nWhat's up?" ) 5771 .run (); 5772 5773tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 5774 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5775 .enable_thinking (true) 5776 .messages ({message_user ,message_assist_prefill_reasoning }) 5777 .add_generation_prompt (false) 5778 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5779 .expect_reasoning ("I'm thinking" ) 5780 .expect_content ("Hello, world!\nWhat's up?" ) 5781 .run (); 5782 } 5783// llama-cpp DeepSeek R1 template (always forced-open thinking) 5784 { 5785auto tst = peg_tester ("models/templates/llama-cpp-deepseek-r1.jinja" ,detailed_debug ); 5786tst .test ("</think>Hello, world!\nWhat's up?" ).expect (message_assist ).reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ).run (); 5787tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 5788 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5789 .expect (message_assist_thoughts ) 5790 .run (); 5791tst .test ( 5792"</think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n" 5793"```json\n{\"arg1\": 1}```<|tool▁call▁end|><|tool▁calls▁end|>" ) 5794 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5795 .tools ({special_function_tool }) 5796 .parallel_tool_calls (true) 5797 .expect (message_assist_call ) 5798 .run (); 5799 5800// Continuation tests 5801tst .test ("world!\nWhat's up?" ) 5802 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5803 .enable_thinking (true) 5804 .messages ({message_user ,message_assist_prefill_content }) 5805 .add_generation_prompt (false) 5806 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5807 .expect_reasoning ("I'm thinking" ) 5808 .expect_content ("Hello, world!\nWhat's up?" ) 5809 .run (); 5810 5811tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 5812 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5813 .enable_thinking (true) 5814 .messages ({message_user ,message_assist_prefill_reasoning }) 5815 .add_generation_prompt (false) 5816 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5817 .expect_reasoning ("I'm thinking" ) 5818 .expect_content ("Hello, world!\nWhat's up?" ) 5819 .run (); 5820 } 5821// DeepSeek R1 Distill Qwen 32B - reasoning tests only (forced open thinking) 5822// Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag 5823 { 5824auto tst = peg_tester ("models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja" ,detailed_debug ); 5825tst .test ("</think>Hello, world!\nWhat's up?" ).enable_thinking (true). 5826reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ). 5827expect (message_assist ).run (); 5828tst .test ("I'm\nthinking</think>Hello, world!\nWhat's up?" ) 5829 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5830 .expect (message_assist_thoughts ) 5831 .run (); 5832tst .test ( 5833"<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>special_function\n" 5834"```json\n{\"arg1\": 1}```<|tool▁call▁end|><|tool▁calls▁end|>" ) 5835 .tools ({special_function_tool }) 5836 .enable_thinking (false) 5837 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5838 .expect (message_assist_call ) 5839 .run (); 5840 5841// Continuation tests 5842tst .test ("world!\nWhat's up?" ) 5843 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5844 .enable_thinking (true) 5845 .messages ({message_user ,message_assist_prefill_content }) 5846 .add_generation_prompt (false) 5847 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5848 .expect_reasoning ("I'm thinking" ) 5849 .expect_content ("Hello, world!\nWhat's up?" ) 5850 .run (); 5851 5852tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 5853 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5854 .enable_thinking (true) 5855 .messages ({message_user ,message_assist_prefill_reasoning }) 5856 .add_generation_prompt (false) 5857 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5858 .expect_reasoning ("I'm thinking" ) 5859 .expect_content ("Hello, world!\nWhat's up?" ) 5860 .run (); 5861 } 5862 5863// MiMo-VL / Hermes 3 / Qwen 2.5 (Common <tool_call> JSON format) 5864for (const auto & path : 5865 {"models/templates/MiMo-VL.jinja" ,"models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja" , 5866"models/templates/Qwen-Qwen2.5-7B-Instruct.jinja" }) { 5867auto tst = peg_tester (path ,detailed_debug ); 5868tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 5869tst .test ("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>" ) 5870 .tools ({special_function_tool }) 5871 .expect (message_assist_call ) 5872 .expect_reconstruction () 5873 .run (); 5874 5875// Continuation tests 5876tst .test ("world!\nWhat's up?" ) 5877 .messages ({message_user ,message_assist_prefill_content }) 5878 .add_generation_prompt (false) 5879 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5880 .expect_content ("Hello, world!\nWhat's up?" ) 5881 .run (); 5882 } 5883 5884// Reka Edge 5885 { 5886auto tst = peg_tester ("models/templates/Reka-Edge.jinja" ,detailed_debug ); 5887tst .test ("Hello, world!\nWhat's up?" ) 5888 .enable_thinking (false) 5889 .expect (message_assist ) 5890 .run (); 5891tst .test ("I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?" ) 5892 .enable_thinking (true) 5893 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5894 .expect (message_assist_thoughts ) 5895 .run (); 5896tst .test ("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>" ) 5897 .enable_thinking (false) 5898 .tools ({special_function_tool }) 5899 .expect (message_assist_call ) 5900 .run (); 5901tst .test ("Hello, world!\nWhat's up?\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>" ) 5902 .enable_thinking (false) 5903 .tools ({special_function_tool }) 5904 .expect (message_assist_call_content ) 5905 .run (); 5906tst .test ("I'm\nthinking\n</think>\n\n<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}\n</tool_call>" ) 5907 .enable_thinking (true) 5908 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5909 .tools ({special_function_tool }) 5910 .expect (message_assist_call_thoughts ) 5911 .run (); 5912tst .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>" ) 5913 .enable_thinking (false) 5914 .parallel_tool_calls (true) 5915 .tools ({special_function_tool ,special_function_tool_with_optional_param }) 5916 .expect_tool_calls ({ 5917 {"special_function" ,R"({"arg1": 1})" , {} }, 5918 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" , {} }, 5919 }) 5920 .run (); 5921tst .test ("<tool_call>\n{\"name\": \"special_function\", \"arguments\": {\"arg" ) 5922 .enable_thinking (false) 5923 .tools ({special_function_tool }) 5924 .is_partial (true) 5925 .expect (message_assist_call_cutoff_args ) 5926 .run (); 5927 5928// Continuation tests 5929tst .test ("world!\nWhat's up?" ) 5930 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5931 .enable_thinking (true) 5932 .messages ({message_user ,message_assist_prefill_content }) 5933 .add_generation_prompt (false) 5934 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5935 .expect_reasoning ("I'm thinking" ) 5936 .expect_content ("Hello, world!\nWhat's up?" ) 5937 .run (); 5938 5939tst .test (" thinking\n</think>\n\nHello, world!\nWhat's up?" ) 5940 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 5941 .enable_thinking (true) 5942 .messages ({message_user ,message_assist_prefill_reasoning }) 5943 .add_generation_prompt (false) 5944 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 5945 .expect_reasoning ("I'm thinking" ) 5946 .expect_content ("Hello, world!\nWhat's up?" ) 5947 .run (); 5948 } 5949 5950// Apriel 1.5 5951 { 5952auto tst = peg_tester ("models/templates/unsloth-Apriel-1.5.jinja" ,detailed_debug ); 5953tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 5954tst .test ("<tool_calls>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]</tool_calls>" ) 5955 .tools ({special_function_tool }) 5956 .expect (message_assist_call ) 5957 .run (); 5958 5959// Continuation tests 5960tst .test ("world!\nWhat's up?" ) 5961 .messages ({message_user ,message_assist_prefill_content }) 5962 .add_generation_prompt (false) 5963 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5964 .expect_content ("Hello, world!\nWhat's up?" ) 5965 .run (); 5966 } 5967 5968// Apriel 1.6 Thinker (reasoning-only support) 5969 { 5970auto tst = peg_tester ("models/templates/Apriel-1.6-15b-Thinker-fixed.jinja" ,detailed_debug ); 5971 5972// Implicit reasoning start (forced open) 5973tst .test ("I'm\nthinking\n[BEGIN FINAL RESPONSE]\nHello, world!\nWhat's up?" ) 5974 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5975 .enable_thinking (true) 5976 .expect (simple_assist_msg ("Hello, world!\nWhat's up?" ,"Here are my reasoning steps:\nI'm\nthinking" )) 5977 .expect_reconstruction () 5978 .run (); 5979 5980// Reasoning + Tool calls 5981tst .test ( 5982"I'm\nthinking\n[BEGIN FINAL RESPONSE]\n<tool_calls>[{\"name\": \"special_function\", \"arguments\": " 5983"{\"arg1\": 1}}]</tool_calls>" ) 5984 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5985 .enable_thinking (true) 5986 .tools ({special_function_tool }) 5987 .expect (simple_assist_msg ("" ,"Here are my reasoning steps:\nI'm\nthinking" ,"special_function" ,"{\"arg1\":1}" )) 5988 .run (); 5989 5990// Continuation tests 5991tst .test ("world!\nWhat's up?" ) 5992 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 5993 .enable_thinking (true) 5994 .messages ({message_user ,message_assist_prefill_content }) 5995 .add_generation_prompt (false) 5996 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 5997 .expect_reasoning ("I'm thinking" ) 5998 .expect_content ("Hello, world!\nWhat's up?" ) 5999 .run (); 6000 6001tst .test (" thinking\n[BEGIN FINAL RESPONSE]\nHello, world!\nWhat's up?" ) 6002 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6003 .enable_thinking (true) 6004 .messages ({message_user ,message_assist_prefill_reasoning }) 6005 .add_generation_prompt (false) 6006 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 6007 .expect_reasoning ("I'm thinking" ) 6008 .expect_content ("Hello, world!\nWhat's up?" ) 6009 .run (); 6010 } 6011 6012// Mistral Small 3.2 - FUNC_BRACKET_TAG format: [TOOL_CALLS]func_name[CALL_ID]id[ARGS]{...} 6013 { 6014auto tst = peg_tester ("models/templates/Mistral-Small-3.2-24B-Instruct-2506.jinja" ,detailed_debug ); 6015tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 6016tst .test ("[TOOL_CALLS]special_function[CALL_ID]123456789[ARGS]{\"arg1\": 1}" ) 6017 .tools ({special_function_tool }) 6018 .expect (message_assist_call_id ) 6019 .expect_reconstruction () 6020 .run (); 6021 6022tst .test ("[TOOL_CALLS]special_function[CALL_ID]000000001[ARGS]{\"arg1\": 1}" 6023"[TOOL_CALLS]special_function_with_opt[CALL_ID]000000002[ARGS]{\"arg1\": 1, \"arg2\": 2}" ) 6024 .parallel_tool_calls (true) 6025 .tools ({ 6026special_function_tool ,special_function_tool_with_optional_param 6027 }) 6028 .expect_tool_calls ({ 6029 {"special_function" ,R"({"arg1": 1})" ,"000000001" }, 6030 {"special_function_with_opt" ,R"({"arg1": 1, "arg2": 2})" ,"000000002" }, 6031 }) 6032 .expect_reconstruction () 6033 .run (); 6034 6035// Continuation tests 6036tst .test ("world!\nWhat's up?" ) 6037 .messages ({message_user ,message_assist_prefill_content }) 6038 .add_generation_prompt (false) 6039 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6040 .expect_content ("Hello, world!\nWhat's up?" ) 6041 .run (); 6042 } 6043// Devstral 6044 { 6045auto tst = peg_tester ("models/templates/unsloth-mistral-Devstral-Small-2507.jinja" ,detailed_debug ); 6046tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 6047tst .test ("[TOOL_CALLS]special_function[ARGS]{\"arg1\": 1}" ) 6048 .tools ({special_function_tool }) 6049 .expect (message_assist_call ) 6050 .expect_reconstruction () 6051 .run (); 6052tst .test ("Hello, world!\nWhat's up?[TOOL_CALLS]special_function[ARGS]{\"arg1\": 1}" ) 6053 .tools ({special_function_tool }) 6054 .expect (message_assist_call_content ) 6055 .expect_reconstruction () 6056 .run (); 6057 } 6058 6059 { 6060// Llama 3.1 6061auto tst = peg_tester ("models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja" ,detailed_debug ); 6062tst .test ("Hello, world!\nWhat's up?" ).tools ({special_function_tool }).expect (message_assist ).expect_reconstruction ().run (); 6063 6064tst .test ( 6065"```json\n\"42\"\n```" ) 6066 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6067 .json_schema (const_schema ) 6068 .expect_content (R"("42")" ) 6069 .run (); 6070 6071tst .test ( 6072"\"42\"\n" ) 6073 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6074 .json_schema (const_schema ) 6075 .expect_content (R"("42")" ) 6076 .run (); 6077 6078// Continuation tests 6079tst .test ("world!\nWhat's up?" ) 6080 .messages ({message_user ,message_assist_prefill_content }) 6081 .add_generation_prompt (false) 6082 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6083 .expect_content ("Hello, world!\nWhat's up?" ) 6084 .run (); 6085 } 6086 6087 { 6088// Llama 3.2 6089auto tst = peg_tester ("models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja" ,detailed_debug ); 6090tst .test ("Hello, world!\nWhat's up?" ).tools ({special_function_tool }).expect (message_assist ).expect_reconstruction ().run (); 6091 6092// Continuation tests 6093tst .test ("world!\nWhat's up?" ) 6094 .messages ({message_user ,message_assist_prefill_content }) 6095 .add_generation_prompt (false) 6096 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6097 .expect_content ("Hello, world!\nWhat's up?" ) 6098 .run (); 6099 } 6100 6101 { 6102// Llama 3.3 6103auto tst = peg_tester ("models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja" ,detailed_debug ); 6104tst .test ("Hello, world!\nWhat's up?" ).tools ({python_tool }).expect (message_assist ).expect_reconstruction ().run (); 6105 6106// Continuation tests 6107tst .test ("world!\nWhat's up?" ) 6108 .messages ({message_user ,message_assist_prefill_content }) 6109 .add_generation_prompt (false) 6110 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6111 .expect_content ("Hello, world!\nWhat's up?" ) 6112 .run (); 6113 } 6114 6115// Muse Glimmer format tests 6116 { 6117auto tst = peg_tester ("models/templates/muse-glimmer.jinja" ,detailed_debug ); 6118 6119const std::string call_markup = 6120"<atem:function_calls>\n" 6121"<atem:invoke name=\"special_function\">\n" 6122"<atem:parameter name=\"arg1\">1</atem:parameter>\n" 6123"</atem:invoke>\n" 6124"</atem:function_calls>" ; 6125 6126// A plain answer is unaffected 6127tst .test (" to=user<|message|>Hello, world!\nWhat's up?<|eot|>" ) 6128 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6129 .expect (message_assist ) 6130 .run (); 6131 6132// "Inform then act": the model answers the user and calls a tool in ONE generation, 6133// closing the answer with <|eom|>. The answer must stop there rather than swallow it. 6134tst .test (" to=user<|message|>Hello, world!\nWhat's up?<|eom|>" 6135"<|start|>assistant to=special_function<|message|>" + 6136call_markup ) 6137 .tools ({special_function_tool }) 6138 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6139 .expect (message_with_content_and_tool_call ("Hello, world!\nWhat's up?" ,"special_function" , 6140"{\"arg1\":1}" )) 6141 .run (); 6142 6143// Markup quoted in an answer has no preceding <|eom|>, so it stays content instead of 6144// becoming an invocation the user never asked for 6145tst .test (" to=user<|message|>You invoke it like this:\n" + call_markup + "<|eot|>" ) 6146 .tools ({special_function_tool }) 6147 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6148 .expect_content ("You invoke it like this:\n" + call_markup ) 6149 .run (); 6150 6151// Tool markup inside the analysis channel is reasoning, not a call 6152tst .test (" to=self<|message|>I could use " + call_markup + " here<|eom|>" 6153"<|start|>assistant to=user<|message|>Hello!<|eot|>" ) 6154 .tools ({special_function_tool }) 6155 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6156 .expect_reasoning ("I could use " + call_markup + " here" ) 6157 .expect_content ("Hello!" ) 6158 .run (); 6159 } 6160 6161// GPT-OSS format tests 6162 { 6163auto tst = peg_tester ("models/templates/openai-gpt-oss-120b.jinja" ,detailed_debug ); 6164 6165// Basic content only - final channel 6166tst .test ("<|channel|>final<|message|>Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 6167 6168// Basic content only - commentary channel 6169tst .test ("<|channel|>commentary<|message|>Hello, world!\nWhat's up?" ).expect (message_assist ).run (); 6170 6171// Analysis channel (reasoning) with final channel (content) 6172tst .test ( 6173"<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's " 6174"up?" ) 6175 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6176 .expect (message_assist_thoughts ) 6177 .run (); 6178 6179// Analysis channel (reasoning) with final channel (content) with reasoning_format = none 6180tst .test ( 6181"<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's " 6182"up?" ) 6183 .reasoning_format (COMMON_REASONING_FORMAT_NONE ) 6184 .expect_content ("<|channel|>analysis<|message|>I'm\nthinking<|end|>Hello, world!\nWhat's up?" ) 6185 .run (); 6186 6187// Analysis channel only (partial) - still works when reasoning format is set 6188tst .test ("<|channel|>analysis<|message|>I'm\nthinking" ) 6189 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6190 .is_partial (true) 6191 .expect_reasoning ("I'm\nthinking" ) 6192 .run (); 6193 6194// Tool call with recipient in role header: " to=functions.NAME<|channel|>analysis<|message|>JSON" 6195tst .test (" to=functions.special_function<|channel|>analysis<|message|>{\"arg1\": 1}" ) 6196 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6197 .tools ({special_function_tool }) 6198 .expect (message_assist_call ) 6199 .run (); 6200 6201// Tool call with recipient in channel header: "<|channel|>analysis to=functions.NAME<|message|>JSON" 6202tst .test ("<|channel|>analysis to=functions.special_function<|message|>{\"arg1\": 1}" ) 6203 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6204 .tools ({special_function_tool }) 6205 .expect (message_assist_call ) 6206 .run (); 6207 6208// Tool call with constraint: " to=functions.NAME<|channel|>analysis <|constrain|>json<|message|>JSON" 6209tst .test (" to=functions.special_function<|channel|>analysis <|constrain|>json<|message|>{\"arg1\": 1}" ) 6210 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6211 .tools ({special_function_tool }) 6212 .expect (message_assist_call ) 6213 .run (); 6214 6215// Tool call in commentary channel (channel header variant) 6216tst .test ("<|channel|>commentary to=functions.special_function<|message|>{\"arg1\": 1}" ) 6217 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6218 .tools ({special_function_tool }) 6219 .expect (message_assist_call ) 6220 .run (); 6221 6222// Tool call with reasoning + content (analysis first, then tool call) 6223tst .test ( 6224"<|channel|>analysis<|message|>I'm\nthinking<|end|>" 6225"<|start|>assistant to=functions.special_function<|channel|>analysis<|message|>{\"arg1\": 1}" ) 6226 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6227 .tools ({special_function_tool }) 6228 .expect (message_assist_call_thoughts ) 6229 .run (); 6230 6231// Complex tool calling 6232tst .test ( 6233"<|channel|>analysis<|message|>Thinking about edit...<|end|>" 6234"<|start|>assistant<|channel|>commentary to=functions.edit <|constrain|>json" 6235"<|message|>{\"oldString\": \"if (part < railCount - 1) {\", \"newString\": \"if (part < 4) {\", \"replaceAll\": false}" 6236 ) 6237 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6238 .tools ({ 6239 { 6240/* .name = */ "edit" , 6241/* .description = */ "Edit a file" , 6242/* .parameters = */ R"({ 6243"type": "object", 6244"properties": { 6245"oldString": { 6246"type": "string", 6247"description": "Old string to replace." 6248}, 6249"newString": { 6250"type": "string", 6251"description": "New replacement string." 6252}, 6253"replaceAll": { 6254"type": "boolean", 6255"description": "Whether to replace all occurrences." 6256} 6257}, 6258"required": ["oldString", "newString"] 6259})" , 6260 } 6261 }) 6262 .expect_reasoning ("Thinking about edit..." ) 6263 .expect_tool_calls ({ 6264 {"edit" ,R"({"oldString": "if (part < railCount - 1) {", "newString": "if (part < 4) {", "replaceAll": false})" , {} } 6265 }) 6266 .run (); 6267 6268// Structured output 6269tst .test ( 6270"<|channel|>analysis<|message|>I need to output the invoice details in JSON<|end|>" 6271"<|start|>assistant<|channel|>final <|constrain|>json" 6272"<|message|>" 6273R"({"amount": 123.45, "date": "2025-12-03"})" 6274 ) 6275 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6276 .json_schema (invoice_schema ) 6277 .expect_reasoning ("I need to output the invoice details in JSON" ) 6278 .expect_content (R"({"amount": 123.45, "date": "2025-12-03"})" ) 6279 .run (); 6280 6281 6282// Unsolicited tool calls. There is no good way to handle these, so we return empty content. 6283 6284// Builtin function - recipient in role 6285tst .test ( 6286"<|channel|>analysis<|message|>I will execute python to say hello<|end|>" 6287"<|start|>assistant to=container.exec<|channel|>commentary<|message|>python3 -c 'print(\"hello\")'" ) 6288 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6289 .expect_reasoning ("I will execute python to say hello" ) 6290 .expect_content ("" ) 6291 .run (); 6292 6293// Builtin function - recipient in channel 6294tst .test ( 6295"<|channel|>analysis<|message|>I will execute python to say hello<|end|>" 6296"<|start|>assistant<|channel|>commentary to=python <|constrain|>code<|message|>print(\"hello\")" ) 6297 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6298 .expect_reasoning ("I will execute python to say hello" ) 6299 .expect_content ("" ) 6300 .run (); 6301 6302// Edge cases 6303 6304// "<|channel|>commentary to=assistant" before reasoning 6305tst .test ( 6306"<|channel|>commentary to=assistant<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's " 6307"up?" ) 6308 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6309 .expect (message_assist_thoughts ) 6310 .run (); 6311 6312// "<|channel|>commentary to=assistant" before final message 6313tst .test ( 6314"<|channel|>analysis<|message|>I'm\nthinking<|end|><|start|>assistant<|channel|>commentary to=assistant<|channel|>final<|message|>Hello, world!\nWhat's " 6315"up?" ) 6316 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6317 .expect (message_assist_thoughts ) 6318 .run (); 6319 6320// Continuation tests 6321tst .test ("world!\nWhat's up?" ) 6322 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6323 .enable_thinking (true) 6324 .messages ({message_user ,message_assist_prefill_content }) 6325 .add_generation_prompt (false) 6326 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6327 .expect_reasoning ("I'm thinking" ) 6328 .expect_content ("Hello, world!\nWhat's up?" ) 6329 .run (); 6330 6331tst .test (" thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world!\nWhat's up?" ) 6332 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6333 .enable_thinking (true) 6334 .messages ({message_user ,message_assist_prefill_reasoning }) 6335 .add_generation_prompt (false) 6336 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 6337 .expect_reasoning ("I'm thinking" ) 6338 .expect_content ("Hello, world!\nWhat's up?" ) 6339 .run (); 6340 } 6341 6342 { 6343auto tst = peg_tester ("models/templates/StepFun3.5-Flash.jinja" ,detailed_debug ); 6344 6345tst .test ("I was thinking\n</think>\nNow I'm not." ). 6346enable_thinking (true). 6347reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ). 6348expect_reasoning ("I was thinking" ). 6349expect_content ("Now I'm not." ) 6350 .run (); 6351 6352// Test that numeric-looking string values are coerced to strings per the schema 6353tst .test ( 6354"Let me call the magic tool\n" 6355"</think>\n" 6356"<tool_call>\n" 6357"<function=magic>\n" 6358"<parameter=name>\nfooBar\n</parameter>\n" 6359"<parameter=ref>\n5123123\n</parameter>\n" 6360"</function>\n" 6361"</tool_call>" ) 6362 .enable_thinking (true) 6363 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6364 .tools ({magic_tool }) 6365 .expect_reasoning ("Let me call the magic tool" ) 6366 .expect_tool_calls ({ 6367 {"magic" ,R"({"name": "fooBar", "ref": "5123123"})" , {} }, 6368 }) 6369 .run (); 6370 6371// Test that numeric values are correctly interpreted as numbers when schema calls for number 6372tst .test ( 6373"Let me call the special function\n" 6374"</think>\n" 6375"<tool_call>\n" 6376"<function=special_function>\n" 6377"<parameter=arg1>\n42555916\n</parameter>\n" 6378"</function>\n" 6379"</tool_call>" ) 6380 .enable_thinking (true) 6381 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6382 .tools ({special_function_tool }) 6383 .expect_reasoning ("Let me call the special function" ) 6384 .expect_tool_calls ({ 6385 {"special_function" ,R"({"arg1": 42555916})" , {} }, 6386 }) 6387 .run (); 6388 6389tst .test ( 6390"Let me call the special function with opt\n" 6391"</think>\n" 6392"<tool_call>\n" 6393"<function=special_function_with_opt>\n" 6394"<parameter=arg1>\n42555916\n</parameter>\n" 6395"</function>\n" 6396"</tool_call>" ) 6397 .enable_thinking (true) 6398 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6399 .tools ({special_function_tool_with_optional_param }) 6400 .expect_reasoning ("Let me call the special function with opt" ) 6401 .expect_tool_calls ({ 6402 {"special_function_with_opt" ,R"({"arg1": 42555916})" , {} }, 6403 }) 6404 .run (); 6405 6406tst .test ( 6407"Let me call the magic_int function\n" 6408"</think>\n" 6409"<tool_call>\n" 6410"<function=magic_int>\n" 6411"<parameter=ref>\n42555916\n</parameter>\n" 6412"<parameter=name>\nbaz\n</parameter>\n" 6413"</function>\n" 6414"</tool_call>" ) 6415 .enable_thinking (true) 6416 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6417 .tools ({magic_int_tool }) 6418 .expect_reasoning ("Let me call the magic_int function" ) 6419 .expect_tool_calls ({ 6420 {"magic_int" ,R"({"ref": 42555916, "name": "baz"})" , {} }, 6421 }) 6422 .run (); 6423 6424tst .test ( 6425"Call string_param with empty text\n" 6426"</think>\n" 6427"<tool_call>\n" 6428"<function=string_param>\n" 6429"<parameter=text>\n\n</parameter>\n" 6430"</function>\n" 6431"</tool_call>" ) 6432 .enable_thinking (true) 6433 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6434 .tools ({string_param_tool }) 6435 .expect_reasoning ("Call string_param with empty text" ) 6436 .expect_tool_calls ({ 6437 {"string_param" ,R"({"text": ""})" , {} }, 6438 }) 6439 .run (); 6440 6441tst .test ( 6442"Test simple quoted unquoted\n" 6443"</think>\n" 6444"<tool_call>\n" 6445"<function=quoted_unquoted>\n" 6446"<parameter=quoted>\n\"foo\"\n</parameter>\n" 6447"<parameter=unquoted>\nfoo\n</parameter>\n" 6448"</function>\n" 6449"</tool_call>" ) 6450 .enable_thinking (true) 6451 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6452 .tools ({quoted_unquoted_tool }) 6453 .expect_reasoning ("Test simple quoted unquoted" ) 6454 .expect_tool_calls ({ 6455 {"quoted_unquoted" ,R"({"quoted": "\"foo\"", "unquoted": "foo"})" , {} }, 6456 }) 6457 .run (); 6458 6459tst .test ( 6460"Test complex quoted unquoted\n" 6461"</think>\n" 6462"<tool_call>\n" 6463"<function=quoted_unquoted>\n" 6464"<parameter=quoted>\n\"printf(\\\"foo\\\");\"\n</parameter>\n" 6465"<parameter=unquoted>\nprintf(\"foo\");\n</parameter>\n" 6466"</function>\n" 6467"</tool_call>" ) 6468 .enable_thinking (true) 6469 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6470 .tools ({quoted_unquoted_tool }) 6471 .expect_reasoning ("Test complex quoted unquoted" ) 6472 .expect_tool_calls ({ 6473 {"quoted_unquoted" ,R"({ "quoted" : "\"printf(\\\"foo\\\");\"", "unquoted": "printf(\"foo\");" })" , {} } 6474 }) 6475 .run (); 6476 6477tst .test ( 6478"Test negative number\n" 6479"</think>\n" 6480"<tool_call>\n" 6481"<function=magic_int>\n" 6482"<parameter=ref>\n-14\n</parameter>\n" 6483"</function>\n" 6484"</tool_call>" ) 6485 .enable_thinking (true) 6486 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6487 .tools ({magic_int_tool }) 6488 .expect_reasoning ("Test negative number" ) 6489 .expect_tool_calls ({ 6490 {"magic_int" ,R"({ "ref" : -14 })" , {} } 6491 }) 6492 .run (); 6493 6494tst .test ( 6495"Test decimal number\n" 6496"</think>\n" 6497"<tool_call>\n" 6498"<function=amount>\n" 6499"<parameter=orig>\n3.14\n</parameter>\n" 6500"</function>\n" 6501"</tool_call>" ) 6502 .enable_thinking (true) 6503 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6504 .tools ({amount_tool }) 6505 .expect_reasoning ("Test decimal number" ) 6506 .expect_tool_calls ({ 6507 {"amount" ,R"({ "orig" : 3.14 })" , {} } 6508 }) 6509 .run (); 6510 6511tst .test ( 6512"Test imaginary number\n" 6513"</think>\n" 6514"<tool_call>\n" 6515"<function=imaginary_number>\n" 6516"<parameter=number>\n" 6517"{ \"real\": 3.14, \"imaginary\": 2.71 }\n" 6518"</parameter>\n" 6519"</function>\n" 6520"</tool_call>" ) 6521 .enable_thinking (true) 6522 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6523 .tools ({imaginary_number_tool }) 6524 .expect_reasoning ("Test imaginary number" ) 6525 .expect_tool_calls ({ 6526 {"imaginary_number" ,R"({ "number" : {"real":3.14,"imaginary":2.71 } })" , {} } 6527 }) 6528 .run (); 6529 6530// Continuation tests 6531tst .test ("world!\nWhat's up?" ) 6532 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6533 .enable_thinking (true) 6534 .messages ({message_user ,message_assist_prefill_content }) 6535 .add_generation_prompt (false) 6536 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6537 .expect_reasoning ("I'm thinking" ) 6538 .expect_content ("Hello, world!\nWhat's up?" ) 6539 .run (); 6540 6541tst .test (" thinking\n</think>\nHello, world!\nWhat's up?" ) 6542 .reasoning_format (COMMON_REASONING_FORMAT_DEEPSEEK ) 6543 .enable_thinking (true) 6544 .messages ({message_user ,message_assist_prefill_reasoning }) 6545 .add_generation_prompt (false) 6546 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 6547 .expect_reasoning ("I'm thinking" ) 6548 .expect_content ("Hello, world!\nWhat's up?" ) 6549 .run (); 6550 } 6551 6552// GigaChat V3 6553 { 6554auto tst = peg_tester ("models/templates/GigaChat3-10B-A1.8B.jinja" ,detailed_debug ); 6555tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 6556tst .test ("<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" ) 6557 .tools ({special_function_tool }) 6558 .expect (message_assist_call ) 6559 .expect_reconstruction () 6560 .run (); 6561 6562tst .test ( 6563"Hello, world!\nWhat's up?" 6564"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" 6565 ) 6566 .tools ({special_function_tool }) 6567 .expect (message_assist_call_content ) 6568 .expect_reconstruction () 6569 .run (); 6570 6571// Continuation tests 6572tst .test ("world!\nWhat's up?" ) 6573 .messages ({message_user ,message_assist_prefill_content }) 6574 .add_generation_prompt (false) 6575 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6576 .expect_content ("Hello, world!\nWhat's up?" ) 6577 .run (); 6578 } 6579 6580// GigaChat V3.1 6581 { 6582auto tst = peg_tester ("models/templates/GigaChat3.1-10B-A1.8B.jinja" ,detailed_debug ); 6583tst .test ("Hello, world!\nWhat's up?" ).expect (message_assist ).expect_reconstruction ().run (); 6584tst .test ("<|function_call|>{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" ) 6585 .tools ({special_function_tool }) 6586 .expect (message_assist_call ) 6587 .expect_reconstruction () 6588 .run (); 6589 6590tst .test ( 6591"Hello, world!\nWhat's up?" 6592"<|function_call|>{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}" 6593 ) 6594 .tools ({special_function_tool }) 6595 .expect (message_assist_call_content ) 6596 .expect_reconstruction () 6597 .run (); 6598 6599// Continuation tests 6600tst .test ("world!\nWhat's up?" ) 6601 .messages ({message_user ,message_assist_prefill_content }) 6602 .add_generation_prompt (false) 6603 .continue_final_message (COMMON_CHAT_CONTINUATION_CONTENT ) 6604 .expect_content ("Hello, world!\nWhat's up?" ) 6605 .run (); 6606 } 6607 6608// MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function> 6609 { 6610auto tst = peg_tester ("models/templates/openbmb-MiniCPM5-1B.jinja" ,detailed_debug ); 6611 6612tst .test ("Hello, world!\nWhat's up?" ) 6613 .enable_thinking (false) 6614 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6615 .expect (message_assist ) 6616 .run (); 6617 6618tst .test (R"(<function name="python"><param name="code">print('Hello, World!')</param></function>)" ) 6619 .enable_thinking (false) 6620 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6621 .tools ({python_tool }) 6622 .expect_tool_calls ({ {"python" ,R"#({"code": "print('Hello, World!')"})#" , {} } }) 6623 .run (); 6624 6625tst .test (R"(<function name="empty_args"></function>)" ) 6626 .enable_thinking (false) 6627 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6628 .tools ({empty_args_tool }) 6629 .expect (simple_assist_msg ("" ,"" ,"empty_args" ,"{}" )) 6630 .run (); 6631 6632tst .test (R"(<function name="python"><param name="code">print('x')</param></function>)" ) 6633 .enable_thinking (false) 6634 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6635 .parallel_tool_calls (true) 6636 .tools ({python_tool }) 6637 .expect_tool_calls ({ {"python" ,R"#({"code": "print('x')"})#" , {} } }) 6638 .run (); 6639 6640// CDATA lets a string value carry characters that would otherwise close the tag. 6641tst .test (R"(<function name="html"><param name="markup"><![CDATA[<a href="/x">hi</a> </param>]]></param></function>)" ) 6642 .enable_thinking (false) 6643 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6644 .tools ({html_tool }) 6645 .expect_tool_calls ({ {"html" ,R"#({"markup": "<a href=\"/x\">hi</a> </param>"})#" , {} } }) 6646 .run (); 6647 6648tst .test (R"(I'm thinking</think><function name="python"><param name="code">print('hey')</param></function>)" ) 6649 .enable_thinking (true) 6650 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6651 .tools ({python_tool }) 6652 .expect_reasoning ("I'm thinking" ) 6653 .expect_tool_calls ({ {"python" ,R"#({"code": "print('hey')"})#" , {} } }) 6654 .run (); 6655 6656tst .test (R"(<function name="python"><param name="code">print('x')</param></function> 6657<function name="python"><param name="code">print('y')</param></function>)" ) 6658 .enable_thinking (false) 6659 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6660 .parallel_tool_calls (true) 6661 .tools ({python_tool }) 6662 .expect_tool_calls ({ 6663 {"python" ,R"#({"code": "print('x')"})#" , {} }, 6664 {"python" ,R"#({"code": "print('y')"})#" , {} }, 6665 }) 6666 .run (); 6667 6668tst .test (" thinking</think>Hello, world!\nWhat's up?" ) 6669 .enable_thinking (true) 6670 .reasoning_format (COMMON_REASONING_FORMAT_AUTO ) 6671 .messages ({message_user ,message_assist_prefill_reasoning }) 6672 .add_generation_prompt (false) 6673 .continue_final_message (COMMON_CHAT_CONTINUATION_REASONING ) 6674 .expect_reasoning ("I'm thinking" ) 6675 .expect_content ("Hello, world!\nWhat's up?" ) 6676 .run (); 6677 } 6678} 6679 6680static void test_template_generation_prompt () { 6681common_chat_msg system_msg ; 6682system_msg .role = "system" ; 6683system_msg .content = "You are a helpful assistant." ; 6684 6685common_chat_msg tool_call_msg = simple_assist_msg ("" ,"" ,"special_function" ,"{\"arg1\": 1}" ); 6686 6687common_chat_msg tool_msg ; 6688tool_msg .role = "tool" ; 6689tool_msg .tool_name = "special_function" ; 6690tool_msg .tool_call_id = "call0" ; 6691tool_msg .content = "Sunny" ; 6692 6693struct test_case_options { 6694 std::vector < common_chat_msg > messages ; 6695bool add_generation_prompt = true; 6696common_chat_continuation continue_final_message = COMMON_CHAT_CONTINUATION_NONE ; 6697bool enable_thinking = true; 6698 }; 6699 6700auto basic = [& ]() { 6701test_case_options opts ; 6702opts .messages = {system_msg ,message_user }; 6703return opts ; 6704 }; 6705 6706auto continuation_content = [& ]() { 6707test_case_options opts ; 6708opts .messages = {system_msg ,message_user ,message_assist_prefill_content }; 6709opts .add_generation_prompt = false; 6710opts .continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT ; 6711return opts ; 6712 }; 6713 6714auto continuation_reasoning = [& ]() { 6715test_case_options opts ; 6716opts .messages = {system_msg ,message_user ,message_assist_prefill_reasoning }; 6717opts .add_generation_prompt = false; 6718opts .continue_final_message = COMMON_CHAT_CONTINUATION_REASONING ; 6719return opts ; 6720 }; 6721 6722auto check = [& ](const common_chat_templates_ptr & tmpls , 6723const test_case_options & opts , 6724const std::string & expected_generation_prompt ) { 6725common_chat_templates_inputs inputs ; 6726inputs .messages = opts .messages ; 6727inputs .add_generation_prompt = opts .add_generation_prompt ; 6728inputs .continue_final_message = opts .continue_final_message ; 6729inputs .enable_thinking = opts .enable_thinking ; 6730 6731auto params = common_chat_templates_apply (tmpls .get (),inputs ); 6732 6733assert_contains (params .prompt ,system_msg .content ); 6734assert_contains (params .prompt ,message_user .content ); 6735assert_equals (expected_generation_prompt ,params .generation_prompt ); 6736assert_ends_with (params .prompt ,expected_generation_prompt ); 6737 }; 6738 6739 { 6740auto tmpls = read_templates ("models/templates/Qwen3.5-4B.jinja" ); 6741check (tmpls ,basic (),"<|im_start|>assistant\n<think>\n" ); 6742check (tmpls ,continuation_content (),"<|im_start|>assistant\n<think>\nI'm thinking\n</think>\n\nHello, " ); 6743check (tmpls ,continuation_reasoning (),"<|im_start|>assistant\n<think>\nI'm" ); 6744 } 6745 6746 { 6747auto tmpls = read_templates ("models/templates/openai-gpt-oss-120b.jinja" ); 6748check (tmpls ,basic (),"<|start|>assistant" ); 6749check (tmpls ,continuation_content (),"<|start|>assistant<|channel|>analysis<|message|>I'm thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, " ); 6750check (tmpls ,continuation_reasoning (),"<|start|>assistant<|channel|>analysis<|message|>I'm" ); 6751 } 6752 6753 { 6754auto tmpls = read_templates ("models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja" ); 6755check (tmpls ,basic (),"" ); 6756check (tmpls ,continuation_content (),"[THINK]I'm thinking[/THINK]Hello, " ); 6757check (tmpls ,continuation_reasoning (),"[THINK]I'm" ); 6758 } 6759 6760 { 6761auto tmpls = read_templates ("models/templates/google-gemma-4-31B-it.jinja" ); 6762check (tmpls ,basic (),"<|turn>model\n" ); 6763check (tmpls ,continuation_content (),"<|turn>model\n<|channel>thought\nI'm thinking<channel|>Hello, " ); 6764check (tmpls ,continuation_reasoning (),"<|turn>model\n<|channel>thought\nI'm" ); 6765 6766// Special case when last message is a tool response 6767test_case_options after_tool_call = continuation_reasoning (); 6768after_tool_call .messages = {system_msg ,message_user ,tool_call_msg ,tool_msg ,message_assist_prefill_reasoning }; 6769check (tmpls ,after_tool_call ,"<|channel>thought\nI'm" ); 6770 } 6771 6772 { 6773auto tmpls = read_templates ("models/templates/meetkai-functionary-medium-v3.2.jinja" ); 6774check (tmpls ,basic (),"<|start_header_id|>assistant<|end_header_id|>\n\n>>>" ); 6775check (tmpls ,continuation_content (),"<|start_header_id|>assistant<|end_header_id|>\n\n>>>all\nHello, " ); 6776check (tmpls ,continuation_reasoning (),"<|start_header_id|>assistant<|end_header_id|>\n\n>>>all\n" ); 6777 } 6778 6779 { 6780auto tmpls = read_templates ("models/templates/Reka-Edge.jinja" ); 6781check (tmpls ,basic (),"assistant: <think>\n" ); 6782check (tmpls ,continuation_content (),"assistant: <think>\nI'm thinking\n</think>\n\nHello, " ); 6783check (tmpls ,continuation_reasoning (),"assistant: <think>\nI'm" ); 6784 } 6785 6786 { 6787auto tmpls = read_templates ("models/templates/moonshotai-Kimi-K2.jinja" ); 6788check (tmpls ,basic (),"<|im_assistant|>assistant<|im_middle|>" ); 6789check (tmpls ,continuation_content (),"<|im_assistant|>assistant<|im_middle|><think>I'm thinking</think>Hello, " ); 6790check (tmpls ,continuation_reasoning (),"<|im_assistant|>assistant<|im_middle|><think>I'm" ); 6791 } 6792 6793for (const char * tmpl : { 6794"models/templates/LFM2-8B-A1B.jinja" , 6795"models/templates/LFM2.5-Instruct.jinja" , 6796"models/templates/LFM2.5-8B-A1B.jinja" , 6797 }) { 6798auto tmpls = read_templates (tmpl ); 6799check (tmpls ,basic (),"<|im_start|>assistant\n" ); 6800check (tmpls ,continuation_content (),"<|im_start|>assistant\n<think>I'm thinking</think>Hello, " ); 6801check (tmpls ,continuation_reasoning (),"<|im_start|>assistant\n<think>I'm" ); 6802 } 6803 6804 { 6805// 8B-A1B renders prior-turn reasoning via the "thinking" field 6806auto tmpls = read_templates ("models/templates/LFM2.5-8B-A1B.jinja" ); 6807common_chat_templates_inputs inputs ; 6808inputs .messages = {message_user ,message_assist_call_thoughts ,tool_msg }; 6809inputs .add_generation_prompt = true; 6810auto params = common_chat_templates_apply (tmpls .get (),inputs ); 6811assert_contains (params .prompt ,"<think>I'm\nthinking</think>" ); 6812 } 6813 6814 { 6815auto tmpls = read_templates ("models/templates/GigaChat3-10B-A1.8B.jinja" ); 6816check (tmpls ,basic (),"assistant<|role_sep|>\n" ); 6817check (tmpls ,continuation_content (),"assistant<|role_sep|>\nHello, " ); 6818check (tmpls ,continuation_reasoning (),"assistant<|role_sep|>\n" ); 6819 } 6820 6821 { 6822auto tmpls = read_templates ("models/templates/deepseek-ai-DeepSeek-V3.2.jinja" ); 6823check (tmpls ,basic (),"<|Assistant|><think>" ); 6824check (tmpls ,continuation_content (),"<|Assistant|><think>I'm thinking</think>Hello, " ); 6825check (tmpls ,continuation_reasoning (),"<|Assistant|><think>I'm" ); 6826 } 6827 6828const std::string deepseek_v4_reasoning_effort_max = "Reasoning Effort: Absolute maximum" ; 6829const std::string deepseek_v4_flash_0731_reasoning_effort_max = "Reasoning Effort: Beyond maximum" ; 6830 6831 { 6832auto tmpls = read_templates ("models/templates/deepseek-ai-DeepSeek-V4.jinja" ); 6833check (tmpls ,basic (),"<|Assistant|><think>" ); 6834check (tmpls ,continuation_content (),"<|Assistant|><think>I'm thinking</think>Hello, " ); 6835check (tmpls ,continuation_reasoning (),"<|Assistant|><think>I'm" ); 6836 6837auto continuation_content_no_thinking = continuation_content (); 6838continuation_content_no_thinking .messages = {system_msg ,message_user ,simple_assist_msg ("Hello, " ) }; 6839continuation_content_no_thinking .enable_thinking = false; 6840check (tmpls ,continuation_content_no_thinking ,"<|Assistant|></think>Hello, " ); 6841 6842common_chat_templates_inputs max_inputs ; 6843max_inputs .messages = {system_msg ,message_user }; 6844max_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("max")" ; 6845auto max_params = common_chat_templates_apply (tmpls .get (),max_inputs ); 6846assert_contains (max_params .prompt ,deepseek_v4_reasoning_effort_max ); 6847 6848auto high_inputs = max_inputs ; 6849high_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("high")" ; 6850auto high_params = common_chat_templates_apply (tmpls .get (),high_inputs ); 6851assert_not_contains (high_params .prompt ,deepseek_v4_reasoning_effort_max ); 6852 6853auto low_inputs = max_inputs ; 6854low_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("low")" ; 6855auto low_params = common_chat_templates_apply (tmpls .get (),low_inputs ); 6856assert_not_contains (low_params .prompt ,deepseek_v4_reasoning_effort_max ); 6857 6858common_chat_templates_inputs default_effort_inputs ; 6859default_effort_inputs .messages = {system_msg ,message_user }; 6860auto default_effort_params = common_chat_templates_apply (tmpls .get (),default_effort_inputs ); 6861assert_not_contains (default_effort_params .prompt ,deepseek_v4_reasoning_effort_max ); 6862 6863auto non_thinking_max_inputs = max_inputs ; 6864non_thinking_max_inputs .enable_thinking = false; 6865auto non_thinking_max_params = common_chat_templates_apply (tmpls .get (),non_thinking_max_inputs ); 6866assert_not_contains (non_thinking_max_params .prompt ,deepseek_v4_reasoning_effort_max ); 6867 6868common_chat_templates_inputs response_format_inputs ; 6869response_format_inputs .messages = {system_msg ,message_user }; 6870response_format_inputs .tools = {get_time_tool }; 6871response_format_inputs .json_schema = 6872R"({"type":"object","properties":{"answer":{"type":"string"}}})" ; 6873auto response_format_params = common_chat_templates_apply (tmpls .get (),response_format_inputs ); 6874const auto tools_pos = response_format_params .prompt .find ("## Tools" ); 6875const auto response_format_pos = response_format_params .prompt .find ( 6876"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n" ); 6877if (tools_pos == std::string::npos || response_format_pos == std::string::npos || tools_pos > response_format_pos ) { 6878LOG_ERR ("Expected response format after tools\nActual: %s\n" ,response_format_params .prompt .c_str ()); 6879common_log_flush (common_log_main ()); 6880throw std::runtime_error ("Test failed" ); 6881 } 6882assert_contains (response_format_params .prompt ,R"("answer": {"type": "string"})" ); 6883 6884response_format_inputs .json_schema = "{}" ; 6885auto json_object_params = common_chat_templates_apply (tmpls .get (),response_format_inputs ); 6886assert_contains (json_object_params .prompt , 6887"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{}" ); 6888 6889common_chat_msg assistant_history ; 6890assistant_history .role = "assistant" ; 6891assistant_history .content = "Previous answer" ; 6892assistant_history .reasoning_content = "Previous reasoning" ; 6893 6894common_chat_msg user_followup ; 6895user_followup .role = "user" ; 6896user_followup .content = "Follow up" ; 6897 6898common_chat_templates_inputs default_history_inputs ; 6899default_history_inputs .messages = {message_user ,assistant_history ,user_followup }; 6900auto default_history_params = common_chat_templates_apply (tmpls .get (),default_history_inputs ); 6901assert_contains (default_history_params .prompt ,"<|Assistant|></think>Previous answer" ); 6902 6903auto drop_thinking_inputs = default_history_inputs ; 6904drop_thinking_inputs .chat_template_kwargs ["drop_thinking" ]= "false" ; 6905auto drop_thinking_params = common_chat_templates_apply (tmpls .get (),drop_thinking_inputs ); 6906assert_contains (drop_thinking_params .prompt ,"<|Assistant|><think>Previous reasoning</think>Previous answer" ); 6907 6908auto preserve_reasoning_inputs = default_history_inputs ; 6909preserve_reasoning_inputs .chat_template_kwargs ["preserve_reasoning" ]= "true" ; 6910auto preserve_reasoning_params = common_chat_templates_apply (tmpls .get (),preserve_reasoning_inputs ); 6911assert_contains (preserve_reasoning_params .prompt ,"<|Assistant|><think>Previous reasoning</think>Previous answer" ); 6912assert_equals (true,common_chat_templates_get_caps (tmpls .get ()).at ("supports_preserve_reasoning" )); 6913 6914auto no_preserve_reasoning_inputs = default_history_inputs ; 6915no_preserve_reasoning_inputs .chat_template_kwargs ["preserve_reasoning" ]= "false" ; 6916auto no_preserve_reasoning_params = common_chat_templates_apply (tmpls .get (),no_preserve_reasoning_inputs ); 6917assert_contains (no_preserve_reasoning_params .prompt ,"<|Assistant|></think>Previous answer" ); 6918 6919common_chat_msg empty_tool_call = simple_assist_msg ("" ,"" ,"empty_args" ,"{}" ); 6920common_chat_templates_inputs empty_tool_inputs ; 6921empty_tool_inputs .messages = {message_user ,empty_tool_call }; 6922empty_tool_inputs .tools = {empty_args_tool }; 6923auto empty_tool_params = common_chat_templates_apply (tmpls .get (),empty_tool_inputs ); 6924assert_contains (empty_tool_params .prompt , 6925"<|DSML|invoke name=\"empty_args\">\n\n</|DSML|invoke>" ); 6926 } 6927 6928 { 6929auto tmpls = read_templates ("models/templates/deepseek-ai-DeepSeek-V4-Flash-0731.jinja" ); 6930check (tmpls ,basic (),"<|Assistant|><think>" ); 6931check (tmpls ,continuation_content (),"<|Assistant|><think>I'm thinking</think>Hello, " ); 6932check (tmpls ,continuation_reasoning (),"<|Assistant|><think>I'm" ); 6933 6934auto continuation_content_no_thinking = continuation_content (); 6935continuation_content_no_thinking .messages = {system_msg ,message_user ,simple_assist_msg ("Hello, " ) }; 6936continuation_content_no_thinking .enable_thinking = false; 6937check (tmpls ,continuation_content_no_thinking ,"<|Assistant|></think>Hello, " ); 6938 6939common_chat_templates_inputs high_inputs ; 6940high_inputs .messages = {system_msg ,message_user }; 6941high_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("high")" ; 6942auto high_params = common_chat_templates_apply (tmpls .get (),high_inputs ); 6943assert_contains (high_params .prompt ,deepseek_v4_reasoning_effort_max ); 6944 6945auto max_inputs = high_inputs ; 6946max_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("max")" ; 6947auto max_params = common_chat_templates_apply (tmpls .get (),max_inputs ); 6948assert_contains (max_params .prompt ,deepseek_v4_flash_0731_reasoning_effort_max ); 6949 6950auto low_inputs = high_inputs ; 6951low_inputs .chat_template_kwargs ["reasoning_effort" ]= R"("low")" ; 6952auto low_params = common_chat_templates_apply (tmpls .get (),low_inputs ); 6953assert_not_contains (low_params .prompt ,deepseek_v4_reasoning_effort_max ); 6954assert_not_contains (low_params .prompt ,deepseek_v4_flash_0731_reasoning_effort_max ); 6955 6956common_chat_templates_inputs default_effort_inputs ; 6957default_effort_inputs .messages = {system_msg ,message_user }; 6958auto default_effort_params = common_chat_templates_apply (tmpls .get (),default_effort_inputs ); 6959assert_not_contains (default_effort_params .prompt ,deepseek_v4_reasoning_effort_max ); 6960assert_not_contains (default_effort_params .prompt ,deepseek_v4_flash_0731_reasoning_effort_max ); 6961 6962auto non_thinking_max_inputs = max_inputs ; 6963non_thinking_max_inputs .enable_thinking = false; 6964auto non_thinking_max_params = common_chat_templates_apply (tmpls .get (),non_thinking_max_inputs ); 6965assert_not_contains (non_thinking_max_params .prompt ,deepseek_v4_flash_0731_reasoning_effort_max ); 6966 6967common_chat_templates_inputs response_format_inputs ; 6968response_format_inputs .messages = {system_msg ,message_user }; 6969response_format_inputs .tools = {get_time_tool }; 6970response_format_inputs .json_schema = 6971R"({"type":"object","properties":{"answer":{"type":"string"}}})" ; 6972auto response_format_params = common_chat_templates_apply (tmpls .get (),response_format_inputs ); 6973assert_contains (response_format_params .prompt , 6974"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n" ); 6975assert_contains (response_format_params .prompt ,R"("answer": {"type": "string"})" ); 6976 } 6977 6978 { 6979auto tmpls = read_templates ("models/templates/openbmb-MiniCPM5-1B.jinja" ); 6980check (tmpls ,basic (),"<|im_start|>assistant\n<think>\n" ); 6981check (tmpls ,continuation_content (),"<|im_start|>assistant\n<think>\nI'm thinking\n</think>\n\nHello, " ); 6982check (tmpls ,continuation_reasoning (),"<|im_start|>assistant\n<think>\nI'm" ); 6983 } 6984} 6985 6986// Test the developer role to system workaround with a simple mock template 6987static void test_developer_role_to_system_workaround () { 6988LOG_DBG ("%s\n" ,__func__ ); 6989 6990// Simple mock template that supports system role 6991const std::string mock_template = 6992"{%- for message in messages -%}\n" 6993" {{- '<|' + message.role + '|>' + message.content + '<|end|>' -}}\n" 6994"{%- endfor -%}\n" 6995"{%- if add_generation_prompt -%}\n" 6996" {{- '<|assistant|>' -}}\n" 6997"{%- endif -%}" ; 6998 6999auto tmpls = common_chat_templates_ptr (common_chat_templates_init (/* model= */ nullptr ,mock_template )); 7000 7001// Test case 1: Developer message - should be changed to system 7002// After simplification we only test this case 7003 { 7004common_chat_templates_inputs inputs ; 7005common_chat_msg developer_msg ; 7006developer_msg .role = "developer" ; 7007developer_msg .content = "You are a helpful developer assistant." ; 7008inputs .messages = {developer_msg }; 7009inputs .add_generation_prompt = false; 7010 7011auto params = common_chat_templates_apply (tmpls .get (),inputs ); 7012 7013// The developer role should have been changed to system 7014if (params .prompt .find ("<|developer|>" )!= std::string::npos ) { 7015throw std::runtime_error ("Test failed: developer role was not changed to system" ); 7016 } 7017if (params .prompt .find ("<|system|>You are a helpful developer assistant.<|end|>" )== std::string::npos ) { 7018throw std::runtime_error ("Test failed: system message not found in output" ); 7019 } 7020LOG_ERR ("Test 1 passed: developer role changed to system\n" ); 7021 } 7022} 7023 7024// Verify reasoning-trace retention rules in the DeepSeek-V4 template: 7025// all traces are retained unless drop_thinking is true AND the conversation 7026// has no tool calls, in which case only the last (after-final-user) trace is 7027// kept and earlier ones are dropped. 7028static void test_deepseek_v4_thinking_retention () { 7029LOG_DBG ("%s\n" ,__func__ ); 7030 7031auto tmpls = read_templates ("models/templates/deepseek-ai-DeepSeek-V4.jinja" ); 7032 7033common_chat_msg user_q1 ;user_q1 .role = "user" ;user_q1 .content = "Question 1" ; 7034common_chat_msg user_q2 ;user_q2 .role = "user" ;user_q2 .content = "Question 2" ; 7035common_chat_msg asst_a1 = simple_assist_msg ("Answer 1" ,"thinking A1" ); 7036common_chat_msg asst_a2 = simple_assist_msg ("Answer 2" ,"thinking A2" ); 7037 7038common_chat_msg tool_assist = message_with_tool_calls ("special_function" ,"{\"arg1\": 1}" ); 7039common_chat_msg tool_result ;tool_result .role = "tool" ; 7040tool_result .tool_name = "special_function" ;tool_result .tool_call_id = "0" ;tool_result .content = "result" ; 7041 7042// The template uses U+FF5C as the role separator and literal think tags 7043// for the reasoning block. 7044const std::string asst_marker = "<\xef\xbd\x9c" "Assistant" "\xef\xbd\x9c>" ; 7045// Built via concatenation so the thinking tokens are not interpreted by 7046// tooling processing this source file. 7047const std::string think_start = "<" "think" ">" ; 7048const std::string think_end = "</" "think" ">" ; 7049 7050const std::string think_a1 = asst_marker + think_start + "thinking A1" + think_end ; 7051const std::string think_a2 = asst_marker + think_start + "thinking A2" + think_end ; 7052const std::string asst_no_think = asst_marker + think_end ; 7053 7054auto render = [& ](const std::vector < common_chat_msg > & messages ,bool drop_thinking ) { 7055common_chat_templates_inputs inputs ; 7056inputs .messages = messages ; 7057inputs .add_generation_prompt = false; 7058inputs .chat_template_kwargs ["thinking" ]= "true" ; 7059inputs .chat_template_kwargs ["drop_thinking" ]= drop_thinking ?"true" :"false" ; 7060return common_chat_templates_apply (tmpls .get (),inputs ).prompt ; 7061 }; 7062 7063// No tools, drop_thinking=false: all reasoning is retained. 7064 { 7065auto prompt = render ({user_q1 ,asst_a1 ,user_q2 ,asst_a2 },/* drop_thinking = */ false); 7066assert_contains (prompt ,think_a1 ); 7067assert_contains (prompt ,think_a2 ); 7068 } 7069 7070// No tools, drop_thinking=true: only the last reasoning trace is kept, 7071// earlier ones are dropped (the assistant block emits just the end token). 7072 { 7073auto prompt = render ({user_q1 ,asst_a1 ,user_q2 ,asst_a2 },/* drop_thinking = */ true); 7074assert_not_contains (prompt ,think_a1 ); 7075assert_contains (prompt ,think_a2 ); 7076// The dropped assistant turn still opens with the marker + bare end token. 7077assert_contains (prompt ,asst_no_think + "Answer 1" ); 7078 } 7079 7080// Single assistant turn, drop_thinking=true: the only trace is the last 7081// one, so it must be retained even with drop_thinking set. 7082 { 7083auto prompt = render ({user_q1 ,asst_a1 },/* drop_thinking = */ true); 7084assert_contains (prompt ,think_a1 ); 7085 } 7086 7087// Single assistant turn, drop_thinking=false: reasoning is retained. 7088 { 7089auto prompt = render ({user_q1 ,asst_a1 },/* drop_thinking = */ false); 7090assert_contains (prompt ,think_a1 ); 7091 } 7092 7093// With tool calls, drop_thinking=true: tool presence forces all reasoning 7094// to be retained, including the pre-tool-call trace. 7095 { 7096auto prompt = render ({user_q1 ,asst_a1 ,user_q2 ,tool_assist ,tool_result ,asst_a2 }, 7097/* drop_thinking = */ true); 7098assert_contains (prompt ,think_a1 ); 7099assert_contains (prompt ,think_a2 ); 7100 } 7101 7102// With tool calls, drop_thinking=false: all reasoning retained. 7103 { 7104auto prompt = render ({user_q1 ,asst_a1 ,user_q2 ,tool_assist ,tool_result ,asst_a2 }, 7105/* drop_thinking = */ false); 7106assert_contains (prompt ,think_a1 ); 7107assert_contains (prompt ,think_a2 ); 7108 } 7109} 7110 7111// Verify that consecutive tool results are rendered in the tool call order of the 7112// preceding assistant message (matched by tool call id), as required by the reference 7113// DeepSeek-V4 implementation. 7114static void test_deepseek_v4_tool_result_ordering () { 7115LOG_DBG ("%s\n" ,__func__ ); 7116 7117auto tmpls = read_templates ("models/templates/deepseek-ai-DeepSeek-V4.jinja" ); 7118 7119common_chat_msg user_q ;user_q .role = "user" ;user_q .content = "Question" ; 7120 7121common_chat_msg assist_calls ; 7122assist_calls .role = "assistant" ; 7123assist_calls .tool_calls .push_back ({"get_time" ,"{\"city\": \"Paris\"}" ,"call_1" }); 7124assist_calls .tool_calls .push_back ({"get_weather" ,"{\"city\": \"Paris\"}" ,"call_2" }); 7125 7126common_chat_msg time_result ;time_result .role = "tool" ; 7127time_result .tool_name = "get_time" ;time_result .tool_call_id = "call_1" ;time_result .content = "12:00" ; 7128common_chat_msg weather_result ;weather_result .role = "tool" ; 7129weather_result .tool_name = "get_weather" ;weather_result .tool_call_id = "call_2" ;weather_result .content = "sunny" ; 7130 7131auto render = [& ](const std::vector < common_chat_msg > & messages ) { 7132common_chat_templates_inputs inputs ; 7133inputs .messages = messages ; 7134inputs .add_generation_prompt = false; 7135return common_chat_templates_apply (tmpls .get (),inputs ).prompt ; 7136 }; 7137 7138// Results sent out of order are reordered to match the tool call order. 7139 { 7140auto prompt = render ({user_q ,assist_calls ,weather_result ,time_result }); 7141assert_contains (prompt ,"<tool_result>12:00</tool_result>\n\n<tool_result>sunny</tool_result>" ); 7142 } 7143 7144// Results already in call order stay put. 7145 { 7146auto prompt = render ({user_q ,assist_calls ,time_result ,weather_result }); 7147assert_contains (prompt ,"<tool_result>12:00</tool_result>\n\n<tool_result>sunny</tool_result>" ); 7148 } 7149 7150// Without tool call ids there is nothing to match against; order is preserved. 7151 { 7152auto no_id_calls = assist_calls ; 7153no_id_calls .tool_calls [0 ].id = "" ; 7154no_id_calls .tool_calls [1 ].id = "" ; 7155auto no_id_weather = weather_result ;no_id_weather .tool_call_id = "" ; 7156auto no_id_time = time_result ;no_id_time .tool_call_id = "" ; 7157auto prompt = render ({user_q ,no_id_calls ,no_id_weather ,no_id_time }); 7158assert_contains (prompt ,"<tool_result>sunny</tool_result>\n\n<tool_result>12:00</tool_result>" ); 7159 } 7160} 7161 7162static void test_reasoning_budget_tokens_per_request () { 7163LOG_DBG ("%s\n" ,__func__ ); 7164// Use Qwen3 template which has <think>...</think> reasoning markers. 7165// The autoparser detects them and sets thinking_start/end_tag, which enables 7166// the reasoning-budget code path in oaicompat_chat_params_parse. 7167auto tmpls = read_templates ("models/templates/Qwen-Qwen3-0.6B.jinja" ); 7168 7169server_chat_params opt ; 7170opt .tmpls = std::move (tmpls ); 7171opt .use_jinja = true; 7172opt .enable_thinking = true; 7173opt .reasoning_budget = -1 ; 7174opt .reasoning_format = COMMON_REASONING_FORMAT_NONE ; 7175 7176// Body with per-request reasoning_budget_tokens=0 (suppress thinking). 7177json body = { 7178 {"messages" , json::array ({json {{"role" ,"user" }, {"content" ,"hello" }}})}, 7179 {"reasoning_budget_tokens" ,0 }, 7180 }; 7181 std::vector < raw_buffer > out_files ; 7182auto llama_params = oaicompat_chat_params_parse (body ,opt ,out_files ); 7183 7184// The per-request value must win over the server default (-1). 7185if (!llama_params .contains ("reasoning_budget_tokens" )) { 7186throw std::runtime_error ("reasoning_budget_tokens missing from llama_params (thinking_end_tag may be empty for this template)" ); 7187 } 7188int got = llama_params ["reasoning_budget_tokens" ].get < int > (); 7189if (got != 0 ) { 7190throw std::runtime_error (std::string ("Expected reasoning_budget_tokens=0, got " )+ std::to_string (got )); 7191 } 7192} 7193 7194static void test_reasoning_budget_message_per_request () { 7195LOG_DBG ("%s\n" ,__func__ ); 7196// Same code path as test_reasoning_budget_tokens_per_request: the Qwen3 template's 7197// <think>...</think> markers enable the reasoning-budget block in oaicompat_chat_params_parse. 7198auto tmpls = read_templates ("models/templates/Qwen-Qwen3-0.6B.jinja" ); 7199 7200server_chat_params opt ; 7201opt .tmpls = std::move (tmpls ); 7202opt .use_jinja = true; 7203opt .enable_thinking = true; 7204opt .reasoning_budget = -1 ; 7205opt .reasoning_format = COMMON_REASONING_FORMAT_NONE ; 7206opt .reasoning_budget_message = "server default" ; 7207 7208// Body with a per-request reasoning_budget_message override. 7209const std::string per_request_message = "per-request message" ; 7210json body = { 7211 {"messages" , json::array ({json {{"role" ,"user" }, {"content" ,"hello" }}})}, 7212 {"reasoning_budget_message" ,per_request_message }, 7213 }; 7214 std::vector < raw_buffer > out_files ; 7215auto llama_params = oaicompat_chat_params_parse (body ,opt ,out_files ); 7216 7217// The per-request value must win over the server default. 7218if (!llama_params .contains ("reasoning_budget_message" )) { 7219throw std::runtime_error ("reasoning_budget_message missing from llama_params (thinking_end_tag may be empty for this template)" ); 7220 } 7221 std::string got = llama_params ["reasoning_budget_message" ].get < std::string > (); 7222if (got != per_request_message ) { 7223throw std::runtime_error ("Expected reasoning_budget_message='" + per_request_message + "', got '" + got + "'" ); 7224 } 7225} 7226 7227static void test_reasoning_effort_caps () { 7228LOG_DBG ("%s\n" ,__func__ ); 7229 7230auto assert_supports_effort = [](const std::string & path ,bool expected ) { 7231auto tmpls = read_templates (path ); 7232assert_equals (expected ,common_chat_templates_get_caps (tmpls .get ()).at ("supports_reasoning_effort" )); 7233 }; 7234 7235assert_supports_effort ("models/templates/deepseek-ai-DeepSeek-V4.jinja" , true); 7236assert_supports_effort ("models/templates/muse-glimmer.jinja" , true); 7237assert_supports_effort ("models/templates/tencent-Hy3.jinja" , true); 7238assert_supports_effort ("models/templates/openai-gpt-oss-120b.jinja" , true); 7239assert_supports_effort ("models/templates/upstage-Solar-Open-100B.jinja" , true); 7240assert_supports_effort ("models/templates/Cohere2MoE.jinja" , true); 7241assert_supports_effort ("models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja" , false); 7242assert_supports_effort ("models/templates/Qwen-Qwen3-0.6B.jinja" , false); 7243} 7244 7245static void test_msg_diffs_compute () { 7246LOG_DBG ("%s\n" ,__func__ ); 7247 { 7248common_chat_msg msg1 ; 7249 7250common_chat_msg msg2 ; 7251msg2 .content = "Hello, world!" ; 7252 7253common_chat_msg_diff diff ; 7254diff .content_delta = "Hello, world!" ; 7255 7256assert_equals ({diff }, common_chat_msg_diff::compute_diffs (msg1 ,msg2 )); 7257 } 7258 { 7259common_chat_msg msg1 ; 7260msg1 .content = "Hello," ; 7261 7262common_chat_msg msg2 ; 7263msg2 .content = "Hello, world!" ; 7264 7265common_chat_msg_diff diff ; 7266diff .content_delta = " world!" ; 7267 7268assert_equals ({diff }, common_chat_msg_diff::compute_diffs (msg1 ,msg2 )); 7269 } 7270 { 7271common_chat_msg msg0 ; 7272 7273common_chat_msg msg1 ; 7274msg1 .tool_calls = { 7275 {"special_function" ,"{\"ar" ,/* .id = */ "123" } 7276 }; 7277 7278common_chat_msg msg2 ; 7279msg2 .tool_calls = { 7280 {"special_function" ,"{\"arg1\": 1}" ,/* .id = */ "123" } 7281 }; 7282 7283common_chat_msg_diff diff01 ; 7284diff01 .tool_call_index = 0 ; 7285diff01 .tool_call_delta .name = "special_function" ; 7286diff01 .tool_call_delta .id = "123" ; 7287diff01 .tool_call_delta .arguments = "{\"ar" ; 7288 7289assert_equals ({diff01 }, common_chat_msg_diff::compute_diffs (msg0 ,msg1 )); 7290 7291common_chat_msg_diff diff12 ; 7292diff12 .tool_call_index = 0 ; 7293// Note: neither id nor name change here. 7294diff12 .tool_call_delta .arguments = "g1\": 1}" ; 7295 7296assert_equals ({diff12 }, common_chat_msg_diff::compute_diffs (msg1 ,msg2 )); 7297 } 7298 { 7299common_chat_msg msg0 ; 7300 7301common_chat_msg msg2 ; 7302msg2 .tool_calls = { 7303 {"f1" ,"{\"arg1\": 1}" ,/* .id = */ "123" }, 7304 {"f2" ,"{\"arg2\": 2}" ,/* .id = */ "222" }, 7305 }; 7306 7307common_chat_msg_diff diff1 ; 7308diff1 .tool_call_index = 0 ; 7309diff1 .tool_call_delta .name = "f1" ; 7310diff1 .tool_call_delta .id = "123" ; 7311diff1 .tool_call_delta .arguments = "{\"arg1\": 1}" ; 7312 7313common_chat_msg_diff diff2 ; 7314diff2 .tool_call_index = 1 ; 7315diff2 .tool_call_delta .name = "f2" ; 7316diff2 .tool_call_delta .id = "222" ; 7317diff2 .tool_call_delta .arguments = "{\"arg2\": 2}" ; 7318 7319assert_equals ({diff1 ,diff2 }, common_chat_msg_diff::compute_diffs (msg0 ,msg2 )); 7320 } 7321} 7322 7323int main (int argc ,char ** argv ) { 7324bool detailed_debug = false; 7325bool only_run_filtered = false; 7326 7327// Check for --template and --detailed flags 7328for (int i = 1 ;i < argc ;i ++ ) { 7329 std::string arg = argv [i ]; 7330if (arg == "--template" && i + 1 < argc ) { 7331g_template_filter = argv [++ i ]; 7332// Only run PEG parser tests with the filter 7333only_run_filtered = true; 7334 } 7335if (arg == "--detailed" ) { 7336detailed_debug = true; 7337common_log_set_verbosity_thold (999 ); 7338 } 7339if (arg == "--force-reconstruction-test" ) { 7340g_force_reconstruction_test = true; 7341only_run_filtered = true; 7342 } 7343 } 7344 7345if (only_run_filtered ) { 7346test_template_output_peg_parsers (detailed_debug ); 7347 std::cout <<"\n[chat] All template tests passed!" <<'\n' ; 7348return 0 ; 7349 } 7350 7351#ifndef _WIN32 7352// Check if any argument is a .jinja file (for template format detection mode) 7353bool has_jinja_files = false; 7354for (int i = 1 ;i < argc ;i ++ ) { 7355 std::string arg = argv [i ]; 7356if (arg == "--detailed" ) { 7357continue ; 7358 } 7359if (arg .size () >=6 && arg .rfind (".jinja" )== arg .size ()- 6 ) { 7360has_jinja_files = true; 7361break ; 7362 } 7363 } 7364 7365if (has_jinja_files ) { 7366common_chat_templates_inputs inputs ; 7367common_chat_msg msg ; 7368msg .role = "user" ; 7369msg .content = "Hey" ; 7370inputs .messages = {msg }; 7371inputs .tools = {special_function_tool }; 7372 7373 std::cout <<"| Template | Format |\n" ; 7374 std::cout <<"|----------|--------|\n" ; 7375 7376for (int i = 1 ;i < argc ;i ++ ) { 7377try { 7378 std::string path = argv [i ]; 7379if (path .rfind (".jinja" )!= path .size ()- 6 ) { 7380 std::cerr <<"Skipping non-jinja file: " <<path <<'\n' ; 7381continue ; 7382 } 7383auto tmpls = read_templates (path ); 7384auto parts = string_split (path ,"/" ); 7385const auto & name = parts [parts .size ()- 1 ]; 7386const auto * format = common_chat_format_name (common_chat_templates_apply (tmpls .get (),inputs ).format ); 7387 std::cout <<"| " <<name <<" | " <<format <<" |\n" ; 7388 }catch (const std::exception & e ) { 7389 std::cerr <<"Failed to process " <<argv [i ] <<": " <<e .what () <<'\n' ; 7390 } 7391 } 7392 }else 7393#endif 7394 { 7395test_msg_diffs_compute (); 7396test_msgs_oaicompat_json_conversion (); 7397test_msg_token_delimiters_split (); 7398test_tools_oaicompat_json_conversion (); 7399test_convert_responses_to_chatcmpl (); 7400test_developer_role_to_system_workaround (); 7401test_deepseek_v4_thinking_retention (); 7402test_deepseek_v4_tool_result_ordering (); 7403test_template_generation_prompt (); 7404test_reasoning_effort_caps (); 7405test_reasoning_budget_tokens_per_request (); 7406test_reasoning_budget_message_per_request (); 7407test_template_output_peg_parsers (detailed_debug ); 7408 std::cout <<"\n[chat] All tests passed!" <<'\n' ; 7409 } 7410return 0 ; 7411}