char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
acecd5603
char/aliceai-t5-moe
1#include "chat-peg-parser.h" 2 3#include "chat-auto-parser.h" 4#include "ggml.h" 5#include "peg-parser.h" 6 7#include <cstdint> 8#include <functional> 9 10using ordered_json = common_json ; 11 12static std::string_view trim_trailing_space (std::string_view sv ,int max = -1 ) { 13int count = 0 ; 14while (!sv .empty ()&& std::isspace (static_cast < unsigned char > (sv .back ()))) { 15if (max != -1 && count >=max ) { 16break ; 17 } 18sv .remove_suffix (1 ); 19count ++ ; 20 } 21return sv ; 22} 23 24static std::string_view trim_leading_space (std::string_view sv ,int max = -1 ) { 25int count = 0 ; 26while (!sv .empty ()&& std::isspace (static_cast < unsigned char > (sv .front ()))) { 27if (max != -1 && count >=max ) { 28break ; 29 } 30sv .remove_prefix (1 ); 31count ++ ; 32 } 33return sv ; 34} 35 36static std::string_view trim (std::string_view sv ) { 37return trim_trailing_space (trim_leading_space (sv ,1 )); 38} 39 40// Count the number of unclosed '{' braces in a JSON-like string, 41// properly skipping braces inside quoted strings. 42static int json_brace_depth (const std::string & s ) { 43int depth = 0 ; 44bool in_string = false; 45bool escaped = false; 46for (char c :s ) { 47if (escaped ) { 48escaped = false; 49continue ; 50 } 51if (c == '\\' && in_string ) { 52escaped = true; 53continue ; 54 } 55if (c == '"' ) { 56in_string = !in_string ; 57continue ; 58 } 59if (!in_string ) { 60if (c == '{' ) { 61depth ++ ; 62 }else if (c == '}' ) { 63depth -- ; 64 } 65 } 66 } 67return depth ; 68} 69 70// JSON-escape a string and return the inner content (without surrounding quotes). 71static std::string escape_json_string_inner (const std::string & s ) { 72 std::string escaped = ordered_json (s ).dump (); 73if (escaped .size () >=2 && escaped .front ()== '"' && escaped .back ()== '"' ) { 74return escaped .substr (1 ,escaped .size ()- 2 ); 75 } 76return escaped ; 77} 78 79// Convert Python-style single-quoted strings to JSON double-quoted strings 80// Only converts outer string delimiters, properly handling escape sequences: 81// - {'key': 'value'} -> {"key": "value"} 82// - {'code': 'print(\'hello\')'} -> {"code": "print('hello')"} 83// - {'msg': 'He said "hi"'} -> {"msg": "He said \"hi\""} 84static std::string normalize_quotes_to_json (const std::string & input ) { 85 std::string result ; 86result .reserve (input .size ()+ 16 );// May need extra space for escaping 87 88bool in_single_quoted = false; 89bool in_double_quoted = false; 90 91auto is_word_char = [](char ch ) {return std::isalnum (static_cast < unsigned char > (ch ))|| ch == '_' ; }; 92 93for (size_t i = 0 ;i < input .size ();++ i ) { 94char c = input [i ]; 95 96// Handle escape sequences 97if (c == '\\' && i + 1 < input .size ()) { 98char next = input [i + 1 ]; 99 100if (in_single_quoted ) { 101// Inside a single-quoted string being converted to double quotes 102if (next == '\'' ) { 103// \' -> ' (escaped single quote becomes unescaped in double-quoted string) 104result += '\'' ; 105++ i ; 106continue ; 107 } 108if (next == '"' ) { 109// \" stays as \" (already escaped, works in double-quoted string) 110result += "\\\"" ; 111++ i ; 112continue ; 113 } 114// Other escapes (\n, \\, etc.): pass through both characters 115result += c ; 116result += next ; 117++ i ; 118continue ; 119 } 120 121if (in_double_quoted ) { 122// Inside a double-quoted string - pass through escape sequences as-is 123result += c ; 124result += next ; 125++ i ; 126continue ; 127 } 128 129// Outside any string - just pass through the backslash 130result += c ; 131continue ; 132 } 133 134// Handle quote characters 135if (c == '"' ) { 136if (in_single_quoted ) { 137// Unescaped double quote inside single-quoted string -> must escape for JSON 138result += "\\\"" ; 139 }else { 140// Double quote as string delimiter or outside strings 141in_double_quoted = !in_double_quoted ; 142result += c ; 143 } 144 }else if (c == '\'' ) { 145if (in_double_quoted ) { 146// Single quote inside double-quoted string -> pass through 147result += c ; 148 }else if (in_single_quoted ) { 149// Closing single quote -> convert to double quote 150in_single_quoted = false; 151result += '"' ; 152 }else { 153// Opening single quote -> convert to double quote 154in_single_quoted = true; 155result += '"' ; 156 } 157 }else if (!in_single_quoted && !in_double_quoted && (c == 'T' || c == 'F' || c == 'N' )&& 158 (i == 0 || !is_word_char (input [i - 1 ]))) { 159// Python literals -> JSON; prefix match keeps streamed partials monotonic. 160static constexpr std::pair < std::string_view , std::string_view > literals []= { 161 {"True" ,"true" }, {"False" ,"false" }, {"None" ,"null" }, 162 }; 163size_t n = 0 ; 164while (i + n < input .size ()&& is_word_char (input [i + n ])) { 165++ n ; 166 } 167 std::string_view token (input .data ()+ i ,n ); 168bool matched = false; 169for (const auto & [py ,js ] :literals ) { 170if (py .substr (0 ,n )== token ) { 171result += js .substr (0 ,n ); 172i += n - 1 ; 173matched = true; 174break ; 175 } 176 } 177if (!matched ) { 178result += c ; 179 } 180 }else { 181result += c ; 182 } 183 } 184 185return result ; 186} 187 188void tag_based_peg_mapper::from_ast (const common_peg_ast_arena & arena ,const common_peg_parse_result & result ) { 189arena .visit (result , [this ](const common_peg_ast_node & node ) { 190if (!node .tag .empty ()) { 191tags [node .tag ]= std::string (node .text ); 192 } 193 }); 194} 195 196tagged_parse_result tagged_peg_parser::parse_and_extract (const std::string & input ,common_peg_parse_flags extra_flags )const { 197common_peg_parse_context ctx (input ,flags |extra_flags ); 198auto parse_result = arena .parse (ctx ); 199 200tag_based_peg_mapper mapper ; 201mapper .from_ast (ctx .ast ,parse_result ); 202 203return { std::move (parse_result ), std::move (mapper .tags ) }; 204} 205 206tagged_parse_result tagged_peg_parser::parse_anywhere_and_extract (const std::string & input )const { 207if (input .empty ()) { 208return parse_and_extract (input ); 209 } 210for (size_t i = 0 ;i < input .size ();i ++ ) { 211common_peg_parse_context ctx (input ,flags ); 212auto parse_result = arena .parse (ctx ,i ); 213if (parse_result .success ()|| i == input .size ()- 1 ) { 214tag_based_peg_mapper mapper ; 215mapper .from_ast (ctx .ast ,parse_result ); 216return { std::move (parse_result ), std::move (mapper .tags ) }; 217 } 218 } 219GGML_ABORT ("Should not happen" ); 220} 221 222tagged_peg_parser build_tagged_peg_parser ( 223const std::function < common_peg_parser (common_peg_parser_builder & builder )> & fn ) { 224common_peg_parser_builder builder ; 225builder .set_root (fn (builder )); 226return {builder .build () }; 227} 228 229common_peg_parser common_chat_peg_builder::tag_with_safe_content (const std::string & tag_name , 230const std::string & marker , 231const common_peg_parser & p ) { 232if (marker .empty ()) { 233return zero_or_more (choice ({p ,rule (tag_name ,content (any ())) })); 234 } 235auto content_chunk = rule (tag_name ,content (negate (literal (marker ))+ any ()+ until (marker ))); 236return zero_or_more (choice ({p ,content_chunk })); 237} 238 239common_peg_parser common_chat_peg_builder::permute (const std::string & rule_prefix , 240const std::vector < common_peg_parser > & parsers ) { 241if (parsers .empty ()) { 242return eps (); 243 } 244 245if (parsers .size ()== 1 || parsers .size ()> COMMON_CHAT_MAX_PERMUTE ) { 246return sequence (parsers ); 247 } 248 249 std::map < uint32_t ,common_peg_parser > rules ; 250 std::function < common_peg_parser (uint32_t )> remaining_of ; 251 252remaining_of = [& ](uint32_t remaining )-> common_peg_parser { 253if (remaining == 0 ) { 254return eps (); 255 } 256 257auto cached = rules .find (remaining ); 258if (cached != rules .end ()) { 259return cached -> second ; 260 } 261 262auto alternatives = choice (); 263for (size_t i = 0 ;i < parsers .size ();i ++ ) { 264const uint32_t bit = 1u <<i ; 265if (remaining & bit ) { 266alternatives |=parsers [i ]+ remaining_of (remaining & ~bit ); 267 } 268 } 269 270return rules .emplace (remaining ,rule (rule_prefix + "-" + std::to_string (remaining ),alternatives )).first -> second ; 271 }; 272 273return remaining_of ((1u <<parsers .size ())- 1 ); 274} 275 276std::string & common_chat_peg_mapper::args_target () { 277return (current_tool && !current_tool -> name .empty ()) ?current_tool -> arguments :args_buffer ; 278} 279 280std::string common_chat_peg_mapper::normalize_container_value (const std::string & input ) { 281return normalize_quotes_to_json (input ); 282} 283 284void common_chat_peg_mapper::from_ast (const common_peg_ast_arena & arena , 285const common_peg_parse_result & parse_result_arg ) { 286arena .visit (parse_result_arg , [this ](const common_peg_ast_node & node ) {map (node ); }); 287// Flush any pending tool call that was started but never got a name 288// This happens during partial parsing when the tool call is incomplete 289if (pending_tool_call .has_value ()&& !pending_tool_call -> name .empty ()) { 290if (!args_buffer .empty ()) { 291pending_tool_call -> arguments = args_buffer ; 292 } 293if (closing_quote_pending && !pending_tool_call -> arguments .empty ()) { 294pending_tool_call -> arguments += "\"" ; 295 } 296result .tool_calls .push_back (pending_tool_call .value ()); 297pending_tool_call .reset (); 298 } 299 300// Discard whitespace-only reasoning content (e.g. from <think></think> prefill) 301if (!result .reasoning_content .empty ()) { 302bool all_whitespace = true; 303for (char c :result .reasoning_content ) { 304if (c != ' ' && c != '\n' && c != '\r' && c != '\t' ) { 305all_whitespace = false; 306break ; 307 } 308 } 309if (all_whitespace ) { 310result .reasoning_content .clear (); 311 } 312 } 313} 314 315void common_chat_peg_mapper::map (const common_peg_ast_node & node ) { 316// Handle reasoning/content tags 317bool is_reasoning = node .tag == common_chat_peg_builder::REASONING ; 318bool is_content = node .tag == common_chat_peg_builder::CONTENT ; 319 320if (is_reasoning ) {// GPT OSS can have more than 1 reasoning block, so concatenate here 321result .reasoning_content += std::string (node .text ); 322 } 323 324if (is_content ) { 325// Concatenate content from multiple content nodes (e.g., when reasoning markers 326// are preserved before content markers in reasoning_format=NONE mode) 327result .content += std::string (node .text ); 328 } 329 330// Handle tool-related tags (supporting both JSON and tagged formats) 331bool is_tool_open = node .tag == common_chat_peg_builder::TOOL_OPEN ; 332bool is_tool_close = node .tag == common_chat_peg_builder::TOOL_CLOSE ; 333bool is_tool_name = node .tag == common_chat_peg_builder::TOOL_NAME ; 334bool is_tool_id = node .tag == common_chat_peg_builder::TOOL_ID ; 335bool is_tool_args = node .tag == common_chat_peg_builder::TOOL_ARGS ; 336bool is_arg_open = node .tag == common_chat_peg_builder::TOOL_ARG_OPEN ; 337bool is_arg_close = node .tag == common_chat_peg_builder::TOOL_ARG_CLOSE ; 338bool is_arg_name = node .tag == common_chat_peg_builder::TOOL_ARG_NAME ; 339bool is_arg_value = node .tag == common_chat_peg_builder::TOOL_ARG_VALUE ; 340bool is_arg_string_value = node .tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE ; 341 342if (is_tool_open ) { 343pending_tool_call = common_chat_tool_call (); 344current_tool = & pending_tool_call .value (); 345arg_count = 0 ; 346args_buffer .clear (); 347closing_quote_pending = false; 348 } 349 350if (is_tool_id && current_tool ) { 351auto text = trim_trailing_space (node .text ); 352if (text .size () >=2 && text .front ()== '"' && text .back ()== '"' ) { 353text = text .substr (1 ,text .size ()- 2 ); 354 } 355current_tool -> id = std::string (text ); 356 } 357 358if (is_tool_name && current_tool ) { 359current_tool -> name = std::string (trim_trailing_space (node .text )); 360// Now that we have the name, populate the arguments from the buffer 361if (!args_buffer .empty ()) { 362current_tool -> arguments = args_buffer ; 363args_buffer .clear (); 364 }else if (current_tool -> arguments .empty ()) { 365current_tool -> arguments = "{" ; 366 } 367// Add the tool call to results so streaming can see it 368if (pending_tool_call .has_value ()) { 369result .tool_calls .push_back (pending_tool_call .value ()); 370pending_tool_call .reset (); 371current_tool = & result .tool_calls .back (); 372 } 373 } 374 375if (is_tool_args && current_tool ) { 376// For JSON format: arguments come as a complete JSON object 377// For tagged format: built up from individual arg_name/arg_value nodes 378auto text = trim_trailing_space (node .text ); 379if (!text .empty ()&& text .front ()== '{' ) { 380args_target ()= std::string (text ); 381 } 382 } 383 384if (is_arg_open ) { 385closing_quote_pending = false; 386 } 387 388if (is_arg_name && current_tool ) { 389 std::string arg_entry ; 390if (arg_count > 0 ) { 391arg_entry = "," ; 392 } 393arg_entry += ordered_json (trim (node .text )).dump ()+ ":" ; 394++ arg_count ; 395 396auto & target = args_target (); 397if (target .empty ()) { 398target = "{" ; 399 } 400target += arg_entry ; 401 } 402 403if ((is_arg_value || is_arg_string_value )&& current_tool ) { 404 std::string value_content = std::string (node .text ); 405 406 std::string value_to_add ; 407if (value_content .empty ()&& is_arg_string_value ) { 408// Empty string value - arg_close will add the closing quote 409value_to_add = "\"" ; 410closing_quote_pending = true; 411 }else if (!value_content .empty ()&& is_arg_string_value ) { 412// Schema declares this as string type - always treat as literal string value 413if (!closing_quote_pending ) { 414value_to_add = "\"" ; 415closing_quote_pending = true; 416 } 417value_to_add += escape_json_string_inner (value_content ); 418 }else if (!value_content .empty ()) { 419// Pythonic scalars/containers -> JSON. 420value_to_add += normalize_container_value (value_content ); 421 } 422 423args_target ()+= value_to_add ; 424 } 425 426if (is_arg_close && current_tool ) { 427if (closing_quote_pending ) { 428args_target ()+= "\"" ; 429closing_quote_pending = false; 430 } 431 } 432 433if (is_tool_close && current_tool ) { 434// Flush buffer to arguments if tool name was never seen 435if (current_tool -> name .empty ()&& !args_buffer .empty ()) { 436current_tool -> arguments = args_buffer ; 437args_buffer .clear (); 438 } 439// Close any pending string quote 440if (closing_quote_pending ) { 441current_tool -> arguments += "\"" ; 442closing_quote_pending = false; 443 } 444// Close any unclosed braces (accounts for nested objects) 445for (int d = json_brace_depth (current_tool -> arguments );d > 0 ;d -- ) { 446current_tool -> arguments += "}" ; 447 } 448// Add tool call to results if named; otherwise discard 449if (pending_tool_call .has_value ()) { 450if (!current_tool -> name .empty ()) { 451result .tool_calls .push_back (pending_tool_call .value ()); 452 } 453pending_tool_call .reset (); 454 } 455 } 456} 457 458common_peg_parser common_chat_peg_builder::standard_constructed_tools ( 459const std::map < std::string , std::string > & markers , 460const ordered_json & tools , 461bool parallel_tool_calls , 462bool force_tool_calls ) { 463if (!tools .is_array ()|| tools .empty ()) { 464return eps (); 465 } 466 467// Extract markers with defaults 468auto get_marker = [& markers ](const std::string & key ,const std::string & default_val = "" )-> std::string { 469auto it = markers .find (key ); 470return it != markers .end () ?it -> second :default_val ; 471 }; 472 473 std::string section_start = get_marker ("tool_call_start_marker" ,"<tool_call>" ); 474 std::string section_end = get_marker ("tool_call_end_marker" ,"</tool_call>" ); 475 std::string func_opener = get_marker ("function_opener" ,"<function=" ); 476 std::string func_name_suffix = get_marker ("function_name_suffix" ,">" ); 477 std::string func_closer = get_marker ("function_closer" ,"</function>" ); 478 std::string param_key_prefix = get_marker ("parameter_key_prefix" ,"<param=" ); 479 std::string param_key_suffix = get_marker ("parameter_key_suffix" ,">" ); 480 std::string param_closer = get_marker ("parameter_closer" ,"</param>" ); 481 482// Build tool choices for tagged format 483auto tool_choices = choice (); 484 485for (const auto & tool_def :tools ) { 486if (!tool_def .contains ("function" )) { 487continue ; 488 } 489const auto & function = tool_def .at ("function" ); 490 std::string name = function .at ("name" ); 491ordered_json params = common_chat_tool_parameters (function ); 492 493// Build argument parsers 494auto args = eps (); 495if (params .contains ("properties" )&& !params ["properties" ].empty ()) { 496auto arg_choice = choice (); 497for (const auto & el :params ["properties" ].items ()) { 498const std::string & prop_name = el .key (); 499 500auto arg_name_parser = 501choice ({literal (prop_name ),literal ("\"" + prop_name + "\"" ),literal ("'" + prop_name + "'" ) }); 502 503auto arg_rule = tool_arg (tool_arg_open (literal (param_key_prefix ))+ tool_arg_name (arg_name_parser )+ 504literal (param_key_suffix )+ tool_arg_value (until (param_closer ))+ 505tool_arg_close (literal (param_closer ))); 506arg_choice |=arg_rule ; 507 } 508args = zero_or_more (arg_choice + space ()); 509 } 510 511// Build function parser: <function=name>args</function> 512auto tool_parser = tool (tool_open (literal (func_opener )+ tool_name (literal (name ))+ literal (func_name_suffix ))+ 513space ()+ tool_args (args )+ space ()+ tool_close (literal (func_closer ))); 514 515tool_choices |=rule ("tool-" + name ,tool_parser ); 516 } 517 518// Build the section with markers 519auto section = 520parallel_tool_calls ? 521trigger_rule ("tool-call" ,literal (section_start )+ space ()+ one_or_more (tool_choices + space ())+ 522literal (section_end )) : 523trigger_rule ("tool-call" ,literal (section_start )+ space ()+ tool_choices + space ()+ literal (section_end )); 524 525return force_tool_calls ?section :optional (section ); 526} 527 528// Like python_value(), but the leaf also accepts JSON-cased true/false/null, used by LFM2/LFM2.5 529common_peg_parser common_chat_peg_builder::python_or_json_value () { 530return rule ("python-or-json-value" , [this ]() { 531auto ws = space (); 532auto value = python_or_json_value (); 533 534auto member = sequence ({python_string (),ws ,literal (":" ),ws ,value }); 535auto members = sequence ({member ,zero_or_more (sequence ({ws ,literal ("," ),ws ,member })) }); 536auto dict = rule ("python-or-json-dict" , [& ]() { 537return sequence ({literal ("{" ),ws ,choice ({literal ("}" ),sequence ({members ,ws ,literal ("}" ) }) }),ws }); 538 }); 539 540auto elements = sequence ({value ,zero_or_more (sequence ({literal ("," ),ws ,value })) }); 541auto array = rule ("python-or-json-array" , [& ]() { 542return sequence ({literal ("[" ),ws ,choice ({literal ("]" ),sequence ({elements ,ws ,literal ("]" ) }) }),ws }); 543 }); 544 545return choice ({dict ,array ,python_string (),python_number (), 546python_bool (),python_null (),json_bool (),json_null () }); 547 }); 548} 549 550// Python-style tool calls: name(arg1="value1", arg2=123) 551// Used only by LFM2 for now, so we don't merge it into autoparser 552common_peg_parser common_chat_peg_builder::python_style_tool_calls ( 553const ordered_json & tools , 554bool parallel_tool_calls , 555bool allow_json_literals ) { 556if (!tools .is_array ()|| tools .empty ()) { 557return eps (); 558 } 559 560auto tool_choices = choice (); 561 562for (const auto & tool_def :tools ) { 563if (!tool_def .contains ("function" )) { 564continue ; 565 } 566const auto & function = tool_def .at ("function" ); 567 std::string name = function .at ("name" ); 568ordered_json params = common_chat_tool_parameters (function ); 569 570auto args = eps (); 571if (params .contains ("properties" )&& !params ["properties" ].empty ()) { 572auto arg_choice = choice (); 573for (const auto & el :params ["properties" ].items ()) { 574const std::string & prop_name = el .key (); 575const auto & prop_def = el .value (); 576bool is_string_type = (prop_def .contains ("type" )&& prop_def ["type" ]== "string" ); 577 578auto arg_name_parser = literal (prop_name ); 579 580common_peg_parser arg_value_parser = eps (); 581// Quoted literal as a value: normalize_quotes_to_json preserves escapes. 582auto string_value_parser = tool_arg_value (choice ({ 583literal ("\"" )+ string_content ('"' )+ literal ("\"" ), 584literal ("'" )+ string_content ('\'' )+ literal ("'" ) 585 })); 586 587if (is_string_type ) { 588arg_value_parser = string_value_parser ; 589 }else { 590arg_value_parser = tool_arg_value (allow_json_literals ?python_or_json_value () :python_value ()); 591 } 592 593// Full argument: name="value" or name=value 594auto arg_rule = tool_arg ( 595tool_arg_open (tool_arg_name (arg_name_parser )+ literal ("=" ))+ 596arg_value_parser + 597tool_arg_close (eps ()) 598 ); 599arg_choice |=arg_rule ; 600 } 601 602args = arg_choice + zero_or_more ("," + space ()+ arg_choice ); 603 } 604 605auto tool_parser = tool (tool_open (tool_name (literal (name ))+ literal ("(" ))+ 606space ()+ tool_args (args )+ space ()+ tool_close (literal (")" )) 607 ); 608 609tool_choices |=rule ("tool-" + name ,tool_parser ); 610 } 611 612if (parallel_tool_calls ) { 613return "[" + space ()+ tool_choices + zero_or_more ("," + space ()+ tool_choices )+ space ()+ "]" ; 614 } 615return "[" + space ()+ tool_choices + space ()+ "]" ; 616} 617 618// Helper: Parse dot notation key into prefix and field name 619static std::pair < std::string , std::string > parse_key_spec (const std::string & key ) { 620auto dot_pos = key .find ('.' ); 621if (dot_pos == std::string::npos ) { 622return {"" ,key };// Top-level field 623 } 624return {key .substr (0 ,dot_pos ),key .substr (dot_pos + 1 )}; 625} 626 627// Mode 1: function_is_key — parse {"function_name": {...}} 628common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key ( 629const ordered_json & tools , 630const std::string & args_key , 631const std::string & effective_args_key , 632const std::string & call_id_key , 633const std::string & gen_call_id_key ) { 634 635auto tool_choices = choice (); 636 637for (const auto & tool_def :tools ) { 638if (!tool_def .contains ("function" )) { 639continue ; 640 } 641const auto & function = tool_def .at ("function" ); 642 std::string name = function .at ("name" ); 643ordered_json params = common_chat_tool_parameters (function ); 644 645// Build inner object fields 646 std::vector < common_peg_parser > inner_fields ; 647 648if (!call_id_key .empty ()) { 649auto id_parser = atomic ( 650literal ("\"" + call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 651literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ) 652 ); 653inner_fields .push_back (optional (id_parser + space ()+ optional (literal ("," )+ space ()))); 654 } 655 656if (!gen_call_id_key .empty ()) { 657auto gen_id_parser = atomic ( 658literal ("\"" + gen_call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 659choice ({ 660literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ), 661tool_id (json_number ()) 662 }) 663 ); 664inner_fields .push_back (optional (gen_id_parser + space ()+ optional (literal ("," )+ space ()))); 665 } 666 667// Arguments — either wrapped in args_key or parsed directly 668common_peg_parser args_parser = eps (); 669if (args_key .empty ()) { 670args_parser = tool_args (schema (json (),"tool-" + name + "-schema" ,params )); 671 }else { 672args_parser = literal ("\"" + effective_args_key + "\"" )+ space ()+ literal (":" )+ space ()+ 673tool_args (schema (json (),"tool-" + name + "-schema" ,params )); 674 } 675inner_fields .push_back (args_parser ); 676 677// Build inner object parser 678common_peg_parser inner_object = eps (); 679if (args_key .empty ()&& inner_fields .size ()== 1 ) { 680inner_object = inner_fields [0 ]; 681 }else { 682inner_object = literal ("{" )+ space (); 683for (size_t i = 0 ;i < inner_fields .size ();i ++ ) { 684inner_object = inner_object + inner_fields [i ]; 685if (i < inner_fields .size ()- 1 ) { 686inner_object = inner_object + space (); 687 } 688 } 689inner_object = inner_object + space ()+ literal ("}" ); 690 } 691 692auto tool_parser = tool ( 693tool_open (literal ("{" ))+ space ()+ 694literal ("\"" )+ tool_name (literal (name ))+ literal ("\"" )+ 695space ()+ literal (":" )+ space ()+ 696inner_object + 697space ()+ tool_close (literal ("}" )) 698 ); 699 700tool_choices |=rule ("tool-" + name ,tool_parser ); 701 } 702 703return tool_choices ; 704} 705 706// Mode 2: Nested keys (dot notation like "function.name") 707common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys ( 708const ordered_json & tools , 709const std::string & effective_name_key , 710const std::string & effective_args_key , 711const std::string & call_id_key , 712const std::string & gen_call_id_key ) { 713 714auto tool_choices = choice (); 715 716auto name_spec = parse_key_spec (effective_name_key ); 717auto args_spec = parse_key_spec (effective_args_key ); 718 719 std::string nested_prefix = !name_spec .first .empty () ?name_spec .first :args_spec .first ; 720 std::string nested_name_field = !name_spec .first .empty () ?name_spec .second :effective_name_key ; 721 std::string nested_args_field = !args_spec .first .empty () ?args_spec .second :effective_args_key ; 722 723for (const auto & tool_def :tools ) { 724if (!tool_def .contains ("function" )) { 725continue ; 726 } 727const auto & function = tool_def .at ("function" ); 728 std::string name = function .at ("name" ); 729ordered_json params = common_chat_tool_parameters (function ); 730 731auto nested_name = literal ("\"" + nested_name_field + "\"" )+ space ()+ literal (":" )+ space ()+ 732atomic (literal ("\"" )+ tool_name (literal (name ))+ literal ("\"" )); 733auto nested_args = literal ("\"" + nested_args_field + "\"" )+ space ()+ literal (":" )+ space ()+ 734tool_args (schema (json (),"tool-" + name + "-schema" ,params )); 735 736auto nested_object = literal ("{" )+ space ()+ 737nested_name + space ()+ literal ("," )+ space ()+ 738nested_args + 739space ()+ literal ("}" ); 740 741// Format: { id?, "function": {...} } 742auto tool_parser_body = tool_open (literal ("{" ))+ space (); 743 744if (!call_id_key .empty ()) { 745auto id_spec = parse_key_spec (call_id_key ); 746if (id_spec .first .empty ()) { 747auto id_parser = atomic ( 748literal ("\"" + call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 749literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ) 750 ); 751tool_parser_body = tool_parser_body + optional (id_parser + space ()+ literal ("," )+ space ()); 752 } 753 } 754 755if (!gen_call_id_key .empty ()) { 756auto gen_id_spec = parse_key_spec (gen_call_id_key ); 757if (gen_id_spec .first .empty ()) { 758auto gen_id_parser = atomic ( 759literal ("\"" + gen_call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 760choice ({ 761literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ), 762tool_id (json_number ()) 763 }) 764 ); 765tool_parser_body = tool_parser_body + optional (gen_id_parser + space ()+ literal ("," )+ space ()); 766 } 767 } 768 769auto nested_field = literal ("\"" + nested_prefix + "\"" )+ space ()+ literal (":" )+ space ()+ nested_object ; 770tool_parser_body = tool_parser_body + nested_field + space ()+ tool_close (literal ("}" )); 771 772tool_choices |=rule ("tool-" + name ,tool (tool_parser_body )); 773 } 774 775return tool_choices ; 776} 777 778// Mode 3: Flat keys with optional ID fields and parameter ordering 779common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys ( 780const ordered_json & tools , 781const std::string & effective_name_key , 782const std::string & effective_args_key , 783const std::string & call_id_key , 784const std::string & gen_call_id_key , 785const std::vector < std::string > & parameters_order , 786bool accept_openai_wrapper ) { 787 788auto tool_choices = choice (); 789auto name_key_parser = literal ("\"" + effective_name_key + "\"" ); 790auto args_key_parser = literal ("\"" + effective_args_key + "\"" ); 791 792for (const auto & tool_def :tools ) { 793if (!tool_def .contains ("function" )) { 794continue ; 795 } 796const auto & function = tool_def .at ("function" ); 797 std::string name = function .at ("name" ); 798ordered_json params = common_chat_tool_parameters (function ); 799 800auto tool_name_ = name_key_parser + space ()+ literal (":" )+ space ()+ 801atomic (literal ("\"" )+ tool_name (literal (name ))+ literal ("\"" )); 802auto tool_args_ = args_key_parser + space ()+ literal (":" )+ space ()+ 803tool_args (schema (json (),"tool-" + name + "-schema" ,params )); 804 805// Build ID parsers if keys are provided 806common_peg_parser id_parser = eps (); 807if (!call_id_key .empty ()) { 808id_parser = atomic ( 809literal ("\"" + call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 810choice ({ 811literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ), 812tool_id (json_number ()) 813 }) 814 ); 815 } 816 817common_peg_parser gen_id_parser = eps (); 818if (!gen_call_id_key .empty ()) { 819gen_id_parser = atomic ( 820literal ("\"" + gen_call_id_key + "\"" )+ space ()+ literal (":" )+ space ()+ 821choice ({ 822literal ("\"" )+ tool_id (string_content ('"' ))+ literal ("\"" ), 823tool_id (json_number ()) 824 }) 825 ); 826 } 827 828// Create (parser, key) pairs for all fields, then sort by parameters_order 829 std::vector < std::pair < common_peg_parser , std::string >> parser_pairs ; 830parser_pairs .emplace_back (tool_name_ ,effective_name_key ); 831parser_pairs .emplace_back (tool_args_ ,effective_args_key ); 832if (!call_id_key .empty ()) { 833parser_pairs .emplace_back (optional (id_parser ),call_id_key ); 834 } 835if (!gen_call_id_key .empty ()) { 836parser_pairs .emplace_back (optional (gen_id_parser ),gen_call_id_key ); 837 } 838 839 std::sort (parser_pairs .begin (),parser_pairs .end (), 840 [& parameters_order ](const auto & a ,const auto & b ) { 841auto pos_a = std::find (parameters_order .begin (),parameters_order .end (),a .second ); 842auto pos_b = std::find (parameters_order .begin (),parameters_order .end (),b .second ); 843size_t idx_a = (pos_a == parameters_order .end ()) ?parameters_order .size () : std::distance (parameters_order .begin (),pos_a ); 844size_t idx_b = (pos_b == parameters_order .end ()) ?parameters_order .size () : std::distance (parameters_order .begin (),pos_b ); 845return idx_a < idx_b ; 846 }); 847 848// accept an optional leading "type": "function" field when the model emits the OpenAI wrapper 849common_peg_parser type_field = eps (); 850if (accept_openai_wrapper ) { 851type_field = optional (literal ("\"type\"" )+ space ()+ literal (":" )+ space ()+ 852literal ("\"function\"" )+ space ()+ literal ("," )+ space ()); 853 } 854auto ordered_body = tool_open (literal ("{" ))+ space ()+ type_field ; 855for (size_t i = 0 ;i < parser_pairs .size ();i ++ ) { 856ordered_body = ordered_body + parser_pairs [i ].first ; 857if (i < parser_pairs .size ()- 1 ) { 858ordered_body = ordered_body + space ()+ literal ("," )+ space (); 859 } 860 } 861ordered_body = ordered_body + space ()+ tool_close (literal ("}" )); 862 863tool_choices |=rule ("tool-" + name ,tool (ordered_body )); 864 } 865 866return tool_choices ; 867} 868 869common_peg_parser common_chat_peg_builder::prefix (const std::string & s ,const std::string & delimiter ) { 870if (s .empty ()) { 871return eps (); 872 } 873if (delimiter .empty ()) { 874return literal (s ); 875 } 876return literal (s .substr (0 ,s .find (delimiter ))); 877} 878 879common_peg_parser common_chat_peg_builder::optspace (const std::string & tag ) { 880auto parser = eps (); 881size_t end_of_prefix_space = tag .size (); 882size_t start_of_suffix_space = tag .size (); 883for (size_t i = 0 ;i < tag .size ();i ++ ) { 884if (!std::isspace (tag [i ])) { 885end_of_prefix_space = i ; 886break ; 887 } 888 } 889for (size_t i = tag .size ();i > 0 ;i -- ) { 890if (!std::isspace (tag [i - 1 ])) { 891start_of_suffix_space = i ; 892break ; 893 } 894 } 895for (size_t i = 0 ;i < end_of_prefix_space ;i ++ ) { 896parser += optional (literal (std::string (1 ,tag [i ]))); 897 } 898parser += literal (tag .substr (end_of_prefix_space ,start_of_suffix_space - end_of_prefix_space )); 899for (size_t i = start_of_suffix_space ;i < tag .size ();i ++ ) { 900parser += optional (literal (std::string (1 ,tag [i ]))); 901 } 902return parser ; 903} 904 905common_peg_parser common_chat_peg_builder::standard_json_tools ( 906const std::string & section_start , 907const std::string & section_end , 908const ordered_json & tools , 909bool parallel_tool_calls , 910bool force_tool_calls , 911const std::string & name_key , 912const std::string & args_key , 913bool array_wrapped , 914bool function_is_key , 915const std::string & call_id_key , 916const std::string & gen_call_id_key , 917const std::vector < std::string > & parameters_order , 918bool accept_openai_wrapper ) { 919if (!tools .is_array ()|| tools .empty ()) { 920return eps (); 921 } 922 923 std::string effective_name_key = name_key .empty () ?"name" :name_key ; 924 std::string effective_args_key = args_key .empty () ?"arguments" :args_key ; 925 926// Dispatch to the appropriate builder based on the JSON layout mode 927common_peg_parser tool_choices = eps (); 928if (function_is_key ) { 929tool_choices = build_json_tools_function_is_key (tools ,args_key ,effective_args_key ,call_id_key ,gen_call_id_key ); 930 }else { 931auto name_spec = parse_key_spec (effective_name_key ); 932auto args_spec = parse_key_spec (effective_args_key ); 933if (!name_spec .first .empty ()|| !args_spec .first .empty ()) { 934tool_choices = build_json_tools_nested_keys (tools ,effective_name_key ,effective_args_key ,call_id_key ,gen_call_id_key ); 935 }else { 936tool_choices = build_json_tools_flat_keys (tools ,effective_name_key ,effective_args_key ,call_id_key ,gen_call_id_key ,parameters_order ,accept_openai_wrapper ); 937 } 938 } 939 940// Build the section with markers 941auto tool_calls = tool_choices ; 942if (parallel_tool_calls ) { 943tool_calls = tool_calls + zero_or_more (space ()+ literal ("," )+ space ()+ tool_choices ); 944 } 945 946if (array_wrapped ) { 947tool_calls = literal ("[" )+ space ()+ tool_calls + space ()+ literal ("]" ); 948 } 949 950auto section = 951trigger_rule ("tool-call" ,literal (section_start )+ space ()+ tool_calls + space ()+ literal (section_end )); 952 953return force_tool_calls ?section :optional (section ); 954} 955 956void common_chat_peg_gemma4_mapper::from_ast (const common_peg_ast_arena & arena ,const common_peg_parse_result & result ) { 957for (const auto & node :result .nodes ) { 958visit (arena ,node ); 959 } 960} 961 962static std::string gemma4_to_json (const common_peg_ast_arena & arena ,common_peg_ast_id id ) { 963const auto & node = arena .get (id ); 964 965if (node .text .empty ()) { 966return "" ; 967 } 968 969if (node .rule == "gemma4-number" || node .rule == "gemma4-bool" || node .rule == "gemma4-null" ) { 970return std::string (node .text ); 971 } 972 973if (node .rule == "gemma4-string-content" ) { 974return escape_json_string_inner (std::string (node .text )); 975 } 976 977if (node .rule == "gemma4-string" ) { 978 std::string result = "\"" ; 979if (!node .children .empty ()) { 980result += gemma4_to_json (arena ,node .children [0 ]); 981if (!node .is_partial ) { 982result += "\"" ; 983 } 984 } 985return result ; 986 } 987 988if (node .rule == "gemma4-array" ) { 989 std::string result = "[" ; 990 991bool add_comma = false; 992for (auto child_id :node .children ) { 993if (add_comma ) { 994result += ',' ; 995 } 996add_comma = true; 997result += gemma4_to_json (arena ,child_id ); 998 } 999 1000if (!node .is_partial ) { 1001result += ']' ; 1002 } 1003return result ; 1004 } 1005 1006if (node .rule == "gemma4-dict-key-name" ) { 1007return std::string (node .text ); 1008 } 1009 1010if (node .rule == "gemma4-dict-key" ) { 1011 std::string result = "\"" ; 1012if (!node .children .empty ()) { 1013result += escape_json_string_inner (gemma4_to_json (arena ,node .children [0 ])); 1014 } 1015if (!node .is_partial ) { 1016result += "\":" ; 1017 } 1018return result ; 1019 } 1020 1021if (node .rule == "gemma4-dict-kv" ) { 1022 std::string result ; 1023for (auto child_id :node .children ) { 1024result += gemma4_to_json (arena ,child_id ); 1025 } 1026return result ; 1027 } 1028 1029if (node .rule == "gemma4-dict" ) { 1030 std::string result = "{" ; 1031 1032bool add_comma = false; 1033for (auto child_id :node .children ) { 1034if (add_comma ) { 1035result += ',' ; 1036 } 1037add_comma = true; 1038result += gemma4_to_json (arena ,child_id ); 1039 } 1040 1041if (!node .is_partial ) { 1042result += '}' ; 1043 } 1044return result ; 1045 } 1046 1047if (node .rule == "gemma4-value" ) { 1048if (!node .children .empty ()) { 1049return gemma4_to_json (arena ,node .children [0 ]); 1050 } 1051return "" ; 1052 } 1053 1054return "" ; 1055} 1056 1057void common_chat_peg_gemma4_mapper::visit (const common_peg_ast_arena & arena ,common_peg_ast_id id ) { 1058const auto & node = arena .get (id ); 1059 1060if (node .tag == "reasoning" ) { 1061result .reasoning_content += std::string (node .text ); 1062return ; 1063 } 1064 1065if (node .tag == "content" ) { 1066result .content += std::string (node .text ); 1067return ; 1068 } 1069 1070if (node .tag == "tool" ) { 1071auto name_id = arena .find_by_tag (node ,"tool-name" ); 1072auto args_id = arena .find_by_tag (node ,"tool-args" ); 1073 1074if (name_id != COMMON_PEG_INVALID_AST_ID && args_id != COMMON_PEG_INVALID_AST_ID ) { 1075const auto & name_node = arena .get (name_id ); 1076const auto & args_node = arena .get (args_id ); 1077 1078if (!name_node .is_partial ) { 1079common_chat_tool_call call ; 1080call .name = std::string (name_node .text ); 1081if (!args_node .children .empty ()) { 1082call .arguments = gemma4_to_json (arena ,args_node .children [0 ]); 1083 } 1084result .tool_calls .push_back (call ); 1085 } 1086 } 1087 1088return ; 1089 } 1090 1091for (auto child_id :node .children ) { 1092visit (arena ,child_id ); 1093 } 1094} 1095 1096static void minimax_m3_collect (const common_peg_ast_arena & arena , 1097const common_peg_ast_node & node , 1098const std::string & tag , 1099 std::vector < common_peg_ast_id > & out ) { 1100for (auto child_id :node .children ) { 1101const auto & child = arena .get (child_id ); 1102if (child .tag == tag ) { 1103out .push_back (child_id ); 1104 }else { 1105minimax_m3_collect (arena ,child ,tag ,out ); 1106 } 1107 } 1108} 1109 1110static common_peg_ast_id minimax_m3_value_of (const common_peg_ast_arena & arena ,const common_peg_ast_node & node ) { 1111for (auto child_id :node .children ) { 1112const auto & tag = arena .get (child_id ).tag ; 1113if (tag == common_chat_peg_builder::TOOL_ARG_VALUE || 1114tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE || 1115tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT || 1116tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY ) { 1117return child_id ; 1118 } 1119 } 1120return COMMON_PEG_INVALID_AST_ID ; 1121} 1122 1123static std::string minimax_m3_value_to_json (const common_peg_ast_arena & arena ,common_peg_ast_id id ,bool closed ); 1124 1125static std::string minimax_m3_member_to_json (const common_peg_ast_arena & arena ,const common_peg_ast_node & node ) { 1126auto name_id = arena .find_by_tag (node , common_chat_peg_builder::TOOL_ARG_NAME ); 1127if (name_id == COMMON_PEG_INVALID_AST_ID ) { 1128return "" ; 1129 } 1130 1131return ordered_json (arena .get (name_id ).text ).dump ()+ ":" + 1132minimax_m3_value_to_json (arena ,minimax_m3_value_of (arena ,node ), !node .is_partial ); 1133} 1134 1135static std::string minimax_m3_container_to_json (const common_peg_ast_arena & arena , 1136const common_peg_ast_node & node , 1137bool is_object , 1138bool closed ) { 1139const std::string tag = is_object ? common_chat_peg_builder::TOOL_ARG 1140 : common_chat_peg_minimax_m3_mapper::TOOL_ARG_ITEM ; 1141 1142 std::vector < common_peg_ast_id > entries ; 1143minimax_m3_collect (arena ,node ,tag ,entries ); 1144 1145 std::string result = is_object ?"{" :"[" ; 1146 1147bool add_comma = false; 1148for (auto entry_id :entries ) { 1149const auto & entry = arena .get (entry_id ); 1150 1151 std::string text ; 1152if (is_object ) { 1153text = minimax_m3_member_to_json (arena ,entry ); 1154 }else { 1155text = minimax_m3_value_to_json (arena ,minimax_m3_value_of (arena ,entry ), !entry .is_partial ); 1156 } 1157 1158if (text .empty ()) { 1159continue ; 1160 } 1161 1162if (add_comma ) { 1163result += "," ; 1164 } 1165add_comma = true; 1166result += text ; 1167 } 1168 1169if (closed ) { 1170result += is_object ?"}" :"]" ; 1171 } 1172return result ; 1173} 1174 1175static std::string minimax_m3_value_to_json (const common_peg_ast_arena & arena ,common_peg_ast_id id ,bool closed ) { 1176if (id == COMMON_PEG_INVALID_AST_ID ) { 1177return "" ; 1178 } 1179 1180const auto & node = arena .get (id ); 1181 1182if (node .tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT ) { 1183return minimax_m3_container_to_json (arena ,node ,/* is_object = */ true,closed ); 1184 } 1185 1186if (node .tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY ) { 1187return minimax_m3_container_to_json (arena ,node ,/* is_object = */ false,closed ); 1188 } 1189 1190if (node .tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE ) { 1191return "\"" + escape_json_string_inner (std::string (node .text ))+ (closed ?"\"" :"" ); 1192 } 1193 1194// Numbers and booleans are written verbatim by the template 1195return std::string (node .text ); 1196} 1197 1198void common_chat_peg_minimax_m3_mapper::from_ast (const common_peg_ast_arena & arena , 1199const common_peg_parse_result & result ) { 1200for (const auto & node :result .nodes ) { 1201visit (arena ,node ); 1202 } 1203} 1204 1205void common_chat_peg_minimax_m3_mapper::visit (const common_peg_ast_arena & arena ,common_peg_ast_id id ) { 1206const auto & node = arena .get (id ); 1207 1208if (node .tag == common_chat_peg_builder::REASONING ) { 1209result .reasoning_content += std::string (node .text ); 1210return ; 1211 } 1212 1213if (node .tag == common_chat_peg_builder::CONTENT ) { 1214result .content += std::string (node .text ); 1215return ; 1216 } 1217 1218if (node .tag == common_chat_peg_builder::TOOL ) { 1219auto name_id = arena .find_by_tag (node , common_chat_peg_builder::TOOL_NAME ); 1220if (name_id != COMMON_PEG_INVALID_AST_ID ) { 1221common_chat_tool_call call ; 1222call .name = std::string (arena .get (name_id ).text ); 1223call .arguments = minimax_m3_container_to_json (arena ,node ,/* is_object = */ true, !node .is_partial ); 1224result .tool_calls .push_back (call ); 1225 } 1226return ; 1227 } 1228 1229for (auto child_id :node .children ) { 1230visit (arena ,child_id ); 1231 } 1232}