char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
acecd5603
char/aliceai-t5-moe
1#include "chat.h" 2 3#include "chat-auto-parser-helpers.h" 4#include "chat-auto-parser.h" 5#include "chat-peg-parser.h" 6#include "common.h" 7#include "ggml.h" 8#include "json-schema-to-grammar.h" 9#include "json.h" 10#include "log.h" 11#include "parsers/parsers.h" 12 13#include "jinja/value.h" 14#include "jinja/runtime.h" 15#include "jinja/caps.h" 16#include "peg-parser.h" 17 18#include <algorithm> 19#include <cstdio> 20#include <cstdlib> 21#include <ctime> 22#include <exception> 23#include <functional> 24#include <iomanip> 25#include <map> 26 27#include <optional> 28#include <sstream> 29#include <stdexcept> 30#include <string> 31#include <utility> 32#include <vector> 33 34using json = common_json ; 35 36static std::string format_time (const std::chrono::system_clock::time_point & now ,const std::string & format ) { 37auto time = std::chrono::system_clock::to_time_t (now ); 38auto local_time = * std::localtime (& time ); 39 std::ostringstream ss ; 40ss << std::put_time (& local_time ,format .c_str ()); 41auto res = ss .str (); 42return res ; 43} 44 45static json safe_args_parse (const std::string & to_parse ) { 46 std::string stripped = to_parse ; 47if (to_parse .at (0 )== '"' && to_parse .at (to_parse .length ()- 1 )== '"' ) { 48stripped = to_parse .substr (1 ,to_parse .length ()- 1 ); 49 } 50try { 51return json::parse (stripped ); 52 }catch (const common_json_error & e ) { 53return stripped ; 54 } 55} 56 57static std::string string_diff (const std::string & last ,const std::string & current ) { 58if (last .empty ()) { 59return current ; 60 } 61if (!string_starts_with (current ,last )) { 62if (string_starts_with (last ,current )) { 63// This happens if the last generation ended on a partial stop word (not erased), 64// and the current ended on a stop word (erased). 65return "" ; 66 } 67throw std::runtime_error ("Invalid diff: '" + last + "' not found at start of '" + current + "'" ); 68 } 69return current .substr (last .size ()); 70} 71 72static bool has_content_or_tool_calls (const common_chat_msg & msg ) { 73return !msg .content .empty ()|| !msg .tool_calls .empty (); 74} 75 76std::string common_chat_msg::render_content (const std::string & delimiter )const { 77if (!content .empty ()&& !content_parts .empty ()) { 78throw std::runtime_error ("Cannot specify both content and content_parts" ); 79 } 80if (!content .empty ()) { 81return content ; 82 } 83 84 std::string text ; 85for (const auto & part :content_parts ) { 86if (part .type == "text" ) { 87if (!text .empty ()) { 88text += delimiter ; 89 } 90text += part .text ; 91 } 92 } 93return text ; 94} 95 96common_chat_role common_chat_role_from_string (const std::string & role ) { 97if (role == "system" ) {return COMMON_CHAT_ROLE_SYSTEM ; } 98if (role == "assistant" ) {return COMMON_CHAT_ROLE_ASSISTANT ; } 99if (role == "user" ) {return COMMON_CHAT_ROLE_USER ; } 100if (role == "tool" ) {return COMMON_CHAT_ROLE_TOOL ; } 101return COMMON_CHAT_ROLE_UNKNOWN ; 102} 103 104const char * common_chat_role_to_string (common_chat_role role ) { 105switch (role ) { 106case COMMON_CHAT_ROLE_SYSTEM :return "system" ; 107case COMMON_CHAT_ROLE_ASSISTANT :return "assistant" ; 108case COMMON_CHAT_ROLE_USER :return "user" ; 109case COMMON_CHAT_ROLE_TOOL :return "tool" ; 110case COMMON_CHAT_ROLE_UNKNOWN :return "" ; 111 } 112return "" ; 113} 114 115json common_chat_msg_delimiters::to_json ()const { 116json result = json::array (); 117for (const auto & d :delimiters ) { 118result .push_back ({ 119 {"role" ,common_chat_role_to_string (d .role ) }, 120 {"delimiter" ,d .delimiter }, 121 }); 122 } 123return result ; 124} 125 126common_chat_msg_delimiters common_chat_msg_delimiters_parse (const json & delimiters ) { 127common_chat_msg_delimiters result ; 128 129if (!delimiters .is_array ()) { 130return result ; 131 } 132 133result .delimiters .reserve (delimiters .size ()); 134for (const auto & d :delimiters ) { 135if (!d .is_object ()) { 136continue ; 137 } 138result .delimiters .push_back ({ 139common_chat_role_from_string (d .value ("role" , std::string ())), 140d .value ("delimiter" , std::string ()), 141 }); 142 } 143 144return result ; 145} 146 147void common_chat_msg_delimiters::tokenize (const llama_vocab * vocab ) { 148for (auto & d :delimiters ) { 149d .tokens = common_tokenize (vocab ,d .delimiter , false, true); 150 } 151} 152 153common_chat_msg_spans common_chat_msg_delimiters::split (const llama_tokens & tokens ,const std::map < size_t ,size_t > & skips )const { 154 std::vector < std::pair < common_chat_role ,size_t >> matches ; 155 156auto skip = skips .begin (); 157for (size_t i = 0 ;i < tokens .size ();) { 158if (skip != skips .end ()&& i == skip -> first ) { 159i += skip -> second ; 160++ skip ; 161continue ; 162 } 163for (const auto & d :delimiters ) { 164if (i + d .tokens .size ()> tokens .size ()) { 165continue ; 166 } 167if (std::equal (d .tokens .begin (),d .tokens .end (),tokens .begin ()+ i )) { 168matches .emplace_back (d .role ,i ); 169break ; 170 } 171 } 172i ++ ; 173 } 174 175matches .emplace_back (COMMON_CHAT_ROLE_UNKNOWN ,tokens .size ()); 176 177common_chat_msg_spans spans ; 178for (size_t i = 0 ;i + 1 < matches .size ();i ++ ) { 179const auto & curr = matches [i ]; 180const auto & next = matches [i + 1 ]; 181spans .add (curr .first ,curr .second ,next .second - curr .second ); 182 } 183 184return spans ; 185} 186 187json common_chat_msg::to_json_oaicompat (bool concat_typed_text )const { 188if (!content .empty ()&& !content_parts .empty ()) { 189throw std::runtime_error ("Cannot specify both content and content_parts" ); 190 } 191json jmsg { 192 {"role" ,role }, 193 }; 194if (!content .empty ()) { 195jmsg ["content" ]= content ; 196 }else if (!content_parts .empty ()) { 197if (concat_typed_text || contains_media ()) { 198 std::string text ; 199bool last_was_media_marker = false; 200// join parts with newline, do not add newline before or after media markers 201for (const auto & part :content_parts ) { 202bool add_new_line = true; 203if (part .type == "text" ) { 204add_new_line = !last_was_media_marker && !text .empty (); 205last_was_media_marker = false; 206 }else if (part .type == "media_marker" ) { 207add_new_line = false; 208last_was_media_marker = true; 209 }else { 210LOG_WRN ("Ignoring content part type: %s\n" ,part .type .c_str ()); 211continue ; 212 } 213 214if (add_new_line ) { 215text += '\n' ; 216 } 217 218text += part .text ; 219 } 220jmsg ["content" ]= text ; 221 }else { 222auto & parts = jmsg ["content" ]= json::array (); 223for (const auto & part :content_parts ) { 224parts .push_back ({ 225 {"type" ,part .type }, 226 {"text" ,part .text }, 227 }); 228 } 229 } 230 }else { 231jmsg ["content" ]= "" ; 232 } 233if (!reasoning_content .empty ()) { 234jmsg ["reasoning_content" ]= reasoning_content ; 235 } 236if (!tool_name .empty ()) { 237jmsg ["name" ]= tool_name ; 238 } 239if (!tool_call_id .empty ()) { 240jmsg ["tool_call_id" ]= tool_call_id ; 241 } 242if (!tool_calls .empty ()) { 243jmsg ["tool_calls" ]= json::array (); 244auto & jtool_calls = jmsg ["tool_calls" ]; 245for (const auto & tool_call :tool_calls ) { 246json tc { 247 {"type" ,"function" }, 248 {"function" , { 249 {"name" ,tool_call .name }, 250 {"arguments" ,json (tool_call .arguments )}, 251 }}, 252 }; 253if (!tool_call .id .empty ()) { 254tc ["id" ]= tool_call .id ; 255 } 256// Some templates generate and require an id (sometimes in a very specific format, e.g. Mistral Nemo). 257// We only generate a random id for the ones that don't generate one by themselves 258// (they also won't get to see it as their template likely doesn't use it, so it's all for the client) 259// {"id", tc.id.empty() ? gen_tool_call_id() : tc.id}, 260jtool_calls .push_back (tc ); 261 } 262 } 263 264return jmsg ; 265} 266 267std::vector < common_chat_msg_diff > common_chat_msg_diff::compute_diffs (const common_chat_msg & msg_prv , 268const common_chat_msg & msg_new ) { 269 std::vector < common_chat_msg_diff > diffs ; 270if (msg_new .tool_calls .size ()> msg_prv .tool_calls .size ()) { 271diffs .reserve (msg_new .tool_calls .size ()- msg_prv .tool_calls .size ()+ 3 ); 272 }else { 273diffs .reserve (3 ); 274 } 275 276// TODO: these can become expensive for long messages - how to optimize? 277if (msg_prv .reasoning_content != msg_new .reasoning_content ) { 278auto & diff = diffs .emplace_back (); 279diff .reasoning_content_delta = string_diff (msg_prv .reasoning_content ,msg_new .reasoning_content ); 280 } 281if (msg_prv .content != msg_new .content ) { 282auto & diff = diffs .emplace_back (); 283diff .content_delta = string_diff (msg_prv .content ,msg_new .content ); 284 } 285 286if (msg_new .tool_calls .size ()< msg_prv .tool_calls .size ()) { 287 std::string err = "Invalid diff: now finding less tool calls!\n" ; 288err += " Previous (" + std::to_string (msg_prv .tool_calls .size ())+ "):\n" ; 289for (const auto & tc :msg_prv .tool_calls ) { 290err += " - name: '" + tc .name + "', args: '" + tc .arguments + "'\n" ; 291 } 292err += " Current (" + std::to_string (msg_new .tool_calls .size ())+ "):\n" ; 293for (const auto & tc :msg_new .tool_calls ) { 294err += " - name: '" + tc .name + "', args: '" + tc .arguments + "'\n" ; 295 } 296err += " Current msg text content:\n" + msg_new .content + "\n" ; 297throw std::runtime_error (err ); 298 } 299 300if (!msg_prv .tool_calls .empty ()) { 301const auto idx = msg_prv .tool_calls .size ()- 1 ; 302const auto & pref = msg_prv .tool_calls [idx ]; 303const auto & newf = msg_new .tool_calls [idx ]; 304// Allow tool name to change during incremental parsing: 305// - empty -> non-empty (initial discovery) 306// - prefix -> longer string (name grows as more input is parsed) 307if (pref .name != newf .name && !pref .name .empty ()&& !newf .name .empty ()) { 308// Check if one is a prefix of the other (for incremental parsing where names grow or shrink) 309bool is_prefix = (newf .name .rfind (pref .name ,0 )== 0 ); 310if (!is_prefix ) { 311LOG_ERR ("Tool call mismatch: prev='%s' new='%s'\n" ,pref .name .c_str (),newf .name .c_str ()); 312throw std::runtime_error ("Invalid diff: tool call mismatch!" ); 313 } 314 } 315const auto args_diff = string_diff (pref .arguments ,newf .arguments ); 316if (!args_diff .empty ()|| pref .id != newf .id || pref .name != newf .name ) { 317auto & diff = diffs .emplace_back (); 318diff .tool_call_index = idx ; 319if (pref .id != newf .id || pref .name != newf .name ) { 320diff .tool_call_delta .id = newf .id ; 321diff .tool_call_delta .name = newf .name ; 322 } 323diff .tool_call_delta .arguments = args_diff ; 324 } 325 } 326for (size_t idx = msg_prv .tool_calls .size ();idx < msg_new .tool_calls .size ();++ idx ) { 327auto & diff = diffs .emplace_back (); 328diff .tool_call_index = idx ; 329diff .tool_call_delta = msg_new .tool_calls [idx ]; 330 } 331 332return diffs ; 333} 334 335using chat_template_caps = jinja::caps ; 336 337struct common_chat_templates { 338bool add_bos ; 339bool add_eos ; 340bool has_explicit_template ;// Model had builtin template or template overridden was specified. 341 std::unique_ptr < common_chat_template > template_default ;// always set (defaults to chatml) 342 std::unique_ptr < common_chat_template > template_tool_use ; 343}; 344 345common_chat_tool_choice common_chat_tool_choice_parse_oaicompat (const std::string & tool_choice ) { 346if (tool_choice == "auto" ) { 347return COMMON_CHAT_TOOL_CHOICE_AUTO ; 348 } 349if (tool_choice == "none" ) { 350return COMMON_CHAT_TOOL_CHOICE_NONE ; 351 } 352if (tool_choice == "required" ) { 353return COMMON_CHAT_TOOL_CHOICE_REQUIRED ; 354 } 355throw std::invalid_argument ("Invalid tool_choice: " + tool_choice ); 356} 357 358bool common_chat_templates_support_enable_thinking (const common_chat_templates * chat_templates ) { 359common_chat_templates_inputs inputs ; 360inputs .reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK ; 361common_chat_msg msg ; 362msg .role = "user" ; 363msg .content = "test" ; 364inputs .messages = {msg }; 365inputs .enable_thinking = true; 366inputs .add_generation_prompt = true; 367inputs .reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK ; 368 369auto params = common_chat_templates_apply (chat_templates ,inputs ); 370return params .supports_thinking ; 371} 372 373std::vector < common_chat_msg > common_chat_msgs_parse_oaicompat (const json & messages ) { 374 std::vector < common_chat_msg > msgs ; 375 376try { 377if (!messages .is_array ()) { 378throw std::invalid_argument ("Expected 'messages' to be an array, got " + messages .dump ()); 379 } 380 381for (const auto & message :messages ) { 382if (!message .is_object ()) { 383throw std::invalid_argument ("Expected 'message' to be an object, got " + message .dump ()); 384 } 385 386common_chat_msg msg ; 387if (!message .contains ("role" )) { 388throw std::invalid_argument ("Missing 'role' in message: " + message .dump ()); 389 } 390msg .role = message .at ("role" ); 391 392auto has_content = message .contains ("content" ); 393auto has_tool_calls = message .contains ("tool_calls" ); 394if (has_content ) { 395const auto & content = message .at ("content" ); 396if (content .is_string ()) { 397msg .content = content ; 398 }else if (content .is_array ()) { 399for (const auto & part :content ) { 400if (!part .contains ("type" )) { 401throw std::invalid_argument ("Missing content part type: " + part .dump ()); 402 } 403const auto & type = part .at ("type" ); 404if (type != "text" && type != "media_marker" ) { 405throw std::invalid_argument ("Unsupported content part type: " + type .dump ()); 406 } 407common_chat_msg_content_part msg_part ; 408msg_part .type = type ; 409msg_part .text = part .at ("text" ); 410msg .content_parts .push_back (msg_part ); 411 } 412 }else if (!content .is_null ()) { 413throw std::invalid_argument ("Invalid 'content' type: expected string or array, got " + 414content .dump ()+ 415" (ref: https://github.com/ggml-org/llama.cpp/issues/8367)" ); 416 } 417 } 418if (has_tool_calls ) { 419for (const auto & tool_call :message .at ("tool_calls" )) { 420common_chat_tool_call tc ; 421if (!tool_call .contains ("type" )) { 422throw std::invalid_argument ("Missing tool call type: " + tool_call .dump ()); 423 } 424const auto & type = tool_call .at ("type" ); 425if (type != "function" ) { 426throw std::invalid_argument ("Unsupported tool call type: " + tool_call .dump ()); 427 } 428if (!tool_call .contains ("function" )) { 429throw std::invalid_argument ("Missing tool call function: " + tool_call .dump ()); 430 } 431const auto & fc = tool_call .at ("function" ); 432if (!fc .contains ("name" )) { 433throw std::invalid_argument ("Missing tool call name: " + tool_call .dump ()); 434 } 435tc .name = fc .at ("name" ); 436const auto & args = fc .at ("arguments" ); 437if (args .is_string ()) { 438tc .arguments = args ; 439 }else { 440tc .arguments = args .dump (); 441 } 442if (tool_call .contains ("id" )) { 443tc .id = tool_call .at ("id" ); 444 } 445msg .tool_calls .push_back (tc ); 446 } 447 } 448if (!has_content && !has_tool_calls ) { 449throw std::invalid_argument ( 450"Expected 'content' or 'tool_calls' (ref: https://github.com/ggml-org/llama.cpp/issues/8367 & " 451"https://github.com/ggml-org/llama.cpp/issues/12279)" ); 452 } 453if (message .contains ("reasoning_content" )) { 454msg .reasoning_content = message .at ("reasoning_content" ); 455 } 456if (message .contains ("name" )) { 457msg .tool_name = message .at ("name" ); 458 } 459if (message .contains ("tool_call_id" )) { 460msg .tool_call_id = message .at ("tool_call_id" ); 461 } 462 463msgs .push_back (msg ); 464 } 465 }catch (const std::exception & e ) { 466// @ngxson : disable otherwise it's bloating the API response 467// printf("%s\n", std::string("; messages = ") + messages.dump(2)); 468throw std::runtime_error ("Failed to parse messages: " + std::string (e .what ())); 469 } 470 471return msgs ; 472} 473 474struct messages_inp_normalizer { 475const jinja::caps & caps ; 476 477messages_inp_normalizer (const jinja::caps & c ) :caps (c ) {} 478 479// handle supports_string_content / supports_typed_content 480// if string=true and array=false, convert array to string 481// if string=false and array=true, convert string to array 482// if both are true, do nothing 483json normalize (const json & messages ) { 484bool only_string = caps .supports_string_content && !caps .supports_typed_content ; 485bool only_typed = !caps .supports_string_content && caps .supports_typed_content ; 486if ((!only_string && !only_typed )|| !messages .is_array ()) { 487return messages ; 488 } 489json normalized = json::array (); 490for (const auto & msg :messages ) { 491json copy = msg ; 492if (copy .contains ("content" )) { 493json & it = copy .at ("content" ); 494if (only_typed && it .is_string ()) { 495it = json::array ({ 496json { 497 {"type" ,"text" }, 498 {"text" ,it .get < std::string > ()}, 499 } 500 }); 501 }else if (only_string && it .is_array ()) { 502it = concat_content_parts (it ); 503 } 504 } 505normalized .push_back (std::move (copy )); 506 } 507return normalized ; 508 } 509 510// join parts with newline, do not add newline before or after media markers 511static std::string concat_content_parts (const json & parts ) { 512 std::string text ; 513bool last_was_media_marker = false; 514for (const auto & part :parts ) { 515 std::string type = part .value ("type" ,"" ); 516bool add_new_line = true; 517if (type == "text" ) { 518add_new_line = !last_was_media_marker && !text .empty (); 519last_was_media_marker = false; 520 }else if (type == "media_marker" ) { 521add_new_line = false; 522last_was_media_marker = true; 523 }else { 524LOG_WRN ("Ignoring content part type: %s\n" ,type .c_str ()); 525continue ; 526 } 527 528if (add_new_line ) { 529text += '\n' ; 530 } 531 532text += part .value ("text" ,"" ); 533 } 534return text ; 535 } 536}; 537 538static json render_message_to_json (const std::vector < common_chat_msg > & msgs ,const jinja::caps & c ) { 539if (!c .supports_string_content && !c .supports_typed_content ) { 540LOG_WRN ("%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues.\n" ,__func__ ); 541 } 542 543json messages = json::array (); 544for (const auto & msg :msgs ) { 545messages .push_back (msg .to_json_oaicompat (/* concat_typed_text= */ false)); 546 } 547return messages_inp_normalizer (c ).normalize (messages ); 548} 549 550// DEPRECATED: only used in tests 551json common_chat_msgs_to_json_oaicompat (const std::vector < common_chat_msg > & msgs ,bool concat_typed_text ) { 552 jinja::caps c ; 553c .supports_string_content = true; 554c .supports_typed_content = !concat_typed_text ; 555return render_message_to_json (msgs ,c ); 556} 557 558json common_chat_tools_to_json_oaicompat (const std::vector < common_chat_tool > & tools ) { 559if (tools .empty ()) { 560return json (); 561 } 562 563auto result = json::array (); 564for (const auto & tool :tools ) { 565result .push_back ({ 566 {"type" ,"function" }, 567 {"function" , { 568 {"name" ,tool .name }, 569 {"description" ,tool .description }, 570 {"parameters" , json::parse (tool .parameters ) }, 571 }}, 572 }); 573 } 574return result ; 575} 576 577json common_chat_tool_parameters (const json & function ) { 578if (function .contains ("parameters" )) { 579const auto & params = function .at ("parameters" ); 580if (!params .is_null ()&& !(params .is_object ()&& params .empty ())) { 581return params ; 582 } 583 } 584return json {{"type" ,"object" }, {"properties" , json::object ()}}; 585} 586 587std::vector < common_chat_tool > common_chat_tools_parse_oaicompat (const json & tools ) { 588 std::vector < common_chat_tool > result ; 589 590try { 591if (!tools .is_null ()) { 592if (!tools .is_array ()) { 593throw std::invalid_argument ("Expected 'tools' to be an array, got " + tools .dump ()); 594 } 595for (const auto & tool :tools ) { 596if (!tool .contains ("type" )) { 597throw std::invalid_argument ("Missing tool type: " + tool .dump ()); 598 } 599const auto & type = tool .at ("type" ); 600if (!type .is_string ()|| type != "function" ) { 601throw std::invalid_argument ("Unsupported tool type: " + tool .dump ()); 602 } 603if (!tool .contains ("function" )) { 604throw std::invalid_argument ("Missing tool function: " + tool .dump ()); 605 } 606 607const auto & function = tool .at ("function" ); 608result .push_back ({ 609/* .name = */ function .at ("name" ), 610/* .description = */ function .value ("description" ,"" ), 611/* .parameters = */ function .value ("parameters" , json::object ()).dump (), 612 }); 613 } 614 } 615 }catch (const std::exception & e ) { 616throw std::runtime_error ("Failed to parse tools: " + std::string (e .what ())+ "; tools = " + tools .dump (2 )); 617 } 618 619return result ; 620} 621 622common_chat_continuation common_chat_continuation_parse (const common_json & value ) { 623if (value .is_boolean ()&& value .get < bool > ()) { 624return COMMON_CHAT_CONTINUATION_AUTO ; 625 } 626if (value .is_string ()) { 627auto value_str = value .get < std::string > (); 628if (value_str == "reasoning_content" ) { 629return COMMON_CHAT_CONTINUATION_REASONING ; 630 } 631if (value_str == "content" ) { 632return COMMON_CHAT_CONTINUATION_CONTENT ; 633 } 634 } 635return COMMON_CHAT_CONTINUATION_NONE ; 636} 637 638bool common_chat_verify_template (const std::string & tmpl ,bool use_jinja ) { 639if (use_jinja ) { 640try { 641common_chat_msg msg ; 642msg .role = "user" ; 643msg .content = "test" ; 644 645auto tmpls = common_chat_templates_init (/* model= */ nullptr ,tmpl ); 646 647common_chat_templates_inputs inputs ; 648inputs .messages = {msg }; 649 650common_chat_templates_apply (tmpls .get (),inputs ); 651return true; 652 }catch (const std::exception & e ) { 653LOG_ERR ("%s: failed to apply template: %s\n" ,__func__ ,e .what ()); 654return false; 655 } 656 } 657llama_chat_message chat []= { 658 {"user" ,"test" } 659 }; 660const int res = llama_chat_apply_template (tmpl .c_str (),chat ,1 , true,nullptr ,0 ); 661return res >=0 ; 662} 663 664std::string common_chat_format_single (const struct common_chat_templates * tmpls , 665const std::vector < common_chat_msg > & past_msg , 666const common_chat_msg & new_msg , 667bool add_ass , 668bool use_jinja ) { 669common_chat_templates_inputs inputs ; 670inputs .use_jinja = use_jinja ; 671inputs .add_bos = tmpls -> add_bos ; 672inputs .add_eos = tmpls -> add_eos ; 673 674 std::string fmt_past_msg ; 675if (!past_msg .empty ()) { 676inputs .messages = past_msg ; 677inputs .add_generation_prompt = false; 678fmt_past_msg = common_chat_templates_apply (tmpls ,inputs ).prompt ; 679 } 680 std::ostringstream ss ; 681// if the past_msg ends with a newline, we must preserve it in the formatted version 682if (add_ass && !fmt_past_msg .empty ()&& fmt_past_msg .back ()== '\n' ) { 683ss <<"\n" ; 684 }; 685// format chat with new_msg 686inputs .messages .push_back (new_msg ); 687inputs .add_generation_prompt = add_ass ; 688auto fmt_new_msg = common_chat_templates_apply (tmpls ,inputs ).prompt ; 689// get the diff part 690ss <<fmt_new_msg .substr (fmt_past_msg .size (),fmt_new_msg .size ()- fmt_past_msg .size ()); 691return ss .str (); 692} 693 694std::string common_chat_format_example (const struct common_chat_templates * tmpls , 695bool use_jinja , 696const std::map < std::string , std::string > & chat_template_kwargs ) { 697common_chat_templates_inputs inputs ; 698inputs .use_jinja = use_jinja ; 699inputs .add_bos = tmpls -> add_bos ; 700inputs .add_eos = tmpls -> add_eos ; 701inputs .chat_template_kwargs = chat_template_kwargs ; 702auto add_simple_msg = [& ](auto role ,auto content ) { 703common_chat_msg msg ; 704msg .role = role ; 705msg .content = content ; 706inputs .messages .push_back (msg ); 707 }; 708add_simple_msg ("system" ,"You are a helpful assistant" ); 709add_simple_msg ("user" ,"Hello" ); 710add_simple_msg ("assistant" ,"Hi there" ); 711add_simple_msg ("user" ,"How are you?" ); 712return common_chat_templates_apply (tmpls ,inputs ).prompt ; 713} 714 715#define CHATML_TEMPLATE_SRC \ 716 "{%- for message in messages -%}\n" \ 717 " {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' -}}\n" \ 718 "{%- endfor -%}\n" \ 719 "{%- if add_generation_prompt -%}\n" \ 720 " {{- '<|im_start|>assistant\n' -}}\n" \ 721 "{%- endif -%}" 722 723void common_chat_templates_free (struct common_chat_templates * tmpls ) { 724delete tmpls ; 725} 726 727bool common_chat_templates_was_explicit (const struct common_chat_templates * tmpls ) { 728return tmpls -> has_explicit_template ; 729} 730 731common_chat_prompt_preset common_chat_get_asr_prompt (const common_chat_templates * chat_templates ) { 732common_chat_prompt_preset asr_preset ; 733asr_preset .system = "" ; 734asr_preset .user = "Transcribe audio to text" ; 735 736if (chat_templates && chat_templates -> template_default && is_lfm2_template (chat_templates -> template_default -> source ())) { 737asr_preset .system = "Perform ASR." ; 738asr_preset .user = "" ; 739 } 740 741return asr_preset ; 742} 743 744std::string common_chat_templates_source (const struct common_chat_templates * tmpls ,const std::string & variant ) { 745if (!variant .empty ()) { 746if (variant == "tool_use" ) { 747if (tmpls -> template_tool_use ) { 748return tmpls -> template_tool_use -> source (); 749 } 750return "" ; 751 } 752LOG_DBG ("%s: unknown template variant: %s\n" ,__func__ ,variant .c_str ()); 753 } 754return tmpls -> template_default -> source (); 755} 756 757common_chat_templates_ptr common_chat_templates_init (const struct llama_model * model , 758const std::string & chat_template_override , 759const std::string & bos_token_override , 760const std::string & eos_token_override ) { 761 std::string default_template_src ; 762 std::string template_tool_use_src ; 763 764bool has_explicit_template = !chat_template_override .empty (); 765if (chat_template_override .empty ()) { 766GGML_ASSERT (model != nullptr ); 767const auto * str = llama_model_chat_template (model ,/* name */ nullptr ); 768if (str ) { 769default_template_src = str ; 770has_explicit_template = true; 771 } 772str = llama_model_chat_template (model ,/* name */ "tool_use" ); 773if (str ) { 774template_tool_use_src = str ; 775has_explicit_template = true; 776 } 777 }else { 778default_template_src = chat_template_override ; 779 } 780if (default_template_src .empty ()|| default_template_src == "chatml" ) { 781if (!template_tool_use_src .empty ()) { 782default_template_src = template_tool_use_src ; 783 }else { 784default_template_src = CHATML_TEMPLATE_SRC ; 785 } 786 } 787 788// TODO @ngxson : this is a temporary hack to prevent chat template from throwing an error 789// Ref: https://github.com/ggml-org/llama.cpp/pull/15230#issuecomment-3173959633 790if (default_template_src .find ("<|channel|>" )!= std::string::npos 791// search for the error message and patch it 792&& default_template_src .find ("in message.content or" )!= std::string::npos ) { 793string_replace_all (default_template_src , 794"{%- if \"<|channel|>analysis<|message|>\" in message.content or " 795"\"<|channel|>final<|message|>\" in message.content %}" , 796"{%- if false %}" ); 797 } 798 799// TODO @aldehir : this is a temporary fix, pending Minja changes 800// Ref: https://github.com/ggml-org/llama.cpp/pull/17713#issuecomment-3631342664 801if (default_template_src .find ("[TOOL_CALLS]" )!= std::string::npos 802// search for the error message and patch it 803&& default_template_src .find ("if (message['content'] is none or" )!= std::string::npos ) { 804string_replace_all (default_template_src , 805"{%- if (message['content'] is none or message['content'] == '' or " 806"message['content']|length == 0) and (message['tool_calls'] is not defined or " 807"message['tool_calls'] is none or message['tool_calls']|length == 0) %}" , 808"{%- if false %}" ); 809 } 810 811 std::string token_bos = bos_token_override ; 812 std::string token_eos = eos_token_override ; 813bool add_bos = false; 814bool add_eos = false; 815if (model ) { 816const auto * vocab = llama_model_get_vocab (model ); 817const auto get_token = [& ](llama_token token ,const char * name ,const char * jinja_variable_name ) { 818if (token == LLAMA_TOKEN_NULL ) { 819if (default_template_src .find (jinja_variable_name )!= std::string::npos || 820template_tool_use_src .find (jinja_variable_name )!= std::string::npos ) { 821LOG_WRN ( 822"common_chat_templates_init: warning: vocab does not have a %s token, jinja template won't " 823"work as intended.\n" , 824name ); 825 } 826return std::string (); 827 } 828return common_token_to_piece (vocab ,token , true); 829 }; 830token_bos = get_token (llama_vocab_bos (vocab ),"BOS" ,"bos_token" ); 831token_eos = get_token (llama_vocab_eos (vocab ),"EOS" ,"eos_token" ); 832add_bos = llama_vocab_get_add_bos (vocab ); 833add_eos = llama_vocab_get_add_eos (vocab ); 834 } 835common_chat_templates_ptr tmpls (new common_chat_templates ()); 836tmpls -> has_explicit_template = has_explicit_template ; 837tmpls -> add_bos = add_bos ; 838tmpls -> add_eos = add_eos ; 839try { 840tmpls -> template_default = std::make_unique < common_chat_template > (default_template_src ,token_bos ,token_eos ); 841 }catch (const std::exception & e ) { 842LOG_ERR ("%s: error: %s\n" ,__func__ ,e .what ()); 843LOG_ERR ("%s: failed to initialize chat template\n" ,__func__ ); 844LOG_ERR ("%s: please consider disabling jinja via --no-jinja, or using another chat template\n" ,__func__ ); 845throw e ; 846 } 847if (!template_tool_use_src .empty ()) { 848try { 849tmpls -> template_tool_use = std::make_unique < common_chat_template > (template_tool_use_src ,token_bos ,token_eos ); 850 }catch (const std::exception & e ) { 851LOG_ERR ("%s: failed to parse tool use chat template (ignoring it): %s\n" ,__func__ ,e .what ()); 852 } 853 } 854return tmpls ; 855} 856 857const char * common_chat_format_name (common_chat_format format ) { 858switch (format ) { 859case COMMON_CHAT_FORMAT_CONTENT_ONLY : 860return "Content-only" ; 861case COMMON_CHAT_FORMAT_PEG_SIMPLE : 862return "peg-simple" ; 863case COMMON_CHAT_FORMAT_PEG_NATIVE : 864return "peg-native" ; 865case COMMON_CHAT_FORMAT_PEG_GEMMA4 : 866return "peg-gemma4" ; 867case COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 : 868return "peg-minimax-m3" ; 869default : 870throw std::runtime_error ("Unknown chat format" ); 871 } 872} 873 874const char * common_reasoning_format_name (common_reasoning_format format ) { 875switch (format ) { 876case COMMON_REASONING_FORMAT_NONE : 877return "none" ; 878case COMMON_REASONING_FORMAT_AUTO : 879return "auto" ; 880case COMMON_REASONING_FORMAT_DEEPSEEK : 881return "deepseek" ; 882case COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY : 883return "deepseek-legacy" ; 884default : 885throw std::runtime_error ("Unknown reasoning format" ); 886 } 887} 888 889common_reasoning_format common_reasoning_format_from_name (const std::string & format ) { 890if (format == "none" ) { 891return COMMON_REASONING_FORMAT_NONE ; 892 } 893if (format == "auto" ) { 894return COMMON_REASONING_FORMAT_AUTO ; 895 } 896if (format == "deepseek" ) { 897return COMMON_REASONING_FORMAT_DEEPSEEK ; 898 } 899if (format == "deepseek-legacy" ) { 900return COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY ; 901 } 902throw std::runtime_error ("Unknown reasoning format: " + format ); 903} 904 905std::string common_chat_template_direct_apply_impl ( 906const common_chat_template & tmpl , 907const autoparser::generation_params & inputs , 908const std::optional < json > & messages_override , 909const std::optional < json > & tools_override , 910const std::optional < json > & additional_context ) { 911 jinja::context ctx (tmpl .source ()); 912 913// messages_override is already built for this template, do not touch its content parts 914json inp = json { 915 {"messages" ,messages_override .has_value () 916 ?* messages_override 917 :messages_inp_normalizer (tmpl .original_caps ()).normalize (inputs .messages )}, 918 {"bos_token" ,tmpl .bos_token ()}, 919 {"eos_token" ,tmpl .eos_token ()}, 920 {"enable_thinking" ,inputs .enable_thinking }, 921 }; 922if (tools_override .has_value ()|| !inputs .tools .empty ()) { 923inp ["tools" ]= tools_override .has_value () ?* tools_override :inputs .tools ; 924 } 925if (inputs .extra_context .is_object ()) { 926// TODO: do we need to merge, or replacing is fine? 927for (const auto & [k ,v ] :inputs .extra_context .items ()) { 928inp [k ]= v ; 929 } 930 } 931if (additional_context .has_value ()) { 932// TODO: merge properly instead of overwriting (matching old behavior) 933for (const auto & [k ,v ] :additional_context -> items ()) { 934inp [k ]= v ; 935 } 936 } 937if (inputs .add_generation_prompt ) { 938inp ["add_generation_prompt" ]= true; 939 } 940if (inp .contains ("preserve_reasoning" )&& inp ["preserve_reasoning" ].is_boolean ()) { 941bool enabled = inp ["preserve_reasoning" ].get < bool > (); 942 jinja::caps_apply_preserve_reasoning (ctx ,enabled ); 943 } 944if (inp .contains ("reasoning_effort" )&& inp ["reasoning_effort" ].is_string ()&& !inp ["reasoning_effort" ].empty ()) { 945 std::string reasoning_effort = inp ["reasoning_effort" ].get < std::string > (); 946 jinja::caps_apply_reasoning_effort (ctx ,reasoning_effort ); 947 } 948 949 jinja::global_from_json (ctx ,inp ,inputs .mark_input ); 950 951// render 952 jinja::runtime runtime (ctx ); 953const jinja::value results = runtime .execute (tmpl .prog ); 954auto parts = jinja::runtime::gather_string_parts (results ); 955 956 std::string result = parts -> as_string ().str (); 957 958// TODO: improve this later 959if (inputs .add_bos && string_starts_with (result ,tmpl .bos_token ())) { 960result = result .substr (tmpl .bos_token ().size ()); 961 } 962if (inputs .add_eos && string_ends_with (result ,tmpl .eos_token ())) { 963result = result .substr (0 ,result .size ()- tmpl .eos_token ().size ()); 964 } 965return result ; 966} 967 968std::string common_chat_template_direct_apply ( 969const common_chat_template & tmpl , 970const autoparser::generation_params & inputs ) { 971return common_chat_template_direct_apply_impl (tmpl ,inputs , std::nullopt , std::nullopt , std::nullopt ); 972} 973 974std::string common_chat_template_generation_prompt_impl ( 975const common_chat_template & tmpl , 976const autoparser::generation_params & inputs , 977const std::optional < json > & messages_override , 978const std::optional < json > & tools_override , 979const std::optional < json > & additional_context ) { 980 981 autoparser::generation_params params = inputs ; 982params .add_generation_prompt = false; 983params .continue_final_message = COMMON_CHAT_CONTINUATION_NONE ; 984 std::string no_gen_prompt = common_chat_template_direct_apply_impl (tmpl ,params ,messages_override ,tools_override ,additional_context ); 985params .add_generation_prompt = true; 986 std::string gen_prompt = common_chat_template_direct_apply_impl (tmpl ,params ,messages_override ,tools_override ,additional_context ); 987 988size_t prefix_len = 0 ; 989size_t min_size = std::min (no_gen_prompt .size (),gen_prompt .size ()); 990while (prefix_len < min_size && no_gen_prompt [prefix_len ]== gen_prompt [prefix_len ]) { 991prefix_len ++ ; 992 } 993return gen_prompt .substr (prefix_len ); 994} 995 996std::string common_chat_template_generation_prompt ( 997const common_chat_template & tmpl , 998const autoparser::generation_params & inputs ) { 999return common_chat_template_generation_prompt_impl (tmpl ,inputs , std::nullopt , std::nullopt , std::nullopt ); 1000} 1001 1002namespace workaround { 1003 1004static void map_developer_role_to_system (json & messages ) { 1005for (auto & message :messages ) { 1006if (message .contains ("role" )) { 1007if (message ["role" ]== "developer" ) { 1008message ["role" ]= "system" ; 1009 } 1010 } 1011 } 1012} 1013 1014 1015// if first message is system and template does not support it, merge it with next message 1016static void system_message_not_supported (json & messages ) { 1017if (!messages .empty ()&& messages .front ().at ("role" )== "system" ) { 1018if (messages .size ()> 1 ) { 1019LOG_DBG ("Merging system prompt into next message\n" ); 1020auto & first_msg = messages .front (); 1021auto & second_msg = messages [1 ]; 1022second_msg ["content" ]= first_msg .at ("content" ).get < std::string > () 1023+ "\n" + second_msg .at ("content" ).get < std::string > (); 1024messages .erase (0 ); 1025 }else { 1026LOG_WRN ("Removing system prompt due to template not supporting system role\n" ); 1027messages .erase (0 ); 1028 } 1029 } 1030} 1031 1032static void requires_non_null_content (json & messages ) { 1033GGML_ASSERT (messages .is_array ()); 1034for (auto & message :messages ) { 1035if (message .contains ("tool_calls" )&& !message .contains ("content" )) { 1036message ["content" ]= "" ; 1037 } 1038 } 1039} 1040 1041static void func_args_not_string (json & messages ) { 1042GGML_ASSERT (messages .is_array ()); 1043for (auto & message :messages ) { 1044if (message .contains ("tool_calls" )) { 1045for (auto & tool_call :message ["tool_calls" ]) { 1046if (tool_call .contains ("function" )&& tool_call ["function" ].contains ("arguments" )) { 1047auto & args = tool_call ["function" ]["arguments" ]; 1048if (args .is_string ()) { 1049try { 1050args = json::parse (args .get < std::string > ()); 1051 }catch (const std::exception & e ) { 1052throw std::runtime_error ("Failed to parse tool call arguments as JSON: " + std::string (e .what ())); 1053 } 1054 } 1055 } 1056 } 1057 } 1058 } 1059} 1060 1061// Trim leading/trailing whitespace from message contents before rendering. This 1062// has to run on the messages (not on the rendered JSON) because templates with 1063// string-only content caps concatenate typed content parts into a single string 1064// during rendering, after which the per-part whitespace can no longer be reached. 1065// Both the plain string content and the text of typed content parts are trimmed. 1066static void trim_all_content (std::vector < common_chat_msg > & messages ) { 1067for (auto & message :messages ) { 1068message .content = trim_whitespace (message .content ); 1069message .reasoning_content = trim_whitespace (message .reasoning_content ); 1070for (auto & part :message .content_parts ) { 1071if (part .type == "text" ) { 1072part .text = trim_whitespace (part .text ); 1073 } 1074 } 1075 } 1076} 1077 1078} 1079 1080static json common_chat_extra_context () { 1081json ctx = json::object (); 1082 std::chrono::system_clock::time_point now = std::chrono::system_clock::now (); 1083 std::string datetime_str = format_time (now ,"%b %d %Y" ); 1084 std::string date_str = format_time (now ,"%d %b %Y" ); 1085ctx ["datetime" ]= datetime_str ; 1086ctx ["date_string" ]= date_str ; 1087return ctx ; 1088} 1089 1090std::optional < common_chat_params > common_chat_try_specialized_template ( 1091const common_chat_template & tmpl , 1092const std::string & src , 1093 autoparser::generation_params & params ) { 1094// Ministral/Mistral Large 3 - uses special reasoning structure fixes, can't use autoparser 1095// Note: Mistral Small 3.2 uses [CALL_ID] which Ministral doesn't have, so we can distinguish them 1096if (src .find ("[SYSTEM_PROMPT]" )!= std::string::npos && src .find ("[TOOL_CALLS]" )!= std::string::npos && 1097src .find ("[ARGS]" )!= std::string::npos && src .find ("[CALL_ID]" )== std::string::npos ) { 1098LOG_DBG ("Using specialized template: Ministral/Magistral Large 3\n" ); 1099return common_chat_params_init_ministral_3 (tmpl ,params ); 1100 } 1101 1102// GPT-OSS - has unique channel-based structure that needs dedicated handler 1103if (src .find ("<|channel|>" )!= std::string::npos ) { 1104LOG_DBG ("Using specialized template: GPT-OSS\n" ); 1105return common_chat_params_init_gpt_oss (tmpl ,params ); 1106 } 1107 1108// Muse Glimmer format using " to=<recipient>" recipients and <|eom|>/<|eot|> message terminators. 1109if (src .find ("<atem:function_calls>" )!= std::string::npos && src .find ("<|eom|>" )!= std::string::npos ) { 1110LOG_DBG ("Using specialized template: Muse Glimmer\n" ); 1111return common_chat_params_init_muse_glimmer (tmpl ,params ); 1112 } 1113 1114// Functionary v3.2 - uses recipient-based format with >>>recipient\n{content} 1115// Detection: template has ">>>all" for content and ">>>" prefix for tool calls 1116if (src .find (">>>all" )!= std::string::npos && src .find (">>>${recipient}" )!= std::string::npos ) { 1117LOG_DBG ("Using specialized template: Functionary v3.2\n" ); 1118return common_chat_params_init_functionary_v3_2 (tmpl ,params ); 1119 } 1120 1121// Kimi K2 Thinking - uses unique tool call ID format: functions.<name>:<index> 1122// Detection: template has "<|tool_calls_section_begin|>" and "functions." prefix in tool call IDs 1123if (src .find ("<|tool_calls_section_begin|>" )!= std::string::npos && 1124src .find ("<|tool_call_begin|>" )!= std::string::npos ) { 1125LOG_DBG ("Using specialized template: Kimi K2 Thinking\n" ); 1126return common_chat_params_init_kimi_k2 (tmpl ,params ); 1127 } 1128 1129// Kimi K3 - the <|open|>/<|close|>/<|end_of_msg|> markers are unique to it 1130if (src .find ("<|open|>" )!= std::string::npos && src .find ("<|close|>" )!= std::string::npos && 1131src .find ("<|end_of_msg|>" )!= std::string::npos ) { 1132LOG_DBG ("Using specialized template: Kimi K3\n" ); 1133return common_chat_params_init_kimi_k3 (tmpl ,params ); 1134 } 1135 1136// Cohere2 MoE / North Code - marker-wrapped format with <|START_TEXT|> content and 1137// <|START_ACTION|> JSON tool calls. <|START_TEXT|> is unique to this template (the older 1138// Command-R templates use <|START_RESPONSE|>). 1139if (src .find ("<|START_TEXT|>" )!= std::string::npos && 1140src .find ("<|START_ACTION|>" )!= std::string::npos ) { 1141LOG_DBG ("Using specialized template: Cohere2 MoE\n" ); 1142return common_chat_params_init_cohere2moe (tmpl ,params ); 1143 } 1144 1145if (is_lfm2_template (src )) { 1146LOG_DBG ("Using specialized template: LFM2\n" ); 1147return common_chat_params_init_lfm2 (tmpl ,params ,/* tool_list_tokens = */ true); 1148 } 1149 1150// LFM2.5 format detection: template uses plain "List of tools: [...]" with no special tokens 1151if (src .find ("List of tools: [" )!= std::string::npos && 1152src .find ("<|tool_list_start|>" )== std::string::npos ) { 1153LOG_DBG ("Using specialized template: LFM2.5\n" ); 1154return common_chat_params_init_lfm2 (tmpl ,params ,/* tool_list_tokens = */ false); 1155 } 1156 1157// GigaChatV3 format detection 1158if (src .find ("<|role_sep|>" )!= std::string::npos && 1159src .find ("<|message_sep|>" )!= std::string::npos && 1160src .find ("<|function_call|>" )== std::string::npos ) { 1161LOG_DBG ("Using specialized template: GigaChatV3\n" ); 1162return common_chat_params_init_gigachat_v3 (tmpl ,params ); 1163 } 1164 1165// MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's 1166// markup delimiters, so detect the template and use a dedicated parser. 1167if (src .find ("]<]minimax[>[" )!= std::string::npos && 1168src .find ("<tool_call>" )!= std::string::npos && 1169src .find ("<invoke name=" )!= std::string::npos ) { 1170LOG_DBG ("Using specialized template: MiniMax-M3\n" ); 1171return common_chat_params_init_minimax_m3 (tmpl ,params ); 1172 } 1173 1174// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls. 1175// The template source contains the token as a variable assignment, not as a literal in markup. 1176// V3.2 names the tool call block "function_calls", V4 names it "tool_calls". 1177if (src .find ("dsml_token" )!= std::string::npos && 1178src .find ("DSML" )!= std::string::npos && 1179 (src .find ("function_calls" )!= std::string::npos || 1180src .find ("tool_calls" )!= std::string::npos )) { 1181LOG_DBG ("Using specialized template: DeepSeek V3.2/V4\n" ); 1182return common_chat_params_init_deepseek_v3_2 (tmpl ,params ); 1183 } 1184 1185// Gemma4 format detection 1186if (src .find ("'<|tool_call>call:'" )!= std::string::npos ) { 1187if (src .find ("{#- OpenAI Chat Completions:" )== std::string::npos ) { 1188// apply workarounds if using the older gemma4 templates 1189LOG_WRN ("%s: detected an outdated gemma4 chat template, applying compatibility workarounds. " 1190"Consider updating to the official template.\n" ,__func__ ); 1191 workaround::convert_tool_responses_gemma4 (params .messages ); 1192 } 1193return common_chat_params_init_gemma4 (tmpl ,params ); 1194 } 1195 1196// MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function> 1197if (src .find ("Tool usage guidelines:" )!= std::string::npos && 1198src .find ("<function name=\"" )!= std::string::npos && 1199src .find ("<param name=\"" )!= std::string::npos ) { 1200LOG_DBG ("Using specialized template: MiniCPM5\n" ); 1201return common_chat_params_init_minicpm5 (tmpl ,params ); 1202 } 1203 1204// Qwen3-Coder XML tool calls, also used by Nemotron Nano 3, Qwen3.5 and StepFun-3.5-Flash 1205if (src .find ("<tool_call>" )!= std::string::npos && 1206src .find ("<function=" )!= std::string::npos && 1207src .find ("<parameter=" )!= std::string::npos ) { 1208LOG_DBG ("Using specialized template: Qwen3-Coder\n" ); 1209return common_chat_params_init_qwen3_coder (tmpl ,params ); 1210 } 1211 1212return std::nullopt ; 1213} 1214 1215static common_chat_params common_chat_templates_apply_jinja (const struct common_chat_templates * tmpls , 1216const struct common_chat_templates_inputs & inputs ) { 1217 autoparser::generation_params params ; 1218params .tools = common_chat_tools_to_json_oaicompat (inputs .tools ); 1219const auto & tmpl = 1220params .tools .is_array ()&& tmpls -> template_tool_use ?* tmpls -> template_tool_use :* tmpls -> template_default ; 1221const auto & src = tmpl .source (); 1222const auto & caps = tmpl .original_caps (); 1223 std::vector < common_chat_msg > trimmed_messages ; 1224const std::vector < common_chat_msg > * messages_to_render = & inputs .messages ; 1225if (src .find ("You have access to the following functions in JSONSchema format" )!= std::string::npos ) { 1226// StepFun: trim message contents (including typed content parts) before rendering, 1227// otherwise leftover whitespace drives the model into reasoning loops (issue #24181) 1228trimmed_messages = inputs .messages ; 1229 workaround::trim_all_content (trimmed_messages ); 1230messages_to_render = & trimmed_messages ; 1231 } 1232params .messages = render_message_to_json (* messages_to_render ,tmpl .original_caps ()); 1233params .tool_choice = inputs .tool_choice ; 1234params .reasoning_format = inputs .reasoning_format ; 1235params .enable_thinking = inputs .enable_thinking ; 1236params .grammar = inputs .grammar ; 1237params .now = inputs .now ; 1238params .add_generation_prompt = inputs .add_generation_prompt ; 1239params .add_bos = tmpls -> add_bos ; 1240params .add_eos = tmpls -> add_eos ; 1241 1242params .continue_final_message = inputs .continue_final_message ; 1243if (params .continue_final_message != COMMON_CHAT_CONTINUATION_NONE ) { 1244params .add_generation_prompt = false; 1245 1246if (!inputs .messages .empty ()) { 1247// Render messages[:-1] and store continuation message separately 1248params .continue_msg = inputs .messages .back (); 1249params .messages .erase (params .messages .size ()- 1 ); 1250 } 1251 1252if (params .continue_final_message == COMMON_CHAT_CONTINUATION_AUTO && !inputs .messages .empty ()) { 1253// Resolve based on message content 1254params .continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT ; 1255if (!params .continue_msg .reasoning_content .empty ()&& 1256params .continue_msg .content .empty ()&& 1257params .continue_msg .content_parts .empty ()) { 1258params .continue_final_message = COMMON_CHAT_CONTINUATION_REASONING ; 1259 } 1260 } 1261 } 1262 1263if (src .find ("<|channel|>" )== std::string::npos ) { 1264// map developer to system for all models except for GPT-OSS 1265 workaround::map_developer_role_to_system (params .messages ); 1266 } 1267 1268if (!tmpl .original_caps ().supports_system_role ) { 1269 workaround::system_message_not_supported (params .messages ); 1270 } 1271 1272if (tmpl .original_caps ().supports_tool_calls ) { 1273// some templates will require the content field in tool call messages 1274// to still be non-null, this puts an empty string everywhere where the 1275// content field is null 1276 workaround::requires_non_null_content (params .messages ); 1277 } 1278 1279if (tmpl .original_caps ().supports_object_arguments ) { 1280 workaround::func_args_not_string (params .messages ); 1281 } 1282 1283params .extra_context = common_chat_extra_context (); 1284for (auto el :inputs .chat_template_kwargs ) { 1285params .extra_context [el .first ]= json::parse (el .second ); 1286 } 1287 1288if (!inputs .json_schema .empty ()) { 1289params .json_schema = json::parse (inputs .json_schema ); 1290 } 1291 1292params .parallel_tool_calls = inputs .parallel_tool_calls ; 1293 1294if (params .tools .is_array ()) { 1295if (params .tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && !params .grammar .empty ()) { 1296throw std::runtime_error ("Cannot specify grammar with tools" ); 1297 } 1298if (caps .supports_tool_calls && !caps .supports_tools ) { 1299LOG_WRN ( 1300"Template supports tool calls but does not natively describe tools. The fallback behaviour used may " 1301"produce bad results, inspect prompt w/ --verbose & consider overriding the template.\n" ); 1302 } 1303 } 1304 1305if (inputs .force_pure_content ) { 1306LOG_WRN ("Forcing pure content template, will not render reasoning or tools separately." ); 1307// Create the result structure 1308common_chat_params data ; 1309auto params_copy = params ; 1310params_copy .reasoning_format = COMMON_REASONING_FORMAT_NONE ; 1311data .prompt = common_chat_template_direct_apply_impl (tmpl ,params_copy ); 1312data .generation_prompt = common_chat_template_generation_prompt_impl (tmpl ,params ); 1313data .format = COMMON_CHAT_FORMAT_PEG_NATIVE ; 1314auto parser = build_chat_peg_parser ([& data ](common_chat_peg_builder & p ) { 1315return p .literal (data .generation_prompt ) <<p .content (p .rest ()); 1316 }); 1317data .parser = parser .save (); 1318return data ; 1319 } 1320 1321if (auto result = common_chat_try_specialized_template (tmpl ,src ,params )) { 1322return * result ; 1323 } 1324 1325try { 1326LOG_DBG ("%s: using differential autoparser\n" ,__func__ ); 1327struct autoparser::autoparser autoparser ; 1328autoparser .analyze_template (tmpl ); 1329auto auto_params = autoparser::peg_generator::generate_parser (tmpl ,params ,autoparser ); 1330 1331common_chat_msg_delimiters delimiters ; 1332if (!autoparser .assistant_start .empty ()) { 1333delimiters .add (COMMON_CHAT_ROLE_ASSISTANT ,autoparser .assistant_start ); 1334 } 1335if (!autoparser .user_start .empty ()) { 1336delimiters .add (COMMON_CHAT_ROLE_USER ,autoparser .user_start ); 1337 } 1338 1339auto_params .message_delimiters = std::move (delimiters ); 1340 1341auto_params .supports_thinking = autoparser .reasoning .mode != autoparser::reasoning_mode::NONE ; 1342if (auto_params .supports_thinking ) { 1343auto_params .thinking_start_tag = trim_whitespace (autoparser .reasoning .start ); 1344auto end_tag = trim_whitespace (autoparser .reasoning .end ); 1345if (!end_tag .empty ()) { 1346auto_params .thinking_end_tags = {std::move (end_tag )}; 1347 } 1348 } 1349common_peg_arena arena ; 1350arena .load (auto_params .parser ); 1351LOG_DBG ("%s: generated parser:\n%s\n\nparser generation prompt: %s\n" ,__func__ ,arena .dump (arena .root ()).c_str (),auto_params .generation_prompt .c_str ()); 1352return auto_params ; 1353 }catch (const std::exception & e ) { 1354throw std::invalid_argument (std::string ("Unable to generate parser for this template. Automatic parser generation failed: " )+ e .what ()); 1355 } 1356} 1357 1358// Legacy template route (adhoc C++ implementation of known templates), forward to llama_chat_apply_template. 1359static common_chat_params common_chat_templates_apply_legacy (const struct common_chat_templates * tmpls , 1360const struct common_chat_templates_inputs & inputs ) { 1361size_t alloc_size = 0 ; 1362 std::vector < llama_chat_message > chat ; 1363 std::vector < std::string > contents ; 1364 1365for (const auto & msg :inputs .messages ) { 1366auto content = msg .content ; 1367for (const auto & part :msg .content_parts ) { 1368if (part .type != "text" && part .type != "media_marker" ) { 1369LOG_WRN ("Ignoring non-text content part: %s\n" ,part .type .c_str ()); 1370continue ; 1371 } 1372if (!content .empty ()) { 1373content += "\n" ; 1374 ; 1375 } 1376content += part .text ; 1377 } 1378contents .emplace_back (std::move (content )); 1379 } 1380for (size_t i = 0 ;i < contents .size ();++ i ) { 1381const auto & msg = inputs .messages [i ]; 1382const auto & content = contents [i ]; 1383chat .push_back ({msg .role .c_str (),content .c_str () }); 1384size_t msg_size = msg .role .size ()+ content .size (); 1385alloc_size += msg_size + (msg_size /4 );// == msg_size * 1.25 but avoiding float ops 1386 } 1387 1388 std::vector < char > buf (alloc_size ); 1389 1390// run the first time to get the total output length 1391const auto & src = tmpls -> template_default -> source (); 1392int32_t res = llama_chat_apply_template (src .c_str (),chat .data (),chat .size (),inputs .add_generation_prompt , 1393buf .data (),buf .size ()); 1394 1395// error: chat template is not supported 1396if (res < 0 ) { 1397// if the custom "tmpl" is not supported, we throw an error 1398// this is a bit redundant (for good), since we're not sure if user validated the custom template with llama_chat_verify_template() 1399throw std::runtime_error ("this custom template is not supported, try using --jinja" ); 1400 } 1401 1402// if it turns out that our buffer is too small, we resize it 1403if ((size_t )res > buf .size ()) { 1404buf .resize (res ); 1405res = llama_chat_apply_template (src .c_str (),chat .data (),chat .size (),inputs .add_generation_prompt ,buf .data (), 1406buf .size ()); 1407 } 1408 1409// for safety, we check the result again 1410if (res < 0 || (size_t )res > buf .size ()) { 1411throw std::runtime_error ("failed to apply chat template, try using --jinja" ); 1412 } 1413 1414common_chat_params params ; 1415params .prompt = std::string (buf .data (),res ); 1416if (!inputs .json_schema .empty ()) { 1417params .grammar = json_schema_to_grammar (json::parse (inputs .json_schema )); 1418 }else { 1419params .grammar = inputs .grammar ; 1420 } 1421return params ; 1422} 1423 1424common_chat_params common_chat_templates_apply (const struct common_chat_templates * tmpls , 1425const struct common_chat_templates_inputs & inputs ) { 1426GGML_ASSERT (tmpls != nullptr ); 1427return inputs .use_jinja ?common_chat_templates_apply_jinja (tmpls ,inputs ) : 1428common_chat_templates_apply_legacy (tmpls ,inputs ); 1429} 1430 1431common_chat_msg common_chat_parse (const std::string & input , 1432bool is_partial , 1433const common_chat_parser_params & params ) { 1434return common_chat_peg_parse (params .parser ,input ,is_partial ,params ); 1435} 1436 1437common_chat_msg common_chat_peg_parse (const common_peg_arena & src_parser , 1438const std::string & input , 1439bool is_partial , 1440const common_chat_parser_params & params ) { 1441const common_peg_arena & parser = src_parser .empty () ? 1442build_chat_peg_parser ([](common_chat_peg_builder & p ) {return p .content (p .rest ())+ p .end (); }) : 1443src_parser ; 1444 1445if (src_parser .empty ()) { 1446LOG_DBG ("No parser definition detected, assuming pure content parser." ); 1447 } 1448 1449const std::string effective_input = params .generation_prompt .empty () 1450 ?input 1451 :params .generation_prompt + input ; 1452 1453//LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.c_str()); 1454 1455common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT ; 1456if (params .debug ) { 1457flags |=COMMON_PEG_PARSE_FLAG_DEBUG ; 1458 } 1459 1460common_peg_parse_context ctx (effective_input ,flags ); 1461auto result = parser .parse (ctx ); 1462 1463if (result .fail ()) { 1464// During partial parsing, return partial results if any AST nodes were captured 1465// This allows streaming to work correctly for formats like FUNC_MARKDOWN_CODE_BLOCK 1466if (is_partial && result .end > 0 ) { 1467// Try to extract any partial results from what was successfully parsed 1468common_chat_msg msg ; 1469msg .role = "assistant" ; 1470 std::unique_ptr < common_chat_peg_mapper > mapper ; 1471if (params .format == COMMON_CHAT_FORMAT_PEG_GEMMA4 ) { 1472mapper = std::make_unique < common_chat_peg_gemma4_mapper > (msg ); 1473 }else if (params .format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 ) { 1474mapper = std::make_unique < common_chat_peg_minimax_m3_mapper > (msg ); 1475 }else { 1476mapper = std::make_unique < common_chat_peg_mapper > (msg ); 1477 } 1478mapper -> from_ast (ctx .ast ,result ); 1479 1480if (ctx .is_debug ()) { 1481fprintf (stderr ,"\nAST for partial parse (fail):\n%s\n" ,ctx .ast .dump ().c_str ()); 1482fflush (stderr ); 1483 } 1484return msg ; 1485 } 1486LOG_WRN ("%s: unparsed %s output: %s\n" ,__func__ ,common_chat_format_name (params .format ),effective_input .substr (result .end ).c_str ()); 1487LOG_DBG ("%s: full %s output triggering error:\n=== BEGIN ===\n%s\n=== END ===\n" ,__func__ ,common_chat_format_name (params .format ),effective_input .c_str ()); 1488throw std::runtime_error (std::string ("The model produced output that does not match the expected " )+ common_chat_format_name (params .format )+ " format" ); 1489 } 1490 1491common_chat_msg msg ; 1492msg .role = "assistant" ; 1493 1494 std::unique_ptr < common_chat_peg_mapper > mapper ; 1495if (params .format == COMMON_CHAT_FORMAT_PEG_GEMMA4 ) { 1496mapper = std::make_unique < common_chat_peg_gemma4_mapper > (msg ); 1497 }else if (params .format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 ) { 1498mapper = std::make_unique < common_chat_peg_minimax_m3_mapper > (msg ); 1499 }else { 1500mapper = std::make_unique < common_chat_peg_mapper > (msg ); 1501 } 1502mapper -> from_ast (ctx .ast ,result ); 1503 1504if (ctx .is_debug ()) { 1505fprintf (stderr ,"\nAST for %s parse:\n%s\n" ,is_partial ?"partial" :"full" ,ctx .ast .dump ().c_str ()); 1506fflush (stderr ); 1507 } 1508 1509if (!is_partial ) { 1510LOG_DBG ("Parsed message: %s\n" ,common_chat_msgs_to_json_oaicompat ({msg }).at (0 ).dump ().c_str ()); 1511 } 1512return msg ; 1513} 1514 1515std::map < std::string ,bool > common_chat_templates_get_caps (const common_chat_templates * chat_templates ) { 1516GGML_ASSERT (chat_templates != nullptr ); 1517GGML_ASSERT (chat_templates -> template_default != nullptr ); 1518if (chat_templates -> template_tool_use != nullptr ) { 1519// take the more expressive template when available 1520return chat_templates -> template_tool_use -> caps .to_map (); 1521 } 1522return chat_templates -> template_default -> caps .to_map (); 1523}