2025-02-18 18:03:23 +00:00
#include "chat.h"
2026-03-06 21:01:00 +01:00
2026-03-19 16:58:21 +01:00
#include "chat-auto-parser-helpers.h"
2026-03-06 21:01:00 +01:00
#include "chat-auto-parser.h"
2025-12-09 17:31:04 -06:00
#include "chat-peg-parser.h"
2025-05-25 01:48:08 +01:00
#include "common.h"
2026-03-06 21:01:00 +01:00
#include "ggml.h"
2025-01-30 19:13:58 +00:00
#include "json-schema-to-grammar.h"
#include "log.h"
2025-02-18 18:03:23 +00:00
2026-01-16 11:22:06 +01:00
#include "jinja/value.h"
#include "jinja/runtime.h"
#include "jinja/caps.h"
2026-03-06 21:01:00 +01:00
#include "peg-parser.h"
2025-05-30 16:25:45 +03:00
2026-04-03 09:07:59 +03:00
#include "nlohmann/json.hpp"
2026-07-22 12:54:40 +02:00
#include <algorithm>
2025-05-25 01:48:08 +01:00
#include <cstdio>
2026-03-06 21:01:00 +01:00
#include <cstdlib>
#include <ctime>
2025-05-25 01:48:08 +01:00
#include <exception>
2025-10-27 18:54:01 -04:00
#include <functional>
2026-07-22 12:54:40 +02:00
#include <map>
2026-03-06 21:01:00 +01:00
2025-02-18 18:03:23 +00:00
#include <optional>
2026-03-06 21:01:00 +01:00
#include <sstream>
2025-05-25 01:48:08 +01:00
#include <stdexcept>
#include <string>
2026-03-19 16:58:21 +01:00
#include <utility>
2025-05-25 01:48:08 +01:00
#include <vector>
2025-06-29 20:02:53 +02:00
using json = nlohmann :: ordered_json ;
2025-05-15 02:39:51 +01:00
static std :: string format_time ( const std :: chrono :: system_clock :: time_point & now , const std :: string & format ) {
2026-03-06 21:01:00 +01:00
auto time = std :: chrono :: system_clock :: to_time_t ( now );
auto local_time = * std :: localtime ( & time );
2025-05-15 02:39:51 +01:00
std :: ostringstream ss ;
ss << std :: put_time ( & local_time , format . c_str ());
auto res = ss . str ();
return res ;
}
2026-03-06 21:01:00 +01:00
static json safe_args_parse ( const std :: string & to_parse ) {
std :: string stripped = to_parse ;
if ( to_parse . at ( 0 ) == '"' && to_parse . at ( to_parse . length () - 1 ) == '"' ) {
stripped = to_parse . substr ( 1 , to_parse . length () - 1 );
}
try {
return json :: parse ( stripped );
} catch ( json :: exception & e ) {
return stripped ;
}
}
2025-05-25 01:48:08 +01:00
static std :: string string_diff ( const std :: string & last , const std :: string & current ) {
if ( last . empty ()) {
return current ;
}
if ( ! string_starts_with ( current , last )) {
2025-05-26 06:16:37 -07:00
if ( string_starts_with ( last , current )) {
// This happens if the last generation ended on a partial stop word (not erased),
// and the current ended on a stop word (erased).
return "" ;
}
2025-05-25 01:48:08 +01:00
throw std :: runtime_error ( "Invalid diff: '" + last + "' not found at start of '" + current + "'" );
}
return current . substr ( last . size ());
}
static bool has_content_or_tool_calls ( const common_chat_msg & msg ) {
return ! msg . content . empty () || ! msg . tool_calls . empty ();
}
2026-05-17 07:36:05 -04:00
std :: string common_chat_msg :: render_content ( const std :: string & delimiter ) const {
if ( ! content . empty () && ! content_parts . empty ()) {
throw std :: runtime_error ( "Cannot specify both content and content_parts" );
}
if ( ! content . empty ()) {
return content ;
}
std :: string text ;
for ( const auto & part : content_parts ) {
if ( part . type == "text" ) {
if ( ! text . empty ()) {
text += delimiter ;
}
text += part . text ;
}
}
return text ;
}
2026-06-23 00:27:28 -05:00
common_chat_role common_chat_role_from_string ( const std :: string & role ) {
if ( role == "system" ) { return COMMON_CHAT_ROLE_SYSTEM ; }
if ( role == "assistant" ) { return COMMON_CHAT_ROLE_ASSISTANT ; }
if ( role == "user" ) { return COMMON_CHAT_ROLE_USER ; }
if ( role == "tool" ) { return COMMON_CHAT_ROLE_TOOL ; }
return COMMON_CHAT_ROLE_UNKNOWN ;
}
const char * common_chat_role_to_string ( common_chat_role role ) {
switch ( role ) {
case COMMON_CHAT_ROLE_SYSTEM : return "system" ;
case COMMON_CHAT_ROLE_ASSISTANT : return "assistant" ;
case COMMON_CHAT_ROLE_USER : return "user" ;
case COMMON_CHAT_ROLE_TOOL : return "tool" ;
case COMMON_CHAT_ROLE_UNKNOWN : return "" ;
}
return "" ;
}
json common_chat_msg_delimiters :: to_json () const {
json result = json :: array ();
for ( const auto & d : delimiters ) {
result . push_back ({
{ "role" , common_chat_role_to_string ( d . role ) },
{ "delimiter" , d . delimiter },
});
}
return result ;
}
common_chat_msg_delimiters common_chat_msg_delimiters_parse ( const json & delimiters ) {
common_chat_msg_delimiters result ;
if ( ! delimiters . is_array ()) {
return result ;
2026-05-25 07:56:18 +02:00
}
2026-06-23 00:27:28 -05:00
result . delimiters . reserve ( delimiters . size ());
for ( const auto & d : delimiters ) {
if ( ! d . is_object ()) {
continue ;
2026-05-25 07:56:18 +02:00
}
2026-06-23 00:27:28 -05:00
result . delimiters . push_back ({
common_chat_role_from_string ( d . value ( "role" , std :: string ())),
d . value ( "delimiter" , std :: string ()),
});
2026-05-25 07:56:18 +02:00
}
2026-06-23 00:27:28 -05:00
return result ;
}
void common_chat_msg_delimiters :: tokenize ( const llama_vocab * vocab ) {
for ( auto & d : delimiters ) {
d . tokens = common_tokenize ( vocab , d . delimiter , false , true );
}
}
common_chat_msg_spans common_chat_msg_delimiters :: split ( const llama_tokens & tokens , const std :: map < size_t , size_t > & skips ) const {
std :: vector < std :: pair < common_chat_role , size_t >> matches ;
auto skip = skips . begin ();
for ( size_t i = 0 ; i < tokens . size ();) {
if ( skip != skips . end () && i == skip -> first ) {
i += skip -> second ;
++ skip ;
continue ;
2026-05-25 07:56:18 +02:00
}
2026-06-23 00:27:28 -05:00
for ( const auto & d : delimiters ) {
if ( i + d . tokens . size () > tokens . size ()) {
continue ;
}
if ( std :: equal ( d . tokens . begin (), d . tokens . end (), tokens . begin () + i )) {
matches . emplace_back ( d . role , i );
break ;
}
}
i ++ ;
}
matches . emplace_back ( COMMON_CHAT_ROLE_UNKNOWN , tokens . size ());
common_chat_msg_spans spans ;
for ( size_t i = 0 ; i + 1 < matches . size (); i ++ ) {
const auto & curr = matches [ i ];
const auto & next = matches [ i + 1 ];
spans . add ( curr . first , curr . second , next . second - curr . second );
}
2026-05-25 07:56:18 +02:00
return spans ;
}
2026-01-22 21:30:06 +01:00
json common_chat_msg :: to_json_oaicompat ( bool concat_typed_text ) const {
if ( ! content . empty () && ! content_parts . empty ()) {
throw std :: runtime_error ( "Cannot specify both content and content_parts" );
2025-05-25 01:48:08 +01:00
}
2026-01-22 21:30:06 +01:00
json jmsg {
{ "role" , role },
};
if ( ! content . empty ()) {
jmsg [ "content" ] = content ;
} else if ( ! content_parts . empty ()) {
2026-05-07 12:50:56 -05:00
if ( concat_typed_text || contains_media ()) {
2026-01-22 21:30:06 +01:00
std :: string text ;
2026-02-19 12:18:57 +01:00
bool last_was_media_marker = false ;
// join parts with newline, do not add newline before or after media markers
2026-01-22 21:30:06 +01:00
for ( const auto & part : content_parts ) {
2026-02-19 12:18:57 +01:00
bool add_new_line = true ;
if ( part . type == "text" ) {
add_new_line = ! last_was_media_marker && ! text . empty ();
last_was_media_marker = false ;
} else if ( part . type == "media_marker" ) {
add_new_line = false ;
last_was_media_marker = true ;
} else {
2026-01-22 21:30:06 +01:00
LOG_WRN ( "Ignoring content part type: %s \n " , part . type . c_str ());
continue ;
}
2026-02-19 12:18:57 +01:00
if ( add_new_line ) {
2026-01-22 21:30:06 +01:00
text += '\n' ;
}
2026-02-19 12:18:57 +01:00
2026-01-22 21:30:06 +01:00
text += part . text ;
}
jmsg [ "content" ] = text ;
} else {
auto & parts = jmsg [ "content" ] = json :: array ();
for ( const auto & part : content_parts ) {
parts . push_back ({
{ "type" , part . type },
{ "text" , part . text },
});
}
}
2025-05-25 01:48:08 +01:00
} else {
2026-01-22 21:30:06 +01:00
jmsg [ "content" ] = "" ;
}
if ( ! reasoning_content . empty ()) {
jmsg [ "reasoning_content" ] = reasoning_content ;
}
if ( ! tool_name . empty ()) {
jmsg [ "name" ] = tool_name ;
}
if ( ! tool_call_id . empty ()) {
jmsg [ "tool_call_id" ] = tool_call_id ;
2025-05-25 01:48:08 +01:00
}
if ( ! tool_calls . empty ()) {
2026-01-22 21:30:06 +01:00
jmsg [ "tool_calls" ] = json :: array ();
auto & jtool_calls = jmsg [ "tool_calls" ];
for ( const auto & tool_call : tool_calls ) {
json tc {
2025-05-25 01:48:08 +01:00
{ "type" , "function" },
{ "function" , {
2026-01-22 21:30:06 +01:00
{ "name" , tool_call . name },
2026-03-08 11:33:03 +01:00
{ "arguments" , json ( tool_call . arguments )},
2025-05-25 01:48:08 +01:00
}},
2026-01-22 21:30:06 +01:00
};
if ( ! tool_call . id . empty ()) {
tc [ "id" ] = tool_call . id ;
}
// Some templates generate and require an id (sometimes in a very specific format, e.g. Mistral Nemo).
// We only generate a random id for the ones that don't generate one by themselves
// (they also won't get to see it as their template likely doesn't use it, so it's all for the client)
// {"id", tc.id.empty() ? gen_tool_call_id() : tc.id},
jtool_calls . push_back ( tc );
2025-05-25 01:48:08 +01:00
}
}
2026-01-22 21:30:06 +01:00
return jmsg ;
2025-05-25 01:48:08 +01:00
}
2026-03-06 21:01:00 +01:00
std :: vector < common_chat_msg_diff > common_chat_msg_diff :: compute_diffs ( const common_chat_msg & msg_prv ,
const common_chat_msg & msg_new ) {
2025-05-25 01:48:08 +01:00
std :: vector < common_chat_msg_diff > diffs ;
2025-12-03 17:22:10 +02:00
if ( msg_new . tool_calls . size () > msg_prv . tool_calls . size ()) {
diffs . reserve ( msg_new . tool_calls . size () - msg_prv . tool_calls . size () + 3 );
} else {
diffs . reserve ( 3 );
2025-05-25 01:48:08 +01:00
}
2025-12-03 17:22:10 +02:00
// TODO: these can become expensive for long messages - how to optimize?
if ( msg_prv . reasoning_content != msg_new . reasoning_content ) {
2026-03-06 21:01:00 +01:00
auto & diff = diffs . emplace_back ();
2025-12-03 17:22:10 +02:00
diff . reasoning_content_delta = string_diff ( msg_prv . reasoning_content , msg_new . reasoning_content );
}
if ( msg_prv . content != msg_new . content ) {
2026-03-06 21:01:00 +01:00
auto & diff = diffs . emplace_back ();
2025-12-03 17:22:10 +02:00
diff . content_delta = string_diff ( msg_prv . content , msg_new . content );
}
if ( msg_new . tool_calls . size () < msg_prv . tool_calls . size ()) {
2026-03-06 21:01:00 +01:00
std :: string err = "Invalid diff: now finding less tool calls! \n " ;
err += " Previous (" + std :: to_string ( msg_prv . tool_calls . size ()) + "): \n " ;
for ( const auto & tc : msg_prv . tool_calls ) {
err += " - name: '" + tc . name + "', args: '" + tc . arguments + "' \n " ;
}
err += " Current (" + std :: to_string ( msg_new . tool_calls . size ()) + "): \n " ;
for ( const auto & tc : msg_new . tool_calls ) {
err += " - name: '" + tc . name + "', args: '" + tc . arguments + "' \n " ;
}
err += " Current msg text content: \n " + msg_new . content + " \n " ;
throw std :: runtime_error ( err );
2025-05-25 01:48:08 +01:00
}
2025-12-03 17:22:10 +02:00
if ( ! msg_prv . tool_calls . empty ()) {
2026-03-06 21:01:00 +01:00
const auto idx = msg_prv . tool_calls . size () - 1 ;
2025-12-03 17:22:10 +02:00
const auto & pref = msg_prv . tool_calls [ idx ];
const auto & newf = msg_new . tool_calls [ idx ];
2026-03-06 21:01:00 +01:00
// Allow tool name to change during incremental parsing:
// - empty -> non-empty (initial discovery)
// - prefix -> longer string (name grows as more input is parsed)
if ( pref . name != newf . name && ! pref . name . empty () && ! newf . name . empty ()) {
// Check if one is a prefix of the other (for incremental parsing where names grow or shrink)
bool is_prefix = ( newf . name . rfind ( pref . name , 0 ) == 0 );
if ( ! is_prefix ) {
LOG_ERR ( "Tool call mismatch: prev='%s' new='%s' \n " , pref . name . c_str (), newf . name . c_str ());
throw std :: runtime_error ( "Invalid diff: tool call mismatch!" );
}
2025-05-25 01:48:08 +01:00
}
2025-12-03 17:22:10 +02:00
const auto args_diff = string_diff ( pref . arguments , newf . arguments );
2026-03-06 21:01:00 +01:00
if ( ! args_diff . empty () || pref . id != newf . id || pref . name != newf . name ) {
auto & diff = diffs . emplace_back ();
2025-05-25 01:48:08 +01:00
diff . tool_call_index = idx ;
2026-03-06 21:01:00 +01:00
if ( pref . id != newf . id || pref . name != newf . name ) {
diff . tool_call_delta . id = newf . id ;
2025-05-26 06:56:49 -07:00
diff . tool_call_delta . name = newf . name ;
2025-05-25 01:48:08 +01:00
}
diff . tool_call_delta . arguments = args_diff ;
}
}
2025-12-03 17:22:10 +02:00
for ( size_t idx = msg_prv . tool_calls . size (); idx < msg_new . tool_calls . size (); ++ idx ) {
2026-03-06 21:01:00 +01:00
auto & diff = diffs . emplace_back ();
2025-05-25 01:48:08 +01:00
diff . tool_call_index = idx ;
2025-12-03 17:22:10 +02:00
diff . tool_call_delta = msg_new . tool_calls [ idx ];
2025-05-25 01:48:08 +01:00
}
2025-12-03 17:22:10 +02:00
2025-05-25 01:48:08 +01:00
return diffs ;
}
2026-01-16 11:22:06 +01:00
using chat_template_caps = jinja :: caps ;
2025-02-18 18:03:23 +00:00
struct common_chat_templates {
2025-08-05 20:43:36 +02:00
bool add_bos ;
bool add_eos ;
2026-03-31 12:50:51 +01:00
bool has_explicit_template ; // Model had builtin template or template overridden was specified.
2026-03-06 21:01:00 +01:00
std :: unique_ptr < common_chat_template > template_default ; // always set (defaults to chatml)
2025-02-18 18:03:23 +00:00
std :: unique_ptr < common_chat_template > template_tool_use ;
};
common_chat_tool_choice common_chat_tool_choice_parse_oaicompat ( const std :: string & tool_choice ) {
if ( tool_choice == "auto" ) {
return COMMON_CHAT_TOOL_CHOICE_AUTO ;
}
if ( tool_choice == "none" ) {
return COMMON_CHAT_TOOL_CHOICE_NONE ;
}
if ( tool_choice == "required" ) {
return COMMON_CHAT_TOOL_CHOICE_REQUIRED ;
}
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Invalid tool_choice: " + tool_choice );
2025-02-18 18:03:23 +00:00
}
2025-09-05 14:31:24 -06:00
bool common_chat_templates_support_enable_thinking ( const common_chat_templates * chat_templates ) {
2026-03-06 21:01:00 +01:00
common_chat_templates_inputs inputs ;
inputs . reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK ;
2025-09-05 14:31:24 -06:00
common_chat_msg msg ;
2026-03-06 21:01:00 +01:00
msg . role = "user" ;
2025-09-05 14:31:24 -06:00
msg . content = "test" ;
2026-03-06 21:01:00 +01:00
inputs . messages = { msg };
inputs . enable_thinking = true ;
inputs . add_generation_prompt = true ;
inputs . reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK ;
auto params = common_chat_templates_apply ( chat_templates , inputs );
return params . supports_thinking ;
2025-09-05 14:31:24 -06:00
}
2025-02-18 18:03:23 +00:00
std :: vector < common_chat_msg > common_chat_msgs_parse_oaicompat ( const json & messages ) {
std :: vector < common_chat_msg > msgs ;
try {
if ( ! messages . is_array ()) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Expected 'messages' to be an array, got " + messages . dump ());
2025-02-18 18:03:23 +00:00
}
for ( const auto & message : messages ) {
if ( ! message . is_object ()) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Expected 'message' to be an object, got " + message . dump ());
2025-02-18 18:03:23 +00:00
}
common_chat_msg msg ;
if ( ! message . contains ( "role" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing 'role' in message: " + message . dump ());
2025-02-18 18:03:23 +00:00
}
msg . role = message . at ( "role" );
2026-03-06 21:01:00 +01:00
auto has_content = message . contains ( "content" );
2025-03-10 09:45:07 +00:00
auto has_tool_calls = message . contains ( "tool_calls" );
if ( has_content ) {
2025-02-18 18:03:23 +00:00
const auto & content = message . at ( "content" );
if ( content . is_string ()) {
msg . content = content ;
} else if ( content . is_array ()) {
for ( const auto & part : content ) {
if ( ! part . contains ( "type" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing content part type: " + part . dump ());
2025-02-18 18:03:23 +00:00
}
const auto & type = part . at ( "type" );
2026-02-19 12:18:57 +01:00
if ( type != "text" && type != "media_marker" ) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Unsupported content part type: " + type . dump ());
2025-02-18 18:03:23 +00:00
}
common_chat_msg_content_part msg_part ;
msg_part . type = type ;
msg_part . text = part . at ( "text" );
msg . content_parts . push_back ( msg_part );
}
} else if ( ! content . is_null ()) {
2026-03-06 21:01:00 +01:00
throw std :: invalid_argument ( "Invalid 'content' type: expected string or array, got " +
content . dump () +
" (ref: https://github.com/ggml-org/llama.cpp/issues/8367)" );
2025-02-18 18:03:23 +00:00
}
}
2025-03-10 09:45:07 +00:00
if ( has_tool_calls ) {
2025-02-18 18:03:23 +00:00
for ( const auto & tool_call : message . at ( "tool_calls" )) {
common_chat_tool_call tc ;
if ( ! tool_call . contains ( "type" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing tool call type: " + tool_call . dump ());
2025-02-18 18:03:23 +00:00
}
const auto & type = tool_call . at ( "type" );
if ( type != "function" ) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Unsupported tool call type: " + tool_call . dump ());
2025-02-18 18:03:23 +00:00
}
if ( ! tool_call . contains ( "function" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing tool call function: " + tool_call . dump ());
2025-02-18 18:03:23 +00:00
}
const auto & fc = tool_call . at ( "function" );
if ( ! fc . contains ( "name" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing tool call name: " + tool_call . dump ());
2025-02-18 18:03:23 +00:00
}
2026-03-06 21:01:00 +01:00
tc . name = fc . at ( "name" );
const auto & args = fc . at ( "arguments" );
if ( args . is_string ()) {
tc . arguments = args ;
} else {
tc . arguments = args . dump ();
}
2025-02-18 18:03:23 +00:00
if ( tool_call . contains ( "id" )) {
tc . id = tool_call . at ( "id" );
}
msg . tool_calls . push_back ( tc );
}
}
2025-03-10 09:45:07 +00:00
if ( ! has_content && ! has_tool_calls ) {
2026-03-06 21:01:00 +01:00
throw std :: invalid_argument (
"Expected 'content' or 'tool_calls' (ref: https://github.com/ggml-org/llama.cpp/issues/8367 & "
"https://github.com/ggml-org/llama.cpp/issues/12279)" );
2025-03-10 09:45:07 +00:00
}
if ( message . contains ( "reasoning_content" )) {
msg . reasoning_content = message . at ( "reasoning_content" );
}
if ( message . contains ( "name" )) {
msg . tool_name = message . at ( "name" );
}
if ( message . contains ( "tool_call_id" )) {
msg . tool_call_id = message . at ( "tool_call_id" );
}
2025-02-18 18:03:23 +00:00
msgs . push_back ( msg );
}
} catch ( const std :: exception & e ) {
2025-05-08 15:37:29 +02:00
// @ngxson : disable otherwise it's bloating the API response
// printf("%s\n", std::string("; messages = ") + messages.dump(2));
throw std :: runtime_error ( "Failed to parse messages: " + std :: string ( e . what ()));
2025-02-18 18:03:23 +00:00
}
return msgs ;
}
2026-02-09 22:14:12 +01:00
static json render_message_to_json ( const std :: vector < common_chat_msg > & msgs , const jinja :: caps & c ) {
if ( ! c . supports_string_content && ! c . supports_typed_content ) {
LOG_WRN ( "%s: Neither string content nor typed content is supported by the template. This is unexpected and may lead to issues. \n " , __func__ );
}
bool only_string_accepted = c . supports_string_content && ! c . supports_typed_content ;
bool only_typed_accepted = ! c . supports_string_content && c . supports_typed_content ;
2025-02-18 18:03:23 +00:00
json messages = json :: array ();
for ( const auto & msg : msgs ) {
2026-02-09 22:14:12 +01:00
if ( only_string_accepted ) {
json jmsg = msg . to_json_oaicompat ( /* concat_typed_text= */ true );
messages . push_back ( jmsg );
} else if ( only_typed_accepted ) {
json jmsg = msg . to_json_oaicompat ( /* concat_typed_text= */ false );
if ( jmsg . at ( "content" ). is_string ()) {
jmsg [ "content" ] = json :: array ({
json {
{ "type" , "text" },
{ "text" , jmsg . at ( "content" ). get < std :: string > ()},
}
});
}
messages . push_back ( jmsg );
} else {
json jmsg = msg . to_json_oaicompat ( /* concat_typed_text= */ false );
messages . push_back ( jmsg );
}
2025-02-18 18:03:23 +00:00
}
return messages ;
}
2026-02-09 22:14:12 +01:00
// DEPRECATED: only used in tests
json common_chat_msgs_to_json_oaicompat ( const std :: vector < common_chat_msg > & msgs , bool concat_typed_text ) {
jinja :: caps c ;
c . supports_string_content = true ;
c . supports_typed_content = ! concat_typed_text ;
return render_message_to_json ( msgs , c );
}
2026-04-22 10:28:45 +02:00
json common_chat_tools_to_json_oaicompat ( const std :: vector < common_chat_tool > & tools ) {
if ( tools . empty ()) {
return json ();
}
auto result = json :: array ();
for ( const auto & tool : tools ) {
result . push_back ({
{ "type" , "function" },
{ "function" , {
{ "name" , tool . name },
{ "description" , tool . description },
{ "parameters" , json :: parse ( tool . parameters ) },
}},
});
}
return result ;
}
2025-02-18 18:03:23 +00:00
std :: vector < common_chat_tool > common_chat_tools_parse_oaicompat ( const json & tools ) {
std :: vector < common_chat_tool > result ;
try {
if ( ! tools . is_null ()) {
if ( ! tools . is_array ()) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Expected 'tools' to be an array, got " + tools . dump ());
2025-02-18 18:03:23 +00:00
}
for ( const auto & tool : tools ) {
if ( ! tool . contains ( "type" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing tool type: " + tool . dump ());
2025-02-18 18:03:23 +00:00
}
const auto & type = tool . at ( "type" );
if ( ! type . is_string () || type != "function" ) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Unsupported tool type: " + tool . dump ());
2025-02-18 18:03:23 +00:00
}
if ( ! tool . contains ( "function" )) {
2025-12-02 10:33:50 -06:00
throw std :: invalid_argument ( "Missing tool function: " + tool . dump ());
2025-02-18 18:03:23 +00:00
}
const auto & function = tool . at ( "function" );
result . push_back ({
/* .name = */ function . at ( "name" ),
2026-01-01 01:21:37 +02:00
/* .description = */ function . value ( "description" , "" ),
/* .parameters = */ function . value ( "parameters" , json :: object ()). dump (),
2025-02-18 18:03:23 +00:00
});
}
}
} catch ( const std :: exception & e ) {
throw std :: runtime_error ( "Failed to parse tools: " + std :: string ( e . what ()) + "; tools = " + tools . dump ( 2 ));
}
return result ;
}
2026-05-17 07:36:05 -04:00
common_chat_continuation common_chat_continuation_parse ( const nlohmann :: ordered_json & value ) {
if ( value . is_boolean () && value . get < bool > ()) {
return COMMON_CHAT_CONTINUATION_AUTO ;
}
if ( value . is_string ()) {
auto value_str = value . get < std :: string > ();
if ( value_str == "reasoning_content" ) {
return COMMON_CHAT_CONTINUATION_REASONING ;
}
if ( value_str == "content" ) {
return COMMON_CHAT_CONTINUATION_CONTENT ;
}
}
return COMMON_CHAT_CONTINUATION_NONE ;
}
2025-02-18 18:03:23 +00:00
bool common_chat_verify_template ( const std :: string & tmpl , bool use_jinja ) {
if ( use_jinja ) {
try {
common_chat_msg msg ;
2026-03-06 21:01:00 +01:00
msg . role = "user" ;
2025-02-18 18:03:23 +00:00
msg . content = "test" ;
auto tmpls = common_chat_templates_init ( /* model= */ nullptr , tmpl );
common_chat_templates_inputs inputs ;
2026-03-06 21:01:00 +01:00
inputs . messages = { msg };
2025-02-18 18:03:23 +00:00
common_chat_templates_apply ( tmpls . get (), inputs );
return true ;
} catch ( const std :: exception & e ) {
LOG_ERR ( "%s: failed to apply template: %s \n " , __func__ , e . what ());
return false ;
}
}
2026-03-06 21:01:00 +01:00
llama_chat_message chat [] = {
{ "user" , "test" }
};
2025-02-18 18:03:23 +00:00
const int res = llama_chat_apply_template ( tmpl . c_str (), chat , 1 , true , nullptr , 0 );
return res >= 0 ;
}
2026-03-06 21:01:00 +01:00
std :: string common_chat_format_single ( const struct common_chat_templates * tmpls ,
const std :: vector < common_chat_msg > & past_msg ,
const common_chat_msg & new_msg ,
bool add_ass ,
bool use_jinja ) {
2025-02-18 18:03:23 +00:00
common_chat_templates_inputs inputs ;
inputs . use_jinja = use_jinja ;
2026-03-06 21:01:00 +01:00
inputs . add_bos = tmpls -> add_bos ;
inputs . add_eos = tmpls -> add_eos ;
2025-02-18 18:03:23 +00:00
std :: string fmt_past_msg ;
if ( ! past_msg . empty ()) {
2026-03-06 21:01:00 +01:00
inputs . messages = past_msg ;
2025-02-18 18:03:23 +00:00
inputs . add_generation_prompt = false ;
2026-03-06 21:01:00 +01:00
fmt_past_msg = common_chat_templates_apply ( tmpls , inputs ). prompt ;
2025-02-18 18:03:23 +00:00
}
std :: ostringstream ss ;
// if the past_msg ends with a newline, we must preserve it in the formatted version
if ( add_ass && ! fmt_past_msg . empty () && fmt_past_msg . back () == '\n' ) {
ss << " \n " ;
};
// format chat with new_msg
inputs . messages . push_back ( new_msg );
inputs . add_generation_prompt = add_ass ;
2026-03-06 21:01:00 +01:00
auto fmt_new_msg = common_chat_templates_apply ( tmpls , inputs ). prompt ;
2025-02-18 18:03:23 +00:00
// get the diff part
ss << fmt_new_msg . substr ( fmt_past_msg . size (), fmt_new_msg . size () - fmt_past_msg . size ());
return ss . str ();
}
2026-03-06 21:01:00 +01:00
std :: string common_chat_format_example ( const struct common_chat_templates * tmpls ,
bool use_jinja ,
const std :: map < std :: string , std :: string > & chat_template_kwargs ) {
2025-02-18 18:03:23 +00:00
common_chat_templates_inputs inputs ;
2026-03-06 21:01:00 +01:00
inputs . use_jinja = use_jinja ;
inputs . add_bos = tmpls -> add_bos ;
inputs . add_eos = tmpls -> add_eos ;
2025-08-14 10:28:29 -07:00
inputs . chat_template_kwargs = chat_template_kwargs ;
2026-03-06 21:01:00 +01:00
auto add_simple_msg = [ & ]( auto role , auto content ) {
2025-02-18 18:03:23 +00:00
common_chat_msg msg ;
2026-03-06 21:01:00 +01:00
msg . role = role ;
2025-02-18 18:03:23 +00:00
msg . content = content ;
inputs . messages . push_back ( msg );
};
2026-03-06 21:01:00 +01:00
add_simple_msg ( "system" , "You are a helpful assistant" );
add_simple_msg ( "user" , "Hello" );
2025-02-18 18:03:23 +00:00
add_simple_msg ( "assistant" , "Hi there" );
2026-03-06 21:01:00 +01:00
add_simple_msg ( "user" , "How are you?" );
2025-02-18 18:03:23 +00:00
return common_chat_templates_apply ( tmpls , inputs ). prompt ;
}
2026-03-06 21:01:00 +01:00
#define CHATML_TEMPLATE_SRC \
"{%- for message in messages -%}\n" \
2025-02-18 18:03:23 +00:00
" {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' -}}\n" \
2026-03-06 21:01:00 +01:00
"{%- endfor -%}\n" \
"{%- if add_generation_prompt -%}\n" \
" {{- '<|im_start|>assistant\n' -}}\n" \
2025-02-18 18:03:23 +00:00
"{%- endif -%}"
void common_chat_templates_free ( struct common_chat_templates * tmpls ) {
delete tmpls ;
}
bool common_chat_templates_was_explicit ( const struct common_chat_templates * tmpls ) {
return tmpls -> has_explicit_template ;
}
2026-04-23 10:47:26 +02:00
// LFM2 format detection: template uses <|tool_list_start|>[...]<|tool_list_end|> around the tool list
// and <|tool_call_start|>[...]<|tool_call_end|> around each tool call
static bool is_lfm2_template ( const std :: string & src ) {
return src . find ( "<|tool_list_start|>" ) != std :: string :: npos &&
src . find ( "<|tool_list_end|>" ) != std :: string :: npos ;
}
common_chat_prompt_preset common_chat_get_asr_prompt ( const common_chat_templates * chat_templates ) {
common_chat_prompt_preset asr_preset ;
asr_preset . system = "" ;
asr_preset . user = "Transcribe audio to text" ;
if ( chat_templates && chat_templates -> template_default && is_lfm2_template ( chat_templates -> template_default -> source ())) {
asr_preset . system = "Perform ASR." ;
asr_preset . user = "" ;
}
return asr_preset ;
}
2026-01-19 23:28:01 +01:00
std :: string common_chat_templates_source ( const struct common_chat_templates * tmpls , const std :: string & variant ) {
if ( ! variant . empty ()) {
if ( variant == "tool_use" ) {
2025-02-18 18:03:23 +00:00
if ( tmpls -> template_tool_use ) {
2026-01-19 23:28:01 +01:00
return tmpls -> template_tool_use -> source ();
2025-02-18 18:03:23 +00:00
}
2026-01-19 23:28:01 +01:00
return "" ;
2025-02-18 18:03:23 +00:00
}
2026-03-06 21:01:00 +01:00
LOG_DBG ( "%s: unknown template variant: %s \n " , __func__ , variant . c_str ());
2025-02-18 18:03:23 +00:00
}
2026-01-19 23:28:01 +01:00
return tmpls -> template_default -> source ();
2025-02-18 18:03:23 +00:00
}
2026-03-06 21:01:00 +01:00
common_chat_templates_ptr common_chat_templates_init ( const struct llama_model * model ,
const std :: string & chat_template_override ,
const std :: string & bos_token_override ,
const std :: string & eos_token_override ) {
2025-02-18 18:03:23 +00:00
std :: string default_template_src ;
std :: string template_tool_use_src ;
bool has_explicit_template = ! chat_template_override . empty ();
if ( chat_template_override . empty ()) {
GGML_ASSERT ( model != nullptr );
const auto * str = llama_model_chat_template ( model , /* name */ nullptr );
if ( str ) {
2026-03-06 21:01:00 +01:00
default_template_src = str ;
2025-02-18 18:03:23 +00:00
has_explicit_template = true ;
}
str = llama_model_chat_template ( model , /* name */ "tool_use" );
if ( str ) {
template_tool_use_src = str ;
has_explicit_template = true ;
}
} else {
default_template_src = chat_template_override ;
}
if ( default_template_src . empty () || default_template_src == "chatml" ) {
if ( ! template_tool_use_src . empty ()) {
default_template_src = template_tool_use_src ;
} else {
default_template_src = CHATML_TEMPLATE_SRC ;
}
}
2025-08-11 15:31:35 +02:00
// TODO @ngxson : this is a temporary hack to prevent chat template from throwing an error
// Ref: https://github.com/ggml-org/llama.cpp/pull/15230#issuecomment-3173959633
if ( default_template_src . find ( "<|channel|>" ) != std :: string :: npos
2026-03-06 21:01:00 +01:00
// search for the error message and patch it
&& default_template_src . find ( "in message.content or" ) != std :: string :: npos ) {
2025-08-11 15:31:35 +02:00
string_replace_all ( default_template_src ,
2026-03-06 21:01:00 +01:00
"{%- if \" <|channel|>analysis<|message|> \" in message.content or "
" \" <|channel|>final<|message|> \" in message.content %}" ,
"{%- if false %}" );
2025-08-11 15:31:35 +02:00
}
2025-12-09 17:31:04 -06:00
// TODO @aldehir : this is a temporary fix, pending Minja changes
// Ref: https://github.com/ggml-org/llama.cpp/pull/17713#issuecomment-3631342664
if ( default_template_src . find ( "[TOOL_CALLS]" ) != std :: string :: npos
2026-03-06 21:01:00 +01:00
// search for the error message and patch it
&& default_template_src . find ( "if (message['content'] is none or" ) != std :: string :: npos ) {
2025-12-09 17:31:04 -06:00
string_replace_all ( default_template_src ,
2026-03-06 21:01:00 +01:00
"{%- if (message['content'] is none or message['content'] == '' or "
"message['content']|length == 0) and (message['tool_calls'] is not defined or "
"message['tool_calls'] is none or message['tool_calls']|length == 0) %}" ,
"{%- if false %}" );
2025-12-09 17:31:04 -06:00
}
2025-02-18 18:03:23 +00:00
std :: string token_bos = bos_token_override ;
std :: string token_eos = eos_token_override ;
2026-03-06 21:01:00 +01:00
bool add_bos = false ;
bool add_eos = false ;
2025-02-18 18:03:23 +00:00
if ( model ) {
2026-03-06 21:01:00 +01:00
const auto * vocab = llama_model_get_vocab ( model );
const auto get_token = [ & ]( llama_token token , const char * name , const char * jinja_variable_name ) {
2025-02-18 18:03:23 +00:00
if ( token == LLAMA_TOKEN_NULL ) {
2026-03-06 21:01:00 +01:00
if ( default_template_src . find ( jinja_variable_name ) != std :: string :: npos ||
template_tool_use_src . find ( jinja_variable_name ) != std :: string :: npos ) {
LOG_WRN (
"common_chat_templates_init: warning: vocab does not have a %s token, jinja template won't "
"work as intended. \n " ,
name );
2025-02-18 18:03:23 +00:00
}
return std :: string ();
}
return common_token_to_piece ( vocab , token , true );
};
token_bos = get_token ( llama_vocab_bos ( vocab ), "BOS" , "bos_token" );
token_eos = get_token ( llama_vocab_eos ( vocab ), "EOS" , "eos_token" );
2026-03-06 21:01:00 +01:00
add_bos = llama_vocab_get_add_bos ( vocab );
add_eos = llama_vocab_get_add_eos ( vocab );
2025-02-18 18:03:23 +00:00
}
common_chat_templates_ptr tmpls ( new common_chat_templates ());
tmpls -> has_explicit_template = has_explicit_template ;
2026-03-06 21:01:00 +01:00
tmpls -> add_bos = add_bos ;
tmpls -> add_eos = add_eos ;
2025-02-18 18:03:23 +00:00
try {
2026-01-16 11:22:06 +01:00
tmpls -> template_default = std :: make_unique < common_chat_template > ( default_template_src , token_bos , token_eos );
2025-02-18 18:03:23 +00:00
} catch ( const std :: exception & e ) {
2026-01-16 11:22:06 +01:00
LOG_ERR ( "%s: error: %s \n " , __func__ , e . what ());
LOG_ERR ( "%s: failed to initialize chat template \n " , __func__ );
LOG_ERR ( "%s: please consider disabling jinja via --no-jinja, or using another chat template \n " , __func__ );
throw e ;
2025-02-18 18:03:23 +00:00
}
if ( ! template_tool_use_src . empty ()) {
try {
2026-01-16 11:22:06 +01:00
tmpls -> template_tool_use = std :: make_unique < common_chat_template > ( template_tool_use_src , token_bos , token_eos );
2025-02-18 18:03:23 +00:00
} catch ( const std :: exception & e ) {
LOG_ERR ( "%s: failed to parse tool use chat template (ignoring it): %s \n " , __func__ , e . what ());
}
}
return tmpls ;
}
2025-01-30 19:13:58 +00:00
2025-05-26 00:30:51 +01:00
const char * common_chat_format_name ( common_chat_format format ) {
2025-01-30 19:13:58 +00:00
switch ( format ) {
2026-03-06 21:01:00 +01:00
case COMMON_CHAT_FORMAT_CONTENT_ONLY :
return "Content-only" ;
case COMMON_CHAT_FORMAT_PEG_SIMPLE :
return "peg-simple" ;
case COMMON_CHAT_FORMAT_PEG_NATIVE :
return "peg-native" ;
2026-04-02 23:31:02 +02:00
case COMMON_CHAT_FORMAT_PEG_GEMMA4 :
return "peg-gemma4" ;
2026-07-28 04:27:20 -05:00
case COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 :
return "peg-minimax-m3" ;
2025-01-30 19:13:58 +00:00
default :
throw std :: runtime_error ( "Unknown chat format" );
}
}
2025-05-26 00:30:51 +01:00
const char * common_reasoning_format_name ( common_reasoning_format format ) {
switch ( format ) {
2026-03-06 21:01:00 +01:00
case COMMON_REASONING_FORMAT_NONE :
return "none" ;
case COMMON_REASONING_FORMAT_AUTO :
return "auto" ;
case COMMON_REASONING_FORMAT_DEEPSEEK :
return "deepseek" ;
case COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY :
return "deepseek-legacy" ;
2025-05-26 00:30:51 +01:00
default :
throw std :: runtime_error ( "Unknown reasoning format" );
}
}
2025-08-11 14:48:41 +02:00
common_reasoning_format common_reasoning_format_from_name ( const std :: string & format ) {
if ( format == "none" ) {
return COMMON_REASONING_FORMAT_NONE ;
2026-03-06 21:01:00 +01:00
}
if ( format == "auto" ) {
2025-08-11 14:48:41 +02:00
return COMMON_REASONING_FORMAT_AUTO ;
2026-03-06 21:01:00 +01:00
}
if ( format == "deepseek" ) {
2025-08-11 14:48:41 +02:00
return COMMON_REASONING_FORMAT_DEEPSEEK ;
2026-03-06 21:01:00 +01:00
}
if ( format == "deepseek-legacy" ) {
2025-08-11 14:48:41 +02:00
return COMMON_REASONING_FORMAT_DEEPSEEK_LEGACY ;
}
throw std :: runtime_error ( "Unknown reasoning format: " + format );
}
2025-01-30 19:13:58 +00:00
static void foreach_function ( const json & tools , const std :: function < void ( const json & ) > & fn ) {
for ( const auto & tool : tools ) {
2025-02-13 10:05:16 +00:00
if ( ! tool . contains ( "type" ) || tool . at ( "type" ) != "function" || ! tool . contains ( "function" )) {
2025-01-30 19:13:58 +00:00
LOG_INF ( "Skipping tool without function: %s" , tool . dump ( 2 ). c_str ());
continue ;
}
fn ( tool );
}
}
2026-03-06 21:01:00 +01:00
static void foreach_parameter ( const json & function ,
const std :: function < void ( const std :: string & , const json & , bool ) > & fn ) {
2025-12-16 04:05:23 -06:00
if ( ! function . contains ( "parameters" ) || ! function . at ( "parameters" ). is_object ()) {
return ;
}
const auto & params = function . at ( "parameters" );
if ( ! params . contains ( "properties" ) || ! params . at ( "properties" ). is_object ()) {
return ;
}
2026-03-06 21:01:00 +01:00
const auto & props = params . at ( "properties" );
2025-12-16 04:05:23 -06:00
std :: set < std :: string > required ;
if ( params . contains ( "required" ) && params . at ( "required" ). is_array ()) {
params . at ( "required" ). get_to ( required );
}
for ( const auto & [ name , prop ] : props . items ()) {
bool is_required = ( required . find ( name ) != required . end ());
fn ( name , prop , is_required );
}
}
2026-04-03 09:07:59 +03:00
static std :: string common_chat_template_direct_apply_impl (
2025-02-05 01:00:12 +00:00
const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ,
2026-04-03 09:07:59 +03:00
const std :: optional < json > & messages_override = std :: nullopt ,
const std :: optional < json > & tools_override = std :: nullopt ,
const std :: optional < json > & additional_context = std :: nullopt ) {
2026-01-16 11:22:06 +01:00
jinja :: context ctx ( tmpl . source ());
2025-02-05 01:00:12 +00:00
2026-01-16 11:22:06 +01:00
nlohmann :: ordered_json inp = nlohmann :: ordered_json {
{ "messages" , messages_override . has_value () ? * messages_override : inputs . messages },
{ "bos_token" , tmpl . bos_token ()},
{ "eos_token" , tmpl . eos_token ()},
2026-03-06 21:01:00 +01:00
{ "enable_thinking" , inputs . enable_thinking },
2026-01-16 11:22:06 +01:00
};
2026-01-29 14:06:54 +01:00
if ( tools_override . has_value () || ! inputs . tools . empty ()) {
inp [ "tools" ] = tools_override . has_value () ? * tools_override : inputs . tools ;
}
2026-01-16 11:22:06 +01:00
if ( inputs . extra_context . is_object ()) {
// TODO: do we need to merge, or replacing is fine?
for ( const auto & [ k , v ] : inputs . extra_context . items ()) {
inp [ k ] = v ;
}
}
if ( additional_context . has_value ()) {
// TODO: merge properly instead of overwriting (matching old behavior)
for ( const auto & [ k , v ] : additional_context -> items ()) {
inp [ k ] = v ;
}
}
if ( inputs . add_generation_prompt ) {
inp [ "add_generation_prompt" ] = true ;
}
2026-06-28 23:33:51 +02:00
if ( inp . contains ( "preserve_reasoning" ) && inp [ "preserve_reasoning" ]. is_boolean ()) {
bool enabled = inp [ "preserve_reasoning" ]. get < bool > ();
jinja :: caps_apply_preserve_reasoning ( ctx , enabled );
}
2026-01-16 11:22:06 +01:00
jinja :: global_from_json ( ctx , inp , inputs . mark_input );
// render
jinja :: runtime runtime ( ctx );
const jinja :: value results = runtime . execute ( tmpl . prog );
2026-03-06 21:01:00 +01:00
auto parts = jinja :: runtime :: gather_string_parts ( results );
2026-01-16 11:22:06 +01:00
std :: string result = parts -> as_string (). str ();
// TODO: improve this later
2025-08-05 20:43:36 +02:00
if ( inputs . add_bos && string_starts_with ( result , tmpl . bos_token ())) {
2025-02-18 18:03:23 +00:00
result = result . substr ( tmpl . bos_token (). size ());
}
2025-08-05 20:43:36 +02:00
if ( inputs . add_eos && string_ends_with ( result , tmpl . eos_token ())) {
2025-02-18 18:03:23 +00:00
result = result . substr ( 0 , result . size () - tmpl . eos_token (). size ());
}
return result ;
2025-02-05 01:00:12 +00:00
}
2026-04-03 09:07:59 +03:00
std :: string common_chat_template_direct_apply (
const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
return common_chat_template_direct_apply_impl ( tmpl , inputs , std :: nullopt , std :: nullopt , std :: nullopt );
}
2026-05-17 07:36:05 -04:00
static std :: string common_chat_template_generation_prompt_impl (
const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ,
const std :: optional < json > & messages_override = std :: nullopt ,
const std :: optional < json > & tools_override = std :: nullopt ,
const std :: optional < json > & additional_context = std :: nullopt ) {
auto adjusted_messages = messages_override ? * messages_override : inputs . messages ;
autoparser :: generation_params params = inputs ;
params . add_generation_prompt = false ;
params . continue_final_message = COMMON_CHAT_CONTINUATION_NONE ;
std :: string no_gen_prompt = common_chat_template_direct_apply_impl ( tmpl , params , adjusted_messages , tools_override , additional_context );
params . add_generation_prompt = true ;
std :: string gen_prompt = common_chat_template_direct_apply_impl ( tmpl , params , adjusted_messages , tools_override , additional_context );
size_t prefix_len = 0 ;
size_t min_size = std :: min ( no_gen_prompt . size (), gen_prompt . size ());
while ( prefix_len < min_size && no_gen_prompt [ prefix_len ] == gen_prompt [ prefix_len ]) {
prefix_len ++ ;
}
return gen_prompt . substr ( prefix_len );
}
std :: string common_chat_template_generation_prompt (
const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
return common_chat_template_generation_prompt_impl ( tmpl , inputs , std :: nullopt , std :: nullopt , std :: nullopt );
}
2026-03-06 21:01:00 +01:00
static common_chat_params common_chat_params_init_ministral_3 ( const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ) {
2025-12-09 17:31:04 -06:00
common_chat_params data ;
// Build up messages to follow the format: https://huggingface.co/mistralai/Ministral-3-14B-Reasoning-2512/blob/main/chat_template.jinja
auto adjusted_messages = json :: array ();
for ( const auto & msg : inputs . messages ) {
auto role = msg . value ( "role" , "" );
if ( role != "system" && role != "assistant" ) {
// Only adjust system and assistant messages. Interestingly, the system message may contain thinking.
adjusted_messages . push_back ( msg );
continue ;
}
auto content = json :: array ();
// If message contains `reasoning_content`, add it as a block of type `thinking`
if ( msg . contains ( "reasoning_content" ) && msg . at ( "reasoning_content" ). is_string ()) {
content . push_back ({
2026-03-06 21:01:00 +01:00
{ "type" , "thinking" },
{ "thinking" , msg . at ( "reasoning_content" ). get < std :: string > () },
2025-12-09 17:31:04 -06:00
});
}
// If message contains `content`, add it as a block of type `text`
if ( msg . contains ( "content" )) {
if ( msg . at ( "content" ). is_string ()) {
content . push_back ({
2026-03-06 21:01:00 +01:00
{ "type" , "text" },
{ "text" , msg . at ( "content" ). get < std :: string > () },
2025-12-09 17:31:04 -06:00
});
} else if ( msg . at ( "content" ). is_array ()) {
auto blocks = msg . at ( "content" );
content . insert ( content . end (), blocks . begin (), blocks . end ());
}
}
2026-03-06 21:01:00 +01:00
auto adjusted = msg ;
2025-12-09 17:31:04 -06:00
adjusted [ "content" ] = content ;
adjusted . erase ( "reasoning_content" );
adjusted_messages . push_back ( adjusted );
}
2026-04-11 01:26:36 +02:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = inputs . json_schema . is_object () && ! inputs . json_schema . empty ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = true ;
2025-12-09 17:31:04 -06:00
2026-03-11 10:26:12 +01:00
data . supports_thinking = true ;
data . thinking_start_tag = "[THINK]" ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { "[/THINK]" };
2026-04-03 09:07:59 +03:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs , /* messages_override = */ adjusted_messages );
2026-05-17 07:36:05 -04:00
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs , /* messages_override = */ adjusted_messages );
2026-03-06 21:01:00 +01:00
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . preserved_tokens = {
2025-12-09 17:31:04 -06:00
"[THINK]" ,
"[/THINK]" ,
"[TOOL_CALLS]" ,
"[ARGS]" ,
};
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = "[THINK]" + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += "[/THINK]" + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-03-06 21:01:00 +01:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-05-17 07:36:05 -04:00
auto generation_prompt = p . eps ();
2026-03-06 21:01:00 +01:00
auto reasoning =
extract_reasoning ? p . optional ( "[THINK]" + p . reasoning ( p . until ( "[/THINK]" )) + "[/THINK]" ) : p . eps ();
2025-12-09 17:31:04 -06:00
// Response format parser
2026-04-11 01:26:36 +02:00
if ( has_response_format ) {
2025-12-09 17:31:04 -06:00
// Ministral wants to emit json surrounded by code fences
2026-03-23 22:21:47 -05:00
return generation_prompt + ( reasoning << "```json" << p . content ( p . schema ( p . json (), "response-format" , inputs . json_schema )) << "```" );
2025-12-09 17:31:04 -06:00
}
// Tool call parser
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
2026-03-06 21:01:00 +01:00
std :: string name = function . at ( "name" );
const auto & schema = function . at ( "parameters" );
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tool_choice |=
p . rule ( "tool-" + name , p . tool_open ( p . tool_name ( p . literal ( name )) + "[ARGS]" ) +
p . tool_args ( p . schema ( p . json (), "tool-" + name + "-schema" , schema )));
2025-12-09 17:31:04 -06:00
});
2026-03-06 21:01:00 +01:00
auto min_calls = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0 ;
auto max_calls = inputs . parallel_tool_calls ? - 1 : 1 ;
2025-12-09 17:31:04 -06:00
auto tool_calls = p . trigger_rule ( "tool-call" , p . repeat ( "[TOOL_CALLS]" + tool_choice , min_calls , max_calls ));
2026-03-23 22:21:47 -05:00
return generation_prompt + ( reasoning << p . content ( p . until ( "[TOOL_CALLS]" )) << tool_calls );
2025-12-09 17:31:04 -06:00
}
// Content only parser
include_grammar = false ;
2026-03-23 22:21:47 -05:00
return generation_prompt + ( reasoning << p . content ( p . rest ()));
2025-12-09 17:31:04 -06:00
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
2026-03-06 21:01:00 +01:00
auto schema = function . at ( "parameters" );
2025-12-09 17:31:04 -06:00
builder . resolve_refs ( schema );
});
2026-04-11 01:26:36 +02:00
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
2025-12-09 17:31:04 -06:00
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
2026-03-06 21:01:00 +01:00
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "[TOOL_CALLS]" }
2025-12-09 17:31:04 -06:00
};
}
return data ;
}
2026-08-02 04:13:20 -05:00
static common_chat_params common_chat_params_init_qwen3_coder ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
const std :: string GEN_PREFIX = "<|im_start|>assistant \n " ;
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
auto supports_reasoning = tmpl . source (). find ( "<think>" ) != std :: string :: npos ;
data . supports_thinking = supports_reasoning ;
data . preserved_tokens = {
"<tool_call>" ,
"</tool_call>" ,
};
if ( supports_reasoning ) {
data . thinking_start_tag = "<think>" ;
// Support both </think> and <tool_call> as reasoning end sequences.
// <function= is omitted, as it is a workaround for Qwen3-Coder which is not a thinking model
data . thinking_end_tags = { "</think>" , "<tool_call>" };
data . preserved_tokens . insert ( data . preserved_tokens . end (), { "<think>" , "</think>" });
}
data . message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT , "<|im_start|>assistant" },
{ COMMON_CHAT_ROLE_TOOL , "<|im_start|>user \n <tool_response>" }, // Qwen3-Coder, Qwen3.5, Nemotron Nano 3
{ COMMON_CHAT_ROLE_TOOL , "<|im_start|>tool_response" }, // StepFun-3.5-Flash
{ COMMON_CHAT_ROLE_USER , "<|im_start|>user" },
{ COMMON_CHAT_ROLE_SYSTEM , "<|im_start|>system" },
};
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = inputs . json_schema . is_object () && ! inputs . json_schema . empty ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PREFIX ;
if ( supports_reasoning ) {
data . generation_prompt += "<think> \n " + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += " \n </think> \n\n " ;
}
}
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
auto generation_prompt = p . literal ( GEN_PREFIX );
auto reasoning = p . eps ();
if ( supports_reasoning && extract_reasoning ) {
reasoning = p . optional ( "<think>" + p . space () +
p . reasoning ( p . until_one_of ({ "</think>" , "<tool_call>" })) +
( p . literal ( "</think>" ) | p . peek ( p . literal ( "<tool_call>" ))));
}
// Response format parser
if ( has_response_format ) {
return generation_prompt + ( reasoning << p . content ( p . schema ( p . json (), "response-format" , inputs . json_schema )));
}
// Tool call parser
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
auto arg_close = p . tool_arg_close ( p . literal ( " \n </parameter> \n " ));
auto arg_string = p . rule ( "xml-arg-string" ,
p . ac ( p . tool_arg_string_value ( p . until ( " \n </parameter> \n " )) + arg_close , " \n </parameter> \n " ));
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
auto parameters = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
auto schema_info = common_schema_info ();
schema_info . resolve_refs ( parameters );
std :: vector < common_peg_parser > required_args ;
std :: vector < common_peg_parser > optional_args ;
foreach_parameter ( function , [ & ]( const std :: string & param_name , const json & param_schema , bool is_required ) {
auto rule_name = "tool-" + name + "-arg-" + param_name ;
auto arg_open = p . tool_arg_open ( "<parameter=" + p . tool_arg_name ( p . literal ( param_name )) + "> \n " );
auto arg_value = schema_info . resolves_to_string ( param_schema ) ?
arg_string :
p . tool_arg_json_value ( p . schema ( p . json (), rule_name + "-schema" , param_schema )) + arg_close ;
auto arg_rule = p . rule ( rule_name , p . tool_arg ( arg_open + arg_value ));
( is_required ? required_args : optional_args ). push_back ( arg_rule );
});
// Accept required arguments in any order, as Qwen does not always adhere to the
// order provided.
auto args = p . permute ( "tool-" + name + "-args" , required_args );
if ( ! optional_args . empty ()) {
args = args + p . zero_or_more ( p . choice ( optional_args ));
}
auto func = p . tool ( p . tool_open ( "<function=" + p . tool_name ( p . literal ( name )) + "> \n " ) +
p . tool_args ( args ) +
p . tool_close ( p . literal ( "</function> \n " )));
tool_choice |= p . rule ( "tool-" + name , func );
});
auto min_calls = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0 ;
// Qwen3-Coder models may occasionally omit the <tool_call> token.
auto tool_call_body = tool_choice + "</tool_call>" + p . space ();
auto tool_call_first = p . rule ( "tool-call-first" , p . optional ( p . literal ( "<tool_call> \n " )) + tool_call_body );
auto tool_call = p . rule ( "tool-call" , "<tool_call> \n " + tool_call_body );
auto calls = inputs . parallel_tool_calls ? tool_call_first + p . zero_or_more ( tool_call ) : tool_call_first ;
auto tool_calls = p . trigger_rule ( "tool-call-root" , p . repeat ( calls , min_calls , 1 ));
return generation_prompt +
( reasoning << p . content ( p . until_one_of ({ "<tool_call>" , "<function=" })) << tool_calls );
}
// Content only parser
return generation_prompt + ( reasoning << p . content ( p . rest ()));
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
builder . resolve_refs ( schema );
});
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
parser . build_grammar ( builder , data . grammar_lazy );
});
if ( data . grammar_lazy ) {
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "<tool_call>" },
// Trigger on "<function" and not "<function=" because the trailing "=" is part of
// the token with the function name e.g. "=read"
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "<function" },
};
}
}
return data ;
}
2026-03-06 21:01:00 +01:00
static common_chat_params common_chat_params_init_gpt_oss ( const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ) {
2025-08-05 22:10:36 +03:00
common_chat_params data ;
2025-11-02 08:56:28 -06:00
// Copy reasoning to the "thinking" field as expected by the gpt-oss template
auto adjusted_messages = json :: array ();
2026-03-18 04:41:25 -05:00
for ( auto msg : inputs . messages ) {
if ( msg . contains ( "reasoning_content" ) && msg . at ( "reasoning_content" ). is_string ()) {
msg [ "thinking" ] = msg . at ( "reasoning_content" );
2026-03-19 05:40:39 -05:00
if ( msg . contains ( "tool_calls" ) && msg . at ( "tool_calls" ). is_array () && ! msg . at ( "tool_calls" ). empty ()) {
msg . erase ( "content" );
}
2025-11-02 08:56:28 -06:00
}
2026-03-18 04:41:25 -05:00
adjusted_messages . push_back ( msg );
2025-11-02 08:56:28 -06:00
}
2026-04-03 09:07:59 +03:00
auto prompt = common_chat_template_direct_apply_impl ( tmpl , inputs , /* messages_override= */ adjusted_messages );
2025-08-05 22:10:36 +03:00
2025-08-20 14:26:01 +02:00
// Check if we need to replace the return token with end token during
// inference and without generation prompt. For more details see:
// https://github.com/ggml-org/llama.cpp/issues/15417
if ( inputs . is_inference && ! inputs . add_generation_prompt ) {
static constexpr std :: string_view return_token = "<|return|>" ;
static constexpr std :: string_view end_token = "<|end|>" ;
if ( size_t pos = prompt . rfind ( return_token ); pos != std :: string :: npos ) {
prompt . replace ( pos , return_token . length (), end_token );
}
}
2026-03-06 21:01:00 +01:00
data . prompt = prompt ;
2026-05-17 07:36:05 -04:00
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs , /* messages_override= */ adjusted_messages );
2026-06-23 00:27:28 -05:00
data . message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT , "<|start|>assistant" },
{ COMMON_CHAT_ROLE_USER , "<|start|>user" },
{ COMMON_CHAT_ROLE_SYSTEM , "<|start|>developer" },
{ COMMON_CHAT_ROLE_SYSTEM , "<|start|>system" },
{ COMMON_CHAT_ROLE_TOOL , "<|start|>functions" },
};
2026-05-25 07:56:18 +02:00
2026-03-06 21:01:00 +01:00
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
2025-08-05 22:10:36 +03:00
2026-07-25 04:58:09 -05:00
data . thinking_start_tag = "<|channel|>analysis<|message|>" ;
data . thinking_end_tags = { "<|end|>" };
2025-08-14 09:23:11 -05:00
// These special tokens are required to parse properly, so we include them
// even if parse_tool_calls is false.
data . preserved_tokens = {
2026-03-06 21:01:00 +01:00
"<|channel|>" , "<|constrain|>" , "<|message|>" , "<|start|>" , "<|end|>" ,
2025-08-14 09:23:11 -05:00
};
2026-05-17 07:36:05 -04:00
// Adjust prompt for continuation
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = "<|start|>assistant<|channel|>analysis<|message|>" + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += "<|end|><|start|>assistant<|channel|>final<|message|>" + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-03-18 04:41:25 -05:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = ! inputs . json_schema . is_null () && inputs . json_schema . is_object ();
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
2026-03-28 09:33:39 -05:00
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
2025-08-22 11:04:08 -05:00
2026-03-06 21:01:00 +01:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-03-18 04:41:25 -05:00
auto start = p . rule ( "start" , p . literal ( "<|start|>assistant" ));
auto end = p . rule ( "end" , p . literal ( "<|end|>" ));
auto content = p . rule ( "message-content" , p . until ( "<|end|>" ));
auto channel = p . literal ( "<|channel|>" ) + ( p . literal ( "commentary" ) | p . literal ( "analysis" ));
auto constrain_type = p . chars ( "[A-Za-z0-9_-]" , 1 , - 1 );
2025-08-22 11:04:08 -05:00
2026-04-02 08:59:59 -05:00
// Occasionally, gpt-oss-20b will prefix channels with this commentary
auto stray_commentary = p . optional ( p . literal ( "<|channel|>commentary" ) + p . optional ( p . literal ( " to=assistant" )));
auto start_analysis = stray_commentary + p . literal ( "<|channel|>analysis<|message|>" );
2026-03-28 09:33:39 -05:00
if ( extract_reasoning ) {
2026-04-02 08:59:59 -05:00
p . rule ( "analysis" , start_analysis + p . reasoning ( content ) + end );
2026-03-28 09:33:39 -05:00
} else {
2026-04-02 08:59:59 -05:00
p . rule ( "analysis" , p . content ( start_analysis + content + end ));
2026-03-28 09:33:39 -05:00
}
auto analysis = p . ref ( "analysis" );
2026-03-18 04:41:25 -05:00
auto preamble = p . rule ( "preamble" , p . literal ( "<|channel|>commentary<|message|>" ) + p . content ( content ) + end );
2026-04-02 08:59:59 -05:00
auto final_msg = p . rule ( "final" , stray_commentary + p . literal ( "<|channel|>final<|message|>" ) + p . content ( content ));
2026-03-31 06:52:42 -05:00
// Consume any unsolicited tool calls, e.g. builtin functions
auto unsolicited = p . rule ( "unsolicited" , p . atomic ( p . optional ( channel ) + p . literal ( " to=" ) + content + end ));
2026-03-18 04:41:25 -05:00
auto any = p . rule ( "any" , preamble | analysis );
2025-08-22 11:04:08 -05:00
2026-03-18 04:41:25 -05:00
if ( has_response_format ) {
2026-04-02 08:59:59 -05:00
auto constraint = p . optional ( p . space () + p . optional ( p . literal ( "<|constrain|>" )) + constrain_type );
2026-03-18 04:41:25 -05:00
auto response_format = p . rule ( "response-format" ,
p . literal ( "<|channel|>final" ) + constraint + p . literal ( "<|message|>" ) +
p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema )));
2025-08-14 09:23:11 -05:00
2026-03-23 22:21:47 -05:00
return p . zero_or_more ( start + analysis ) + start + response_format ;
2026-01-29 09:06:15 -06:00
}
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
2026-03-06 21:01:00 +01:00
auto tool_choice = p . choice ();
2026-01-29 09:06:15 -06:00
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
2026-03-06 21:01:00 +01:00
std :: string name = function . at ( "name" );
const auto & params = function . at ( "parameters" );
2026-01-29 09:06:15 -06:00
2026-03-18 04:41:25 -05:00
auto func_name = p . literal ( " to=functions." ) + p . tool_name ( p . literal ( name ));
2026-04-02 08:59:59 -05:00
auto constraint = p . optional ( p . space () + p . optional ( p . literal ( "<|constrain|>" )) + constrain_type );
2026-03-06 21:01:00 +01:00
auto args = p . tool_args ( p . schema ( p . json (), "tool-" + name + "-schema" , params ));
2026-03-18 04:41:25 -05:00
// recipient in role header
// <|start|>assistant to=functions.NAME<|channel|>(commentary|analysis)[constraint]<|message|>ARGS
auto tool_in_role = p . tool ( p . tool_open ( func_name + channel + constraint + p . literal ( "<|message|>" )) + args );
2026-03-06 21:01:00 +01:00
2026-03-18 04:41:25 -05:00
// recipient in channel header
// <|channel|>(commentary|analysis) to=functions.NAME[constraint]<|message|>ARGS
auto tool_in_channel = p . tool ( p . tool_open ( channel + func_name + constraint + p . literal ( "<|message|>" )) + args );
2026-03-06 21:01:00 +01:00
2026-03-18 04:41:25 -05:00
tool_choice |= p . rule ( "tool-" + name , tool_in_role | tool_in_channel );
2026-01-29 09:06:15 -06:00
});
2026-03-18 04:41:25 -05:00
auto tool_call = p . trigger_rule ( "tool-call" , tool_choice );
2026-01-29 09:06:15 -06:00
2026-03-18 04:41:25 -05:00
if ( inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ) {
2026-03-23 22:21:47 -05:00
return p . zero_or_more ( start + any ) + start + tool_call ;
2026-03-18 04:41:25 -05:00
}
2026-01-29 09:06:15 -06:00
2026-03-23 22:21:47 -05:00
return p . zero_or_more ( start + any ) + start + ( tool_call | final_msg );
2026-01-29 09:06:15 -06:00
}
2026-03-31 06:52:42 -05:00
return p . zero_or_more ( start + any ) + start + ( final_msg | unsolicited );
2026-01-29 09:06:15 -06:00
});
data . parser = parser . save ();
if ( include_grammar ) {
2026-03-18 04:41:25 -05:00
data . grammar_lazy = ! ( has_response_format || ( has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ));
2026-03-06 21:01:00 +01:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
2026-01-29 09:06:15 -06:00
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
2026-03-06 21:01:00 +01:00
auto schema = function . at ( "parameters" );
2026-01-29 09:06:15 -06:00
builder . resolve_refs ( schema );
});
2026-04-11 01:26:36 +02:00
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
2026-01-29 09:06:15 -06:00
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
2026-03-18 04:41:25 -05:00
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN , "^ \\ s+to$" },
2026-04-02 08:59:59 -05:00
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN , "^< \\ |channel \\ |>(?:commentary|analysis) \\ s+to=functions$" },
2026-03-18 04:41:25 -05:00
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN , "< \\ |start \\ |>assistant( \\ s+to)" },
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN , "< \\ |start \\ |>assistant(< \\ |channel \\ |>(?:commentary|analysis) \\ s+to)" }
2026-01-29 09:06:15 -06:00
};
}
2026-01-02 02:01:43 +09:00
return data ;
}
2026-04-04 13:39:00 -05:00
static common_chat_params common_chat_params_init_gemma4 ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
2026-05-17 07:36:05 -04:00
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
2026-04-13 18:18:18 -05:00
if ( inputs . add_generation_prompt && string_ends_with ( data . prompt , "<turn|> \n " )) {
// This may happen if the model generates content + tool_call, the
// template does not add the model's next turn and confuses the model
// from emitting its proper reasoning token sequence.
2026-05-17 07:36:05 -04:00
data . generation_prompt = "<|turn>model \n " ;
data . prompt += data . generation_prompt ;
2026-04-13 18:18:18 -05:00
}
2026-06-23 00:27:28 -05:00
data . message_delimiters = {
{ COMMON_CHAT_ROLE_USER , "<|turn>user" },
{ COMMON_CHAT_ROLE_ASSISTANT , "<|turn>model" },
};
2026-05-25 07:56:18 +02:00
2026-04-04 13:39:00 -05:00
data . format = COMMON_CHAT_FORMAT_PEG_GEMMA4 ;
2026-04-10 05:49:14 -04:00
data . supports_thinking = true ;
data . thinking_start_tag = "<|channel>thought" ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { "<channel|>" };
2026-04-04 13:39:00 -05:00
data . preserved_tokens = {
"<|channel>" ,
"<channel|>" ,
"<|tool_call>" ,
"<tool_call|>" ,
"<|turn>" ,
};
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = string_ends_with ( data . prompt , "<turn|> \n " ) ? "<|turn>model \n " : "" ;
data . generation_prompt += "<|channel>thought \n " + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += "<channel|>" + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-04-04 13:39:00 -05:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = ! inputs . json_schema . is_null () && inputs . json_schema . is_object ();
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-05-17 07:36:05 -04:00
auto start = p . rule ( "start" , p . optional ( p . literal ( "<|turn>model \n " )));
2026-04-04 13:39:00 -05:00
if ( extract_reasoning ) {
2026-04-10 05:49:14 -04:00
p . rule ( "thought" , p . literal ( "<|channel>thought" ) + p . space () + p . reasoning ( p . until ( "<channel|>" )) + p . literal ( "<channel|>" ));
2026-04-04 13:39:00 -05:00
} else {
2026-04-10 05:49:14 -04:00
p . rule ( "thought" , p . content ( p . literal ( "<|channel>thought" ) + p . space () + p . until ( "<channel|>" ) + p . literal ( "<channel|>" )));
2026-04-04 13:39:00 -05:00
}
2026-04-13 18:18:18 -05:00
auto consume_empty_channels = p . gbnf ( p . zero_or_more ( p . literal ( "<|channel>" ) + p . negate ( p . literal ( "thought" ))), "" );
auto thought = ( p . peek ( p . literal ( "<|channel>" )) + consume_empty_channels + p . ref ( "thought" )) | p . negate ( p . literal ( "<|channel>" ));
2026-04-04 13:39:00 -05:00
if ( has_response_format ) {
auto response_format = p . literal ( "```json" ) <<
p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema )) <<
p . literal ( "```" );
return start + p . optional ( thought ) + response_format ;
}
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
// Gemma4 tool calling syntax
// Rules should match traversal logic in gemma4_to_json()
p . rule ( "gemma4-string-content" , p . until ( "<| \" |>" ));
p . rule ( "gemma4-string" , p . literal ( "<| \" |>" ) + p . ref ( "gemma4-string-content" ) + p . literal ( "<| \" |>" ));
p . rule ( "gemma4-bool" , p . json_bool ());
p . rule ( "gemma4-null" , p . json_null ());
p . rule ( "gemma4-number" , p . json_number ());
2026-04-09 05:25:07 -05:00
p . rule ( "gemma4-dict-key" , p . rule ( "gemma4-dict-key-name" , p . chars ( "[^:}]" , 1 , - 1 )) + p . literal ( ":" ));
2026-04-04 13:39:00 -05:00
p . rule ( "gemma4-dict-kv" , p . ref ( "gemma4-dict-key" ) + p . space () + p . ref ( "gemma4-value" ));
p . rule ( "gemma4-dict" , [ & ]() {
auto ws = p . space ();
auto member = p . ref ( "gemma4-dict-kv" );
auto members = p . sequence ({ member , p . zero_or_more ( p . sequence ({ p . literal ( "," ), ws , member }))});
return p . sequence ({
p . literal ( "{" ), ws ,
p . choice ({ p . literal ( "}" ), p . sequence ({ members , ws , p . literal ( "}" )})})
});
});
p . rule ( "gemma4-array" , [ & ]() {
auto ws = p . space ();
auto value = p . ref ( "gemma4-value" );
auto elements = p . sequence ({ value , p . zero_or_more ( p . sequence ({ p . literal ( "," ), ws , value }))});
return p . sequence ({
p . literal ( "[" ), ws ,
p . choice ({ p . literal ( "]" ), p . sequence ({ elements , ws , p . literal ( "]" )})})
});
});
p . rule ( "gemma4-value" , [ & ]() {
return p . choice ({
p . ref ( "gemma4-string" ), p . ref ( "gemma4-dict" ), p . ref ( "gemma4-array" ),
p . ref ( "gemma4-number" ), p . ref ( "gemma4-bool" ), p . ref ( "gemma4-null" )
});
});
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
// TODO @aldehir : need to extend json-schema-to-grammar to produce more than JSON rules
// const auto & params = function.at("parameters");
tool_choice |= p . rule ( "tool-" + name , p . tool ( p . sequence ({
p . tool_open ( p . tool_name ( p . literal ( name )) + p . peek ( p . literal ( "{" ))),
p . tool_args ( p . ref ( "gemma4-dict" )),
})));
});
auto tool_call = p . trigger_rule ( "tool-call" , p . repeat (
"<|tool_call>call:" + tool_choice + "<tool_call|>" ,
/* min = */ inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0 ,
/* max = */ inputs . parallel_tool_calls ? - 1 : 1
));
2026-04-13 18:18:18 -05:00
auto scan_to_toolcall = p . rule ( "scan-to-toolcall" , p . until ( "<|tool_call>" ));
auto content = p . rule ( "content" , p . content ( p . until_one_of ({ "<|channel>" , "<channel|>" , "<|tool_call>" })));
2026-04-04 13:39:00 -05:00
auto message = p . rule ( "message" , thought + content );
2026-04-13 18:18:18 -05:00
return start + p . zero_or_more ( message ) + scan_to_toolcall + tool_call ;
2026-04-04 13:39:00 -05:00
}
2026-04-13 18:18:18 -05:00
// Gemma 4 may emit an extra <|channel>thought\n<channel|> at the end of the content. It may
// also emit a single trailing <channel|> token. Consume all complete reasoning blocks and
// then stop at the first unmatched <channel|> token.
auto content = p . rule ( "content" , p . content ( p . until_one_of ({ "<|channel>" , "<channel|>" })));
2026-04-04 13:39:00 -05:00
auto message = p . rule ( "message" , thought + content );
return start + p . one_or_more ( message );
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = ! ( has_response_format || ( has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ));
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
});
2026-04-11 01:26:36 +02:00
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
2026-04-04 13:39:00 -05:00
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "<|tool_call>" },
};
}
return data ;
}
2026-03-06 21:01:00 +01:00
// Functionary v3.2 - uses recipient-based format: >>>recipient\n{content}
static common_chat_params common_chat_params_init_functionary_v3_2 ( const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ) {
2026-01-14 07:28:38 +09:00
common_chat_params data ;
2026-05-17 07:36:05 -04:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . preserved_tokens = {
2026-03-06 21:01:00 +01:00
">>>all" ,
};
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto include_grammar = has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ;
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = "<|start_header_id|>assistant<|end_header_id|> \n\n >>>all \n " + msg . render_content ();
data . prompt += data . generation_prompt ;
}
2026-03-06 21:01:00 +01:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
// Functionary v3.2 format:
// - Normal content: >>>all\n{content}
// - Tool calls: >>>function_name\n{json_args}
// Generation prompt ends with ">>>" so model outputs recipient immediately
// Build content parser for >>>all\n{content}
// When tools are present, content stops before the next ">>>" (tool call)
// When no tools, content goes until end
2026-03-19 16:58:21 +01:00
auto content_until_tool = p . literal ( "all \n " ) + p . content ( p . until ( ">>>" ));
auto content_until_end = p . literal ( "all \n " ) + p . content ( p . rest ());
2026-05-17 07:36:05 -04:00
auto generation_prompt = p . literal ( "<|start_header_id|>assistant<|end_header_id|> \n\n >>>" );
2026-03-06 21:01:00 +01:00
// If no tools or tool_choice is NONE, just parse content
if ( ! has_tools || inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) {
// When no tools, just match the prefix and capture everything after
2026-03-23 22:21:47 -05:00
return generation_prompt + content_until_end + p . end ();
2026-01-14 07:28:38 +09:00
}
2026-03-06 21:01:00 +01:00
// Build tool call parsers for each available function
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
const auto & schema = function . at ( "parameters" );
// Tool format: >>>function_name\n{json_args}
auto tool_parser = p . tool (
2026-03-19 16:58:21 +01:00
p . tool_open ( p . tool_name ( p . literal ( name )) + p . literal ( " \n " )) +
2026-03-06 21:01:00 +01:00
p . tool_args ( p . schema ( p . json (), "tool-" + name + "-schema" , schema ))
);
tool_choice |= p . rule ( "tool-" + name , tool_parser );
});
auto content_only = content_until_end ;
auto tools_only = p . trigger_rule ( "tools" , p . one_or_more ( tool_choice ));
auto content_and_tools = content_until_tool + tools_only ;
2026-03-19 16:58:21 +01:00
auto ret = p . eps ();
2026-03-06 21:01:00 +01:00
if ( inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ) {
if ( inputs . parallel_tool_calls ) {
2026-03-19 16:58:21 +01:00
ret = p . choice ({ content_and_tools , tools_only }) + p . end ();
} else {
ret = p . choice ({ content_until_tool + tool_choice , tools_only }) + p . end ();
2026-03-06 21:01:00 +01:00
}
2026-03-19 16:58:21 +01:00
} else if ( inputs . parallel_tool_calls ) {
ret = p . choice ({ content_and_tools , content_only , tools_only }) + p . end ();
} else {
auto content_and_tool = content_until_tool + tool_choice ;
ret = p . choice ({ content_and_tool , content_only , tool_choice }) + p . end ();
2026-03-06 21:01:00 +01:00
}
2026-03-23 22:21:47 -05:00
return generation_prompt + ret ;
2026-03-06 21:01:00 +01:00
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
2026-01-14 07:28:38 +09:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
2026-03-06 21:01:00 +01:00
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
2026-01-14 07:28:38 +09:00
});
2026-03-06 21:01:00 +01:00
parser . build_grammar ( builder , data . grammar_lazy );
2026-01-14 07:28:38 +09:00
});
2026-03-06 21:01:00 +01:00
// Grammar trigger for when the model starts outputting a tool call
// (after the initial ">>>" in the generation prompt but recipient other than "all")
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN , ">>>(?!all)" }
};
2026-01-14 07:28:38 +09:00
}
return data ;
}
2026-03-06 21:01:00 +01:00
// Kimi K2 Thinking - uses unique tool call ID format: functions.<name>:<index>
// The ID contains both the function name and an incrementing counter
static common_chat_params common_chat_params_init_kimi_k2 ( const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ) {
2026-01-23 12:03:42 +01:00
common_chat_params data ;
2026-05-17 07:36:05 -04:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
2026-03-06 21:01:00 +01:00
data . preserved_tokens = {
"<|tool_calls_section_begin|>" ,
"<|tool_calls_section_end|>" ,
"<|tool_call_begin|>" ,
"<|tool_call_argument_begin|>" ,
"<|tool_call_end|>" ,
"<think>" ,
"</think>" ,
};
2026-01-23 12:03:42 +01:00
2026-03-06 21:01:00 +01:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ;
2026-01-23 12:03:42 +01:00
2026-03-19 16:58:21 +01:00
const std :: string SECTION_BEGIN = "<|tool_calls_section_begin|>" ;
const std :: string SECTION_END = "<|tool_calls_section_end|>" ;
const std :: string CALL_BEGIN = "<|tool_call_begin|>" ;
const std :: string ARGS_BEGIN = "<|tool_call_argument_begin|>" ;
const std :: string CALL_END = "<|tool_call_end|>" ;
const std :: string THINK_START = "<think>" ;
const std :: string THINK_END = "</think>" ;
2026-05-17 07:36:05 -04:00
const std :: string GEN_PROMPT = "<|im_assistant|>assistant<|im_middle|>" ;
2026-03-19 16:58:21 +01:00
data . thinking_start_tag = THINK_START ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { THINK_END };
2026-03-19 16:58:21 +01:00
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PROMPT + THINK_START + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += THINK_END + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-03-06 21:01:00 +01:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
// Kimi K2 Thinking format:
// - Reasoning: <think>{reasoning}</think>
// - Content: text after reasoning
// - Tool calls section:
// <|tool_calls_section_begin|>
// <|tool_call_begin|>functions.<name>:<index><|tool_call_argument_begin|>{json_args}<|tool_call_end|>
// ...
// <|tool_calls_section_end|>
// The ID format is: functions.<function_name>:<counter> where counter is 0, 1, 2, ...
2026-01-24 17:58:45 +01:00
2026-03-19 16:58:21 +01:00
// Tool call markers
2026-03-06 21:01:00 +01:00
auto end = p . end ();
// Note: this model is CRAZY. It can diverge from its supposed tool calling pattern in so many ways it's not funny.
// For example, it can call tools at the end of reasoning without closing reasoning...
auto reasoning = extract_reasoning ? p . optional ( THINK_START + p . reasoning (
p . until_one_of ({ THINK_END , "<|tool_calls_section_begin|>" , "<|tool_call_begin|>" })) +
p . optional ( p . literal ( THINK_END ))) : p . eps ();
2026-05-17 07:36:05 -04:00
auto generation_prompt = p . literal ( GEN_PROMPT );
2026-03-06 21:01:00 +01:00
// Content only parser (no tools)
if ( ! has_tools || inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) {
2026-03-23 22:21:47 -05:00
return generation_prompt + reasoning + p . content ( p . rest ()) + end ;
2026-01-23 12:03:42 +01:00
}
2026-03-06 21:01:00 +01:00
// Build tool call parsers for each available function
// The ID format is: functions.<name>:<index>
// We need to match: functions.<name>:<digits>
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
const auto & schema = function . at ( "parameters" );
2026-01-23 12:03:42 +01:00
2026-03-06 21:01:00 +01:00
// Match: functions.<name>:<digits>
// Capture the full call id (functions.<name>:<digits>) using tool_id tag
auto tool_id = p . tool_id ( p . literal ( "functions." ) + p . tool_name ( p . literal ( name )) + p . literal ( ":" ) + p . chars ( "[0-9]" , 1 , - 1 ));
auto tool_parser = p . tool (
p . tool_open ( tool_id + p . literal ( ARGS_BEGIN )) +
p . tool_args ( p . schema ( p . json (), "tool-" + name + "-schema" , schema )) +
p . tool_close ( p . optional (( p . literal ( CALL_END ))))
);
2026-01-23 12:03:42 +01:00
2026-03-06 21:01:00 +01:00
tool_choice |= p . rule ( "tool-" + name , tool_parser );
});
2025-01-30 19:13:58 +00:00
2026-03-06 21:01:00 +01:00
// Tool calls section: <|tool_calls_section_begin|> tool_calls <|tool_calls_section_end|>
auto min_calls = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0 ;
auto max_calls = inputs . parallel_tool_calls ? - 1 : 1 ;
// Use trigger_rule so grammar generator knows where to start generating rules
auto tool_calls = p . rule ( "tool-calls" ,
p . optional ( p . literal ( SECTION_BEGIN )) +
p . trigger_rule ( "tool-call" , p . repeat ( CALL_BEGIN + tool_choice , min_calls , max_calls ) +
p . optional ( p . literal ( SECTION_END )))
);
2025-08-29 14:53:41 +02:00
2026-03-06 21:01:00 +01:00
auto content_before_tools = p . content ( p . until_one_of ({ SECTION_BEGIN , CALL_BEGIN }));
2026-03-23 22:21:47 -05:00
return generation_prompt + reasoning + content_before_tools + tool_calls + end ;
2026-03-06 21:01:00 +01:00
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
2025-08-29 14:53:41 +02:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
2026-03-06 21:01:00 +01:00
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
2025-08-29 14:53:41 +02:00
});
2026-03-06 21:01:00 +01:00
parser . build_grammar ( builder , data . grammar_lazy );
2025-08-29 14:53:41 +02:00
});
2026-03-06 21:01:00 +01:00
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "<|tool_call_begin|>" }
};
2025-08-29 14:53:41 +02:00
}
2026-03-06 21:01:00 +01:00
2025-08-29 14:53:41 +02:00
return data ;
}
2026-06-05 21:31:56 +02:00
// LFM2/LFM2.5 parser. Tool calls are almost Python-style and parallel-capable
// (except dotted names and JSON literals true/false/null).
// Always wrapped in <|tool_call_start|>[name(args)]<|tool_call_end|> with optional <think> reasoning.
// tool_list_tokens preserves LFM2 system tool-list markers.
static common_chat_params common_chat_params_init_lfm2 ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ,
bool tool_list_tokens ) {
2026-03-09 01:11:22 +01:00
common_chat_params data ;
2026-06-05 21:31:56 +02:00
const std :: string TOOL_CALL_START = "<|tool_call_start|>" ;
const std :: string TOOL_CALL_END = "<|tool_call_end|>" ;
const std :: string TOOL_LIST_START = "<|tool_list_start|>" ;
const std :: string TOOL_LIST_END = "<|tool_list_end|>" ;
const std :: string THINK_START = "<think>" ;
const std :: string THINK_END = "</think>" ;
const std :: string GEN_PROMPT = "<|im_start|>assistant \n " ;
2026-06-06 22:39:21 +02:00
// Copy reasoning to the "thinking" field the template expects
auto adjusted_messages = json :: array ();
for ( auto msg : inputs . messages ) {
if ( msg . contains ( "reasoning_content" ) && msg . at ( "reasoning_content" ). is_string ()) {
msg [ "thinking" ] = msg . at ( "reasoning_content" );
}
adjusted_messages . push_back ( msg );
}
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs , adjusted_messages );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs , adjusted_messages );
2026-03-09 01:11:22 +01:00
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
2026-06-05 21:31:56 +02:00
data . preserved_tokens = { TOOL_CALL_START , TOOL_CALL_END , THINK_START , THINK_END };
if ( tool_list_tokens ) {
data . preserved_tokens . push_back ( TOOL_LIST_START );
data . preserved_tokens . push_back ( TOOL_LIST_END );
}
data . thinking_start_tag = THINK_START ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { THINK_END };
2026-03-09 01:11:22 +01:00
2026-06-10 14:41:41 +02:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = ! inputs . json_schema . is_null () && inputs . json_schema . is_object ();
2026-06-06 22:39:21 +02:00
// Gate by reasoning format and whether the template supports <think>
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE &&
tmpl . source (). find ( THINK_START ) != std :: string :: npos ;
2026-06-10 14:41:41 +02:00
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
2026-03-09 01:11:22 +01:00
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PROMPT + THINK_START + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += THINK_END + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-03-19 16:58:21 +01:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-05-17 07:36:05 -04:00
auto generation_prompt = p . literal ( GEN_PROMPT );
2026-03-09 01:11:22 +01:00
auto end = p . end ();
auto reasoning = p . eps ();
2026-06-06 22:39:21 +02:00
if ( extract_reasoning ) {
2026-03-09 01:11:22 +01:00
reasoning = p . optional ( THINK_START + p . reasoning ( p . until ( THINK_END )) + THINK_END );
}
if ( ! has_tools || inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) {
2026-06-10 14:41:41 +02:00
if ( has_response_format ) {
auto response_format = p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema ));
return generation_prompt + reasoning + response_format + end ;
}
2026-03-23 22:21:47 -05:00
return generation_prompt + reasoning + p . content ( p . rest ()) + end ;
2026-03-09 01:11:22 +01:00
}
auto tool_calls = p . rule ( "tool-calls" ,
2026-04-01 07:22:44 -07:00
p . trigger_rule ( "tool-call" ,
p . literal ( TOOL_CALL_START ) +
2026-06-05 21:31:56 +02:00
p . python_style_tool_calls ( inputs . tools , inputs . parallel_tool_calls , /* allow_json_literals = */ true ) +
2026-03-09 01:11:22 +01:00
p . literal ( TOOL_CALL_END )
)
);
auto content = p . content ( p . until ( TOOL_CALL_START ));
2026-03-23 22:21:47 -05:00
return generation_prompt + reasoning + content + tool_calls + end ;
2026-03-09 01:11:22 +01:00
});
data . parser = parser . save ();
if ( include_grammar ) {
2026-06-10 14:41:41 +02:00
data . grammar_lazy = ! ( has_response_format || ( has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ));
2026-03-09 01:11:22 +01:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
});
2026-06-10 14:41:41 +02:00
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
2026-03-09 01:11:22 +01:00
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , TOOL_CALL_START }
};
}
return data ;
}
2026-03-12 03:22:25 +03:00
static common_chat_params common_chat_params_init_gigachat_v3 (
const common_chat_template & tmpl ,
2026-03-19 16:58:21 +01:00
const autoparser :: generation_params & inputs ) {
2026-03-12 03:22:25 +03:00
common_chat_params data ;
2026-04-03 09:07:59 +03:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
2026-05-17 07:36:05 -04:00
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
2026-03-12 03:22:25 +03:00
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = false ;
data . preserved_tokens = {
"<|message_sep|> \n\n " ,
"<|role_sep|> \n " ,
};
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = "assistant<|role_sep|> \n " + msg . render_content ();
data . prompt += data . generation_prompt ;
}
2026-03-12 03:22:25 +03:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto include_grammar = has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ;
2026-03-19 16:58:21 +01:00
const auto * tool_call_start_prefix = "<|message_sep|> \n\n function call<|role_sep|> \n " ;
2026-03-12 03:22:25 +03:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-03-19 16:58:21 +01:00
auto ret = p . eps ();
2026-03-12 03:22:25 +03:00
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
// Build a choice of all available tools
auto tool_choice = p . choice ();
for ( const auto & tool : inputs . tools ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
const auto & schema = function . at ( "parameters" );
auto tool_name = p . json_member ( "name" , " \" " + p . tool_name ( p . literal ( name )) + " \" " );
auto tool_args = p . json_member ( "arguments" , p . tool_args ( p . schema ( p . json (), "tool-" + name + "-schema" , schema )));
auto tool_open = p . tool_open ( p . literal ( "{" ) << tool_name );
tool_choice |= p . rule ( "tool-" + name , tool_open << "," << tool_args << "}" );
}
// Define the tool call structure
auto min_calls = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0 ;
auto max_calls = 1 ; // parallel toolcalls are not supported
auto tool_call = p . rule ( "tool-call" , p . literal ( tool_call_start_prefix ) + tool_choice );
auto tool_calls = p . trigger_rule ( "tool-call-root" , p . repeat ( tool_call , /* min = */ min_calls , /* max = */ max_calls ));
2026-03-19 16:58:21 +01:00
ret = p . content ( p . until ( "<|message_sep|> \n\n " )) << tool_calls ;
} else {
// Content only parser
include_grammar = false ;
ret = p . content ( p . rest ());
2026-03-12 03:22:25 +03:00
}
2026-05-17 07:36:05 -04:00
return p . literal ( "assistant<|role_sep|> \n " ) + ret ;
2026-03-12 03:22:25 +03:00
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
});
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , tool_call_start_prefix }
};
}
return data ;
}
2026-07-22 12:54:40 +02:00
// The DeepSeek V4 reference implementation renders consecutive tool results into a single
// user block, ordered by the tool call order of the preceding assistant message (matched
// by tool call id) rather than by the order they appear in the conversation.
static json deepseek_v4_sort_tool_results ( const json & messages ) {
json adjusted = messages ;
std :: map < std :: string , size_t > call_order ;
for ( size_t i = 0 ; i < adjusted . size ();) {
const auto & msg = adjusted [ i ];
const auto role = msg . value ( "role" , "" );
if ( role == "assistant" && msg . contains ( "tool_calls" ) &&
msg . at ( "tool_calls" ). is_array () && ! msg . at ( "tool_calls" ). empty ()) {
call_order . clear ();
const auto & tool_calls = msg . at ( "tool_calls" );
for ( size_t idx = 0 ; idx < tool_calls . size (); idx ++ ) {
auto id = tool_calls [ idx ]. value ( "id" , "" );
if ( ! id . empty ()) {
call_order [ id ] = idx ;
}
}
i ++ ;
continue ;
}
if ( role != "user" && role != "tool" ) {
i ++ ;
continue ;
}
// collect a maximal run of user/tool messages - they render into one user block
std :: vector < size_t > tool_positions ;
size_t run_end = i ;
for (; run_end < adjusted . size (); run_end ++ ) {
const auto r = adjusted [ run_end ]. value ( "role" , "" );
if ( r == "tool" ) {
tool_positions . push_back ( run_end );
} else if ( r != "user" ) {
break ;
}
}
if ( tool_positions . size () > 1 && ! call_order . empty ()) {
std :: vector < json > results ;
results . reserve ( tool_positions . size ());
for ( auto pos : tool_positions ) {
results . push_back ( adjusted [ pos ]);
}
std :: stable_sort ( results . begin (), results . end (), [ & ]( const json & a , const json & b ) {
const auto order = [ & ]( const json & m ) {
auto it = call_order . find ( m . value ( "tool_call_id" , "" ));
return it == call_order . end () ? ( size_t ) 0 : it -> second ;
};
return order ( a ) < order ( b );
});
for ( size_t k = 0 ; k < tool_positions . size (); k ++ ) {
adjusted [ tool_positions [ k ]] = std :: move ( results [ k ]);
}
}
i = run_end ;
}
return adjusted ;
}
2026-04-13 22:23:53 +02:00
static common_chat_params common_chat_params_init_deepseek_v3_2 ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
2026-07-22 12:54:40 +02:00
// V4 uses the same DSML markup as V3.2, but names the tool call block "tool_calls"
// instead of "function_calls", renders tool results in tool call order and its
// non-thinking generation prompt ends with a bare </think> instead of an empty
// <think></think> pair.
const bool is_v4 = tmpl . source (). find ( "function_calls" ) == std :: string :: npos ;
std :: optional < json > adjusted_messages ;
if ( is_v4 ) {
adjusted_messages = deepseek_v4_sort_tool_results ( inputs . messages );
}
2026-04-13 22:23:53 +02:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = ! inputs . json_schema . is_null () && inputs . json_schema . is_object ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
const std :: string DSML = "| DSML| " ;
const std :: string THINK_START = "<think>" ;
const std :: string THINK_END = "</think>" ;
2026-07-22 12:54:40 +02:00
const std :: string TC_BLOCK = is_v4 ? "tool_calls" : "function_calls" ;
const std :: string FC_START = "<" + DSML + TC_BLOCK + ">" ;
const std :: string FC_END = "</" + DSML + TC_BLOCK + ">" ;
2026-04-13 22:23:53 +02:00
const std :: string INVOKE_START = "<" + DSML + "invoke" ;
const std :: string INVOKE_END = "</" + DSML + "invoke>" ;
const std :: string PARAM_START = "<" + DSML + "parameter" ;
const std :: string PARAM_END = "</" + DSML + "parameter>" ;
2026-05-17 07:36:05 -04:00
const std :: string GEN_PROMPT = "<| Assistant| >" ;
2026-08-01 07:13:07 +02:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs , adjusted_messages );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs , adjusted_messages );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
data . thinking_start_tag = THINK_START ;
data . thinking_end_tags = { THINK_END , FC_START };
data . preserved_tokens = {
DSML ,
THINK_START ,
THINK_END ,
};
2026-05-17 07:36:05 -04:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PROMPT + THINK_START + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += THINK_END + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
2026-04-13 22:23:53 +02:00
2026-08-01 07:13:07 +02:00
bool require_tools = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ;
bool has_tool_calls = has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ;
2026-04-13 22:23:53 +02:00
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
2026-05-17 07:36:05 -04:00
auto generation_prompt = p . literal ( GEN_PROMPT );
2026-08-01 07:13:07 +02:00
auto end = p . end ();
// build tool call section first since we might need it in reasoning
auto tool_choice = p . choice ();
if ( has_tool_calls ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
auto params = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
const auto & props = params . contains ( "properties" ) ? params . at ( "properties" ) : json :: object ();
std :: set < std :: string > required ;
if ( params . contains ( "required" )) {
params . at ( "required" ). get_to ( required );
}
auto schema_info = common_schema_info ();
schema_info . resolve_refs ( params );
std :: vector < common_peg_parser > required_parsers ;
std :: vector < common_peg_parser > optional_parsers ;
for ( const auto & [ param_name , param_schema ] : props . items ()) {
bool is_required = required . find ( param_name ) != required . end ();
bool is_string = schema_info . resolves_to_string ( param_schema );
auto arg = p . tool_arg (
p . tool_arg_open ( p . literal ( PARAM_START + " name= \" " ) + p . tool_arg_name ( p . literal ( param_name )) +
p . literal ( " \" string= \" " + std :: string ( is_string ? "true" : "false" ) + " \" >" )) +
( is_string ?
p . tool_arg_string_value ( p . until ( PARAM_END )) :
p . tool_arg_json_value ( p . schema ( p . json (), "tool-" + name + "-arg-" + param_name + "-schema" ,
param_schema , false ))) +
p . tool_arg_close ( p . literal ( PARAM_END )));
auto named_arg = p . rule ( "tool-" + name + "-arg-" + param_name , arg );
if ( is_required ) {
required_parsers . push_back ( named_arg );
} else {
optional_parsers . push_back ( named_arg );
}
}
common_peg_parser args_seq = p . eps ();
for ( size_t i = 0 ; i < required_parsers . size (); i ++ ) {
if ( i > 0 ) {
args_seq = args_seq + p . space ();
}
args_seq = args_seq + required_parsers [ i ];
}
if ( ! optional_parsers . empty ()) {
common_peg_parser any_opt = p . choice ();
for ( const auto & opt : optional_parsers ) {
any_opt |= opt ;
}
args_seq = args_seq + p . repeat ( p . space () + any_opt , 0 , - 1 );
}
common_peg_parser invoke_body = args_seq ;
auto func_parser = p . tool ( p . tool_open ( p . literal ( INVOKE_START + " name= \" " ) +
p . tool_name ( p . literal ( name )) + p . literal ( " \" > \n " )) +
invoke_body + p . space () + p . tool_close ( p . literal ( INVOKE_END )));
tool_choice |= p . rule ( "tool-" + name , func_parser );
});
}
common_peg_parser tool_calls = p . eps ();
if ( inputs . parallel_tool_calls ) {
tool_calls = p . trigger_rule ( "tool-call" ,
p . literal ( FC_START ) + p . space () + tool_choice +
p . zero_or_more ( p . space () + tool_choice ) + p . space () + p . literal ( FC_END ));
} else {
tool_calls = p . trigger_rule ( "tool-call" ,
p . literal ( FC_START ) + p . space () + tool_choice + p . space () + p . literal ( FC_END ));
}
2026-04-13 22:23:53 +02:00
auto reasoning = p . eps ();
2026-08-01 07:13:07 +02:00
auto reasoning_with_tc = p . eps ();
auto obligatory_tool_calls = tool_calls ;
bool allow_reasoning_with_tc = false ;
if ( ! require_tools ) {
tool_calls = p . optional ( tool_calls );
}
2026-04-13 22:23:53 +02:00
if ( extract_reasoning && inputs . enable_thinking ) {
reasoning = p . optional ( THINK_START + p . reasoning ( p . until ( THINK_END )) + THINK_END );
2026-08-01 07:13:07 +02:00
reasoning_with_tc = THINK_START + p . reasoning ( p . until_one_of ({ FC_START , THINK_END })) + obligatory_tool_calls ;
allow_reasoning_with_tc = true ;
2026-04-13 22:23:53 +02:00
} else if ( extract_reasoning ) {
// Thinking disabled but reasoning extraction requested: the generation prompt
2026-07-22 12:54:40 +02:00
// contains an empty <think></think> pair (V3.2) or a bare </think> (V4) that
// must still be consumed.
reasoning = is_v4
? p . optional ( p . literal ( THINK_END ))
: p . optional ( p . literal ( THINK_START ) + p . until ( THINK_END ) + p . literal ( THINK_END ));
2026-04-13 22:23:53 +02:00
}
if ( has_response_format ) {
auto response_format = p . rule ( "response-format" ,
p . literal ( "```json" ) + p . space () +
p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema )) +
p . space () + p . literal ( "```" ));
return generation_prompt + reasoning + response_format + end ;
}
2026-08-01 07:13:07 +02:00
if ( ! has_tool_calls ) {
2026-04-13 22:23:53 +02:00
return generation_prompt + reasoning + p . content ( p . rest ()) + end ;
}
2026-08-01 07:13:07 +02:00
auto content_before_tools = p . negate ( p . literal ( THINK_START )) + p . content ( p . until ( FC_START ));
return allow_reasoning_with_tc ? generation_prompt + ( reasoning_with_tc | ( reasoning + content_before_tools + tool_calls )) + end :
generation_prompt + reasoning + content_before_tools + tool_calls + end ;
2026-04-13 22:23:53 +02:00
});
data . parser = parser . save ();
if ( include_grammar ) {
2026-08-01 07:13:07 +02:00
data . grammar_lazy = has_tools && ! require_tools ;
2026-04-13 22:23:53 +02:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
builder . resolve_refs ( schema );
});
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , FC_START },
};
}
return data ;
}
2026-06-14 20:17:40 +02:00
// Cohere2 MoE (a.k.a. "North Code") parser.
//
// The assistant turn is fully marker-wrapped:
// <|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>
// <|START_THINKING|>{reasoning}<|END_THINKING|>
// then EITHER content: <|START_TEXT|>{content}<|END_TEXT|>
// OR tool calls: <|START_ACTION|>[
// {"tool_call_id": "0", "tool_name": "f", "parameters": {...}}, ...
// ]<|END_ACTION|>
// <|END_OF_TURN_TOKEN|>
//
// The generation prompt forces a leading <|START_THINKING|> (when reasoning is enabled, which is
// the template default), so the model's output continues from *inside* the thinking block. The
// parser literal therefore only covers the stable <|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|> prefix
// and the reasoning rule consumes the <|START_THINKING|> ... <|END_THINKING|> markers itself,
// regardless of whether they came from the generation prompt or the generated text.
static common_chat_params common_chat_params_init_cohere2moe ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
const std :: string TURN_START = "<|START_OF_TURN_TOKEN|>" ;
const std :: string TURN_END = "<|END_OF_TURN_TOKEN|>" ;
const std :: string CHATBOT = "<|CHATBOT_TOKEN|>" ;
const std :: string USER = "<|USER_TOKEN|>" ;
const std :: string SYSTEM = "<|SYSTEM_TOKEN|>" ;
const std :: string THINK_START = "<|START_THINKING|>" ;
const std :: string THINK_END = "<|END_THINKING|>" ;
const std :: string TEXT_START = "<|START_TEXT|>" ;
const std :: string TEXT_END = "<|END_TEXT|>" ;
const std :: string ACTION_START = "<|START_ACTION|>" ;
const std :: string ACTION_END = "<|END_ACTION|>" ;
const std :: string RESULT_START = "<|START_TOOL_RESULT|>" ;
const std :: string RESULT_END = "<|END_TOOL_RESULT|>" ;
// Stable prefix of the generation prompt that precedes the (forced) <|START_THINKING|> marker.
const std :: string GEN_PREFIX = TURN_START + CHATBOT ;
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
data . thinking_start_tag = THINK_START ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { THINK_END };
2026-06-14 20:17:40 +02:00
data . preserved_tokens = {
TURN_START , TURN_END , CHATBOT , USER , SYSTEM ,
THINK_START , THINK_END ,
TEXT_START , TEXT_END ,
ACTION_START , ACTION_END ,
RESULT_START , RESULT_END ,
};
2026-06-23 00:27:28 -05:00
// Declare per-role message delimiters. Tool results are rendered with the
2026-06-14 20:17:40 +02:00
// system token followed by <|START_TOOL_RESULT|>, so the "tool" delimiter must be listed before
// the plain "system" one (it is a strict superset, and the role split tries delimiters in order).
2026-06-23 00:27:28 -05:00
data . message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT , GEN_PREFIX },
{ COMMON_CHAT_ROLE_USER , TURN_START + USER },
{ COMMON_CHAT_ROLE_TOOL , TURN_START + SYSTEM + RESULT_START },
{ COMMON_CHAT_ROLE_SYSTEM , TURN_START + SYSTEM },
};
2026-06-14 20:17:40 +02:00
2026-07-24 03:54:47 -07:00
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = inputs . json_schema . is_object () && ! inputs . json_schema . empty ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
2026-06-14 20:17:40 +02:00
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PREFIX + THINK_START + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += THINK_END + TEXT_START + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
auto generation_prompt = p . literal ( GEN_PREFIX );
auto end = p . end ();
// The thinking block is always present (the generation prompt forces <|START_THINKING|>).
// When extracting reasoning, capture its body; otherwise keep the whole block (markers
// included) inline as content, matching reasoning_format=NONE conventions.
common_peg_parser reasoning = p . eps ();
if ( extract_reasoning ) {
reasoning = p . optional ( p . literal ( THINK_START ) +
p . reasoning ( p . until_one_of ({ THINK_END , TEXT_START , ACTION_START })) +
p . optional ( p . literal ( THINK_END )));
} else {
reasoning = p . optional ( p . content ( p . literal ( THINK_START ) +
p . until_one_of ({ THINK_END , TEXT_START , ACTION_START }) +
p . optional ( p . literal ( THINK_END ))));
}
2026-07-24 03:54:47 -07:00
auto text_content = has_response_format
? p . literal ( TEXT_START ) +
p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema )) +
p . optional ( p . literal ( TEXT_END ))
: p . literal ( TEXT_START ) + p . content ( p . until ( TEXT_END )) + p . optional ( p . literal ( TEXT_END ));
2026-06-14 20:17:40 +02:00
if ( ! has_tools || inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) {
return generation_prompt + reasoning + text_content + p . optional ( p . literal ( TURN_END )) + end ;
}
auto require_tools = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ;
// <|START_ACTION|>[ {"tool_call_id": "0", "tool_name": "f", "parameters": {...}}, ... ]<|END_ACTION|>
auto tool_calls = p . standard_json_tools ( ACTION_START , ACTION_END , inputs . tools , inputs . parallel_tool_calls ,
/* force_tool_calls = */ true ,
/* name_key = */ "tool_name" ,
/* args_key = */ "parameters" ,
/* array_wrapped = */ true ,
/* function_is_key = */ false ,
/* call_id_key = */ "" ,
/* gen_call_id_key = */ "tool_call_id" ,
/* parameters_order = */ { "tool_call_id" , "tool_name" , "parameters" });
// Content and tool calls are mutually exclusive in this format.
common_peg_parser body = require_tools ? tool_calls : p . choice ({ tool_calls , text_content });
return generation_prompt + reasoning + body + p . optional ( p . literal ( TURN_END )) + end ;
});
data . parser = parser . save ();
if ( include_grammar ) {
2026-07-24 03:54:47 -07:00
data . grammar_lazy = ! has_response_format && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO ;
2026-06-14 20:17:40 +02:00
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . at ( "parameters" );
builder . resolve_refs ( schema );
});
2026-07-24 03:54:47 -07:00
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
2026-06-14 20:17:40 +02:00
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , ACTION_START }
};
}
return data ;
}
2026-07-28 04:27:20 -05:00
static common_chat_params common_chat_params_init_minimax_m3 ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 ;
data . supports_thinking = true ;
data . thinking_start_tag = "<mm:think>" ;
data . thinking_end_tags = { "</mm:think>" };
// M3 prefixes every tool tag with the namespace token "]<]minimax[>[";
// params use the parameter name as the tag (<file_path>...</file_path>).
const std :: string NS = "]<]minimax[>[" ;
const std :: string THINK_START = "<mm:think>" ;
const std :: string THINK_END = "</mm:think>" ;
const std :: string FC_START = NS + "<tool_call>" ;
const std :: string FC_END = NS + "</tool_call>" ;
const std :: string INVOKE_END = NS + "</invoke>" ;
data . preserved_tokens = {
NS ,
"<tool_call>" ,
"</tool_call>" ,
THINK_START ,
THINK_END ,
};
data . message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT , "]~b]ai" },
{ COMMON_CHAT_ROLE_USER , "]~b]user" },
{ COMMON_CHAT_ROLE_TOOL , "]~b]tool" },
{ COMMON_CHAT_ROLE_SYSTEM , "]~b]developer" },
{ COMMON_CHAT_ROLE_SYSTEM , "]~b]system" },
};
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = ! inputs . json_schema . is_null () && inputs . json_schema . is_object ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
const std :: string GEN_PROMPT = data . generation_prompt ;
using mm3 = common_chat_peg_minimax_m3_mapper ;
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = GEN_PROMPT + THINK_START + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += THINK_END + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
auto generation_prompt = p . prefix ( GEN_PROMPT , THINK_START );
auto end = p . end ();
auto reasoning = p . eps ();
if ( extract_reasoning ) {
auto block = inputs . enable_thinking
? p . literal ( THINK_START ) + p . space () +
p . ac ( p . reasoning ( p . until ( THINK_END )) + p . literal ( THINK_END ), THINK_END )
: p . literal ( THINK_START ) + p . ac ( p . until ( THINK_END ) + p . literal ( THINK_END ), THINK_END );
// A turn without reasoning is prefixed with a bare </mm:think>, written either by the
// generation prompt (thinking_mode = "disabled") or by the model itself.
reasoning = p . optional ( p . choice ({ block , p . literal ( THINK_END ) }));
}
if ( has_response_format ) {
auto response_format = p . rule ( "response-format" ,
p . literal ( "```json" ) + p . space () +
p . content ( p . schema ( p . json (), "response-format-schema" , inputs . json_schema )) +
p . space () + p . literal ( "```" ));
return generation_prompt + reasoning + response_format + end ;
}
if ( ! has_tools || inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE ) {
return generation_prompt + reasoning + p . content ( p . rest ()) + end ;
}
auto alternatives_of = []( const json & schema ) -> std :: optional < json > {
for ( const auto * keyword : { "oneOf" , "anyOf" }) {
if ( schema . contains ( keyword ) && schema . at ( keyword ). is_array () && ! schema . at ( keyword ). empty ()) {
return schema . at ( keyword );
}
}
return std :: nullopt ;
};
auto tool_choice = p . choice ();
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
std :: string name = function . at ( "name" );
auto params = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
auto schema_info = common_schema_info ();
schema_info . resolve_refs ( params );
// The template expands argument values recursively in XML (see the to_xml() macro)
std :: function < common_peg_parser ( const json & , const std :: string & , const std :: string & ) > value_of ;
std :: function < common_peg_parser ( const json & , const std :: string & ) > members_of ;
auto element_of = [ & ]( const std :: string & tag , const json & schema , const std :: string & rule_name ) {
const std :: string close = NS + "</" + tag + ">" ;
return p . rule ( rule_name ,
p . tool_arg (
p . tool_arg_open (
p . literal ( NS + "<" ) +
p . tool_arg_name ( p . literal ( tag )) +
p . literal ( ">" )) +
value_of ( schema , rule_name , close )));
};
value_of = [ & ]( const json & schema ,
const std :: string & rule_name ,
const std :: string & close ) -> common_peg_parser {
auto close_tag = p . tool_arg_close ( p . literal ( close ));
// A string accepts anything, so a union with a string alternative is a string
if ( schema_info . resolves_to_string ( schema )) {
return p . ac ( p . tool_arg_string_value ( p . until ( close )) + close_tag , close );
}
if ( auto alternatives = alternatives_of ( schema )) {
std :: vector < common_peg_parser > choices ;
size_t index = 0 ;
for ( const auto & alternative : * alternatives ) {
const std :: string alt_name = rule_name + "-" + std :: to_string ( index ++ );
// There is a risk that this breaks streaming deltas, but that's a risk we
// assume to provide tool arg streaming.
choices . push_back ( value_of ( alternative , alt_name , close ));
}
return p . choice ( choices );
}
const std :: string type = schema . contains ( "type" ) && schema . at ( "type" ). is_string ()
? schema . at ( "type" ). get < std :: string > ()
: "" ;
if ( type == "object" && schema . contains ( "properties" )) {
return p . tag ( mm3 :: TOOL_ARG_OBJECT , members_of ( schema , rule_name )) + p . space () + close_tag ;
}
if ( type == "array" && schema . contains ( "items" )) {
const std :: string item_close = NS + "</item>" ;
auto item = p . rule ( rule_name + "-item" ,
p . tag ( mm3 :: TOOL_ARG_ITEM ,
p . literal ( NS + "<item>" ) +
value_of ( schema . at ( "items" ), rule_name + "-item" , item_close )));
return p . tag ( mm3 :: TOOL_ARG_ARRAY , p . repeat ( p . space () + item , 0 , - 1 )) + p . space () + close_tag ;
}
return p . tool_arg_json_value ( p . schema ( p . json (), rule_name + "-schema" , schema , false )) + close_tag ;
};
// Required properties in schema order, then any number of optional ones in any order.
members_of = [ & ]( const json & schema , const std :: string & rule_prefix ) -> common_peg_parser {
const auto & props = schema . at ( "properties" );
std :: set < std :: string > required ;
if ( schema . contains ( "required" )) {
schema . at ( "required" ). get_to ( required );
}
std :: vector < common_peg_parser > required_elements ;
std :: vector < common_peg_parser > optional_elements ;
for ( const auto & [ key , key_schema ] : props . items ()) {
auto element = element_of ( key , key_schema , rule_prefix + "-" + key );
if ( required . find ( key ) != required . end ()) {
required_elements . push_back ( element );
} else {
optional_elements . push_back ( element );
}
}
common_peg_parser members = p . eps ();
for ( size_t i = 0 ; i < required_elements . size (); i ++ ) {
if ( i > 0 ) {
members = members + p . space ();
}
members = members + required_elements [ i ];
}
if ( ! optional_elements . empty ()) {
common_peg_parser any_optional = p . choice ();
for ( const auto & element : optional_elements ) {
any_optional |= element ;
}
members = members + p . repeat ( p . space () + any_optional , 0 , - 1 );
}
return members ;
};
common_peg_parser invoke_body =
params . contains ( "properties" ) ? members_of ( params , "tool-" + name + "-arg" ) : p . eps ();
auto func_parser = p . tool (
p . tool_open ( p . literal ( NS + "<invoke name= \" " ) +
p . tool_name ( p . literal ( name )) + p . literal ( " \" >" )) +
p . space () + invoke_body + p . space () +
p . tool_close ( p . literal ( INVOKE_END )));
tool_choice |= p . rule ( "tool-" + name , func_parser );
});
auto require_tools = inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ;
common_peg_parser tool_calls = p . eps ();
if ( inputs . parallel_tool_calls ) {
tool_calls = p . trigger_rule ( "tool-call" ,
p . literal ( FC_START ) + p . space () + tool_choice +
p . zero_or_more ( p . space () + tool_choice ) + p . space () + p . literal ( FC_END ));
} else {
tool_calls = p . trigger_rule ( "tool-call" ,
p . literal ( FC_START ) + p . space () + tool_choice + p . space () + p . literal ( FC_END ));
}
if ( ! require_tools ) {
tool_calls = p . optional ( tool_calls );
}
auto content_before_tools = p . content ( p . until ( FC_START ));
return generation_prompt + reasoning + content_before_tools + tool_calls + end ;
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = ! ( has_response_format || ( has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ));
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
builder . resolve_refs ( schema );
});
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , FC_START },
};
}
return data ;
}
2026-01-16 11:22:06 +01:00
namespace workaround {
2026-03-09 14:25:11 +01:00
static void map_developer_role_to_system ( json & messages ) {
for ( auto & message : messages ) {
if ( message . contains ( "role" )) {
if ( message [ "role" ] == "developer" ) {
message [ "role" ] = "system" ;
}
}
}
}
2026-01-16 11:22:06 +01:00
// if first message is system and template does not support it, merge it with next message
static void system_message_not_supported ( json & messages ) {
if ( ! messages . empty () && messages . front (). at ( "role" ) == "system" ) {
if ( messages . size () > 1 ) {
LOG_DBG ( "Merging system prompt into next message \n " );
auto & first_msg = messages . front ();
auto & second_msg = messages [ 1 ];
second_msg [ "content" ] = first_msg . at ( "content" ). get < std :: string > ()
+ " \n " + second_msg . at ( "content" ). get < std :: string > ();
messages . erase ( messages . begin ());
} else {
LOG_WRN ( "Removing system prompt due to template not supporting system role \n " );
messages . erase ( messages . begin ());
}
}
}
2026-03-06 21:01:00 +01:00
static void requires_non_null_content ( json & messages ) {
GGML_ASSERT ( messages . is_array ());
for ( auto & message : messages ) {
if ( message . contains ( "tool_calls" ) && ! message . contains ( "content" )) {
message [ "content" ] = "" ;
}
}
}
2026-04-02 17:10:32 +02:00
// Gemma4 uses a custom tool_responses field instead of role:tool messages.
2026-04-04 13:39:00 -05:00
//
// This will transform a sequence of messages:
// assistant(tool_call+) -> tool+ -> assistant(content)
//
// Into a single assistant message containing a tool_responses field:
// assistant(content + tool_call + tool_responses)
//
// This is necessary for the Gemma4 chat template to properly format the prompt.
// See https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4
struct gemma4_model_turn_builder {
json & messages ;
size_t pos ;
json tool_calls = json :: array ();
json tool_responses = json :: array ();
json content ;
json reasoning_content ;
gemma4_model_turn_builder ( json & msgs , size_t pos ) : messages ( msgs ), pos ( pos ) {}
void collect () {
// Collect the first assistant message
auto & msg = messages [ pos ];
if ( msg . contains ( "reasoning_content" ) && msg . at ( "reasoning_content" ). is_string ()) {
// According to the prompt formatting guide, we need to preserve reasoning_content
// between function calls. The current chat templates do not support this, but we will do it anyway.
reasoning_content = msg . at ( "reasoning_content" );
}
for ( auto & tc : msg . at ( "tool_calls" )) {
tool_calls . push_back ( tc );
}
pos ++ ;
// Collect tool call results
while ( pos < messages . size () && messages [ pos ]. value ( "role" , "" ) == "tool" ) {
collect_result ( messages [ pos ]);
pos ++ ;
}
// Check if the next assistant message is the final message
if ( pos < messages . size () && messages [ pos ]. value ( "role" , "" ) == "assistant" ) {
auto & next = messages [ pos ];
if ( ! has_tool_calls ( next ) && has_content ( next )) {
content = next . at ( "content" );
pos ++ ;
}
}
}
void collect_result ( const json & curr ) {
json response ;
if ( curr . contains ( "content" )) {
const auto & content = curr . at ( "content" );
if ( content . is_string ()) {
// Try to parse the content as JSON; fall back to raw string
try {
response = json :: parse ( content . get < std :: string > ());
} catch (...) {
response = content ;
}
} else {
response = content ;
}
}
std :: string name ;
// Match name with corresponding tool call
size_t idx = tool_responses . size ();
if ( idx < tool_calls . size ()) {
auto & tc = tool_calls [ idx ];
if ( tc . contains ( "function" )) {
name = tc . at ( "function" ). value ( "name" , "" );
}
}
// Fallback to the tool call id
if ( name . empty ()) {
name = curr . value ( "tool_call_id" , "" );
}
tool_responses . push_back ({{ "name" , name }, { "response" , response }});
}
json build () {
collect ();
json msg = {
{ "role" , "assistant" },
{ "tool_calls" , tool_calls },
};
if ( ! tool_responses . empty ()) {
msg [ "tool_responses" ] = tool_responses ;
}
if ( ! content . is_null ()) {
msg [ "content" ] = content ;
}
if ( ! reasoning_content . is_null ()) {
msg [ "reasoning_content" ] = reasoning_content ;
}
return msg ;
}
static bool has_content ( const json & msg ) {
if ( ! msg . contains ( "content" ) || msg . at ( "content" ). is_null ()) {
return false ;
}
const auto & content = msg . at ( "content" );
if ( content . is_string () && ! content . get < std :: string > (). empty ()) {
return true ;
}
if ( content . is_array () && ! content . empty ()) {
return true ;
}
return false ;
}
static bool has_tool_calls ( const json & msg ) {
return msg . contains ( "tool_calls" ) && msg . at ( "tool_calls" ). is_array () && ! msg . at ( "tool_calls" ). empty ();
}
};
2026-04-02 17:10:32 +02:00
static void convert_tool_responses_gemma4 ( json & messages ) {
json result = json :: array ();
size_t i = 0 ;
2026-04-04 13:39:00 -05:00
2026-04-02 17:10:32 +02:00
while ( i < messages . size ()) {
2026-04-04 13:39:00 -05:00
auto & msg = messages [ i ];
if ( msg . value ( "role" , "" ) != "assistant" || ! msg . contains ( "tool_calls" ) ||
! msg . at ( "tool_calls" ). is_array () || msg . at ( "tool_calls" ). empty ()) {
result . push_back ( msg );
2026-04-02 17:10:32 +02:00
i ++ ;
2026-04-04 13:39:00 -05:00
continue ;
2026-04-02 17:10:32 +02:00
}
2026-04-04 13:39:00 -05:00
gemma4_model_turn_builder builder ( messages , i );
result . push_back ( builder . build ());
i = builder . pos ;
2026-04-02 17:10:32 +02:00
}
2026-04-04 13:39:00 -05:00
2026-04-02 17:10:32 +02:00
messages = result ;
}
2026-01-16 11:22:06 +01:00
static void func_args_not_string ( json & messages ) {
GGML_ASSERT ( messages . is_array ());
for ( auto & message : messages ) {
if ( message . contains ( "tool_calls" )) {
for ( auto & tool_call : message [ "tool_calls" ]) {
if ( tool_call . contains ( "function" ) && tool_call [ "function" ]. contains ( "arguments" )) {
auto & args = tool_call [ "function" ][ "arguments" ];
if ( args . is_string ()) {
try {
args = json :: parse ( args . get < std :: string > ());
} catch ( const std :: exception & e ) {
throw std :: runtime_error ( "Failed to parse tool call arguments as JSON: " + std :: string ( e . what ()));
}
}
}
}
}
}
}
2026-07-03 23:12:11 +02:00
// Trim leading/trailing whitespace from message contents before rendering. This
// has to run on the messages (not on the rendered JSON) because templates with
// string-only content caps concatenate typed content parts into a single string
// during rendering, after which the per-part whitespace can no longer be reached.
// Both the plain string content and the text of typed content parts are trimmed.
static void trim_all_content ( std :: vector < common_chat_msg > & messages ) {
for ( auto & message : messages ) {
message . content = trim_whitespace ( message . content );
message . reasoning_content = trim_whitespace ( message . reasoning_content );
for ( auto & part : message . content_parts ) {
if ( part . type == "text" ) {
part . text = trim_whitespace ( part . text );
}
}
}
}
2026-01-16 11:22:06 +01:00
}
2026-06-28 09:53:32 -05:00
// MiniCPM5 format:
// - Reasoning: <think>{reasoning}</think> (optional)
// - Tool calls: <function name="foo"><param name="bar">value</param></function>
static common_chat_params common_chat_params_init_minicpm5 ( const common_chat_template & tmpl ,
const autoparser :: generation_params & inputs ) {
common_chat_params data ;
data . prompt = common_chat_template_direct_apply_impl ( tmpl , inputs );
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , inputs );
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
data . supports_thinking = true ;
data . preserved_tokens = {
"<function" ,
"<param" ,
"</function>" ,
"</param>" ,
"<think>" ,
"</think>" ,
};
data . thinking_start_tag = "<think>" ;
2026-07-25 04:58:09 -05:00
data . thinking_end_tags = { "</think>" };
2026-06-28 09:53:32 -05:00
data . message_delimiters = {
{ COMMON_CHAT_ROLE_ASSISTANT , "<|im_start|>assistant" },
{ COMMON_CHAT_ROLE_TOOL , "<|im_start|>user \n <tool_response>" },
{ COMMON_CHAT_ROLE_USER , "<|im_start|>user" },
{ COMMON_CHAT_ROLE_SYSTEM , "<|im_start|>system" },
};
auto has_tools = inputs . tools . is_array () && ! inputs . tools . empty ();
auto has_response_format = inputs . json_schema . is_object () && ! inputs . json_schema . empty ();
auto extract_reasoning = inputs . reasoning_format != COMMON_REASONING_FORMAT_NONE ;
auto include_grammar = has_response_format || ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE );
if ( inputs . has_continuation ()) {
const auto & msg = inputs . continue_msg ;
data . generation_prompt = "<|im_start|>assistant \n <think> \n " + msg . reasoning_content ;
if ( inputs . continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT ) {
data . generation_prompt += " \n </think> \n\n " + msg . render_content ();
}
data . prompt += data . generation_prompt ;
}
auto parser = build_chat_peg_parser ([ & ]( common_chat_peg_builder & p ) {
auto generation_prompt = p . literal ( "<|im_start|>assistant \n " );
auto reasoning = p . eps ();
if ( extract_reasoning ) {
reasoning = ( "<think>" << p . reasoning ( p . until ( "</think>" )) << "</think>" ) + p . space ();
}
// Response format parser
if ( has_response_format ) {
return generation_prompt + reasoning + p . content ( p . schema ( p . json (), "response-format" , inputs . json_schema ));
}
if ( has_tools && inputs . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE ) {
// CDATA lets a value carry characters that would otherwise close the tag (e.g.
// </param>); capture the inner text only, excluding the CDATA markers.
auto string_value = p . choice ({
p . literal ( "<![CDATA[" ) + p . ac ( p . tool_arg_string_value ( p . until ( "]]>" )) + p . literal ( "]]>" ), "]]>" ) + p . tool_arg_close ( p . literal ( "</param>" )),
p . negate ( p . literal ( "< {
const auto & function = tool . at ( "function" );
const std :: string name = function . at ( "name" );
auto params = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
auto args = p . eps ();
if ( params . contains ( "properties" ) && params . at ( "properties" ). is_object () && ! params . at ( "properties" ). empty ()) {
auto schema_info = common_schema_info ();
schema_info . resolve_refs ( params );
auto arg_choice = p . choice ();
for ( const auto & [ prop_name , prop_schema ] : params . at ( "properties" ). items ()) {
auto value_parser = p . eps ();
if ( schema_info . resolves_to_string ( prop_schema )) {
value_parser = string_value ;
} else {
value_parser = p . tool_arg_json_value (
p . schema ( p . json (), "tool-" + name + "-arg-" + prop_name + "-schema" , prop_schema , false )
) + p . tool_arg_close ( p . literal ( "</param>" ));
}
auto arg_rule = p . tool_arg (
p . tool_arg_open ( p . literal ( "<param name= \" " ) + p . tool_arg_name ( p . literal ( prop_name )) + p . literal ( " \" >" )) +
value_parser
);
arg_choice |= arg_rule ;
}
args = p . zero_or_more ( arg_choice + p . space ());
}
auto tool_parser = p . tool (
p . tool_open ( p . literal ( "<function name= \" " ) + p . tool_name ( p . literal ( name )) + p . literal ( " \" >" ))
<< p . tool_args ( args )
<< p . tool_close ( p . literal ( "</function>" )));
tool_choice |= p . rule ( "tool-" + name , tool_parser );
});
auto max_calls = inputs . parallel_tool_calls ? - 1 : 1 ;
auto tool_calls = p . trigger_rule ( "tool-call" , p . repeat ( tool_choice + p . space (), 1 , max_calls ));
auto content = p . content ( p . until ( "<function" ));
return generation_prompt + reasoning + content + tool_calls + p . end ();
}
return generation_prompt + reasoning + p . content ( p . rest ()) + p . end ();
});
data . parser = parser . save ();
if ( include_grammar ) {
data . grammar_lazy = ! ( has_response_format || ( has_tools && inputs . tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ));
data . grammar = build_grammar ([ & ]( const common_grammar_builder & builder ) {
foreach_function ( inputs . tools , [ & ]( const json & tool ) {
const auto & function = tool . at ( "function" );
auto schema = function . contains ( "parameters" ) ? function . at ( "parameters" ) : json :: object ();
builder . resolve_refs ( schema );
});
if ( has_response_format ) {
auto schema = inputs . json_schema ;
builder . resolve_refs ( schema );
}
parser . build_grammar ( builder , data . grammar_lazy );
});
data . grammar_triggers = {
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD , "<function" },
};
}
return data ;
}
2026-03-06 21:01:00 +01:00
static json common_chat_extra_context () {
json ctx = json :: object ();
std :: chrono :: system_clock :: time_point now = std :: chrono :: system_clock :: now ();
std :: string datetime_str = format_time ( now , "%b %d %Y" );
std :: string date_str = format_time ( now , "%d %b %Y" );
ctx [ "datetime" ] = datetime_str ;
ctx [ "date_string" ] = date_str ;
return ctx ;
2026-01-16 11:22:06 +01:00
}
2026-04-03 17:51:52 +02:00
std :: optional < common_chat_params > common_chat_try_specialized_template (
2026-03-19 16:58:21 +01:00
const common_chat_template & tmpl ,
const std :: string & src ,
2026-04-04 13:39:00 -05:00
autoparser :: generation_params & params ) {
2026-03-06 21:01:00 +01:00
// Ministral/Mistral Large 3 - uses special reasoning structure fixes, can't use autoparser
// Note: Mistral Small 3.2 uses [CALL_ID] which Ministral doesn't have, so we can distinguish them
if ( src . find ( "[SYSTEM_PROMPT]" ) != std :: string :: npos && src . find ( "[TOOL_CALLS]" ) != std :: string :: npos &&
src . find ( "[ARGS]" ) != std :: string :: npos && src . find ( "[CALL_ID]" ) == std :: string :: npos ) {
LOG_DBG ( "Using specialized template: Ministral/Magistral Large 3 \n " );
2025-12-09 17:31:04 -06:00
return common_chat_params_init_ministral_3 ( tmpl , params );
}
2026-03-06 21:01:00 +01:00
// GPT-OSS - has unique channel-based structure that needs dedicated handler
if ( src . find ( "<|channel|>" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: GPT-OSS \n " );
return common_chat_params_init_gpt_oss ( tmpl , params );
2025-10-03 20:51:48 +02:00
}
2026-03-06 21:01:00 +01:00
// Functionary v3.2 - uses recipient-based format with >>>recipient\n{content}
// Detection: template has ">>>all" for content and ">>>" prefix for tool calls
if ( src . find ( ">>>all" ) != std :: string :: npos && src . find ( ">>>${recipient}" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: Functionary v3.2 \n " );
return common_chat_params_init_functionary_v3_2 ( tmpl , params );
2026-01-02 02:01:43 +09:00
}
2026-03-06 21:01:00 +01:00
// Kimi K2 Thinking - uses unique tool call ID format: functions.<name>:<index>
// Detection: template has "<|tool_calls_section_begin|>" and "functions." prefix in tool call IDs
if ( src . find ( "<|tool_calls_section_begin|>" ) != std :: string :: npos &&
src . find ( "<|tool_call_begin|>" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: Kimi K2 Thinking \n " );
return common_chat_params_init_kimi_k2 ( tmpl , params );
2026-01-23 12:03:42 +01:00
}
2026-06-14 20:17:40 +02:00
// Cohere2 MoE / North Code - marker-wrapped format with <|START_TEXT|> content and
// <|START_ACTION|> JSON tool calls. <|START_TEXT|> is unique to this template (the older
// Command-R templates use <|START_RESPONSE|>).
if ( src . find ( "<|START_TEXT|>" ) != std :: string :: npos &&
src . find ( "<|START_ACTION|>" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: Cohere2 MoE \n " );
return common_chat_params_init_cohere2moe ( tmpl , params );
}
2026-04-23 10:47:26 +02:00
if ( is_lfm2_template ( src )) {
2026-03-09 01:11:22 +01:00
LOG_DBG ( "Using specialized template: LFM2 \n " );
2026-06-05 21:31:56 +02:00
return common_chat_params_init_lfm2 ( tmpl , params , /* tool_list_tokens = */ true );
2026-03-09 01:11:22 +01:00
}
2026-04-01 07:22:44 -07:00
// LFM2.5 format detection: template uses plain "List of tools: [...]" with no special tokens
if ( src . find ( "List of tools: [" ) != std :: string :: npos &&
src . find ( "<|tool_list_start|>" ) == std :: string :: npos ) {
LOG_DBG ( "Using specialized template: LFM2.5 \n " );
2026-06-05 21:31:56 +02:00
return common_chat_params_init_lfm2 ( tmpl , params , /* tool_list_tokens = */ false );
2026-04-01 07:22:44 -07:00
}
2026-03-12 03:22:25 +03:00
// GigaChatV3 format detection
if ( src . find ( "<|role_sep|>" ) != std :: string :: npos &&
src . find ( "<|message_sep|>" ) != std :: string :: npos &&
2026-03-19 16:58:21 +01:00
src . find ( "<|function_call|>" ) == std :: string :: npos ) {
2026-03-12 03:22:25 +03:00
LOG_DBG ( "Using specialized template: GigaChatV3 \n " );
return common_chat_params_init_gigachat_v3 ( tmpl , params );
}
2026-07-28 04:27:20 -05:00
// MiniMax-M3: the namespace token "]<]minimax[>[" collides with the autoparser's
// markup delimiters, so detect the template and use a dedicated parser.
if ( src . find ( "]<]minimax[>[" ) != std :: string :: npos &&
src . find ( "<tool_call>" ) != std :: string :: npos &&
src . find ( "<invoke name=" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: MiniMax-M3 \n " );
return common_chat_params_init_minimax_m3 ( tmpl , params );
}
2026-07-22 12:54:40 +02:00
// DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls.
2026-04-13 22:23:53 +02:00
// The template source contains the token as a variable assignment, not as a literal in markup.
2026-07-22 12:54:40 +02:00
// V3.2 names the tool call block "function_calls", V4 names it "tool_calls".
2026-04-13 22:23:53 +02:00
if ( src . find ( "dsml_token" ) != std :: string :: npos &&
2026-07-22 12:54:40 +02:00
src . find ( "DSML" ) != std :: string :: npos &&
( src . find ( "function_calls" ) != std :: string :: npos ||
src . find ( "tool_calls" ) != std :: string :: npos )) {
LOG_DBG ( "Using specialized template: DeepSeek V3.2/V4 \n " );
2026-04-13 22:23:53 +02:00
return common_chat_params_init_deepseek_v3_2 ( tmpl , params );
}
2026-04-04 13:39:00 -05:00
// Gemma4 format detection
if ( src . find ( "'<|tool_call>call:'" ) != std :: string :: npos ) {
2026-04-10 16:12:53 -05:00
if ( src . find ( "{#- OpenAI Chat Completions:" ) == std :: string :: npos ) {
// apply workarounds if using the older gemma4 templates
LOG_WRN ( "%s: detected an outdated gemma4 chat template, applying compatibility workarounds. "
"Consider updating to the official template. \n " , __func__ );
workaround :: convert_tool_responses_gemma4 ( params . messages );
}
2026-04-04 13:39:00 -05:00
return common_chat_params_init_gemma4 ( tmpl , params );
}
2026-06-28 09:53:32 -05:00
// MiniCPM5 - XML tool calls with <function name="..."><param name="...">...</param></function>
if ( src . find ( "Tool usage guidelines:" ) != std :: string :: npos &&
src . find ( "<function name= \" " ) != std :: string :: npos &&
src . find ( "<param name= \" " ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: MiniCPM5 \n " );
return common_chat_params_init_minicpm5 ( tmpl , params );
}
2026-08-02 04:13:20 -05:00
// Qwen3-Coder XML tool calls, also used by Nemotron Nano 3, Qwen3.5 and StepFun-3.5-Flash
if ( src . find ( "<tool_call>" ) != std :: string :: npos &&
src . find ( "<function=" ) != std :: string :: npos &&
src . find ( "<parameter=" ) != std :: string :: npos ) {
LOG_DBG ( "Using specialized template: Qwen3-Coder \n " );
return common_chat_params_init_qwen3_coder ( tmpl , params );
}
2026-03-19 16:58:21 +01:00
return std :: nullopt ;
}
static common_chat_params common_chat_templates_apply_jinja ( const struct common_chat_templates * tmpls ,
const struct common_chat_templates_inputs & inputs ) {
autoparser :: generation_params params ;
params . tools = common_chat_tools_to_json_oaicompat ( inputs . tools );
const auto & tmpl =
params . tools . is_array () && tmpls -> template_tool_use ? * tmpls -> template_tool_use : * tmpls -> template_default ;
2026-05-03 17:18:23 -05:00
const auto & src = tmpl . source ();
const auto & caps = tmpl . original_caps ();
2026-07-03 23:12:11 +02:00
std :: vector < common_chat_msg > trimmed_messages ;
const std :: vector < common_chat_msg > * messages_to_render = & inputs . messages ;
if ( src . find ( "You have access to the following functions in JSONSchema format" ) != std :: string :: npos ) {
// StepFun: trim message contents (including typed content parts) before rendering,
// otherwise leftover whitespace drives the model into reasoning loops (issue #24181)
trimmed_messages = inputs . messages ;
workaround :: trim_all_content ( trimmed_messages );
messages_to_render = & trimmed_messages ;
}
params . messages = render_message_to_json ( * messages_to_render , tmpl . original_caps ());
2026-05-03 17:18:23 -05:00
params . tool_choice = inputs . tool_choice ;
params . reasoning_format = inputs . reasoning_format ;
params . enable_thinking = inputs . enable_thinking ;
params . grammar = inputs . grammar ;
params . now = inputs . now ;
params . add_generation_prompt = inputs . add_generation_prompt ;
params . add_bos = tmpls -> add_bos ;
params . add_eos = tmpls -> add_eos ;
2026-03-19 16:58:21 +01:00
2026-05-17 07:36:05 -04:00
params . continue_final_message = inputs . continue_final_message ;
if ( params . continue_final_message != COMMON_CHAT_CONTINUATION_NONE ) {
params . add_generation_prompt = false ;
if ( ! inputs . messages . empty ()) {
// Render messages[:-1] and store continuation message separately
params . continue_msg = inputs . messages . back ();
params . messages . erase ( params . messages . size () - 1 );
}
if ( params . continue_final_message == COMMON_CHAT_CONTINUATION_AUTO && ! inputs . messages . empty ()) {
// Resolve based on message content
params . continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT ;
if ( ! params . continue_msg . reasoning_content . empty () &&
params . continue_msg . content . empty () &&
params . continue_msg . content_parts . empty ()) {
params . continue_final_message = COMMON_CHAT_CONTINUATION_REASONING ;
}
}
}
2026-03-19 16:58:21 +01:00
if ( src . find ( "<|channel|>" ) == std :: string :: npos ) {
// map developer to system for all models except for GPT-OSS
workaround :: map_developer_role_to_system ( params . messages );
}
if ( ! tmpl . original_caps (). supports_system_role ) {
workaround :: system_message_not_supported ( params . messages );
}
if ( tmpl . original_caps (). supports_tool_calls ) {
// some templates will require the content field in tool call messages
// to still be non-null, this puts an empty string everywhere where the
// content field is null
workaround :: requires_non_null_content ( params . messages );
}
if ( tmpl . original_caps (). supports_object_arguments ) {
workaround :: func_args_not_string ( params . messages );
}
params . extra_context = common_chat_extra_context ();
for ( auto el : inputs . chat_template_kwargs ) {
params . extra_context [ el . first ] = json :: parse ( el . second );
}
if ( ! inputs . json_schema . empty ()) {
params . json_schema = json :: parse ( inputs . json_schema );
}
params . parallel_tool_calls = inputs . parallel_tool_calls ;
if ( params . tools . is_array ()) {
if ( params . tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && ! params . grammar . empty ()) {
throw std :: runtime_error ( "Cannot specify grammar with tools" );
}
if ( caps . supports_tool_calls && ! caps . supports_tools ) {
LOG_WRN (
"Template supports tool calls but does not natively describe tools. The fallback behaviour used may "
"produce bad results, inspect prompt w/ --verbose & consider overriding the template. \n " );
}
}
if ( inputs . force_pure_content ) {
LOG_WRN ( "Forcing pure content template, will not render reasoning or tools separately." );
// Create the result structure
common_chat_params data ;
auto params_copy = params ;
params_copy . reasoning_format = COMMON_REASONING_FORMAT_NONE ;
2026-04-03 09:07:59 +03:00
data . prompt = common_chat_template_direct_apply_impl ( tmpl , params_copy );
2026-05-17 07:36:05 -04:00
data . generation_prompt = common_chat_template_generation_prompt_impl ( tmpl , params );
2026-03-19 16:58:21 +01:00
data . format = COMMON_CHAT_FORMAT_PEG_NATIVE ;
2026-05-17 07:36:05 -04:00
auto parser = build_chat_peg_parser ([ & data ]( common_chat_peg_builder & p ) {
return p . literal ( data . generation_prompt ) << p . content ( p . rest ());
2026-03-19 16:58:21 +01:00
});
data . parser = parser . save ();
return data ;
}
2026-04-03 17:51:52 +02:00
if ( auto result = common_chat_try_specialized_template ( tmpl , src , params )) {
2026-03-19 16:58:21 +01:00
return * result ;
}
2026-03-06 21:01:00 +01:00
try {
2026-03-19 16:58:21 +01:00
LOG_DBG ( "%s: using differential autoparser \n " , __func__ );
2026-03-06 21:01:00 +01:00
struct autoparser :: autoparser autoparser ;
autoparser . analyze_template ( tmpl );
auto auto_params = autoparser :: peg_generator :: generate_parser ( tmpl , params , autoparser );
2026-05-25 07:56:18 +02:00
2026-06-23 00:27:28 -05:00
common_chat_msg_delimiters delimiters ;
2026-05-25 07:56:18 +02:00
if ( ! autoparser . assistant_start . empty ()) {
2026-06-23 00:27:28 -05:00
delimiters . add ( COMMON_CHAT_ROLE_ASSISTANT , autoparser . assistant_start );
2026-05-25 07:56:18 +02:00
}
if ( ! autoparser . user_start . empty ()) {
2026-06-23 00:27:28 -05:00
delimiters . add ( COMMON_CHAT_ROLE_USER , autoparser . user_start );
2026-05-25 07:56:18 +02:00
}
2026-06-23 00:27:28 -05:00
auto_params . message_delimiters = std :: move ( delimiters );
2026-05-25 07:56:18 +02:00
2026-03-06 21:01:00 +01:00
auto_params . supports_thinking = autoparser . reasoning . mode != autoparser :: reasoning_mode :: NONE ;
2026-03-11 10:26:12 +01:00
if ( auto_params . supports_thinking ) {
2026-05-04 13:18:11 +02:00
auto_params . thinking_start_tag = trim_whitespace ( autoparser . reasoning . start );
2026-07-25 04:58:09 -05:00
auto end_tag = trim_whitespace ( autoparser . reasoning . end );
if ( ! end_tag . empty ()) {
auto_params . thinking_end_tags = { std :: move ( end_tag )};
}
2026-03-11 10:26:12 +01:00
}
2026-03-19 16:58:21 +01:00
common_peg_arena arena ;
arena . load ( auto_params . parser );
LOG_DBG ( "%s: generated parser: \n %s \n\n parser generation prompt: %s \n " , __func__ , arena . dump ( arena . root ()). c_str (), auto_params . generation_prompt . c_str ());
2026-03-06 21:01:00 +01:00
return auto_params ;
} catch ( const std :: exception & e ) {
throw std :: invalid_argument ( std :: string ( "Unable to generate parser for this template. Automatic parser generation failed: " ) + e . what ());
2025-01-30 19:13:58 +00:00
}
2025-02-18 18:03:23 +00:00
}
// Legacy template route (adhoc C++ implementation of known templates), forward to llama_chat_apply_template.
2026-03-06 21:01:00 +01:00
static common_chat_params common_chat_templates_apply_legacy ( const struct common_chat_templates * tmpls ,
const struct common_chat_templates_inputs & inputs ) {
size_t alloc_size = 0 ;
2025-02-18 18:03:23 +00:00
std :: vector < llama_chat_message > chat ;
2026-03-06 21:01:00 +01:00
std :: vector < std :: string > contents ;
2025-08-06 11:27:30 -07:00
2025-02-18 18:03:23 +00:00
for ( const auto & msg : inputs . messages ) {
auto content = msg . content ;
for ( const auto & part : msg . content_parts ) {
2026-02-19 12:18:57 +01:00
if ( part . type != "text" && part . type != "media_marker" ) {
2025-02-18 18:03:23 +00:00
LOG_WRN ( "Ignoring non-text content part: %s \n " , part . type . c_str ());
continue ;
}
if ( ! content . empty ()) {
2026-03-06 21:01:00 +01:00
content += " \n " ;
;
2025-02-18 18:03:23 +00:00
}
content += part . text ;
}
contents . emplace_back ( std :: move ( content ));
}
for ( size_t i = 0 ; i < contents . size (); ++ i ) {
2026-03-06 21:01:00 +01:00
const auto & msg = inputs . messages [ i ];
2025-02-18 18:03:23 +00:00
const auto & content = contents [ i ];
2026-03-06 21:01:00 +01:00
chat . push_back ({ msg . role . c_str (), content . c_str () });
2025-11-18 19:11:53 +01:00
size_t msg_size = msg . role . size () + content . size ();
2026-03-06 21:01:00 +01:00
alloc_size += msg_size + ( msg_size / 4 ); // == msg_size * 1.25 but avoiding float ops
2025-02-18 18:03:23 +00:00
}
std :: vector < char > buf ( alloc_size );
// run the first time to get the total output length
const auto & src = tmpls -> template_default -> source ();
2026-03-06 21:01:00 +01:00
int32_t res = llama_chat_apply_template ( src . c_str (), chat . data (), chat . size (), inputs . add_generation_prompt ,
buf . data (), buf . size ());
2025-02-18 18:03:23 +00:00
// error: chat template is not supported
if ( res < 0 ) {
// if the custom "tmpl" is not supported, we throw an error
// this is a bit redundant (for good), since we're not sure if user validated the custom template with llama_chat_verify_template()
2025-06-16 21:58:42 +02:00
throw std :: runtime_error ( "this custom template is not supported, try using --jinja" );
2025-02-18 18:03:23 +00:00
}
// if it turns out that our buffer is too small, we resize it
if (( size_t ) res > buf . size ()) {
buf . resize ( res );
2026-03-06 21:01:00 +01:00
res = llama_chat_apply_template ( src . c_str (), chat . data (), chat . size (), inputs . add_generation_prompt , buf . data (),
buf . size ());
2025-02-18 18:03:23 +00:00
}
2025-11-18 19:11:53 +01:00
// for safety, we check the result again
if ( res < 0 || ( size_t ) res > buf . size ()) {
throw std :: runtime_error ( "failed to apply chat template, try using --jinja" );
}
2025-02-18 18:03:23 +00:00
common_chat_params params ;
params . prompt = std :: string ( buf . data (), res );
if ( ! inputs . json_schema . empty ()) {
params . grammar = json_schema_to_grammar ( json :: parse ( inputs . json_schema ));
} else {
params . grammar = inputs . grammar ;
}
return params ;
}
2026-03-06 21:01:00 +01:00
common_chat_params common_chat_templates_apply ( const struct common_chat_templates * tmpls ,
const struct common_chat_templates_inputs & inputs ) {
2025-02-18 18:03:23 +00:00
GGML_ASSERT ( tmpls != nullptr );
2026-03-06 21:01:00 +01:00
return inputs . use_jinja ? common_chat_templates_apply_jinja ( tmpls , inputs ) :
common_chat_templates_apply_legacy ( tmpls , inputs );
}
common_chat_msg common_chat_parse ( const std :: string & input ,
bool is_partial ,
const common_chat_parser_params & params ) {
return common_chat_peg_parse ( params . parser , input , is_partial , params );
}
common_chat_msg common_chat_peg_parse ( const common_peg_arena & src_parser ,
const std :: string & input ,
bool is_partial ,
const common_chat_parser_params & params ) {
const common_peg_arena & parser = src_parser . empty () ?
build_chat_peg_parser ([]( common_chat_peg_builder & p ) { return p . content ( p . rest ()) + p . end (); }) :
src_parser ;
2026-03-10 15:21:51 +01:00
if ( src_parser . empty ()) {
LOG_DBG ( "No parser definition detected, assuming pure content parser." );
2026-03-06 21:01:00 +01:00
}
2026-03-19 16:58:21 +01:00
const std :: string effective_input = params . generation_prompt . empty ()
? input
: params . generation_prompt + input ;
2026-04-19 09:24:06 +02:00
//LOG_DBG("Parsing PEG input with format %s: %s\n", common_chat_format_name(params.format), effective_input.c_str());
2026-03-06 21:01:00 +01:00
2026-03-08 11:17:02 -05:00
common_peg_parse_flags flags = COMMON_PEG_PARSE_FLAG_LENIENT ;
if ( params . debug ) {
flags |= COMMON_PEG_PARSE_FLAG_DEBUG ;
}
2026-03-19 16:58:21 +01:00
common_peg_parse_context ctx ( effective_input , flags );
2026-03-06 21:01:00 +01:00
auto result = parser . parse ( ctx );
if ( result . fail ()) {
// During partial parsing, return partial results if any AST nodes were captured
// This allows streaming to work correctly for formats like FUNC_MARKDOWN_CODE_BLOCK
if ( is_partial && result . end > 0 ) {
// Try to extract any partial results from what was successfully parsed
common_chat_msg msg ;
msg . role = "assistant" ;
2026-04-02 23:31:02 +02:00
std :: unique_ptr < common_chat_peg_mapper > mapper ;
if ( params . format == COMMON_CHAT_FORMAT_PEG_GEMMA4 ) {
mapper = std :: make_unique < common_chat_peg_gemma4_mapper > ( msg );
2026-07-28 04:27:20 -05:00
} else if ( params . format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 ) {
mapper = std :: make_unique < common_chat_peg_minimax_m3_mapper > ( msg );
2026-04-02 23:31:02 +02:00
} else {
mapper = std :: make_unique < common_chat_peg_mapper > ( msg );
}
mapper -> from_ast ( ctx . ast , result );
2026-03-06 21:01:00 +01:00
2026-03-08 11:17:02 -05:00
if ( ctx . is_debug ()) {
2026-03-06 21:01:00 +01:00
fprintf ( stderr , " \n AST for partial parse (fail): \n %s \n " , ctx . ast . dump (). c_str ());
fflush ( stderr );
}
return msg ;
}
2026-06-15 17:33:54 +02:00
LOG_WRN ( "%s: unparsed %s output: %s \n " , __func__ , common_chat_format_name ( params . format ), effective_input . substr ( result . end ). c_str ());
LOG_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 ());
throw std :: runtime_error ( std :: string ( "The model produced output that does not match the expected " ) + common_chat_format_name ( params . format ) + " format" );
2026-03-06 21:01:00 +01:00
}
common_chat_msg msg ;
msg . role = "assistant" ;
2026-04-02 23:31:02 +02:00
std :: unique_ptr < common_chat_peg_mapper > mapper ;
if ( params . format == COMMON_CHAT_FORMAT_PEG_GEMMA4 ) {
mapper = std :: make_unique < common_chat_peg_gemma4_mapper > ( msg );
2026-07-28 04:27:20 -05:00
} else if ( params . format == COMMON_CHAT_FORMAT_PEG_MINIMAX_M3 ) {
mapper = std :: make_unique < common_chat_peg_minimax_m3_mapper > ( msg );
2026-04-02 23:31:02 +02:00
} else {
mapper = std :: make_unique < common_chat_peg_mapper > ( msg );
}
mapper -> from_ast ( ctx . ast , result );
2026-03-06 21:01:00 +01:00
2026-03-08 11:17:02 -05:00
if ( ctx . is_debug ()) {
2026-03-06 21:01:00 +01:00
fprintf ( stderr , " \n AST for %s parse: \n %s \n " , is_partial ? "partial" : "full" , ctx . ast . dump (). c_str ());
fflush ( stderr );
}
if ( ! is_partial ) {
LOG_DBG ( "Parsed message: %s \n " , common_chat_msgs_to_json_oaicompat ({ msg }). at ( 0 ). dump (). c_str ());
}
return msg ;
2025-01-30 19:13:58 +00:00
}
2026-01-22 21:30:06 +01:00
std :: map < std :: string , bool > common_chat_templates_get_caps ( const common_chat_templates * chat_templates ) {
GGML_ASSERT ( chat_templates != nullptr );
GGML_ASSERT ( chat_templates -> template_default != nullptr );
2026-06-25 02:49:22 +02:00
if ( chat_templates -> template_tool_use != nullptr ) {
// take the more expressive template when available
return chat_templates -> template_tool_use -> caps . to_map ();
}
2026-01-22 21:30:06 +01:00
return chat_templates -> template_default -> caps . to_map ();
}