char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
4a08fa297
char/aliceai-t5-moe
1#include "chat-auto-parser-helpers.h" 2#include "chat-auto-parser.h" 3#include "chat-peg-parser.h" 4#include "chat.h" 5#include "gguf.h" 6#include "jinja/runtime.h" 7#include "log.h" 8#include "peg-parser.h" 9#include "testing.h" 10 11#include <cstdlib> 12#include <filesystem> 13#include <fstream> 14#include <iostream> 15#include <iterator> 16#include <optional> 17#include <sstream> 18#include <string> 19 20using namespace autoparser ; 21 22static void test_calculate_diff_split_basic (testing & t ); 23static void test_calculate_diff_split_identical (testing & t ); 24static void test_calculate_diff_split_common_prefix (testing & t ); 25static void test_calculate_diff_split_common_suffix (testing & t ); 26static void test_calculate_diff_split_common_both (testing & t ); 27static void test_calculate_diff_split_empty_cases (testing & t ); 28static void test_calculate_diff_split_no_common (testing & t ); 29static void test_calculate_diff_split_single_char (testing & t ); 30static void test_calculate_diff_split_overlaps (testing & t ); 31static void test_calculate_diff_split_tag_boundaries (testing & t ); 32static void test_calculate_diff_split_generation_prompt (testing & t ); 33static void test_calculate_diff_split (testing & t ); 34 35static void test_until_common_prefix_basic (testing & t ); 36static void test_until_common_prefix (testing & t ); 37 38static void test_after_common_suffix_basic (testing & t ); 39static void test_after_common_suffix (testing & t ); 40 41static void test_analyze_tool_call_pure_json (testing & t ); 42static void test_analyze_tool_call_function_name_markers (testing & t ); 43static void test_analyze_tool_call_full_markers (testing & t ); 44static void test_analyze_tool_call_edge_cases (testing & t ); 45 46static void test_compare_variants_basic (testing & t ); 47static void test_compare_variants_messages_modifier (testing & t ); 48static void test_compare_variants_tools_modifier (testing & t ); 49static void test_compare_variants_both_modifiers (testing & t ); 50static void test_compare_variants_template_failure (testing & t ); 51static void test_compare_variants_identity (testing & t ); 52static void test_compare_variants (testing & t ); 53 54// Seed-OSS template tool calling analysis tests 55static void test_seed_oss_tool_analysis (testing & t ); 56static void test_seed_oss_tool_presence (testing & t ); 57static void test_seed_oss_call_count (testing & t ); 58static void test_seed_oss_function_names (testing & t ); 59static void test_seed_oss_argument_count (testing & t ); 60static void test_seed_oss_args_presence (testing & t ); 61static void test_seed_oss_tool_with_reasoning (testing & t ); 62 63// Nemotron template analysis tests 64static void test_nemotron_analysis (testing & t ); 65static void test_nemotron_reasoning_detection (testing & t ); 66static void test_nemotron_tool_format (testing & t ); 67static void test_laguna_analysis (testing & t ); 68static void test_laguna_reasoning_detection (testing & t ); 69static void test_laguna_tool_format (testing & t ); 70static void test_laguna_s_analysis (testing & t ); 71static void test_laguna_s_reasoning_detection (testing & t ); 72static void test_laguna_s_tool_format (testing & t ); 73static void test_laguna_s_preserve_reasoning (testing & t ); 74static void test_laguna_xs2_analysis (testing & t ); 75static void test_laguna_xs2_reasoning_detection (testing & t ); 76static void test_laguna_xs2_tool_format (testing & t ); 77 78// CohereForAI template analysis tests 79static void test_cohere_reasoning_detection (testing & t ); 80static void test_cohere_analysis (testing & t ); 81 82// SmolLM3 template analysis tests 83static void test_smollm3_analysis (testing & t ); 84 85// Marker separation 86static void test_marker_separation (testing & t ); 87 88// standard_json_tools format tests 89static void test_standard_json_tools_formats (testing & t ); 90static void test_standard_json_tools_openai (testing & t ); 91static void test_standard_json_tools_cohere (testing & t ); 92static void test_standard_json_tools_function_key (testing & t ); 93 94// normalize_quotes_to_json tests 95static void test_normalize_quotes_to_json (testing & t ); 96static void test_normalize_quotes_with_embedded_quotes (testing & t ); 97 98// TAG_WITH_TAGGED argument parsing tests 99static void test_tagged_args_with_embedded_quotes (testing & t ); 100static void test_bailing_v3_tool_format (testing & t ); 101 102static void test_role_markers_all_templates (testing & t ); 103 104static json build_tools_definition (); 105 106// 107// debug mode: analyze a single template and dump the generated parser and grammar 108// 109 110enum class output_mode { 111ANALYSIS ,// Only output analysis results (default) 112TEMPLATE ,// Only output rendered template 113BOTH // Output both 114}; 115 116enum class input_message_type { 117NONE ,// Don't render any message scenarios (only analysis) 118CONTENT_ONLY ,// Simple assistant message with content 119REASONING_CONTENT ,// Message with reasoning_content + content 120TOOL_CALL_ONLY ,// Message with tool_calls only 121CONTENT_TOOL_CALL ,// Message with content + tool_calls 122REASONING_TOOL_CALL ,// Message with reasoning_content + tool_calls 123CONTENT_FAKE_TOOL_CALL ,// Message with content but no actual tool_calls (for testing) 124ALL // Render all scenarios 125}; 126 127struct debug_options { 128 std::string template_path ; 129bool with_tools = true; 130bool generation_prompt = true; 131bool enable_reasoning = true; 132bool debug_jinja = false; 133bool force_tool_call = false; 134bool parallel_tool_calls = true; 135output_mode mode = output_mode::BOTH ; 136input_message_type input_message = input_message_type::NONE ; 137}; 138 139static std::string read_file (const std::string & path ) { 140 std::ifstream fin (path , std::ios::binary ); 141if (!fin .is_open ()) { 142throw std::runtime_error ("Could not open file: " + path ); 143 } 144 std::ostringstream buf ; 145buf <<fin .rdbuf (); 146return buf .str (); 147} 148 149static std::string read_gguf_chat_template (const std::string & path ) { 150struct gguf_init_params params = {/*no_alloc =*/ true,// We only need metadata, not tensor data 151/*ctx=*/ nullptr }; 152 153struct gguf_context * ctx = gguf_init_from_file (path .c_str (),params ); 154if (ctx == nullptr ) { 155throw std::runtime_error ("Could not open GGUF file: " + path ); 156 } 157 158const char * key = "tokenizer.chat_template" ; 159int64_t key_id = gguf_find_key (ctx ,key ); 160 161if (key_id == -1 ) { 162gguf_free (ctx ); 163throw std::runtime_error ("GGUF file does not contain chat template key: " + std::string (key )); 164 } 165 166const char * template_str = gguf_get_val_str (ctx ,key_id ); 167if (template_str == nullptr ) { 168gguf_free (ctx ); 169throw std::runtime_error ("GGUF file contains chat template key but value is null" ); 170 } 171 172 std::string result = template_str ; 173gguf_free (ctx ); 174return result ; 175} 176 177static void print_usage (const char * program_name ) { 178LOG_ERR ("Test the chat template auto-parser; also usable as a debug tool that shows the generated PEG parser, GBNF grammar and triggers for a given template.\n" ); 179LOG_ERR ("\nUsage: %s [filter_regex] run the automated tests (default)\n" ,program_name ); 180LOG_ERR (" %s <template_or_gguf_path> [options] debug a single template\n" ,program_name ); 181LOG_ERR ("\nDebug mode options:\n" ); 182LOG_ERR (" --no-tools Disable tool definitions\n" ); 183LOG_ERR (" --force-tool-call Set tool calls to forced\n" ); 184LOG_ERR (" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n" ); 185LOG_ERR (" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n" ); 186LOG_ERR (" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n" ); 187LOG_ERR (" --output=MODE Output mode: analysis, template, both (default: both)\n" ); 188LOG_ERR (" --debug-jinja Enable Jinja fine-grained debug\n" ); 189LOG_ERR (" --input-message=TYPE Message type to render:\n" ); 190LOG_ERR (" content_only, reasoning_content, tool_call_only,\n" ); 191LOG_ERR (" content_tool_call, reasoning_tool_call,\n" ); 192LOG_ERR (" content_fake_tool_call, all\n" ); 193LOG_ERR ("\nExamples:\n" ); 194LOG_ERR (" %s template.jinja --input-message=all --generation-prompt=1\n" ,program_name ); 195LOG_ERR (" %s template.jinja --output=template --input-message=tool_call_only\n" ,program_name ); 196} 197 198static bool parse_bool_option (const std::string & value ) { 199return value == "1" || value == "true" || value == "yes" ; 200} 201 202static bool parse_debug_options (int argc ,char ** argv ,debug_options & opts ) { 203opts .template_path = argv [1 ]; 204 205for (int i = 2 ;i < argc ;++ i ) { 206 std::string arg = argv [i ]; 207 208if (arg == "--force-tool-call" ) { 209opts .force_tool_call = true; 210 }else if (arg == "--debug-jinja" ) { 211opts .debug_jinja = true; 212 }else if (arg == "--no-tools" ) { 213opts .with_tools = false; 214 }else if (arg .rfind ("--parallel-tool-calls=" ,0 )== 0 ) { 215opts .parallel_tool_calls = parse_bool_option (arg .substr (22 )); 216 }else if (arg .rfind ("--generation-prompt=" ,0 )== 0 ) { 217opts .generation_prompt = parse_bool_option (arg .substr (20 )); 218 }else if (arg .rfind ("--enable-reasoning=" ,0 )== 0 ) { 219opts .enable_reasoning = parse_bool_option (arg .substr (19 )); 220 }else if (arg .rfind ("--output=" ,0 )== 0 ) { 221 std::string mode = arg .substr (9 ); 222if (mode == "analysis" ) { 223opts .mode = output_mode::ANALYSIS ; 224 }else if (mode == "template" ) { 225opts .mode = output_mode::TEMPLATE ; 226 }else if (mode == "both" ) { 227opts .mode = output_mode::BOTH ; 228 }else { 229LOG_ERR ("Unknown output mode: %s\n" ,mode .c_str ()); 230return false; 231 } 232 }else if (arg .rfind ("--input-message=" ,0 )== 0 ) { 233 std::string type = arg .substr (16 ); 234if (type == "content_only" ) { 235opts .input_message = input_message_type::CONTENT_ONLY ; 236 }else if (type == "reasoning_content" ) { 237opts .input_message = input_message_type::REASONING_CONTENT ; 238 }else if (type == "tool_call_only" ) { 239opts .input_message = input_message_type::TOOL_CALL_ONLY ; 240 }else if (type == "content_tool_call" ) { 241opts .input_message = input_message_type::CONTENT_TOOL_CALL ; 242 }else if (type == "reasoning_tool_call" ) { 243opts .input_message = input_message_type::REASONING_TOOL_CALL ; 244 }else if (type == "content_fake_tool_call" ) { 245opts .input_message = input_message_type::CONTENT_FAKE_TOOL_CALL ; 246 }else if (type == "all" ) { 247opts .input_message = input_message_type::ALL ; 248 }else { 249LOG_ERR ("Unknown input message type: %s\n" ,type .c_str ()); 250return false; 251 } 252 }else { 253LOG_ERR ("Unknown option: %s\n" ,arg .c_str ()); 254print_usage (argv [0 ]); 255return false; 256 } 257 } 258 259return true; 260} 261 262static json build_debug_user_message () { 263return json { 264 {"role" ,"user" }, 265 {"content" ,"Hello, please help me with a task." } 266 }; 267} 268 269static json build_content_only_message () { 270return json { 271 {"role" ,"assistant" }, 272 {"content" ,"Hello! I'm here to help you with your task." } 273 }; 274} 275 276static json build_reasoning_content_message () { 277return json { 278 {"role" ,"assistant" }, 279 {"content" ,"Hello! I'm here to help you with your task." }, 280 {"reasoning_content" ,"The user is greeting me and asking for help. I should respond politely." } 281 }; 282} 283 284static json build_tool_call_only_message () { 285return json { 286 {"role" ,"assistant" }, 287 {"content" ,nullptr }, 288 {"tool_calls" , 289 json::array ({json { 290 {"type" ,"function" }, 291 {"function" ,json { {"name" ,"test_function_name" }, 292 {"arguments" , json::object ({ {"param1" ,"value1" }, {"param2" ,"value2" } }) } } }, 293 {"id" ,"123456789" } } }) } 294 }; 295} 296 297static json build_content_tool_call_message () { 298return json { 299 {"role" ,"assistant" }, 300 {"content" ,"I'll help you by calling a function." }, 301 {"tool_calls" , 302 json::array ({json { 303 {"type" ,"function" }, 304 {"function" , 305json { {"name" ,"test_function_name" }, 306 {"arguments" , json::object ({ {"param1" ,"value1" }, {"param2" ,"value2" } }) } } } } }) } 307 }; 308} 309 310static json build_reasoning_tool_call_message () { 311return json { 312 {"role" ,"assistant" }, 313 {"content" ,nullptr }, 314 {"reasoning_content" ,"I need to call a function to help with this task." }, 315 {"tool_calls" , 316 json::array ({json { 317 {"type" ,"function" }, 318 {"function" , 319json { {"name" ,"test_function_name" }, 320 {"arguments" , json::object ({ {"param1" ,"value1" }, {"param2" ,"value2" } }) } } } } }) } 321 }; 322} 323 324static json build_content_fake_tool_call_message () { 325// This message has content but NO tool_calls field 326// It's used to test if a template renders tool definitions but not tool calls 327return json { 328 {"role" ,"assistant" }, 329 {"content" ,"I'll help you by calling a function." } 330 }; 331} 332 333static void render_scenario (const common_chat_template & tmpl , 334const std::string & scenario_name , 335const json & messages , 336const json & tools , 337bool add_generation_prompt , 338bool enable_thinking ) { 339LOG_ERR ("\n=== Scenario: %s ===\n" ,scenario_name .c_str ()); 340LOG_ERR ("add_generation_prompt: %s, enable_thinking: %s\n" ,add_generation_prompt ?"true" :"false" , 341enable_thinking ?"true" :"false" ); 342 343// When add_generation_prompt is true, add a trailing user message to trigger the prompt 344json final_messages = messages ; 345if (add_generation_prompt && !messages .empty ()&& messages .back ().value ("role" ,"" )== "assistant" ) { 346final_messages .push_back (json { 347 {"role" ,"user" }, 348 {"content" ,"Now please continue with another response." } 349 }); 350 } 351 352LOG_ERR ("Messages:\n%s\n" ,final_messages .dump (2 ).c_str ()); 353 354try { 355generation_params inputs ; 356inputs .messages = final_messages ; 357inputs .add_generation_prompt = add_generation_prompt ; 358inputs .extra_context ["enable_thinking" ]= enable_thinking ; 359 360if (!tools .is_null ()&& tools .is_array ()&& !tools .empty ()) { 361inputs .tools = tools ; 362 } 363 364 std::string output = common_chat_template_direct_apply (tmpl ,inputs ); 365 366LOG_ERR ("\n--- Rendered Output ---\n" ); 367LOG_ERR ("%s\n" ,output .c_str ()); 368LOG_ERR ("--- End Output (length: %zu) ---\n" ,output .length ()); 369 }catch (const std::exception & e ) { 370LOG_ERR ("Rendering failed: %s\n" ,e .what ()); 371 } 372} 373 374static void render_all_scenarios (const common_chat_template & tmpl , 375const json & tools , 376bool add_generation_prompt , 377bool enable_thinking , 378input_message_type message_type ) { 379json user_msg = build_debug_user_message (); 380 381auto render_if = [& ](input_message_type type ,const std::string & name ,const json & assistant_msg ) { 382if (message_type == input_message_type::ALL || message_type == type ) { 383json messages = json::array ({user_msg ,assistant_msg }); 384render_scenario (tmpl ,name ,messages ,tools ,add_generation_prompt ,enable_thinking ); 385 } 386 }; 387 388render_if (input_message_type::CONTENT_ONLY ,"content_only" ,build_content_only_message ()); 389render_if (input_message_type::REASONING_CONTENT ,"reasoning_content" ,build_reasoning_content_message ()); 390render_if (input_message_type::TOOL_CALL_ONLY ,"tool_call_only" ,build_tool_call_only_message ()); 391render_if (input_message_type::CONTENT_TOOL_CALL ,"content_tool_call" ,build_content_tool_call_message ()); 392render_if (input_message_type::REASONING_TOOL_CALL ,"reasoning_tool_call" ,build_reasoning_tool_call_message ()); 393render_if (input_message_type::CONTENT_FAKE_TOOL_CALL ,"content_fake_tool_call" , 394build_content_fake_tool_call_message ()); 395 396// Also render with add_generation_prompt=true to show the prompt ending 397if (message_type == input_message_type::ALL ) { 398LOG_ERR ("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n" ); 399 400json prompt_messages = json::array ({user_msg }); 401render_scenario (tmpl ,"generation_prompt_only" ,prompt_messages ,tools , true,enable_thinking ); 402 403// With enable_thinking toggled 404render_scenario (tmpl ,"generation_prompt_thinking_disabled" ,prompt_messages ,tools , true, false); 405 } 406} 407 408static generation_params prepare_debug_params (const debug_options & opts ,const json & tools ) { 409generation_params params ; 410params .messages = json::array ({build_debug_user_message () }); 411params .reasoning_format = opts .enable_reasoning ?COMMON_REASONING_FORMAT_DEEPSEEK :COMMON_REASONING_FORMAT_NONE ; 412params .enable_thinking = opts .enable_reasoning ; 413params .add_generation_prompt = opts .generation_prompt ; 414 415if (opts .with_tools ) { 416params .tools = tools ; 417params .tool_choice = opts .force_tool_call ?COMMON_CHAT_TOOL_CHOICE_REQUIRED :COMMON_CHAT_TOOL_CHOICE_AUTO ; 418 }else { 419params .tools = json (); 420params .tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE ; 421 } 422params .parallel_tool_calls = opts .parallel_tool_calls ; 423return params ; 424} 425 426static int debug_single_template (const debug_options & opts ) { 427 std::string template_source ; 428try { 429// Check if the file is a GGUF file 430if (opts .template_path .size () >=5 && 431opts .template_path .compare (opts .template_path .size ()- 5 ,5 ,".gguf" )== 0 ) { 432template_source = read_gguf_chat_template (opts .template_path ); 433 }else { 434template_source = read_file (opts .template_path ); 435 } 436 }catch (const std::exception & e ) { 437LOG_ERR ("Error reading template: %s\n" ,e .what ()); 438return 1 ; 439 } 440 441LOG_ERR ("Analyzing template: %s\n" ,opts .template_path .c_str ()); 442LOG_ERR ("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n" ,opts .with_tools ?"true" :"false" , 443opts .generation_prompt ?"true" :"false" ,opts .enable_reasoning ?"true" :"false" ); 444 445try { 446common_chat_template chat_template (template_source ,"" ,"" ); 447 448json tools = opts .with_tools ?build_tools_definition () :json (); 449 450generation_params params = prepare_debug_params (opts ,tools ); 451common_chat_params parser_data ; 452if (std::optional < common_chat_params > spec_tmpl = 453common_chat_try_specialized_template (chat_template ,template_source ,params )) { 454LOG_ERR ("\n" ); 455LOG_ERR ("This template uses a specialized parser, analysis results will not be available.\n" ); 456parser_data = * spec_tmpl ; 457 }else { 458// Render template scenarios if requested 459if (opts .input_message != input_message_type::NONE && 460 (opts .mode == output_mode::TEMPLATE || opts .mode == output_mode::BOTH )) { 461LOG_ERR ("\n" ); 462LOG_ERR ("================================================================================\n" ); 463LOG_ERR (" TEMPLATE RENDERING OUTPUT\n" ); 464LOG_ERR ("================================================================================\n" ); 465 466render_all_scenarios (chat_template ,tools ,opts .generation_prompt ,opts .enable_reasoning , 467opts .input_message ); 468 } 469 470// Output analysis if requested 471if (opts .mode == output_mode::ANALYSIS || opts .mode == output_mode::BOTH ) { 472LOG_ERR ("\n" ); 473LOG_ERR ("================================================================================\n" ); 474LOG_ERR (" TEMPLATE ANALYSIS\n" ); 475LOG_ERR ("================================================================================\n" ); 476 477struct autoparser analysis ; 478analysis .analyze_template (chat_template ); 479 480// Generate Parser 481parser_data = peg_generator::generate_parser (chat_template ,params ,analysis ); 482 } 483 } 484 485if (!std::empty (parser_data .parser )) { 486LOG_ERR ("\n=== Generated Parser ===\n" ); 487common_peg_arena arena ; 488arena .load (parser_data .parser ); 489LOG_ERR ("%s\n" ,arena .dump (arena .root ()).c_str ()); 490 491LOG_ERR ("\n=== Generated Grammar ===\n" ); 492LOG_ERR ("%s\n" ,parser_data .grammar .c_str ()); 493 494LOG_ERR ("\n=== Generated Lazy Grammar ===\n" ); 495LOG_ERR ("%d\n" ,parser_data .grammar_lazy ); 496 497LOG_ERR ("\n=== Generated Grammar Triggers ===\n" ); 498for (const common_grammar_trigger & cgt :parser_data .grammar_triggers ) { 499LOG_ERR ("Token: %d | Type: %d | Value: %s\n" ,cgt .token ,cgt .type ,cgt .value .c_str ()); 500 } 501 502LOG_ERR ("\n=== Preserved Tokens ===\n" ); 503for (const std::string & token :parser_data .preserved_tokens ) { 504LOG_ERR (" '%s'\n" ,token .c_str ()); 505 } 506 } 507 }catch (const std::exception & e ) { 508LOG_ERR ("Analysis failed: %s\n" ,e .what ()); 509return 1 ; 510 } 511 512return 0 ; 513} 514 515int main (int argc ,char * argv []) { 516if (argc > 1 ) { 517 std::string arg = argv [1 ]; 518if (arg == "-h" || arg == "--help" ) { 519common_log_set_verbosity_thold (99 ); 520print_usage (argv [0 ]); 521return 0 ; 522 } 523 524// debug mode: if the first argument is an existing file, analyze that template instead of running the automated tests 525if (std::filesystem::is_regular_file (arg )) { 526common_log_set_verbosity_thold (99 ); 527 528debug_options opts ; 529if (!parse_debug_options (argc ,argv ,opts )) { 530return 1 ; 531 } 532 533if (opts .debug_jinja || std::getenv ("LLAMA_DEBUG_JINJA" )!= nullptr ) { 534 jinja::enable_debug (true); 535 } 536 537return debug_single_template (opts ); 538 } 539 } 540 541testing t (std::cout ); 542t .verbose = true; 543 544// usage: test-chat-auto-parser [filter_regex] 545 546if (argc > 1 ) { 547t .set_filter (argv [1 ]); 548 } 549 550t .test ("diff_split" ,test_calculate_diff_split ); 551t .test ("common_prefix" ,test_until_common_prefix ); 552t .test ("common_suffix" ,test_after_common_suffix ); 553t .test ("compare_variants" ,test_compare_variants ); 554t .test ("segments" ,test_marker_separation ); 555t .test ("seed_oss_diffs" ,test_seed_oss_tool_analysis ); 556t .test ("cohere" ,test_cohere_analysis ); 557t .test ("nemotron" ,test_nemotron_analysis ); 558t .test ("laguna" ,test_laguna_analysis ); 559t .test ("laguna-s" ,test_laguna_s_analysis ); 560t .test ("laguna-xs2" ,test_laguna_xs2_analysis ); 561t .test ("smollm3" ,test_smollm3_analysis ); 562t .test ("standard_json_tools" ,test_standard_json_tools_formats ); 563t .test ("normalize_quotes_to_json" ,test_normalize_quotes_to_json ); 564t .test ("tagged_args_embedded_quotes" ,test_tagged_args_with_embedded_quotes ); 565t .test ("bailing_v3" ,test_bailing_v3_tool_format ); 566t .test ("role_markers_all_templates" ,test_role_markers_all_templates ); 567 568return t .summary (); 569} 570 571static void test_marker_separation (testing & t ) { 572auto single_square_marker = segmentize_markers ("pre_marker[marker]post_marker" ); 573auto single_diag_marker = segmentize_markers ("pre_marker<marker>post_marker" ); 574auto paired_markers = segmentize_markers ("<hello>world</hello>" ); 575auto double_different_markers = segmentize_markers ("<hello>[hello]<world>[world]" ); 576auto in_between = segmentize_markers ("im<blue>daba<dee>da[hey]" ); 577 578t .test ("single_square_marker" , [& ] (testing & t ) { 579t .assert_equal ("first is text" , segment_type::TEXT ,single_square_marker [0 ].type ); 580t .assert_equal ("second is marker" , segment_type::MARKER ,single_square_marker [1 ].type ); 581t .assert_equal ("last is text" , segment_type::TEXT ,single_square_marker [2 ].type ); 582 583t .assert_equal ("first is 'pre_marker'" ,"pre_marker" ,single_square_marker [0 ].value ); 584t .assert_equal ("second is '[marker]'" ,"[marker]" ,single_square_marker [1 ].value ); 585t .assert_equal ("last is 'post_marker'" ,"post_marker" ,single_square_marker [2 ].value ); 586 }); 587 588t .test ("single_diagonal_marker" , [& ] (testing & t ) { 589t .assert_equal ("first is text" , segment_type::TEXT ,single_diag_marker [0 ].type ); 590t .assert_equal ("second is marker" , segment_type::MARKER ,single_diag_marker [1 ].type ); 591t .assert_equal ("last is text" , segment_type::TEXT ,single_diag_marker [2 ].type ); 592 593t .assert_equal ("first is 'pre_marker'" ,"pre_marker" ,single_diag_marker [0 ].value ); 594t .assert_equal ("second is '<marker>'" ,"<marker>" ,single_diag_marker [1 ].value ); 595t .assert_equal ("last is 'post_marker'" ,"post_marker" ,single_diag_marker [2 ].value ); 596 }); 597 598t .test ("paired_markers" , [& ] (testing & t ) { 599t .assert_equal ("first is marker" , segment_type::MARKER ,paired_markers [0 ].type ); 600t .assert_equal ("second is text" , segment_type::TEXT ,paired_markers [1 ].type ); 601t .assert_equal ("third is marker" , segment_type::MARKER ,paired_markers [2 ].type ); 602 603t .assert_equal ("first is '<hello>'" ,"<hello>" ,paired_markers [0 ].value ); 604t .assert_equal ("second is 'world'" ,"world" ,paired_markers [1 ].value ); 605t .assert_equal ("third is '</hello>'" ,"</hello>" ,paired_markers [2 ].value ); 606 }); 607 608t .test ("double_different_markers" , [& ] (testing & t ) { 609t .assert_equal ("first is marker" , segment_type::MARKER ,double_different_markers [0 ].type ); 610t .assert_equal ("second is marker" , segment_type::MARKER ,double_different_markers [1 ].type ); 611t .assert_equal ("third is marker" , segment_type::MARKER ,double_different_markers [2 ].type ); 612t .assert_equal ("fourth is marker" , segment_type::MARKER ,double_different_markers [3 ].type ); 613 614t .assert_equal ("first is '<hello>'" ,"<hello>" ,double_different_markers [0 ].value ); 615t .assert_equal ("second is '[hello]'" ,"[hello]" ,double_different_markers [1 ].value ); 616t .assert_equal ("third is '<world>'" ,"<world>" ,double_different_markers [2 ].value ); 617t .assert_equal ("fourth is '[world]'" ,"[world]" ,double_different_markers [3 ].value ); 618 }); 619 620t .test ("in_between" , [& ] (testing & t ) { 621t .assert_equal ("first is text" , segment_type::TEXT ,in_between [0 ].type ); 622t .assert_equal ("second is marker" , segment_type::MARKER ,in_between [1 ].type ); 623t .assert_equal ("third is text" , segment_type::TEXT ,in_between [2 ].type ); 624t .assert_equal ("fourth is marker" , segment_type::MARKER ,in_between [3 ].type ); 625t .assert_equal ("fifth is text" , segment_type::TEXT ,in_between [4 ].type ); 626t .assert_equal ("sixth is marker" , segment_type::MARKER ,in_between [5 ].type ); 627 628t .assert_equal ("first is 'im'" ,"im" ,in_between [0 ].value ); 629t .assert_equal ("second is '<blue>'" ,"<blue>" ,in_between [1 ].value ); 630t .assert_equal ("third is 'daba'" ,"daba" ,in_between [2 ].value ); 631t .assert_equal ("fourth is '<dee>'" ,"<dee>" ,in_between [3 ].value ); 632t .assert_equal ("fifth is 'da'" ,"da" ,in_between [4 ].value ); 633t .assert_equal ("sixth is '[hey]'" ,"[hey]" ,in_between [5 ].value ); 634 }); 635} 636 637static void test_calculate_diff_split (testing & t ) { 638t .test ("calculate_diff_split basic" ,test_calculate_diff_split_basic ); 639t .test ("calculate_diff_split identical" ,test_calculate_diff_split_identical ); 640t .test ("calculate_diff_split common prefix" ,test_calculate_diff_split_common_prefix ); 641t .test ("calculate_diff_split common suffix" ,test_calculate_diff_split_common_suffix ); 642t .test ("calculate_diff_split common both" ,test_calculate_diff_split_common_both ); 643t .test ("calculate_diff_split empty cases" ,test_calculate_diff_split_empty_cases ); 644t .test ("calculate_diff_split no common" ,test_calculate_diff_split_no_common ); 645t .test ("calculate_diff_split single char" ,test_calculate_diff_split_single_char ); 646t .test ("calculate_diff_split overlaps" ,test_calculate_diff_split_overlaps ); 647t .test ("calculate_diff_split tag boundaries" ,test_calculate_diff_split_tag_boundaries ); 648t .test ("calculate_diff_split generation prompt" ,test_calculate_diff_split_generation_prompt ); 649} 650 651static void test_calculate_diff_split_basic (testing & t ) { 652diff_split result = calculate_diff_split ("hello world" ,"hello test" ); 653t .assert_equal ("prefix should be 'hello '" ,"hello " ,result .prefix ); 654t .assert_equal ("left should be 'world'" ,"world" ,result .left ); 655t .assert_equal ("right should be 'test'" ,"test" ,result .right ); 656t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 657 658result = calculate_diff_split ("abc" ,"xyz" ); 659t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 660t .assert_equal ("left should be 'abc'" ,"abc" ,result .left ); 661t .assert_equal ("right should be 'xyz'" ,"xyz" ,result .right ); 662t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 663 664result = calculate_diff_split ("prefixA suffix" ,"prefixB suffix" ); 665t .assert_equal ("prefix should be 'prefix'" ,"prefix" ,result .prefix ); 666t .assert_equal ("left should be 'A'" ,"A" ,result .left ); 667t .assert_equal ("right should be 'B'" ,"B" ,result .right ); 668t .assert_equal ("suffix should be ' suffix'" ," suffix" ,result .suffix ); 669} 670 671static void test_calculate_diff_split_identical (testing & t ) { 672diff_split result = calculate_diff_split ("hello" ,"hello" ); 673t .assert_equal ("prefix should be 'hello'" ,"hello" ,result .prefix ); 674t .assert_equal ("left should be empty" ,"" ,result .left ); 675t .assert_equal ("right should be empty" ,"" ,result .right ); 676t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 677 678result = calculate_diff_split ("" ,"" ); 679t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 680t .assert_equal ("left should be empty" ,"" ,result .left ); 681t .assert_equal ("right should be empty" ,"" ,result .right ); 682t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 683 684result = calculate_diff_split ("a" ,"a" ); 685t .assert_equal ("prefix should be 'a'" ,"a" ,result .prefix ); 686t .assert_equal ("left should be empty" ,"" ,result .left ); 687t .assert_equal ("right should be empty" ,"" ,result .right ); 688t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 689 690result = calculate_diff_split ("<row><row><row><your><boat><gently>" ,"<row><row><row><your><boat><gently>" ); 691t .assert_equal ("prefix should be '<row><row><row><your><boat><gently>'" ,"<row><row><row><your><boat><gently>" ,result .prefix ); 692t .assert_equal ("left should be empty" ,"" ,result .left ); 693t .assert_equal ("right should be empty" ,"" ,result .right ); 694t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 695} 696 697static void test_calculate_diff_split_common_prefix (testing & t ) { 698diff_split result = calculate_diff_split ("abcdef" ,"abcxyz" ); 699t .assert_equal ("prefix should be 'abc'" ,"abc" ,result .prefix ); 700t .assert_equal ("left should be 'def'" ,"def" ,result .left ); 701t .assert_equal ("right should be 'xyz'" ,"xyz" ,result .right ); 702t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 703 704result = calculate_diff_split ("same" ,"sameagain" ); 705t .assert_equal ("prefix should be 'same'" ,"same" ,result .prefix ); 706t .assert_equal ("left should be empty" ,"" ,result .left ); 707t .assert_equal ("right should be 'again'" ,"again" ,result .right ); 708t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 709 710result = calculate_diff_split ("test" ,"testing" ); 711t .assert_equal ("prefix should be 'test'" ,"test" ,result .prefix ); 712t .assert_equal ("left should be empty" ,"" ,result .left ); 713t .assert_equal ("right should be 'ing'" ,"ing" ,result .right ); 714t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 715} 716 717static void test_calculate_diff_split_common_suffix (testing & t ) { 718diff_split result = calculate_diff_split ("123end" ,"456end" ); 719t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 720t .assert_equal ("left should be '123'" ,"123" ,result .left ); 721t .assert_equal ("right should be '456'" ,"456" ,result .right ); 722t .assert_equal ("suffix should be 'end'" ,"end" ,result .suffix ); 723 724result = calculate_diff_split ("start" ,"end" ); 725t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 726t .assert_equal ("left should be 'start'" ,"start" ,result .left ); 727t .assert_equal ("right should be 'end'" ,"end" ,result .right ); 728t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 729 730result = calculate_diff_split ("abcsuffix" ,"xyzsuffix" ); 731t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 732t .assert_equal ("left should be 'abc'" ,"abc" ,result .left ); 733t .assert_equal ("right should be 'xyz'" ,"xyz" ,result .right ); 734t .assert_equal ("suffix should be 'suffix'" ,"suffix" ,result .suffix ); 735} 736 737static void test_calculate_diff_split_common_both (testing & t ) { 738diff_split result = calculate_diff_split ("helloXworld" ,"helloYworld" ); 739t .assert_equal ("prefix should be 'hello'" ,"hello" ,result .prefix ); 740t .assert_equal ("left should be 'X'" ,"X" ,result .left ); 741t .assert_equal ("right should be 'Y'" ,"Y" ,result .right ); 742t .assert_equal ("suffix should be 'world'" ,"world" ,result .suffix ); 743 744result = calculate_diff_split ("ABCmiddleXYZ" ,"ABCdifferentXYZ" ); 745t .assert_equal ("prefix should be 'ABC'" ,"ABC" ,result .prefix ); 746t .assert_equal ("left should be 'middle'" ,"middle" ,result .left ); 747t .assert_equal ("right should be 'different'" ,"different" ,result .right ); 748t .assert_equal ("suffix should be 'XYZ'" ,"XYZ" ,result .suffix ); 749 750result = calculate_diff_split ("startAend" ,"startBend" ); 751t .assert_equal ("prefix should be 'start'" ,"start" ,result .prefix ); 752t .assert_equal ("left should be 'A'" ,"A" ,result .left ); 753t .assert_equal ("right should be 'B'" ,"B" ,result .right ); 754t .assert_equal ("suffix should be 'end'" ,"end" ,result .suffix ); 755 756// Edge case: common prefix and suffix overlap 757result = calculate_diff_split ("aa" ,"ab" ); 758t .assert_equal ("prefix should be 'a'" ,"a" ,result .prefix ); 759t .assert_equal ("left should be 'a'" ,"a" ,result .left ); 760t .assert_equal ("right should be 'b'" ,"b" ,result .right ); 761t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 762} 763 764static void test_calculate_diff_split_empty_cases (testing & t ) { 765// Empty left, non-empty right 766diff_split result = calculate_diff_split ("" ,"hello" ); 767t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 768t .assert_equal ("left should be empty" ,"" ,result .left ); 769t .assert_equal ("right should be 'hello'" ,"hello" ,result .right ); 770t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 771 772// Non-empty left, empty right 773result = calculate_diff_split ("hello" ,"" ); 774t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 775t .assert_equal ("left should be 'hello'" ,"hello" ,result .left ); 776t .assert_equal ("right should be empty" ,"" ,result .right ); 777t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 778 779// Both empty 780result = calculate_diff_split ("" ,"" ); 781t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 782t .assert_equal ("left should be empty" ,"" ,result .left ); 783t .assert_equal ("right should be empty" ,"" ,result .right ); 784t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 785 786// Left single char, empty right 787result = calculate_diff_split ("a" ,"" ); 788t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 789t .assert_equal ("left should be 'a'" ,"a" ,result .left ); 790t .assert_equal ("right should be empty" ,"" ,result .right ); 791t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 792 793// Empty left, right single char 794result = calculate_diff_split ("" ,"a" ); 795t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 796t .assert_equal ("left should be empty" ,"" ,result .left ); 797t .assert_equal ("right should be 'a'" ,"a" ,result .right ); 798t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 799} 800 801static void test_calculate_diff_split_no_common (testing & t ) { 802diff_split result = calculate_diff_split ("abc" ,"xyz" ); 803t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 804t .assert_equal ("left should be 'abc'" ,"abc" ,result .left ); 805t .assert_equal ("right should be 'xyz'" ,"xyz" ,result .right ); 806t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 807 808result = calculate_diff_split ("left" ,"right" ); 809// The algorithm finds "t" as a common suffix since both strings end with 't' 810// This is the algorithm's actual behavior - it finds maximal common suffix 811t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 812t .assert_equal ("left should be 'lef'" ,"lef" ,result .left ); 813t .assert_equal ("right should be 'righ'" ,"righ" ,result .right ); 814t .assert_equal ("suffix should be 't'" ,"t" ,result .suffix ); 815 816result = calculate_diff_split ("123" ,"456" ); 817t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 818t .assert_equal ("left should be '123'" ,"123" ,result .left ); 819t .assert_equal ("right should be '456'" ,"456" ,result .right ); 820t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 821} 822 823static void test_calculate_diff_split_single_char (testing & t ) { 824diff_split result = calculate_diff_split ("a" ,"b" ); 825t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 826t .assert_equal ("left should be 'a'" ,"a" ,result .left ); 827t .assert_equal ("right should be 'b'" ,"b" ,result .right ); 828t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 829 830result = calculate_diff_split ("a" ,"a" ); 831t .assert_equal ("prefix should be 'a'" ,"a" ,result .prefix ); 832t .assert_equal ("left should be empty" ,"" ,result .left ); 833t .assert_equal ("right should be empty" ,"" ,result .right ); 834t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 835 836result = calculate_diff_split ("a" ,"ab" ); 837t .assert_equal ("prefix should be 'a'" ,"a" ,result .prefix ); 838t .assert_equal ("left should be empty" ,"" ,result .left ); 839t .assert_equal ("right should be 'b'" ,"b" ,result .right ); 840t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 841 842result = calculate_diff_split ("ab" ,"a" ); 843t .assert_equal ("prefix should be 'a'" ,"a" ,result .prefix ); 844t .assert_equal ("left should be 'b'" ,"b" ,result .left ); 845t .assert_equal ("right should be empty" ,"" ,result .right ); 846t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 847} 848 849static void test_calculate_diff_split_overlaps (testing & t ) { 850// One string is substring of another 851diff_split result = calculate_diff_split ("test" ,"testing" ); 852t .assert_equal ("prefix should be 'test'" ,"test" ,result .prefix ); 853t .assert_equal ("left should be empty" ,"" ,result .left ); 854t .assert_equal ("right should be 'ing'" ,"ing" ,result .right ); 855t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 856 857result = calculate_diff_split ("testing" ,"test" ); 858t .assert_equal ("prefix should be 'test'" ,"test" ,result .prefix ); 859t .assert_equal ("left should be 'ing'" ,"ing" ,result .left ); 860t .assert_equal ("right should be empty" ,"" ,result .right ); 861t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 862 863// Similar strings with one extra char at start 864result = calculate_diff_split ("Xtest" ,"Ytest" ); 865// The algorithm finds "test" as a common suffix since both strings end with "test" 866// This is the algorithm's actual behavior - it finds maximal common suffix 867t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 868t .assert_equal ("left should be 'X'" ,"X" ,result .left ); 869t .assert_equal ("right should be 'Y'" ,"Y" ,result .right ); 870t .assert_equal ("suffix should be 'test'" ,"test" ,result .suffix ); 871 872// Similar strings with one extra char at end 873result = calculate_diff_split ("testX" ,"testY" ); 874t .assert_equal ("prefix should be 'test'" ,"test" ,result .prefix ); 875t .assert_equal ("left should be 'X'" ,"X" ,result .left ); 876t .assert_equal ("right should be 'Y'" ,"Y" ,result .right ); 877t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 878 879// Strings that are reverses 880result = calculate_diff_split ("abc" ,"cba" ); 881t .assert_equal ("prefix should be empty" ,"" ,result .prefix ); 882t .assert_equal ("left should be 'abc'" ,"abc" ,result .left ); 883t .assert_equal ("right should be 'cba'" ,"cba" ,result .right ); 884t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 885} 886 887static void test_calculate_diff_split_tag_boundaries (testing & t ) { 888// Test with unclosed XML tags 889diff_split result = calculate_diff_split ("test<tag" ,"test>content" ); 890// The fix_tag_boundaries should move incomplete tags appropriately 891t .assert_true ("prefix should start with 'test'" ,result .prefix .find ("test" )== 0 ); 892t .assert_true ("should handle tag boundaries" ,result .left != "" || result .right != "" || result .suffix != "" ); 893 894// Test with unclosed brackets 895result = calculate_diff_split ("test[" ,"test]value" ); 896t .assert_true ("should handle bracket boundaries" ,result .left != "" || result .right != "" || result .suffix != "" ); 897 898// Test with partial tags on both sides 899result = calculate_diff_split ("prefix<tag>" ,"prefix</tag>suffix" ); 900// fix_tag_boundaries moves the incomplete '<' from prefix to left/right 901t .assert_equal ("prefix should be 'prefix'" ,"prefix" ,result .prefix ); 902t .assert_equal ("left should be '<tag>'" ,"<tag>" ,result .left ); 903t .assert_equal ("right should be '</tag>suffix'" ,"</tag>suffix" ,result .right ); 904t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 905 906// Test with complex nested tags 907result = calculate_diff_split ("prefix<div>content</div>" ,"prefix<div>different</div>" ); 908// Algorithm finds "ent</div>" as a common suffix because both strings end with it 909// This is the actual algorithm behavior, though not semantically ideal 910t .assert_equal ("prefix should be 'prefix<div>'" ,"prefix<div>" ,result .prefix ); 911t .assert_equal ("left should be 'cont'" ,"cont" ,result .left ); 912t .assert_equal ("right should be 'differ'" ,"differ" ,result .right ); 913t .assert_equal ("suffix should be 'ent</div>'" ,"ent</div>" ,result .suffix ); 914 915// Test with unclosed angle bracket 916result = calculate_diff_split ("Hello <world>" ,"Hello test" ); 917t .assert_equal ("prefix should be 'Hello '" ,"Hello " ,result .prefix ); 918t .assert_true ("left should contain '<world>'" ,result .left .find ("<world>" )!= std::string::npos ); 919t .assert_equal ("right should be 'test'" ,"test" ,result .right ); 920t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 921 922// Test with unclosed square bracket 923result = calculate_diff_split ("test [array]" ,"test other" ); 924t .assert_equal ("prefix should be 'test '" ,"test " ,result .prefix ); 925t .assert_true ("left should contain '[array]'" ,result .left .find ("[array]" )!= std::string::npos ); 926t .assert_equal ("right should be 'other'" ,"other" ,result .right ); 927t .assert_equal ("suffix should be empty" ,"" ,result .suffix ); 928 929// Test empty prefix and suffix with tags 930result = calculate_diff_split ("<tag>left</tag>" ,"<tag>righ</tag>" ); 931t .assert_equal ("prefix should be '<tag>'" ,"<tag>" ,result .prefix ); 932t .assert_equal ("left should be 'left'" ,"left" ,result .left ); 933t .assert_equal ("right should be 'righ'" ,"righ" ,result .right ); 934t .assert_equal ("suffix should be '</tag>'" ,"</tag>" ,result .suffix ); 935 936 { 937// real case from template tests, simplified 938 std::string left = "PREFIX</think>Sure" ; 939 std::string right = "PREFIX<think>Lemme think</think>Sure" ; 940result = calculate_diff_split (left ,right ); 941t .assert_equal ("prefix should be PREFIX" ,"PREFIX" ,result .prefix ); 942t .assert_equal ("suffix should be </think>Sure" ,"</think>Sure" ,result .suffix ); 943t .assert_equal ("left should be empty" ,"" ,result .left ); 944t .assert_equal ("right should be <think>Lemme think" ,"<think>Lemme think" ,result .right ); 945 } 946 947 { 948// Real case: special tokens with |> boundary issue 949// The suffix starts with |> which should be moved to complete <|END_RESPONSE and <|END_ACTION 950 std::string prefix = "SOME_PREFIX" ; 951 std::string suffix = "|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>" ; 952 std::string left_diff = "<|START_RESPONSE|>Let me help you.<|END_RESPONSE" ; 953 std::string right_diff = 954"<|START_THINKING|><|END_THINKING|><|START_ACTION|>[\n" 955" {\"tool_call_id\": \"0\", \"tool_name\": \"test_function_name\", " 956"\"parameters\": {\"param1\": \"value1\", \"param2\": \"value2\"}}\n" 957"]<|END_ACTION" ; 958 959 std::string left = prefix + left_diff + suffix ; 960 std::string right = prefix + right_diff + suffix ; 961result = calculate_diff_split (left ,right ); 962 963t .assert_equal ("special token prefix" ,prefix ,result .prefix ); 964// The |> should be moved from suffix to complete the tokens 965t .assert_equal ("special token left" ,"<|START_RESPONSE|>Let me help you.<|END_RESPONSE|>" ,result .left ); 966t .assert_true ("special token right ends with |>" ,result .right .find ("<|END_ACTION|>" )!= std::string::npos ); 967t .assert_equal ("special token suffix" ,"<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>" , 968result .suffix ); 969 } 970} 971 972static void test_calculate_diff_split_generation_prompt (testing & t ) { 973// ChatML thinking template: left is a prefix of right, generation_prompt is the appended part. 974// The trailing \n in left matches the trailing \n in the generation_prompt, causing 975// the suffix matcher to steal it and rotate the diff result. 976 { 977// Simplified reproduction: left ends with \n, right = left + "<|im_start|>assistant\n<think>\n" 978 std::string left = "<|im_start|>user\nHello<|im_end|>\n" ; 979 std::string right = left + "<|im_start|>assistant\n<think>\n" ; 980diff_split result = calculate_diff_split (left ,right ); 981t .assert_equal ("chatml prefix" ,left ,result .prefix ); 982t .assert_equal ("chatml left" ,"" ,result .left ); 983t .assert_equal ("chatml right should be generation prompt" , 984"<|im_start|>assistant\n<think>\n" ,result .right ); 985t .assert_equal ("chatml suffix" ,"" ,result .suffix ); 986 } 987 988 { 989// More realistic: longer conversation ending with tool_response 990 std::string common = 991"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n" 992"<|im_start|>user\nSearch for files<|im_end|>\n" 993"<|im_start|>assistant\n<think>\nLet me search.\n</think>\n\n" 994"<tool_call>\n<function=search>\n</function>\n</tool_call><|im_end|>\n" 995"<|im_start|>user\n<tool_response>\nNo files found\n</tool_response><|im_end|>\n" ; 996 std::string left = common ; 997 std::string right = common + "<|im_start|>assistant\n<think>\n" ; 998diff_split result = calculate_diff_split (left ,right ); 999t .assert_equal ("tool_response left" ,"" ,result .left ); 1000t .assert_equal ("tool_response right should be generation prompt" , 1001"<|im_start|>assistant\n<think>\n" ,result .right ); 1002 } 1003} 1004 1005static void test_until_common_prefix (testing & t ) { 1006t .test ("until_common_prefix basic" ,test_until_common_prefix_basic ); 1007} 1008 1009static void test_until_common_prefix_basic (testing & t ) { 1010// Test case from the user request 1011 std::string result = until_common_prefix ("<function name=foo><arg name=bar>" ,"<arg name=bar>" ,"<arg name=baz>" ); 1012t .assert_equal ("untilCommonPrefix should return '<function name=foo>'" ,"<function name=foo>" ,result ); 1013 1014// Additional test cases to ensure robustness 1015// Test with different common prefix lengths 1016result = until_common_prefix ("prefix<test>suffix" ,"<test>different" ,"<test>other" ); 1017t .assert_equal ("should return 'prefix'" ,"prefix" ,result ); 1018 1019// Test when common prefix is at the start 1020result = until_common_prefix ("<common>rest" ,"<common>left" ,"<common>right" ); 1021t .assert_equal ("should return empty string when common prefix at start" ,"" ,result ); 1022 1023// Test when there's no common prefix 1024result = until_common_prefix ("something" ,"left" ,"right" ); 1025t .assert_equal ("should return empty string when no common prefix" ,"" ,result ); 1026 1027// Test with empty strings 1028result = until_common_prefix ("test" ,"" ,"right" ); 1029t .assert_equal ("should return empty string when left is empty" ,"" ,result ); 1030 1031// Test with longer common prefix 1032result = until_common_prefix ("abcXYZ<shared_prefix>rest" ,"<shared_prefix>left" ,"<shared_prefix>right" ); 1033t .assert_equal ("should return 'abcXYZ'" ,"abcXYZ" ,result ); 1034} 1035 1036static void test_after_common_suffix (testing & t ) { 1037t .test ("after_common_suffix basic" ,test_after_common_suffix_basic ); 1038} 1039 1040static void test_after_common_suffix_basic (testing & t ) { 1041// Test case from the user request 1042 std::string result = after_common_suffix ("<function name=foo><arg name=bar>100</arg></function>" , 1043"<arg name=bar>100</arg>" , 1044"<arg name=baz>535</arg>" ); 1045t .assert_equal ("afterCommonSuffix should return '</function>'" ,"</function>" ,result ); 1046 1047// Test when common suffix is at the end 1048result = after_common_suffix ("rest<common>" ,"left<common>" ,"right<common>" ); 1049t .assert_equal ("should return empty string when common suffix at end" ,"" ,result ); 1050 1051// Test with empty strings 1052result = after_common_suffix ("test" ,"left" ,"" ); 1053t .assert_equal ("should return empty string when right is empty" ,"" ,result ); 1054 1055// Test case with XML-like structure similar to the main example 1056result = after_common_suffix ("<outer><inner>value</inner></outer>" , 1057"<inner>value</inner>" , 1058"<inner>different</inner>" ); 1059t .assert_equal ("should return '</outer>'" ,"</outer>" ,result ); 1060 1061// Test with longer common suffix appearing at the end of full 1062result = after_common_suffix ("prefix<shared>rest</shared>" ,"prefix<shared>left</shared>" ,"prefix<shared>right</shared>" ); 1063t .assert_equal ("should return '' when common suffix is at end of full" ,"" ,result ); 1064 1065// Test with common suffix appearing in middle but not at end 1066result = after_common_suffix ("<tag>content</tag><extra>" ,"<tag>value</tag>" ,"<tag>other</tag>" ); 1067t .assert_equal ("should return '<extra>' when common suffix appears before end" ,"<extra>" ,result ); 1068 1069// Test with multi-character common suffix at the very end of full 1070result = after_common_suffix ("start<middle>end</middle>" ,"prefix<middle>left</middle>" ,"prefix<middle>right</middle>" ); 1071t .assert_equal ("should return '' when common suffix </middle> is at end of full" ,"" ,result ); 1072} 1073 1074static void test_compare_variants (testing & t ) { 1075t .test ("compare_variants basic" ,test_compare_variants_basic ); 1076t .test ("compare_variants messages modifier" ,test_compare_variants_messages_modifier ); 1077t .test ("compare_variants tools modifier" ,test_compare_variants_tools_modifier ); 1078t .test ("compare_variants both modifiers" ,test_compare_variants_both_modifiers ); 1079t .test ("compare_variants template failure" ,test_compare_variants_template_failure ); 1080t .test ("compare_variants identity" ,test_compare_variants_identity ); 1081} 1082 1083static void test_compare_variants_basic (testing & t ) { 1084// Create a simple template that just echoes messages 1085common_chat_template tmpl ("{{ messages[0]['content'] }}" ,"" ,"" ); 1086 1087template_params params ; 1088params .messages = json::array ({ 1089json {{"role" ,"user" }, {"content" ,"Hello" }} 1090 }); 1091 1092auto modifier = [](template_params & p ) { 1093p .messages [0 ]["content" ]= "World" ; 1094 }; 1095 1096auto result = ::compare_variants (tmpl ,params ,modifier ); 1097 1098if (!t .assert_true ("result should have value" ,result .has_value ())) { 1099return ; 1100 } 1101// The template might not output anything if messages is empty or format is different 1102// Check that we get a valid result 1103t .assert_true ("prefix or left should have content" , !result -> diff .prefix .empty ()|| !result -> diff .left .empty ()); 1104} 1105 1106static void test_compare_variants_messages_modifier (testing & t ) { 1107// Test with messages modifier only 1108common_chat_template tmpl ("{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}" ,"" ,"" ); 1109 1110template_params params ; 1111params .messages = json::array ({ 1112json {{"role" ,"user" }, {"content" ,"A" }} 1113 }); 1114 1115auto modifier = [](template_params & p ) { 1116p .messages [0 ]["content" ]= "B" ; 1117 }; 1118 1119 std::optional < compare_variants_result > result = ::compare_variants (tmpl ,params ,modifier ); 1120 1121if (!t .assert_true ("result should have value" ,result .has_value ())) { 1122return ; 1123 } 1124t .assert_equal ("left should be 'A'" ,"A" ,result -> diff .left ); 1125t .assert_equal ("right should be 'B'" ,"B" ,result -> diff .right ); 1126} 1127 1128static void test_compare_variants_tools_modifier (testing & t ) { 1129// Test with tools modifier only 1130common_chat_template tmpl ( 1131"{% for tool in tools %}{{ tool['name'] }}{% endfor %}" ,"" ,"" ); 1132 1133template_params params ; 1134params .tools = json::array ({ 1135json {{"name" ,"foo" }} 1136 }); 1137 1138auto modifier = [](template_params & p ) { 1139p .tools [0 ]["name" ]= "bar" ; 1140 }; 1141 1142auto result = ::compare_variants (tmpl ,params ,modifier ); 1143 1144if (!t .assert_true ("result should have value" ,result .has_value ())) { 1145return ; 1146 } 1147t .assert_equal ("left should be 'foo'" ,"foo" ,result -> diff .left ); 1148t .assert_equal ("right should be 'bar'" ,"bar" ,result -> diff .right ); 1149} 1150 1151static void test_compare_variants_both_modifiers (testing & t ) { 1152// Test with both messages and tools modifiers using the for loop approach 1153common_chat_template tmpl ( 1154"{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}" ,"" ,"" ); 1155 1156template_params params ; 1157params .messages = json::array ({ 1158json {{"role" ,"user" }, {"content" ,"A" }} 1159 }); 1160 1161auto modifier = [](template_params & p ) { 1162p .messages [0 ]["content" ]= "B" ; 1163p .messages [0 ]["role" ]= "newuser" ; 1164 }; 1165 1166auto result = ::compare_variants (tmpl ,params ,modifier ); 1167 1168if (!t .assert_true ("result should have value" ,result .has_value ())) { 1169return ; 1170 } 1171t .assert_equal ("left should be 'user:A'" ,"user:A" ,result -> diff .left ); 1172t .assert_equal ("right should be 'newuser:B'" ,"newuser:B" ,result -> diff .right ); 1173} 1174 1175static void test_compare_variants_template_failure (testing & t ) { 1176// Test with template that causes failure during application (not construction) 1177// We use a valid template syntax but one that will fail during application 1178common_chat_template tmpl ("{{ messages.cahoot()[0]['nonexistent_field'] }}" ,"" ,"" ); 1179 1180template_params params ; 1181params .messages = json::array ({ 1182json {{"role" ,"user" }, {"content" ,"Hello" }} 1183 }); 1184 1185auto modifier = [](template_params & p ) { 1186p .messages [0 ]["content" ]= "World" ; 1187 }; 1188 1189auto result = ::compare_variants (tmpl ,params ,modifier ); 1190 1191t .assert_true ("result should be nullopt on template failure" , !result .has_value ()); 1192} 1193 1194static void test_compare_variants_identity (testing & t ) { 1195// Test with identity modifier (no change) 1196common_chat_template tmpl ("{{ messages[0]['content'] }}" ,"" ,"" ); 1197 1198template_params params ; 1199params .messages = json::array ({ 1200json {{"role" ,"user" }, {"content" ,"Hello" }} 1201 }); 1202 1203// No modifier - should use identity 1204auto result = ::compare_variants (tmpl ,params ,nullptr ); 1205 1206if (!t .assert_true ("result should have value" ,result .has_value ())) { 1207return ; 1208 } 1209t .assert_equal ("prefix should be 'Hello'" ,"Hello" ,result -> diff .prefix ); 1210t .assert_equal ("left should be empty" ,"" ,result -> diff .left ); 1211t .assert_equal ("right should be empty" ,"" ,result -> diff .right ); 1212t .assert_equal ("suffix should be empty" ,"" ,result -> diff .suffix ); 1213} 1214 1215// ============================================================================ 1216// Seed-OSS Template Tool Calling Analysis Tests 1217// ============================================================================ 1218 1219static void test_seed_oss_tool_analysis (testing & t ) { 1220t .test ("Seed-OSS tool presence" ,test_seed_oss_tool_presence ); 1221t .test ("Seed-OSS call count" ,test_seed_oss_call_count ); 1222t .test ("Seed-OSS function names" ,test_seed_oss_function_names ); 1223t .test ("Seed-OSS argument count" ,test_seed_oss_argument_count ); 1224t .test ("Seed-OSS args presence" ,test_seed_oss_args_presence ); 1225t .test ("Seed-OSS tool with reasoning" ,test_seed_oss_tool_with_reasoning ); 1226} 1227 1228// Helper to load Seed-OSS template 1229static common_chat_template load_seed_oss_template (testing & t ) { 1230 std::string template_path = "models/templates/ByteDance-Seed-OSS.jinja" ; 1231 std::ifstream fin (template_path , std::ios::binary ); 1232 std::ostringstream buf ; 1233if (fin .is_open ()) { 1234buf <<fin .rdbuf (); 1235 } 1236 std::string template_source = buf .str (); 1237common_chat_template tmpl (template_source ,"" ,"" ); 1238t .assert_true ("Seed-OSS template loaded successfully" ,template_source .length ()> 0 ); 1239return tmpl ; 1240} 1241 1242// Helper to build tool call JSON 1243static json build_tool_call (const std::string & name ,const json & args ,const std::string & id = "call_001" ) { 1244return json { 1245 {"id" ,id }, 1246 {"type" ,"function" }, 1247 {"function" ,json { 1248 {"name" ,name }, 1249 {"arguments" ,args } 1250 }} 1251 }; 1252} 1253 1254// Helper to build tools definition 1255static json build_tools_definition () { 1256json parameters_schema = json::object (); 1257parameters_schema ["type" ]= "object" ; 1258parameters_schema ["properties" ]= json::object (); 1259parameters_schema ["properties" ]["param1" ]= json::object ({ 1260 {"type" ,"string" }, 1261 {"description" ,"First parameter" } 1262 }); 1263parameters_schema ["properties" ]["param2" ]= json::object ({ 1264 {"type" ,"string" }, 1265 {"description" ,"Second parameter" } 1266 }); 1267parameters_schema ["required" ]= json::array ({"param1" ,"param2" }); 1268 1269return json::array ({ 1270json { 1271 {"type" ,"function" }, 1272 {"function" ,json { 1273 {"name" ,"test_function_name" }, 1274 {"description" ,"A test function for debugging" }, 1275 {"parameters" ,parameters_schema } 1276 }} 1277 } 1278 }); 1279} 1280 1281// T1: Compare with/without tool call (user, assistant) 1282static void test_seed_oss_tool_presence (testing & t ) { 1283common_chat_template tmpl = load_seed_oss_template (t ); 1284 1285json assistant_no_tools = json { 1286 {"role" ,"assistant" }, 1287 {"content" ,"Let me help you." } 1288 }; 1289 1290json assistant_with_tools = json { 1291 {"role" ,"assistant" }, 1292 {"content" ,nullptr }, 1293 {"tool_calls" , json::array ({ 1294build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})) 1295 })} 1296 }; 1297 1298json user_msg = json { 1299 {"role" ,"user" }, 1300 {"content" ,"Hello, please help me." } 1301 }; 1302 1303template_params params_no_tools ; 1304params_no_tools .messages = json::array ({user_msg ,assistant_no_tools }); 1305params_no_tools .tools = build_tools_definition (); 1306params_no_tools .add_generation_prompt = false; 1307params_no_tools .enable_thinking = true; 1308 1309template_params params_with_tools ; 1310params_with_tools .messages = json::array ({user_msg ,assistant_with_tools }); 1311params_with_tools .tools = build_tools_definition (); 1312params_with_tools .add_generation_prompt = false; 1313params_with_tools .enable_thinking = true; 1314 1315auto result = ::compare_variants (tmpl ,params_no_tools , 1316 [& ](template_params & p ) { 1317p .messages = params_with_tools .messages ; 1318 }); 1319 1320if (!t .assert_true ("T1 result should have value" ,result .has_value ())) { 1321return ; 1322 } 1323 1324const auto & diff = result -> diff ; 1325t .assert_true ("T1 prefix should contain system" ,diff .prefix .find ("system" )!= std::string::npos ); 1326t .assert_true ("T1 prefix should contain user" ,diff .prefix .find ("user" )!= std::string::npos ); 1327t .assert_true ("T1 prefix should contain assistant" ,diff .prefix .find ("assistant" )!= std::string::npos ); 1328 1329// Left should be the assistant content without tool 1330t .assert_equal ("T1 left should contain 'Let me help you.'" ,"Let me help you." ,diff .left ); 1331 1332// Right should contain the tool call markers 1333t .assert_true ("T1 right should contain tool_call begin" ,diff .right .find ("<seed:tool_call>" )!= std::string::npos ); 1334t .assert_true ("T1 right should contain function tag" ,diff .right .find ("<function=test_function_name>" )!= std::string::npos ); 1335t .assert_true ("T1 right should contain parameter=param1" ,diff .right .find ("<parameter=param1>" )!= std::string::npos ); 1336t .assert_true ("T1 right should contain parameter=param2" ,diff .right .find ("<parameter=param2>" )!= std::string::npos ); 1337t .assert_true ("T1 right should contain value1" ,diff .right .find ("value1" )!= std::string::npos ); 1338t .assert_true ("T1 right should contain value2" ,diff .right .find ("value2" )!= std::string::npos ); 1339t .assert_true ("T1 right should contain tool_call end" ,diff .right .find ("</seed:tool_call>" )!= std::string::npos ); 1340 1341// Suffix should be the eos token 1342t .assert_equal ("T1 suffix should be '<seed:eos>'" ,"<seed:eos>" ,diff .suffix ); 1343} 1344 1345// T2: Compare one vs two tool calls 1346static void test_seed_oss_call_count (testing & t ) { 1347common_chat_template tmpl = load_seed_oss_template (t ); 1348 1349json assistant_one_call = json { 1350 {"role" ,"assistant" }, 1351 {"content" ,nullptr }, 1352 {"tool_calls" , json::array ({ 1353build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})) 1354 })} 1355 }; 1356 1357json assistant_two_calls = json { 1358 {"role" ,"assistant" }, 1359 {"content" ,nullptr }, 1360 {"tool_calls" , json::array ({ 1361build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})), 1362build_tool_call ("test_function_name" , json::object ({{"param1" ,"value3" }, {"param2" ,"value4" }}),"call_002" ) 1363 })} 1364 }; 1365 1366json user_msg = json { 1367 {"role" ,"user" }, 1368 {"content" ,"Hello, please help me." } 1369 }; 1370 1371template_params params_one ; 1372params_one .messages = json::array ({user_msg ,assistant_one_call }); 1373params_one .tools = build_tools_definition (); 1374params_one .add_generation_prompt = false; 1375params_one .enable_thinking = true; 1376 1377auto result = ::compare_variants (tmpl ,params_one , 1378 [& ](template_params & p ) { 1379p .messages = json::array ({user_msg ,assistant_two_calls }); 1380 }); 1381 1382if (!t .assert_true ("T2 result should have value" ,result .has_value ())) { 1383return ; 1384 } 1385 1386const auto & diff = result -> diff ; 1387 1388// Prefix should include the first tool call 1389t .assert_true ("T2 prefix should contain first tool_call begin" ,diff .prefix .find ("<seed:tool_call>" )!= std::string::npos ); 1390t .assert_true ("T2 prefix should contain first function" ,diff .prefix .find ("<function=test_function_name>" )!= std::string::npos ); 1391t .assert_true ("T2 prefix should contain value1" ,diff .prefix .find ("value1" )!= std::string::npos ); 1392t .assert_true ("T2 prefix should contain value2" ,diff .prefix .find ("value2" )!= std::string::npos ); 1393t .assert_true ("T2 prefix should contain first tool_call end" ,diff .prefix .find ("</seed:tool_call>" )!= std::string::npos ); 1394 1395// Left should be empty (no second tool call in variant A) 1396t .assert_equal ("T2 left should be empty" ,"" ,diff .left ); 1397 1398// Right should contain the second tool call 1399t .assert_true ("T2 right should contain second tool_call begin" ,diff .right .find ("<seed:tool_call>" )!= std::string::npos ); 1400t .assert_true ("T2 right should contain second function" ,diff .right .find ("<function=test_function_name>" )!= std::string::npos ); 1401t .assert_true ("T2 right should contain value3" ,diff .right .find ("value3" )!= std::string::npos ); 1402t .assert_true ("T2 right should contain value4" ,diff .right .find ("value4" )!= std::string::npos ); 1403t .assert_true ("T2 right should contain second tool_call end" ,diff .right .find ("</seed:tool_call>" )!= std::string::npos ); 1404 1405// Suffix should end with the eos token 1406t .assert_equal ("T2 suffix should end with '<seed:eos>'" ,"<seed:eos>" ,diff .suffix .substr (diff .suffix .length ()- 10 ,10 )); 1407} 1408 1409// T3: Compare different function names 1410static void test_seed_oss_function_names (testing & t ) { 1411common_chat_template tmpl = load_seed_oss_template (t ); 1412 1413// Build tools with two different function names 1414json parameters_schema = json::object (); 1415parameters_schema ["type" ]= "object" ; 1416parameters_schema ["properties" ]= json::object (); 1417parameters_schema ["properties" ]["arg1" ]= json::object ({ 1418 {"type" ,"string" }, 1419 {"description" ,"Argument 1" } 1420 }); 1421parameters_schema ["required" ]= json::array ({"arg1" }); 1422 1423json tools = json::array ({ 1424json { 1425 {"type" ,"function" }, 1426 {"function" ,json { 1427 {"name" ,"func_alpha" }, 1428 {"description" ,"First function" }, 1429 {"parameters" ,parameters_schema } 1430 }} 1431 }, 1432json { 1433 {"type" ,"function" }, 1434 {"function" ,json { 1435 {"name" ,"func_beta" }, 1436 {"description" ,"Second function" }, 1437 {"parameters" ,parameters_schema } 1438 }} 1439 } 1440 }); 1441 1442json assistant_func_alpha = json { 1443 {"role" ,"assistant" }, 1444 {"content" ,nullptr }, 1445 {"tool_calls" , json::array ({ 1446build_tool_call ("func_alpha" , json::object ({{"arg1" ,"test_value" }})) 1447 })} 1448 }; 1449 1450json assistant_func_beta = json { 1451 {"role" ,"assistant" }, 1452 {"content" ,nullptr }, 1453 {"tool_calls" , json::array ({ 1454build_tool_call ("func_beta" , json::object ({{"arg1" ,"test_value" }})) 1455 })} 1456 }; 1457 1458json user_msg = json { 1459 {"role" ,"user" }, 1460 {"content" ,"Hello" } 1461 }; 1462 1463template_params params_alpha ; 1464params_alpha .messages = json::array ({user_msg ,assistant_func_alpha }); 1465params_alpha .tools = tools ; 1466params_alpha .add_generation_prompt = false; 1467params_alpha .enable_thinking = true; 1468 1469auto result = ::compare_variants (tmpl ,params_alpha , 1470 [& ](template_params & p ) { 1471p .messages = json::array ({user_msg ,assistant_func_beta }); 1472 }); 1473 1474if (!t .assert_true ("T3 result should have value" ,result .has_value ())) { 1475return ; 1476 } 1477 1478const auto & diff = result -> diff ; 1479 1480bool func_alpha_in_left = diff .left .find ("func_alpha" )!= std::string::npos ; 1481bool func_alpha_in_prefix = diff .prefix .find ("func_alpha" )!= std::string::npos ; 1482bool func_beta_in_right = diff .right .find ("func_beta" )!= std::string::npos ; 1483bool func_beta_in_prefix = diff .prefix .find ("func_beta" )!= std::string::npos ; 1484bool func_beta_in_suffix = diff .suffix .find ("func_beta" )!= std::string::npos ; 1485 1486// Left should contain func_alpha (or be in prefix) 1487t .assert_true ("T3 left should contain func_alpha (or prefix)" ,func_alpha_in_left || func_alpha_in_prefix ); 1488 1489// Right should contain func_beta 1490t .assert_true ("T3 right should contain func_beta" ,func_beta_in_right || func_beta_in_prefix || func_beta_in_suffix ); 1491 1492// Both should have the same parameter value (in common parts, not in diffs) 1493// Since both have same args, test_value will be in prefix/suffix 1494t .assert_true ("T3 diff should contain test_value (in prefix or suffix)" , 1495diff .prefix .find ("test_value" )!= std::string::npos || diff .suffix .find ("test_value" )!= std::string::npos ); 1496} 1497 1498// T4: Compare different argument counts (zero, one, two parameters) 1499static void test_seed_oss_argument_count (testing & t ) { 1500common_chat_template tmpl = load_seed_oss_template (t ); 1501 1502// Build tools with 0, 1, or 2 required parameters 1503json params_2_required = json::object (); 1504params_2_required ["type" ]= "object" ; 1505params_2_required ["properties" ]= json::object (); 1506params_2_required ["properties" ]["arg1" ]= json::object ({ 1507 {"type" ,"string" }, 1508 {"description" ,"Argument 1" } 1509 }); 1510params_2_required ["properties" ]["arg2" ]= json::object ({ 1511 {"type" ,"string" }, 1512 {"description" ,"Argument 2" } 1513 }); 1514params_2_required ["required" ]= json::array ({"arg1" ,"arg2" }); 1515 1516json params_1_required = json::object (); 1517params_1_required ["type" ]= "object" ; 1518params_1_required ["properties" ]= json::object (); 1519params_1_required ["properties" ]["arg1" ]= json::object ({ 1520 {"type" ,"string" }, 1521 {"description" ,"Argument 1" } 1522 }); 1523params_1_required ["required" ]= json::array ({"arg1" }); 1524 1525json tools = json::array ({ 1526json { 1527 {"type" ,"function" }, 1528 {"function" ,json { 1529 {"name" ,"test_func" }, 1530 {"description" ,"Test function" }, 1531 {"parameters" ,params_2_required } 1532 }} 1533 } 1534 }); 1535 1536// Test: zero args vs one arg 1537json assistant_zero_args = json { 1538 {"role" ,"assistant" }, 1539 {"content" ,nullptr }, 1540 {"tool_calls" , json::array ({ 1541build_tool_call ("test_func" , json::object ()) 1542 })} 1543 }; 1544 1545json assistant_one_arg = json { 1546 {"role" ,"assistant" }, 1547 {"content" ,nullptr }, 1548 {"tool_calls" , json::array ({ 1549build_tool_call ("test_func" , json::object ({{"arg1" ,"value1" }})) 1550 })} 1551 }; 1552 1553json assistant_two_args = json { 1554 {"role" ,"assistant" }, 1555 {"content" ,nullptr }, 1556 {"tool_calls" , json::array ({ 1557build_tool_call ("test_func" , json::object ({{"arg1" ,"value1" }, {"arg2" ,"value2" }})) 1558 })} 1559 }; 1560 1561json user_msg = json { 1562 {"role" ,"user" }, 1563 {"content" ,"Hello" } 1564 }; 1565 1566// Test zero vs one 1567template_params params_zero ; 1568params_zero .messages = json::array ({user_msg ,assistant_zero_args }); 1569params_zero .tools = tools ; 1570params_zero .add_generation_prompt = false; 1571params_zero .enable_thinking = true; 1572 1573auto result_zero_one = ::compare_variants (tmpl ,params_zero , 1574 [& ](template_params & p ) { 1575p .messages = json::array ({user_msg ,assistant_one_arg }); 1576 }); 1577 1578if (!t .assert_true ("T4 zero vs one result should have value" ,result_zero_one .has_value ())) { 1579return ; 1580 } 1581t .assert_true ("T4 zero vs one left should be empty or minimal" ,result_zero_one -> diff .left .empty ()|| result_zero_one -> diff .left == "" ); 1582t .assert_true ("T4 zero vs one right should contain arg1" ,result_zero_one -> diff .right .find ("arg1" )!= std::string::npos ); 1583 1584// Test one vs two 1585template_params params_one ; 1586params_one .messages = json::array ({user_msg ,assistant_one_arg }); 1587params_one .tools = tools ; 1588params_one .add_generation_prompt = false; 1589params_one .enable_thinking = true; 1590 1591auto result_one_two = ::compare_variants (tmpl ,params_one , 1592 [& ](template_params & p ) { 1593p .messages = json::array ({user_msg ,assistant_two_args }); 1594 }); 1595 1596if (!t .assert_true ("T4 one vs two result should have value" ,result_one_two .has_value ())) { 1597return ; 1598 } 1599 1600const auto & diff4 = result_one_two -> diff ; 1601t .assert_true ("T4 one vs two left should contain arg1 (or prefix)" , 1602diff4 .left .find ("arg1" )!= std::string::npos || diff4 .prefix .find ("arg1" )!= std::string::npos ); 1603t .assert_true ("T4 one vs two right should contain arg1 (or prefix)" , 1604diff4 .right .find ("arg1" )!= std::string::npos || diff4 .prefix .find ("arg1" )!= std::string::npos ); 1605t .assert_true ("T4 one vs two right should contain arg2 (or prefix/suffix)" , 1606diff4 .right .find ("arg2" )!= std::string::npos || diff4 .prefix .find ("arg2" )!= std::string::npos || diff4 .suffix .find ("arg2" )!= std::string::npos ); 1607} 1608 1609// T5: Compare different argument values 1610static void test_seed_oss_args_presence (testing & t ) { 1611common_chat_template tmpl = load_seed_oss_template (t ); 1612 1613json assistant_same_arg = json { 1614 {"role" ,"assistant" }, 1615 {"content" ,nullptr }, 1616 {"tool_calls" , json::array ({ 1617build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }})) 1618 })} 1619 }; 1620 1621json assistant_other_arg = json { 1622 {"role" ,"assistant" }, 1623 {"content" ,nullptr }, 1624 {"tool_calls" , json::array ({ 1625build_tool_call ("test_function_name" , json::object ({{"param2" ,"value2" }})) 1626 })} 1627 }; 1628 1629json assistant_both_args = json { 1630 {"role" ,"assistant" }, 1631 {"content" ,nullptr }, 1632 {"tool_calls" , json::array ({ 1633build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})) 1634 })} 1635 }; 1636 1637json user_msg = json { 1638 {"role" ,"user" }, 1639 {"content" ,"Hello" } 1640 }; 1641 1642template_params params_same ; 1643params_same .messages = json::array ({user_msg ,assistant_same_arg }); 1644params_same .tools = build_tools_definition (); 1645params_same .add_generation_prompt = false; 1646params_same .enable_thinking = true; 1647 1648// Test same arg vs other arg 1649auto result_same_other = ::compare_variants (tmpl ,params_same , 1650 [& ](template_params & p ) { 1651p .messages = json::array ({user_msg ,assistant_other_arg }); 1652 }); 1653 1654if (!t .assert_true ("T5 same vs other result should have value" ,result_same_other .has_value ())) { 1655return ; 1656 } 1657const auto & diff5a = result_same_other -> diff ; 1658t .assert_true ("T5 same vs other left should contain param1 (or prefix/suffix)" , 1659diff5a .left .find ("param1" )!= std::string::npos || diff5a .prefix .find ("param1" )!= std::string::npos || diff5a .suffix .find ("param1" )!= std::string::npos ); 1660t .assert_true ("T5 same vs other left should contain value1 (or prefix/suffix)" , 1661diff5a .left .find ("value1" )!= std::string::npos || diff5a .prefix .find ("value1" )!= std::string::npos ); 1662t .assert_true ("T5 same vs other right should contain param2 (or prefix/suffix)" , 1663diff5a .right .find ("param2" )!= std::string::npos || diff5a .prefix .find ("param2" )!= std::string::npos || diff5a .suffix .find ("param2" )!= std::string::npos ); 1664t .assert_true ("T5 same vs other right should contain value2 (or prefix/suffix)" , 1665diff5a .right .find ("value2" )!= std::string::npos || diff5a .prefix .find ("value2" )!= std::string::npos || diff5a .suffix .find ("value2" )!= std::string::npos ); 1666 1667// Test same arg vs both args 1668auto result_same_both = ::compare_variants (tmpl ,params_same , 1669 [& ](template_params & p ) { 1670p .messages = json::array ({user_msg ,assistant_both_args }); 1671 }); 1672 1673if (!t .assert_true ("T5 same vs both result should have value" ,result_same_both .has_value ())) { 1674return ; 1675 } 1676const auto & diff5b = result_same_both -> diff ; 1677t .assert_true ("T5 same vs both left should contain param1 (or prefix/suffix)" , 1678diff5b .left .find ("param1" )!= std::string::npos || diff5b .prefix .find ("param1" )!= std::string::npos || diff5b .suffix .find ("param1" )!= std::string::npos ); 1679t .assert_true ("T5 same vs both right should contain param1 (or prefix/suffix)" , 1680diff5b .right .find ("param1" )!= std::string::npos || diff5b .prefix .find ("param1" )!= std::string::npos || diff5b .suffix .find ("param1" )!= std::string::npos ); 1681t .assert_true ("T5 same vs both right should contain param2 (or prefix/suffix)" , 1682diff5b .right .find ("param2" )!= std::string::npos || diff5b .prefix .find ("param2" )!= std::string::npos || diff5b .suffix .find ("param2" )!= std::string::npos ); 1683} 1684 1685// T6: Tool call with vs without reasoning_content 1686static void test_seed_oss_tool_with_reasoning (testing & t ) { 1687common_chat_template tmpl = load_seed_oss_template (t ); 1688 1689json assistant_tool_only = json { 1690 {"role" ,"assistant" }, 1691 {"content" ,nullptr }, 1692 {"tool_calls" , json::array ({ 1693build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})) 1694 })} 1695 }; 1696 1697json assistant_tool_with_reasoning = json { 1698 {"role" ,"assistant" }, 1699 {"content" ,nullptr }, 1700 {"tool_calls" , json::array ({ 1701build_tool_call ("test_function_name" , json::object ({{"param1" ,"value1" }, {"param2" ,"value2" }})) 1702 })}, 1703 {"reasoning_content" ,"I need to call the tool first." } 1704 }; 1705 1706json user_msg = json { 1707 {"role" ,"user" }, 1708 {"content" ,"Hello, please help me." } 1709 }; 1710 1711template_params params_tool_only ; 1712params_tool_only .messages = json::array ({user_msg ,assistant_tool_only }); 1713params_tool_only .tools = build_tools_definition (); 1714params_tool_only .add_generation_prompt = false; 1715params_tool_only .enable_thinking = true; 1716 1717auto result = ::compare_variants (tmpl ,params_tool_only , 1718 [& ](template_params & p ) { 1719p .messages = json::array ({user_msg ,assistant_tool_with_reasoning }); 1720 }); 1721 1722if (!t .assert_true ("T6 result should have value" ,result .has_value ())) { 1723return ; 1724 } 1725 1726const auto & diff = result -> diff ; 1727 1728// Left should be empty (no reasoning in variant A) 1729t .assert_equal ("T6 left should be empty" ,"" ,diff .left ); 1730 1731// Right should contain the thinking token with reasoning content 1732t .assert_true ("T6 right should contain think begin" ,diff .right .find ("<seed:think>" )!= std::string::npos ); 1733t .assert_true ("T6 right should contain reasoning content" ,diff .right .find ("I need to call the tool first." )!= std::string::npos ); 1734t .assert_true ("T6 right should contain think end" ,diff .right .find ("</seed:think>" )!= std::string::npos ); 1735 1736// Prefix should contain the assistant role 1737t .assert_true ("T6 prefix should contain assistant" ,diff .prefix .find ("assistant" )!= std::string::npos ); 1738 1739// Suffix should contain the tool call 1740t .assert_true ("T6 suffix should contain tool_call begin" ,diff .suffix .find ("<seed:tool_call>" )!= std::string::npos ); 1741t .assert_true ("T6 suffix should contain function name" ,diff .suffix .find ("test_function_name" )!= std::string::npos ); 1742t .assert_true ("T6 suffix should contain eos" ,diff .suffix .find ("<seed:eos>" )!= std::string::npos ); 1743} 1744 1745static common_chat_template load_template (testing & t ,const std::string & template_path ) { 1746 std::ifstream fin (template_path , std::ios::binary ); 1747 std::ostringstream buf ; 1748if (fin .is_open ()) { 1749buf <<fin .rdbuf (); 1750 } 1751 std::string template_source = buf .str (); 1752common_chat_template tmpl (template_source ,"" ,"" ); 1753t .assert_true ("Nemotron template loaded successfully" ,template_source .length ()> 0 ); 1754return tmpl ; 1755} 1756 1757// ============================================================================ 1758// Nemotron Template Analysis Tests 1759// ============================================================================ 1760static common_chat_template load_nemotron_template (testing & t ) { 1761return load_template (t ,"models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja" ); 1762} 1763 1764static void test_nemotron_analysis (testing & t ) { 1765t .test ("Nemotron reasoning detection" ,test_nemotron_reasoning_detection ); 1766t .test ("Nemotron tool format" ,test_nemotron_tool_format ); 1767} 1768 1769static void test_nemotron_reasoning_detection (testing & t ) { 1770common_chat_template tmpl = load_nemotron_template (t ); 1771 1772// Test the comparison manually to see what's happening 1773json user_msg = json { {"role" ,"user" }, {"content" ,"Hello" } }; 1774json assistant_no_reasoning = json { 1775 {"role" ,"assistant" }, 1776 {"content" ,"I can help." } 1777 }; 1778json assistant_with_reasoning = json { 1779 {"role" ,"assistant" }, 1780 {"content" ,"I can help." }, 1781 {"reasoning_content" ,"Let me think about this." } 1782 }; 1783 1784template_params params ; 1785params .messages = json::array ({user_msg ,assistant_no_reasoning }); 1786params .add_generation_prompt = false; 1787params .enable_thinking = true; 1788 1789// Run differential analysis 1790struct autoparser analysis ; 1791analysis .analyze_template (tmpl ); 1792 1793// Check reasoning markers 1794t .assert_equal ("reasoning_start should be '<think>\\n'" ,"<think>\n" ,analysis .reasoning .start ); 1795t .assert_equal ("reasoning_end should be '\\n</think>\\n'" ,"\n</think>\n" ,analysis .reasoning .end ); 1796 1797// Check reasoning mode detection 1798// Nemotron uses tag-based reasoning; prefill handles the template's forced markers 1799t .assert_equal ("reasoning should be TAG_BASED" , reasoning_mode::TAG_BASED ,analysis .reasoning .mode ); 1800 1801// Make sure reasoning markers don't spill over to content markers 1802t .assert_equal ("content start should be empty" ,"" ,analysis .content .start ); 1803t .assert_equal ("content end should be empty" ,"" ,analysis .content .end ); 1804 1805t .assert_equal ("content should be PLAIN" , content_mode::PLAIN ,analysis .content .mode ); 1806} 1807 1808static void test_nemotron_tool_format (testing & t ) { 1809common_chat_template tmpl = load_nemotron_template (t ); 1810 1811// Run differential analysis 1812struct autoparser analysis ; 1813analysis .analyze_template (tmpl ); 1814 1815// Check tool markers - Nemotron uses per-call wrapping (each call individually wrapped) 1816t .assert_equal ("tool_section_start should be empty (per-call format)" ,"" ,analysis .tools .format .section_start ); 1817t .assert_equal ("tool_section_end should be empty (per-call format)" ,"" ,analysis .tools .format .section_end ); 1818t .assert_equal ("per_call_start should be '<tool_call>\\n'" ,"<tool_call>\n" ,analysis .tools .format .per_call_start ); 1819t .assert_equal ("per_call_end should be '</tool_call>'" ,"</tool_call>" ,analysis .tools .format .per_call_end ); 1820t .assert_true ("should support parallel calls" ,analysis .jinja_caps .supports_parallel_tool_calls ); 1821 1822// Check function markers 1823t .assert_equal ("func_name_prefix should be '<function='" ,"<function=" ,analysis .tools .function .name_prefix ); 1824t .assert_equal ("func_name_suffix should be '>\\n'" ,">\n" ,analysis .tools .function .name_suffix ); 1825t .assert_equal ("func_close should be '</function>\\n'" ,"</function>\n" ,analysis .tools .function .close ); 1826 1827// Check argument markers (note: markers retain trailing newlines for proper parsing) 1828t .assert_equal ("arg_name_prefix should be '<parameter='" ,"<parameter=" ,analysis .tools .arguments .name_prefix ); 1829t .assert_equal ("arg_name_suffix should be '>\\n'" ,">\n" ,analysis .tools .arguments .name_suffix ); 1830t .assert_equal ("arg_value_suffix should be '\\n</parameter>\\n'" ,"\n</parameter>\n" ,analysis .tools .arguments .value_suffix ); 1831 1832// Check format classification 1833t .assert_true ("tool format should be TAG_WITH_TAGGED" ,analysis .tools .format .mode == tool_format::TAG_WITH_TAGGED ); 1834 1835// Verify tool support 1836t .assert_true ("should support tools" ,analysis .jinja_caps .supports_tools ); 1837} 1838 1839// ============================================================================ 1840// Laguna Template Analysis Tests 1841// ============================================================================ 1842static common_chat_template load_laguna_template (testing & t ) { 1843return load_template (t ,"models/templates/poolside-Laguna-XS-2.1.jinja" ); 1844} 1845 1846static void test_laguna_reasoning_detection (testing & t ) { 1847common_chat_template tmpl = load_laguna_template (t ); 1848struct autoparser analysis ; 1849analysis .analyze_template (tmpl ); 1850// Laguna's template renders reasoning delimiters with formatting whitespace 1851// ("<think>\n") that the model does not emit; the Laguna patch trims them. 1852t .assert_equal ("reasoning_start should be '<think>'" ,"<think>" ,analysis .reasoning .start ); 1853t .assert_equal ("reasoning_end should be '</think>'" ,"</think>" ,analysis .reasoning .end ); 1854t .assert_equal ("reasoning should be TAG_BASED" , reasoning_mode::TAG_BASED ,analysis .reasoning .mode ); 1855} 1856 1857static void test_laguna_tool_format (testing & t ) { 1858common_chat_template tmpl = load_laguna_template (t ); 1859struct autoparser analysis ; 1860analysis .analyze_template (tmpl ); 1861t .assert_equal ("arg_value_suffix should be '</arg_value>'" ,"</arg_value>" ,analysis .tools .arguments .value_suffix ); 1862} 1863 1864static void test_laguna_stop_string (testing & t ) { 1865// The </assistant> turn terminator can be emitted as ordinary text tokens 1866// (not the single eot token), so it must also be a literal stop string. 1867common_chat_template tmpl = load_laguna_template (t ); 1868struct autoparser analysis ; 1869analysis .analyze_template (tmpl ); 1870bool has_stop = false; 1871for (const auto & stop :analysis .additional_stops ) { 1872if (stop == "</assistant>" ) {has_stop = true;break ; } 1873 } 1874t .assert_true ("Laguna additional_stops contains </assistant>" ,has_stop ); 1875} 1876 1877static void test_laguna_analysis (testing & t ) { 1878t .test ("Laguna reasoning detection" ,test_laguna_reasoning_detection ); 1879t .test ("Laguna tool format" ,test_laguna_tool_format ); 1880t .test ("Laguna stop string" ,test_laguna_stop_string ); 1881} 1882 1883static common_chat_template load_laguna_s_template (testing & t ) { 1884return load_template (t ,"models/templates/poolside-Laguna-S-2.1.jinja" ); 1885} 1886static void test_laguna_s_reasoning_detection (testing & t ) { 1887common_chat_template tmpl = load_laguna_s_template (t ); 1888struct autoparser analysis ; 1889analysis .analyze_template (tmpl ); 1890t .assert_equal ("Laguna-S(v8) reasoning_start should be '<think>'" ,"<think>" ,analysis .reasoning .start ); 1891t .assert_equal ("Laguna-S(v8) reasoning_end should be '</think>'" ,"</think>" ,analysis .reasoning .end ); 1892t .assert_equal ("Laguna-S(v8) reasoning should be TAG_BASED" , reasoning_mode::TAG_BASED ,analysis .reasoning .mode ); 1893} 1894static void test_laguna_s_tool_format (testing & t ) { 1895common_chat_template tmpl = load_laguna_s_template (t ); 1896struct autoparser analysis ; 1897analysis .analyze_template (tmpl ); 1898t .assert_equal ("Laguna-S(v8) arg_value_suffix should be '</arg_value>'" ,"</arg_value>" ,analysis .tools .arguments .value_suffix ); 1899} 1900static void test_laguna_s_preserve_reasoning (testing & t ) { 1901common_chat_template tmpl = load_laguna_s_template (t ); 1902t .assert_true ("Laguna-S(v8) supports preserving reasoning" ,tmpl .original_caps ().supports_preserve_reasoning ); 1903} 1904static void test_laguna_s_analysis (testing & t ) { 1905t .test ("Laguna-S(v8) reasoning detection" ,test_laguna_s_reasoning_detection ); 1906t .test ("Laguna-S(v8) tool format" ,test_laguna_s_tool_format ); 1907t .test ("Laguna-S(v8) preserve reasoning" ,test_laguna_s_preserve_reasoning ); 1908} 1909 1910static common_chat_template load_laguna_xs2_template (testing & t ) { 1911return load_template (t ,"models/templates/poolside-Laguna-XS.2.jinja" ); 1912} 1913static void test_laguna_xs2_reasoning_detection (testing & t ) { 1914common_chat_template tmpl = load_laguna_xs2_template (t ); 1915struct autoparser analysis ; 1916analysis .analyze_template (tmpl ); 1917t .assert_equal ("Laguna-XS.2(v5) reasoning_start should be '<think>'" ,"<think>" ,analysis .reasoning .start ); 1918t .assert_equal ("Laguna-XS.2(v5) reasoning_end should be '</think>'" ,"</think>" ,analysis .reasoning .end ); 1919t .assert_equal ("Laguna-XS.2(v5) reasoning should be TAG_BASED" , reasoning_mode::TAG_BASED ,analysis .reasoning .mode ); 1920} 1921static void test_laguna_xs2_tool_format (testing & t ) { 1922common_chat_template tmpl = load_laguna_xs2_template (t ); 1923struct autoparser analysis ; 1924analysis .analyze_template (tmpl ); 1925t .assert_equal ("Laguna-XS.2(v5) arg_value_suffix should be '</arg_value>'" ,"</arg_value>" ,analysis .tools .arguments .value_suffix ); 1926} 1927static void test_laguna_xs2_analysis (testing & t ) { 1928t .test ("Laguna-XS.2(v5) reasoning detection" ,test_laguna_xs2_reasoning_detection ); 1929t .test ("Laguna-XS.2(v5) tool format" ,test_laguna_xs2_tool_format ); 1930} 1931 1932static common_chat_template load_cohere_template (testing & t ) { 1933return load_template (t ,"models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" ); 1934} 1935 1936static void test_cohere_analysis (testing & t ) { 1937t .test ("Cohere reasoning detection" ,test_cohere_reasoning_detection ); 1938} 1939 1940static void test_cohere_reasoning_detection (testing & t ) { 1941common_chat_template tmpl = load_cohere_template (t ); 1942 1943// Run differential analysis 1944struct autoparser analysis ; 1945analysis .analyze_template (tmpl ); 1946 1947// Check reasoning markers - Cohere uses special token format 1948t .assert_equal ("reasoning_start should be '<|START_THINKING|>'" ,"<|START_THINKING|>" ,analysis .reasoning .start ); 1949t .assert_equal ("reasoning_end should be '<|END_THINKING|>'" ,"<|END_THINKING|>" ,analysis .reasoning .end ); 1950 1951// Check reasoning mode - Cohere only shows reasoning with tool calls (TOOLS_ONLY) 1952t .assert_equal ("reasoning should be TOOLS_ONLY" , reasoning_mode::TOOLS_ONLY ,analysis .reasoning .mode ); 1953 1954// Check content markers - Cohere wraps all content with START/END_RESPONSE 1955t .assert_equal ("content_start should be '<|START_RESPONSE|>'" ,"<|START_RESPONSE|>" ,analysis .content .start ); 1956t .assert_equal ("content_end should be '<|END_RESPONSE|>'" ,"<|END_RESPONSE|>" ,analysis .content .end ); 1957 1958// Content is always wrapped (both with and without tools) 1959t .assert_equal ("content should be ALWAYS_WRAPPED" , content_mode::ALWAYS_WRAPPED ,analysis .content .mode ); 1960} 1961 1962static void test_tool_format_cohere (testing & t ) { 1963common_chat_template tmpl = load_cohere_template (t ); 1964 1965// Run differential analysis 1966struct autoparser analysis ; 1967analysis .analyze_template (tmpl ); 1968 1969// Check tool section markers - Cohere uses ACTION markers 1970t .assert_equal ("tool_section_start should be '<|START_ACTION|>'" ,"<|START_ACTION|>" ,analysis .tools .format .section_start ); 1971t .assert_equal ("tool_section_end should be '<|END_ACTION|>'" ,"<|END_ACTION|>" ,analysis .tools .format .section_end ); 1972 1973// JSON_NATIVE format has no per-call markers 1974t .assert_equal ("per_call_start should be empty" ,"" ,analysis .tools .format .per_call_start ); 1975t .assert_equal ("per_call_end should be empty" ,"" ,analysis .tools .format .per_call_end ); 1976 1977// JSON_NATIVE format has empty function markers (no XML-style markers) 1978t .assert_equal ("func_name_prefix should be empty" ,"" ,analysis .tools .function .name_prefix ); 1979t .assert_equal ("func_name_suffix should be empty" ,"" ,analysis .tools .function .name_suffix ); 1980t .assert_equal ("func_close should be empty" ,"" ,analysis .tools .function .close ); 1981 1982// JSON_NATIVE format has empty args markers 1983t .assert_equal ("args_start should be empty" ,"" ,analysis .tools .arguments .start ); 1984t .assert_equal ("args_end should be empty" ,"" ,analysis .tools .arguments .end ); 1985 1986// JSON_NATIVE format has empty argument markers 1987t .assert_equal ("arg_name_prefix should be empty" ,"" ,analysis .tools .arguments .name_prefix ); 1988t .assert_equal ("arg_name_suffix should be empty" ,"" ,analysis .tools .arguments .name_suffix ); 1989t .assert_equal ("arg_value_prefix should be empty" ,"" ,analysis .tools .arguments .value_prefix ); 1990t .assert_equal ("arg_value_suffix should be empty" ,"" ,analysis .tools .arguments .value_suffix ); 1991t .assert_equal ("arg_separator should be empty" ,"" ,analysis .tools .arguments .separator ); 1992 1993// Check JSON field names - Cohere uses non-standard names 1994t .assert_equal ("name_field should be 'tool_name'" ,"tool_name" ,analysis .tools .format .name_field ); 1995t .assert_equal ("args_field should be 'parameters'" ,"parameters" ,analysis .tools .format .args_field ); 1996// This isn't a real tool call id field, i.e. with the OpenAI tool call ID format 1997t .assert_equal ("id_field should be 'tool_call_id'" ,"" ,analysis .tools .format .id_field ); 1998 1999// Check format classification 2000t .assert_equal ("tool format should be JSON_NATIVE" , tool_format::JSON_NATIVE ,analysis .tools .format .mode ); 2001 2002// Check flags 2003t .assert_true ("should support tools" ,analysis .jinja_caps .supports_tools ); 2004t .assert_true ("should support parallel calls" ,analysis .jinja_caps .supports_parallel_tool_calls ); 2005t .assert_true ("should not require nonnull content" , !analysis .content .requires_nonnull_content ); 2006t .assert_true ("tools_array_wrapped should be true" ,analysis .tools .format .tools_array_wrapped ); 2007} 2008 2009// ============================================================================ 2010// SmolLM3 Template Analysis Tests 2011// Tests for templates that change system message when enable_thinking flips 2012// and prefill an empty <think></think> block in no-think mode. 2013// ============================================================================ 2014static common_chat_template load_smollm3_template (testing & t ) { 2015return load_template (t ,"models/templates/HuggingFaceTB-SmolLM3-3B.jinja" ); 2016} 2017 2018static void test_smollm3_reasoning_detection (testing & t ); 2019 2020static void test_smollm3_analysis (testing & t ) { 2021t .test ("SmolLM3 reasoning detection" ,test_smollm3_reasoning_detection ); 2022} 2023 2024static void test_smollm3_reasoning_detection (testing & t ) { 2025common_chat_template tmpl = load_smollm3_template (t ); 2026 2027// Run differential analysis 2028struct autoparser analysis ; 2029analysis .analyze_template (tmpl ); 2030 2031// SmolLM3 uses <think>/<think> reasoning tags. 2032// The template changes the entire system message when enable_thinking flips, 2033// so the analyzer must compare isolated generation prompts (not full outputs). 2034t .assert_equal ("reasoning_start should be '<think>'" ,"<think>" ,analysis .reasoning .start ); 2035t .assert_equal ("reasoning_end should be '</think>'" ,"</think>" ,analysis .reasoning .end ); 2036t .assert_equal ("reasoning should be TAG_BASED" , reasoning_mode::TAG_BASED ,analysis .reasoning .mode ); 2037 2038// Content should remain plain (no wrappers) 2039t .assert_equal ("content start should be empty" ,"" ,analysis .content .start ); 2040t .assert_equal ("content end should be empty" ,"" ,analysis .content .end ); 2041t .assert_equal ("content should be PLAIN" , content_mode::PLAIN ,analysis .content .mode ); 2042 2043// Preserved tokens should include the reasoning markers 2044bool has_think_start = std::find (analysis .preserved_tokens .begin (),analysis .preserved_tokens .end (),"<think>" )!= analysis .preserved_tokens .end (); 2045bool has_think_end = std::find (analysis .preserved_tokens .begin (),analysis .preserved_tokens .end (),"</think>" )!= analysis .preserved_tokens .end (); 2046t .assert_true ("preserved_tokens should contain '<think>'" ,has_think_start ); 2047t .assert_true ("preserved_tokens should contain '</think>'" ,has_think_end ); 2048} 2049 2050// ============================================================================ 2051// standard_json_tools Format Tests 2052// ============================================================================ 2053 2054// Helper to build tools definition for tests 2055static json build_test_tools () { 2056json parameters_schema = json::object (); 2057parameters_schema ["type" ]= "object" ; 2058parameters_schema ["properties" ]= json::object (); 2059parameters_schema ["properties" ]["location" ]= json::object ({ 2060 {"type" ,"string" }, 2061 {"description" ,"The city and state" } 2062 }); 2063parameters_schema ["properties" ]["unit" ]= json::object ({ 2064 {"type" ,"string" }, 2065 {"description" ,"Temperature unit" }, 2066 {"enum" , json::array ({"celsius" ,"fahrenheit" })} 2067 }); 2068parameters_schema ["required" ]= json::array ({"location" }); 2069 2070return json::array ({ 2071json { 2072 {"type" ,"function" }, 2073 {"function" ,json { 2074 {"name" ,"get_current_weather" }, 2075 {"description" ,"Get the current weather in a given location" }, 2076 {"parameters" ,parameters_schema } 2077 }} 2078 } 2079 }); 2080} 2081 2082static void test_standard_json_tools_formats (testing & t ) { 2083t .test ("OpenAI format" ,test_standard_json_tools_openai ); 2084t .test ("Cohere format" ,test_standard_json_tools_cohere ); 2085t .test ("function-as-key format" ,test_standard_json_tools_function_key ); 2086} 2087 2088// Test 1: OpenAI Standard Format 2089// {"id": "call_abc", "function": {"name": "get_weather", "arguments": {"location": "NYC"}}} 2090static void test_standard_json_tools_openai (testing & t ) { 2091json tools = build_test_tools (); 2092 2093auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 2094auto tool_call = p .standard_json_tools ( 2095"<tool_call>" ,"</tool_call>" ,tools , 2096/* parallel */ true, 2097/* force */ false, 2098/* name_key */ "function.name" , 2099/* args_key */ "function.arguments" , 2100/* array_wrapped */ false, 2101/* function_is_key */ false, 2102/* call_id_key */ "id" , 2103/* gen_call_id_key */ "" , 2104/* parameters_order */ {} 2105 ); 2106return p .content (p .until ("<tool_call>" ))+ p .optional (tool_call )+ p .end (); 2107 }); 2108 2109 std::string input = 2110"Let me check the weather." 2111"<tool_call>" 2112R"({"id": "call_abc123", "function": {"name": "get_current_weather", "arguments": {"location": "NYC"}}})" 2113"</tool_call>" ; 2114 2115common_peg_parse_context ctx (input ); 2116auto result = parser .parse (ctx ); 2117 2118if (!t .assert_true ("parse success" ,result .success ())) { 2119return ; 2120 } 2121 2122common_chat_msg msg ; 2123auto mapper = common_chat_peg_mapper (msg ); 2124mapper .from_ast (ctx .ast ,result ); 2125 2126t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 2127if (!msg .tool_calls .empty ()) { 2128t .assert_equal ("tool name" ,"get_current_weather" ,msg .tool_calls [0 ].name ); 2129t .assert_equal ("tool id" ,"call_abc123" ,msg .tool_calls [0 ].id ); 2130 } 2131t .assert_true ("content present" ,msg .content .find ("Let me check the weather" )!= std::string::npos ); 2132} 2133 2134// Test 2: Cohere Format 2135// {"tool_call_id": 0, "tool_name": "get_weather", "parameters": {"location": "NYC"}} 2136static void test_standard_json_tools_cohere (testing & t ) { 2137json tools = build_test_tools (); 2138 2139auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 2140auto tool_call = p .standard_json_tools ( 2141"<|START_ACTION|>[" ,"]<|END_ACTION|>" ,tools , 2142/* parallel */ true, 2143/* force */ false, 2144/* name_key */ "tool_name" , 2145/* args_key */ "parameters" , 2146/* array_wrapped */ false,// Brackets are part of section markers 2147/* function_is_key */ false, 2148/* call_id_key */ "" , 2149/* gen_call_id_key */ "tool_call_id" , 2150/* parameters_order */ {"tool_call_id" ,"tool_name" ,"parameters" } 2151 ); 2152return p .content (p .until ("<|START_ACTION|>" ))+ p .optional (tool_call )+ p .end (); 2153 }); 2154 2155 std::string input = 2156"Let me search for that." 2157"<|START_ACTION|>[" 2158R"({"tool_call_id": 0, "tool_name": "get_current_weather", "parameters": {"location": "NYC", "unit": "celsius"}})" 2159"]<|END_ACTION|>" ; 2160 2161common_peg_parse_context ctx (input ); 2162auto result = parser .parse (ctx ); 2163 2164if (!t .assert_true ("parse success" ,result .success ())) { 2165return ; 2166 } 2167 2168common_chat_msg msg ; 2169auto mapper = common_chat_peg_mapper (msg ); 2170mapper .from_ast (ctx .ast ,result ); 2171 2172t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 2173if (!msg .tool_calls .empty ()) { 2174t .assert_equal ("tool name" ,"get_current_weather" ,msg .tool_calls [0 ].name ); 2175t .assert_equal ("tool id" ,"0" ,msg .tool_calls [0 ].id ); 2176 } 2177t .assert_true ("content present" ,msg .content .find ("Let me search" )!= std::string::npos ); 2178} 2179 2180// Test 3: Function-as-Key Format 2181// {"get_current_weather": {"id": "call-0001", "args": {"location": "NYC"}}} 2182static void test_standard_json_tools_function_key (testing & t ) { 2183json tools = build_test_tools (); 2184 2185auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 2186auto tool_call = p .standard_json_tools ( 2187"<tool_calls>[" ,"]</tool_calls>" ,tools , 2188/* parallel */ true, 2189/* force */ false, 2190/* name_key */ "" ,// Name is the key itself 2191/* args_key */ "args" , 2192/* array_wrapped */ false, 2193/* function_is_key */ true, 2194/* call_id_key */ "id" , 2195/* gen_call_id_key */ "" , 2196/* parameters_order */ {} 2197 ); 2198return p .content (p .until ("<tool_calls>" ))+ p .optional (tool_call )+ p .end (); 2199 }); 2200 2201 std::string input = 2202"I'll call the weather function." 2203"<tool_calls>[" 2204R"({"get_current_weather": {"id": "call-0001", "args": {"location": "NYC", "unit": "celsius"}}})" 2205"]</tool_calls>" ; 2206 2207common_peg_parse_context ctx (input ); 2208auto result = parser .parse (ctx ); 2209 2210if (!t .assert_true ("parse success" ,result .success ())) { 2211return ; 2212 } 2213 2214common_chat_msg msg ; 2215auto mapper = common_chat_peg_mapper (msg ); 2216mapper .from_ast (ctx .ast ,result ); 2217 2218t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 2219if (!msg .tool_calls .empty ()) { 2220t .assert_equal ("tool name" ,"get_current_weather" ,msg .tool_calls [0 ].name ); 2221t .assert_equal ("tool id" ,"call-0001" ,msg .tool_calls [0 ].id ); 2222 } 2223t .assert_true ("content present" ,msg .content .find ("I'll call the weather" )!= std::string::npos ); 2224} 2225 2226// ============================================================================ 2227// normalize_quotes_to_json Tests 2228// ============================================================================ 2229 2230// Copy of the function for isolated testing (original is static in chat-peg-parser.cpp) 2231static std::string normalize_quotes_to_json (const std::string & input ) { 2232 std::string result ; 2233result .reserve (input .size ()+ 16 ); 2234 2235bool in_single_quoted = false; 2236bool in_double_quoted = false; 2237 2238for (size_t i = 0 ;i < input .size ();++ i ) { 2239char c = input [i ]; 2240 2241if (c == '\\' && i + 1 < input .size ()) { 2242char next = input [i + 1 ]; 2243 2244if (in_single_quoted ) { 2245if (next == '\'' ) { 2246result += '\'' ; 2247++ i ; 2248continue ; 2249 } 2250if (next == '"' ) { 2251result += "\\\"" ; 2252++ i ; 2253continue ; 2254 } 2255result += c ; 2256result += next ; 2257++ i ; 2258continue ; 2259 } 2260 2261if (in_double_quoted ) { 2262result += c ; 2263result += next ; 2264++ i ; 2265continue ; 2266 } 2267 2268result += c ; 2269continue ; 2270 } 2271 2272if (c == '"' ) { 2273if (in_single_quoted ) { 2274result += "\\\"" ; 2275 }else { 2276in_double_quoted = !in_double_quoted ; 2277result += c ; 2278 } 2279 }else if (c == '\'' ) { 2280if (in_double_quoted ) { 2281result += c ; 2282 }else if (in_single_quoted ) { 2283in_single_quoted = false; 2284result += '"' ; 2285 }else { 2286in_single_quoted = true; 2287result += '"' ; 2288 } 2289 }else { 2290result += c ; 2291 } 2292 } 2293 2294return result ; 2295} 2296 2297static void test_normalize_quotes_to_json (testing & t ) { 2298t .test ("basic single to double quotes" , [](testing & t ) { 2299 std::string input = "{'key': 'value'}" ; 2300 std::string expected = "{\"key\": \"value\"}" ; 2301 std::string result = normalize_quotes_to_json (input ); 2302t .assert_equal ("basic conversion" ,expected ,result ); 2303 }); 2304 2305t .test ("escaped single quote inside single-quoted string" , [](testing & t ) { 2306 std::string input = "{'code': 'print(\\'hello\\')'}" ; 2307 std::string expected = "{\"code\": \"print('hello')\"}" ; 2308 std::string result = normalize_quotes_to_json (input ); 2309t .assert_equal ("escaped single quote" ,expected ,result ); 2310 }); 2311 2312t .test ("double quote inside single-quoted string" , [](testing & t ) { 2313 std::string input = "{'msg': 'He said \"hi\"'}" ; 2314 std::string expected = "{\"msg\": \"He said \\\"hi\\\"\"}" ; 2315 std::string result = normalize_quotes_to_json (input ); 2316t .assert_equal ("double quote escaping" ,expected ,result ); 2317 }); 2318 2319t .test ("nested backslash escapes" , [](testing & t ) { 2320 std::string input = "{'path': 'C:\\\\Users\\\\test'}" ; 2321 std::string expected = "{\"path\": \"C:\\\\Users\\\\test\"}" ; 2322 std::string result = normalize_quotes_to_json (input ); 2323t .assert_equal ("backslash escaping" ,expected ,result ); 2324 }); 2325 2326t .test ("newline escapes" , [](testing & t ) { 2327 std::string input = "{'text': 'line1\\nline2'}" ; 2328 std::string expected = "{\"text\": \"line1\\nline2\"}" ; 2329 std::string result = normalize_quotes_to_json (input ); 2330t .assert_equal ("newline escaping" ,expected ,result ); 2331 }); 2332 2333t .test ("mixed quotes" , [](testing & t ) { 2334 std::string input = "{\"already_double\": 'single_value'}" ; 2335 std::string expected = "{\"already_double\": \"single_value\"}" ; 2336 std::string result = normalize_quotes_to_json (input ); 2337t .assert_equal ("mixed quotes" ,expected ,result ); 2338 }); 2339 2340t .test ("embedded quotes - the test case" ,test_normalize_quotes_with_embedded_quotes ); 2341} 2342 2343// Test case that mirrors the Seed-OSS failing test scenario 2344static void test_normalize_quotes_with_embedded_quotes (testing & t ) { 2345// This is similar to the Seed-OSS template test case 2346// The input has embedded double quotes like "14" and "bar" inside string values 2347 std::string input = "{'filename': 'foo.cpp', 'oldString': 'def foo(arg = \"14\"):\\n return arg + \"bar\"\\n', 'newString': 'def foo(arg = \"15\"):\\n pass\\n'}" ; 2348 2349// Expected: Python single quotes -> JSON double quotes, internal double quotes escaped 2350 std::string expected = "{\"filename\": \"foo.cpp\", \"oldString\": \"def foo(arg = \\\"14\\\"):\\n return arg + \\\"bar\\\"\\n\", \"newString\": \"def foo(arg = \\\"15\\\"):\\n pass\\n\"}" ; 2351 2352 std::string result = normalize_quotes_to_json (input ); 2353 2354t .assert_equal ("normalize quotes with embedded double quotes" ,expected ,result ); 2355 2356// Also verify the result is valid JSON 2357try { 2358json parsed = json::parse (result ); 2359t .assert_true ("result is valid JSON" , true); 2360t .assert_equal ("filename field" ,"foo.cpp" ,parsed ["filename" ].get < std::string > ()); 2361t .assert_true ("oldString contains embedded quotes" , 2362parsed ["oldString" ].get < std::string > ().find ("\"14\"" )!= std::string::npos ); 2363t .assert_true ("newString contains embedded quotes" , 2364parsed ["newString" ].get < std::string > ().find ("\"15\"" )!= std::string::npos ); 2365 }catch (const std::exception & e ) { 2366t .assert_true (std::string ("JSON parse failed: " )+ e .what (), false); 2367 } 2368} 2369 2370// ============================================================================ 2371// TAG_WITH_TAGGED Argument Parsing Tests 2372// ============================================================================ 2373 2374// Build tools definition for edit function 2375static json build_edit_tool () { 2376json parameters_schema = json::object (); 2377parameters_schema ["type" ]= "object" ; 2378parameters_schema ["properties" ]= json::object (); 2379parameters_schema ["properties" ]["filename" ]= json::object ({ 2380 {"type" ,"string" }, 2381 {"description" ,"Path of file to edit" } 2382 }); 2383parameters_schema ["properties" ]["oldString" ]= json::object ({ 2384 {"type" ,"string" }, 2385 {"description" ,"String to replace" } 2386 }); 2387parameters_schema ["properties" ]["newString" ]= json::object ({ 2388 {"type" ,"string" }, 2389 {"description" ,"New (replacement) value" } 2390 }); 2391parameters_schema ["required" ]= json::array ({"filename" ,"oldString" ,"newString" }); 2392 2393return json::array ({ 2394json { 2395 {"type" ,"function" }, 2396 {"function" ,json { 2397 {"name" ,"edit" }, 2398 {"description" ,"Edit a file" }, 2399 {"parameters" ,parameters_schema } 2400 }} 2401 } 2402 }); 2403} 2404 2405// ============================================================================ 2406// Role marker detection tests for all autoparser-handled templates 2407// 2408// Verifies that detect_user_start_marker / detect_assistant_start_marker 2409// return the correct boundary text between turns for every template that 2410// falls through to the differential autoparser (i.e. is not handled by a 2411// dedicated specialized template in common_chat_try_specialized_template). 2412// 2413// Markers were deduced manually from the jinja sources in models/templates/. 2414// ============================================================================ 2415struct role_marker_case { 2416 std::string template_file ; 2417 std::string expected_user_start ; 2418 std::string expected_assistant_start ; 2419}; 2420 2421static void test_role_markers_all_templates (testing & t ) { 2422// Each entry is { template filename, user_start, assistant_start } as 2423// produced when rendering the standard chatml-like sequences. The values 2424// come from reading each jinja template and tracing what text precedes 2425// a user/assistant message body once the autoparser strips any reasoning 2426// markers it detected first. 2427const std::vector < role_marker_case > cases = { 2428// ChatML family: <|im_start|>{role} ... <|im_end|> 2429 {"Bielik-11B-v3.0-Instruct.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2430 {"HuggingFaceTB-SmolLM3-3B.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2431 {"MiMo-VL.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2432 {"NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2433 {"NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2434 {"NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2435 {"Qwen3.5-4B.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2436 {"Qwen3-Coder.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2437 {"Qwen-Qwen2.5-7B-Instruct.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2438 {"Qwen-Qwen3-0.6B.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2439 {"Qwen-QwQ-32B.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2440 {"StepFun3.5-Flash.jinja" ,"<|im_start|>user" ,"<|im_start|>assistant" }, 2441 2442// DeepSeek family 2443 {"deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja" ,"<|User|>" ,"<|Assistant|>" }, 2444 {"deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja" ,"<|User|>" ,"<|Assistant|>" }, 2445 {"deepseek-ai-DeepSeek-V3.1.jinja" ,"<|User|>" ,"<|Assistant|>" }, 2446 {"llama-cpp-deepseek-r1.jinja" ,"<|User|>" ,"<|Assistant|>" }, 2447 2448// Llama 3 header family 2449 {"meetkai-functionary-medium-v3.1.jinja" ,"<|start_header_id|>user<|end_header_id|>" ,"<|start_header_id|>assistant<|end_header_id|>" }, 2450 {"meta-llama-Llama-3.1-8B-Instruct.jinja" ,"<|start_header_id|>user<|end_header_id|>" ,"<|start_header_id|>assistant<|end_header_id|>" }, 2451 {"meta-llama-Llama-3.2-3B-Instruct.jinja" ,"<|start_header_id|>user<|end_header_id|>" ,"<|start_header_id|>assistant<|end_header_id|>" }, 2452 {"meta-llama-Llama-3.3-70B-Instruct.jinja" ,"<|start_header_id|>user<|end_header_id|>" ,"<|start_header_id|>assistant<|end_header_id|>" }, 2453// fireworks-ai forces a trailing assistant header even without add_generation_prompt, 2454// so the marker is absorbed into the common suffix and assistant_start is detected as empty. 2455 {"fireworks-ai-llama-3-firefunction-v2.jinja" ,"<|start_header_id|>user<|end_header_id|>" ,"<|start_header_id|>assistant<|end_header_id|>" }, 2456 2457// Phi/GLM/Apriel-style: <|user|> / <|assistant|> 2458 {"microsoft-Phi-3.5-mini-instruct.jinja" ,"<|user|>" ,"<|assistant|>" }, 2459 {"GLM-4.6.jinja" ,"<|user|>" ,"<|assistant|>" }, 2460 {"unsloth-Apriel-1.5.jinja" ,"<|user|>" ,"<|assistant|>" }, 2461 {"GLM-4.7-Flash.jinja" ,"<|user|>" ,"<|assistant|>" }, 2462 2463// Gemma 2: <start_of_turn>{user|model} 2464 {"google-gemma-2-2b-it.jinja" ,"<start_of_turn>user" ,"<start_of_turn>model" }, 2465 2466// IBM Granite 2467 {"ibm-granite-granite-3.3-2B-Instruct.jinja" ,"<|start_of_role|>user<|end_of_role|>" ,"<|start_of_role|>assistant<|end_of_role|>" }, 2468 {"ibm-granite-granite-4.0.jinja" ,"<|start_of_role|>user<|end_of_role|>" ,"<|start_of_role|>assistant<|end_of_role|>" }, 2469 2470// Cohere R-series 2471 {"CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" , 2472"<|START_OF_TURN_TOKEN|><|USER_TOKEN|>" ,"<|START_RESPONSE|>" }, 2473 {"CohereForAI-c4ai-command-r-plus-tool_use.jinja" , 2474"<|START_OF_TURN_TOKEN|><|USER_TOKEN|>" ,"<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>" }, 2475 2476// Mistral: assistant content follows [/INST] immediately, no header 2477 {"mistralai-Mistral-Nemo-Instruct-2407.jinja" ,"[INST]" ,"" }, 2478 {"Mistral-Small-3.2-24B-Instruct-2506.jinja" ,"[INST]" ,"" }, 2479 2480// Apertus uses <|user_start|> / <|assistant_start|> but the user diff 2481// carries the preceding <|assistant_end|> from the previous turn. 2482 {"Apertus-8B-Instruct.jinja" ,"<|user_start|>" ,"<|assistant_start|>" }, 2483 2484// Apriel 1.6 wraps the assistant body with <|begin_assistant|>, but 2485// <|begin_assistant|> is also the detected reasoning start, so the 2486// assistant_start is trimmed back to the preceding newline. 2487 {"Apriel-1.6-15b-Thinker-fixed.jinja" ,"<|begin_user|>" ,"<|begin_assistant|>" }, 2488 2489// ByteDance Seed-OSS: <seed:bos>{role} 2490 {"ByteDance-Seed-OSS.jinja" ,"<seed:bos>user" ,"<seed:bos>assistant" }, 2491 2492// GigaChat 3.1: {role}<|role_sep|> 2493 {"GigaChat3.1-10B-A1.8B.jinja" ,"user<|role_sep|>" ,"assistant<|role_sep|>" }, 2494 2495// MiniMax M2: ]~b]{user|ai} 2496 {"MiniMax-M2.jinja" ,"]~b]user" ,"]~b]ai" }, 2497 2498// HunYuan V3: <|hy_User:opensource|> / <|hy_Assistant:opensource|> 2499 {"tencent-Hy3.jinja" ,"<|hy_User:opensource|>" ,"<|hy_Assistant:opensource|>" }, 2500 2501// Nemotron Nano v2: <SPECIAL_11>{User|Assistant}; assistant marker 2502// is followed by a prefilled <think> block that gets included. 2503 {"NVIDIA-Nemotron-Nano-v2.jinja" ,"<SPECIAL_11>User" ,"<SPECIAL_11>Assistant" }, 2504 2505// Reka Edge: "human: " / "assistant: " — but the rendered preamble 2506// depends on enable_thinking, which currently confuses the user-start 2507// diff and trims the marker down. Lock in the observed value. 2508 {"Reka-Edge.jinja" ,"human:" ,"assistant:" }, 2509 2510// RWKV-world chat preset: "User: " / "Assistant: " 2511 {"llama-cpp-rwkv-world.jinja" ,"User:" ,"Assistant:" }, 2512 2513// Upstage Solar 100B: <|begin|>{role}... but reasoning marker absorbs 2514// the "<|begin|>assistant" prefix from assistant_start. 2515 {"upstage-Solar-Open-100B.jinja" ,"<|begin|>user<|content|>" ,"<|begin|>assistant" }, 2516 }; 2517 2518for (const auto & c :cases ) { 2519t .test (c .template_file , [& ](testing & t ) { 2520common_chat_template tmpl = load_template (t ,"models/templates/" + c .template_file ); 2521struct autoparser ap ; 2522ap .analyze_template (tmpl ); 2523t .assert_equal ("user_start" ,c .expected_user_start ,ap .user_start ); 2524t .assert_equal ("assistant_start" ,c .expected_assistant_start ,ap .assistant_start ); 2525 }); 2526 } 2527} 2528 2529static void test_bailing_v3_tool_format (testing & t ) { 2530const std::string template_source = R"JINJA( 2531{# Bailing V3 chat template #} 2532{%- if tools %}{{ tools | tojson }}{%- endif %} 2533{%- for message in messages %} 2534{%- if message.role == "user" %} 2535{{- '<role>HUMAN</role>' + message.content + '<|role_end|>' }} 2536{%- elif message.role == "assistant" %} 2537{{- '<role>ASSISTANT</role>' }} 2538{%- if message.tool_calls %} 2539{%- for tool_call in message.tool_calls %} 2540{%- set tc = tool_call.function %} 2541{{- '<tool_call>' + tc.name }} 2542{%- for k, v in tc.arguments.items() %} 2543{{- '<arg_key>' + k + '</arg_key>' }} 2544{{- '\n<arg_value>' + v + '</arg_value>' }} 2545{%- endfor %} 2546{{- '\n</tool_call>' }} 2547{%- endfor %} 2548{%- endif %} 2549{{- '<|role_end|>' }} 2550{%- endif %} 2551{%- endfor %} 2552{%- if add_generation_prompt %}{{- '<role>ASSISTANT</role>' }}{%- endif %} 2553)JINJA" ; 2554 2555common_chat_template tmpl (template_source ,"" ,"" ); 2556struct autoparser analysis ; 2557analysis .analyze_template (tmpl ); 2558 2559t .assert_equal ("arg_value_suffix" ,"</arg_value>" ,analysis .tools .arguments .value_suffix ); 2560t .assert_true ("intertag whitespace" ,analysis .tools .arguments .tolerate_intertag_whitespace ); 2561 2562generation_params inputs ; 2563inputs .tools = json::array ({ 2564 { 2565 {"type" ,"function" }, 2566 {"function" , { 2567 {"name" ,"test_function_name" }, 2568 {"parameters" , { 2569 {"type" ,"object" }, 2570 {"properties" , { 2571 {"param1" , { {"type" ,"string" } } }, 2572 {"param2" , { {"type" ,"string" } } }, 2573 } }, 2574 } }, 2575 } }, 2576 }, 2577 }); 2578inputs .reasoning_format = COMMON_REASONING_FORMAT_NONE ; 2579auto parser = analysis .build_parser (inputs ,"" ); 2580const std::string output = 2581"<tool_call>test_function_name\n" 2582"<arg_key>param1</arg_key>\n" 2583"<arg_value>value1</arg_value>" 2584"<arg_key>param2</arg_key>\n" 2585"<arg_value>value2</arg_value>\n" 2586"</tool_call>" ; 2587common_peg_parse_context ctx (output ,COMMON_PEG_PARSE_FLAG_LENIENT ); 2588t .assert_true ("multi-argument tool call" ,parser .parse (ctx ).success ()); 2589} 2590 2591// Test that reproduces the Seed-OSS template issue with embedded quotes 2592static void test_tagged_args_with_embedded_quotes (testing & t ) { 2593json tools = build_edit_tool (); 2594 2595// Build a parser for TAG_WITH_TAGGED format like Seed-OSS/Nemotron 2596auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 2597// Build tool choice for the edit function 2598auto tool_choice = p .choice (); 2599 2600for (const auto & tool_def :tools ) { 2601if (!tool_def .contains ("function" )) {continue ; } 2602const auto & function = tool_def .at ("function" ); 2603 std::string name = function .at ("name" ); 2604const auto & params = function .at ("parameters" ); 2605 2606if (!params .contains ("properties" )|| !params .at ("properties" ).is_object ()) {continue ; } 2607 2608const auto & properties = params .at ("properties" ); 2609 2610// Build argument parsers 2611 std::vector < common_peg_parser > arg_parsers ; 2612for (const auto & [param_name ,param_schema ] :properties .items ()) { 2613auto arg = p .tool_arg ( 2614p .tool_arg_open (p .literal ("<parameter=" )+ p .tool_arg_name (p .literal (param_name ))+ p .literal (">" ))+ 2615p .space ()+ 2616p .tool_arg_string_value (p .until ("</parameter>" ))+ 2617p .space ()+ 2618p .tool_arg_close (p .literal ("</parameter>" )) 2619 ); 2620arg_parsers .push_back (p .optional (p .rule ("arg-" + param_name ,arg ))); 2621 } 2622 2623// Build arg sequence with space() between 2624common_peg_parser args_seq = p .eps (); 2625for (size_t i = 0 ;i < arg_parsers .size ();i ++ ) { 2626if (i > 0 ) { 2627args_seq = args_seq + p .space (); 2628 } 2629args_seq = args_seq + arg_parsers [i ]; 2630 } 2631 2632auto func_parser = 2633p .tool_open (p .literal ("<function=" )+ p .tool_name (p .literal (name ))+ p .literal (">" ))+ 2634p .space ()+ args_seq + p .space ()+ 2635p .tool_close (p .literal ("</function>" )); 2636 2637tool_choice |=p .rule ("tool-" + name ,p .tool (func_parser )); 2638 } 2639 2640auto tool_section = 2641p .literal ("<seed:tool_call>" )+ p .space ()+ 2642tool_choice + 2643p .space ()+ p .literal ("</seed:tool_call>" ); 2644 2645return p .content (p .until ("<seed:tool_call>" ))+ p .optional (tool_section )+ p .end (); 2646 }); 2647 2648 std::string input = 2649"<seed:tool_call>\n" 2650"<function=edit>\n" 2651"<parameter=filename>" 2652"foo.cpp" 2653"</parameter>\n" 2654"<parameter=oldString>" 2655"def foo(arg = \"14\"):\n" 2656" return arg + \"bar\"\n" 2657"\n" 2658"</parameter>\n" 2659"<parameter=newString>" 2660"def foo(arg = \"15\"):\n" 2661" pass\n" 2662"\n" 2663"</parameter>\n" 2664"</function>\n" 2665"</seed:tool_call>" ; 2666 2667common_peg_parse_context ctx (input ); 2668auto result = parser .parse (ctx ); 2669 2670if (!t .assert_true ("parse success" ,result .success ())) { 2671return ; 2672 } 2673 2674common_chat_msg msg ; 2675auto mapper = common_chat_peg_mapper (msg ); 2676mapper .from_ast (ctx .ast ,result ); 2677 2678t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 2679 2680if (!msg .tool_calls .empty ()) { 2681t .assert_equal ("tool name" ,"edit" ,msg .tool_calls [0 ].name ); 2682 2683// Parse the arguments as JSON to verify they're valid 2684 std::string args = msg .tool_calls [0 ].arguments ; 2685 2686try { 2687json parsed = json::parse (args ); 2688t .assert_true ("arguments is valid JSON" , true); 2689 2690// Verify each field has proper value 2691t .assert_equal ("filename" ,"foo.cpp" ,parsed .value ("filename" ,"" )); 2692 2693 std::string oldString = parsed .value ("oldString" ,"" ); 2694t .assert_true ("oldString contains embedded quotes" , 2695oldString .find ("\"14\"" )!= std::string::npos ); 2696t .assert_true ("oldString contains bar with quotes" , 2697oldString .find ("\"bar\"" )!= std::string::npos ); 2698 2699 std::string newString = parsed .value ("newString" ,"" ); 2700t .assert_true ("newString contains embedded quotes" , 2701newString .find ("\"15\"" )!= std::string::npos ); 2702 2703 }catch (const std::exception & e ) { 2704t .assert_true (std::string ("arguments should be valid JSON: " )+ e .what (), false); 2705 } 2706 } 2707}