2026-05-17 07:36:05 -04:00
// Tests chat handling, including grammar genration and parsing for tool calling, for various templates.
2025-01-30 19:13:58 +00:00
//
// Also acts as a CLI to generate a Markdown summary of the formats of Jinja templates,
// e.g. given Minja (http://github.com/google/minja) checked out in parent dir:
//
// cmake -B build && cmake --build build --parallel && ./build/bin/test-chat ../minja/build/tests/*.jinja 2>/dev/null
//
2026-03-06 21:01:00 +01:00
#include "../src/llama-grammar.h"
#include "../src/unicode.h"
2026-04-22 10:28:45 +02:00
#include "../tools/server/server-chat.h"
2026-03-06 21:01:00 +01:00
#include "chat-auto-parser.h"
2025-02-18 18:03:23 +00:00
#include "chat.h"
2026-03-06 21:01:00 +01:00
#include "common.h"
#include "ggml.h"
2025-06-09 11:03:09 -07:00
#include "log.h"
2026-03-06 21:01:00 +01:00
#include <algorithm>
#include <exception>
2025-05-30 16:25:45 +03:00
#include <fstream>
2025-10-27 18:54:01 -04:00
#include <functional>
2026-03-06 21:01:00 +01:00
#include <iostream>
#include <nlohmann/json.hpp>
#include <set>
#include <stdexcept>
2025-05-30 16:25:45 +03:00
#include <string>
2025-01-30 19:13:58 +00:00
using json = nlohmann :: ordered_json ;
2025-05-25 01:48:08 +01:00
static std :: ostream & operator << ( std :: ostream & os , const common_chat_msg_diff & diff ) {
os << "{ content_delta: " << diff . content_delta << "; " ;
2025-06-02 10:15:44 -07:00
os << "reasoning_content_delta: " << diff . reasoning_content_delta << "; " ;
2025-05-25 01:48:08 +01:00
if ( diff . tool_call_index != std :: string :: npos ) {
os << "tool_call_index: " << diff . tool_call_index << "; " ;
os << "tool_call_delta.name: " << diff . tool_call_delta . name << "; " ;
os << "tool_call_delta.id: " << diff . tool_call_delta . id << "; " ;
os << "tool_call_delta.arguments: " << diff . tool_call_delta . arguments << "; " ;
}
os << "}" ;
return os ;
}
2026-03-06 21:01:00 +01:00
2025-05-25 01:48:08 +01:00
// operator<< for vector<common_chat_msg_diff>:
static std :: ostream & operator << ( std :: ostream & os , const std :: vector < common_chat_msg_diff > & diffs ) {
os << "[ \n " ;
for ( const auto & diff : diffs ) {
os << " " << diff << ", \n " ;
}
os << "]" ;
return os ;
}
2026-03-06 21:01:00 +01:00
2025-05-25 01:48:08 +01:00
static std :: ostream & operator << ( std :: ostream & os , const common_chat_msg & msg ) {
os << "{ role: " << msg . role << "; " ;
os << "content: " << msg . content << "; " ;
os << "content_parts: [ \n " ;
for ( const auto & part : msg . content_parts ) {
os << " { type: " << part . type << "; text: " << part . text << " }, \n " ;
}
os << "]; " ;
os << "reasoning_content: " << msg . reasoning_content << "; " ;
os << "tool_calls: [ \n " ;
for ( const auto & tool_call : msg . tool_calls ) {
2026-03-06 21:01:00 +01:00
os << " { name: " << tool_call . name << "; arguments: " << tool_call . arguments << "; id: " << tool_call . id
<< " }, \n " ;
2025-05-25 01:48:08 +01:00
}
os << "]" ;
os << "}" ;
return os ;
}
template < class T > static bool equals ( const T & expected , const T & actual ) {
return expected == actual ;
}
static common_chat_msg normalize ( const common_chat_msg & msg ) {
common_chat_msg normalized = msg ;
for ( auto & tool_call : normalized . tool_calls ) {
try {
tool_call . arguments = json :: parse ( tool_call . arguments ). dump ();
} catch ( const std :: exception & ) {
}
}
return normalized ;
}
2025-11-19 04:54:15 +11:00
2026-03-06 21:01:00 +01:00
template <> bool equals ( const common_chat_msg & expected , const common_chat_msg & actual ) {
2025-05-25 01:48:08 +01:00
return normalize ( expected ) == normalize ( actual );
}
2025-01-30 19:13:58 +00:00
template < class T > static void assert_equals ( const T & expected , const T & actual ) {
2025-05-25 01:48:08 +01:00
if ( ! equals ( expected , actual )) {
2026-03-06 21:01:00 +01:00
std :: ostringstream oss_expected ;
oss_expected << expected ;
std :: ostringstream oss_actual ;
oss_actual << actual ;
LOG_ERR ( "Expected: %s \n " , oss_expected . str (). c_str ());
LOG_ERR ( "Actual: %s \n " , oss_actual . str (). c_str ());
common_log_flush ( common_log_main ());
2025-01-30 19:13:58 +00:00
throw std :: runtime_error ( "Test failed" );
}
}
2026-05-17 07:36:05 -04:00
static void assert_contains ( const std :: string & haystack , const std :: string & needle ) {
if ( haystack . find ( needle ) == std :: string :: npos ) {
LOG_ERR ( "Expected to contain: %s \n " , needle . c_str ());
LOG_ERR ( "Actual: %s \n " , haystack . c_str ());
common_log_flush ( common_log_main ());
throw std :: runtime_error ( "Test failed" );
}
}
static void assert_ends_with ( const std :: string & str , const std :: string & suffix ) {
if ( str . size () < suffix . size () ||
str . compare ( str . size () - suffix . size (), suffix . size (), suffix ) != 0 ) {
LOG_ERR ( "Expected to end with: %s \n " , suffix . c_str ());
LOG_ERR ( "Actual: %s \n " , str . c_str ());
common_log_flush ( common_log_main ());
throw std :: runtime_error ( "Test failed" );
}
}
2025-01-30 19:13:58 +00:00
static std :: string read_file ( const std :: string & path ) {
std :: ifstream fs ( path , std :: ios_base :: binary );
if ( ! fs . is_open ()) {
fs = std :: ifstream ( "../" + path , std :: ios_base :: binary );
if ( ! fs . is_open ()) {
throw std :: runtime_error ( "Failed to open file: " + path );
}
}
fs . seekg ( 0 , std :: ios_base :: end );
auto size = fs . tellg ();
fs . seekg ( 0 );
std :: string out ;
out . resize ( static_cast < size_t > ( size ));
2025-02-18 18:03:23 +00:00
fs . read ( out . data (), static_cast < std :: streamsize > ( size ));
2025-01-30 19:13:58 +00:00
return out ;
}
2025-02-18 18:03:23 +00:00
static common_chat_templates_ptr read_templates ( const std :: string & path ) {
return common_chat_templates_ptr ( common_chat_templates_init ( /* model= */ nullptr , read_file ( path )));
}
2025-01-30 19:13:58 +00:00
static std :: unique_ptr < llama_grammar > build_grammar ( const std :: string & grammar_str ) {
return std :: unique_ptr < llama_grammar > (
llama_grammar_init_impl ( nullptr , grammar_str . c_str (), "root" , false , nullptr , 0 , nullptr , 0 ));
}
2026-03-06 21:01:00 +01:00
// Helper to format a code point as a readable string
static std :: string format_codepoint ( uint32_t cp ) {
if ( cp >= 32 && cp < 127 ) {
return std :: string ( "'" ) + static_cast < char > ( cp ) + "'" ;
} else if ( cp == '\n' ) {
return "' \\ n'" ;
} else if ( cp == '\r' ) {
return "' \\ r'" ;
} else if ( cp == '\t' ) {
return "' \\ t'" ;
} else {
return "U+" + std :: to_string ( cp );
}
}
// Helper to format expected element from grammar stack
static std :: string format_expected_element ( const llama_grammar_rules & /* rules*/ , const llama_grammar_element * elem ) {
if ( ! elem ) {
return "<end>" ;
}
switch ( elem -> type ) {
case LLAMA_GRETYPE_END :
return "<end of rule>" ;
case LLAMA_GRETYPE_ALT :
return "<alternative>" ;
case LLAMA_GRETYPE_RULE_REF :
{
// Find rule name - just show rule ID for now
return "<rule-" + std :: to_string ( elem -> value ) + ">" ;
}
case LLAMA_GRETYPE_CHAR :
{
std :: string result ;
const llama_grammar_element * pos = elem ;
bool first = true ;
do {
if ( ! first ) {
result += " | " ;
}
first = false ;
if ( pos [ 1 ]. type == LLAMA_GRETYPE_CHAR_RNG_UPPER ) {
// Range like [a-z]
result += "[" + format_codepoint ( pos -> value ) + "-" + format_codepoint ( pos [ 1 ]. value ) + "]" ;
pos += 2 ;
} else {
result += format_codepoint ( pos -> value );
pos += 1 ;
}
} while ( pos -> type == LLAMA_GRETYPE_CHAR_ALT );
return result ;
}
case LLAMA_GRETYPE_CHAR_NOT :
{
std :: string result = "[^" ;
const llama_grammar_element * pos = elem ;
bool first = true ;
do {
if ( ! first ) {
result += " " ;
}
first = false ;
if ( pos [ 1 ]. type == LLAMA_GRETYPE_CHAR_RNG_UPPER ) {
result += format_codepoint ( pos -> value ) + "-" + format_codepoint ( pos [ 1 ]. value );
pos += 2 ;
} else {
result += format_codepoint ( pos -> value );
pos += 1 ;
}
} while ( pos -> type == LLAMA_GRETYPE_CHAR_ALT );
return result + "]" ;
}
case LLAMA_GRETYPE_CHAR_ANY :
return "<any char>" ;
case LLAMA_GRETYPE_TOKEN :
return "<token-" + std :: to_string ( elem -> value ) + ">" ;
case LLAMA_GRETYPE_TOKEN_NOT :
return "<not-token-" + std :: to_string ( elem -> value ) + ">" ;
default :
return "<unknown>" ;
}
}
// Get description of what the grammar expects at current position
static std :: string get_expected_description ( const llama_grammar_rules & rules , const llama_grammar_stacks & stacks ) {
if ( stacks . empty ()) {
return "<no valid continuations>" ;
}
std :: string result ;
std :: set < std :: string > seen ;
for ( const auto & stack : stacks ) {
if ( stack . empty ()) {
if ( seen . insert ( "<end>" ). second ) {
if ( ! result . empty ()) {
result += " OR " ;
}
result += "<end>" ;
}
continue ;
}
const llama_grammar_element * elem = stack . back ();
std :: string desc = format_expected_element ( rules , elem );
if ( seen . insert ( desc ). second ) {
if ( ! result . empty ()) {
result += " OR " ;
}
result += desc ;
}
}
return result ;
}
// Result of a detailed grammar match attempt
struct grammar_match_result {
bool success = false ; // Did the string fully match the grammar?
size_t matched_bytes = 0 ; // Bytes successfully matched before failure
size_t matched_codepoints = 0 ; // Codepoints successfully matched before failure
size_t total_bytes = 0 ; // Total bytes in input
size_t total_codepoints = 0 ; // Total codepoints in input
std :: string matched_prefix ; // The portion that was successfully matched
std :: string failing_char ; // The character that caused failure (if any)
std :: string expected_description ; // What the grammar expected at failure point
bool incomplete = false ; // True if matched all input but grammar expects more
};
// Detailed version of match_string that returns failure information
static grammar_match_result match_string_detailed ( const std :: string & input , llama_grammar * grammar ) {
grammar_match_result result ;
result . total_bytes = input . size ();
const auto cpts = unicode_cpts_from_utf8 ( input );
result . total_codepoints = cpts . size ();
auto & stacks_cur = llama_grammar_get_stacks ( grammar );
const auto & rules = llama_grammar_get_rules ( grammar );
size_t byte_pos = 0 ;
for ( size_t i = 0 ; i < cpts . size (); i ++ ) {
const auto & cpt = cpts [ i ];
// Get expected before accepting (for error reporting)
std :: string expected_before = get_expected_description ( rules , stacks_cur );
llama_grammar_accept ( grammar , cpt );
// Calculate byte position for this codepoint
size_t cpt_bytes = 0 ;
if ( cpt < 0x80 ) {
cpt_bytes = 1 ;
} else if ( cpt < 0x800 ) {
cpt_bytes = 2 ;
} else if ( cpt < 0x10000 ) {
cpt_bytes = 3 ;
} else {
cpt_bytes = 4 ;
}
if ( stacks_cur . empty ()) {
// Grammar failed to match at this point
result . matched_bytes = byte_pos ;
result . matched_codepoints = i ;
result . matched_prefix = input . substr ( 0 , byte_pos );
result . failing_char = format_codepoint ( cpt );
result . expected_description = expected_before ;
result . incomplete = false ;
return result ;
}
byte_pos += cpt_bytes ;
}
// All input matched - check if grammar is complete
result . matched_bytes = input . size ();
result . matched_codepoints = cpts . size ();
result . matched_prefix = input ;
if ( std :: any_of ( stacks_cur . begin (), stacks_cur . end (), []( const auto & stack ) { return stack . empty (); })) {
// An empty stack means that the grammar has been completed
result . success = true ;
result . incomplete = false ;
} else {
// Grammar expects more input
result . success = false ;
result . incomplete = true ;
result . expected_description = get_expected_description ( rules , stacks_cur );
}
return result ;
}
2025-01-30 19:13:58 +00:00
// TODO: extract to common helper (copied from test-grammar-integration.cpp)
static bool match_string ( const std :: string & input , llama_grammar * grammar ) {
const auto cpts = unicode_cpts_from_utf8 ( input );
auto & stacks_cur = llama_grammar_get_stacks ( grammar );
for ( const auto & cpt : cpts ) {
llama_grammar_accept ( grammar , cpt );
if ( stacks_cur . empty ()) {
// no stacks means that the grammar failed to match at this point
return false ;
}
}
2025-02-18 18:03:23 +00:00
if ( std :: any_of ( stacks_cur . begin (), stacks_cur . end (), []( const auto & stack ) { return stack . empty (); })) {
// An empty stack means that the grammar has been completed
return true ;
2025-01-30 19:13:58 +00:00
}
return false ;
}
2025-05-25 01:48:08 +01:00
static std :: string renormalize_json ( const std :: string & json_str ) {
try {
auto json_obj = json :: parse ( json_str );
return json_obj . dump ();
} catch ( const std :: exception & e ) {
2026-03-06 21:01:00 +01:00
return "" ; // ignore parial JSON contents for comparison purposes
2025-05-25 01:48:08 +01:00
}
}
2026-03-06 21:01:00 +01:00
static void assert_msg_equals ( const common_chat_msg & expected ,
const common_chat_msg & actual ,
bool ignore_whitespace_differences = false ) {
2025-01-30 19:13:58 +00:00
assert_equals ( expected . role , actual . role );
2025-11-19 04:54:15 +11:00
if ( ignore_whitespace_differences ) {
assert_equals ( string_strip ( expected . content ), string_strip ( actual . content ));
} else {
assert_equals ( expected . content , actual . content );
}
2025-02-18 18:03:23 +00:00
assert_equals ( expected . content_parts . size (), actual . content_parts . size ());
for ( size_t i = 0 ; i < expected . content_parts . size (); i ++ ) {
const auto & expected_part = expected . content_parts [ i ];
const auto & actual_part = actual . content_parts [ i ];
assert_equals ( expected_part . type , actual_part . type );
2025-11-19 04:54:15 +11:00
if ( ignore_whitespace_differences ) {
assert_equals ( string_strip ( expected_part . text ), string_strip ( actual_part . text ));
} else {
assert_equals ( expected_part . text , actual_part . text );
}
}
if ( ignore_whitespace_differences ) {
assert_equals ( string_strip ( expected . reasoning_content ), string_strip ( actual . reasoning_content ));
} else {
assert_equals ( expected . reasoning_content , actual . reasoning_content );
2025-02-18 18:03:23 +00:00
}
2025-01-30 19:13:58 +00:00
assert_equals ( expected . tool_calls . size (), actual . tool_calls . size ());
for ( size_t i = 0 ; i < expected . tool_calls . size (); i ++ ) {
const auto & expected_tool_call = expected . tool_calls [ i ];
const auto & actual_tool_call = actual . tool_calls [ i ];
assert_equals ( expected_tool_call . name , actual_tool_call . name );
2025-05-25 01:48:08 +01:00
assert_equals ( renormalize_json ( expected_tool_call . arguments ), renormalize_json ( actual_tool_call . arguments ));
2025-01-30 19:13:58 +00:00
assert_equals ( expected_tool_call . id , actual_tool_call . id );
}
}
2026-03-06 21:01:00 +01:00
static common_chat_tool special_function_tool {
2025-02-18 18:03:23 +00:00
/* .name = */ "special_function" ,
/* .description = */ "I'm special" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"arg1" : {
"type" : "integer" ,
"description" : "The arg."
}
},
"required" : [ "arg1" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool special_function_tool_with_optional_param {
2025-11-19 04:54:15 +11:00
/* .name = */ "special_function_with_opt" ,
/* .description = */ "I'm special but have optional stuff" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"arg1" : {
"type" : "integer" ,
"description" : "The arg."
},
"arg2" : {
"type" : "integer" ,
"description" : "The optional arg."
}
},
"required" : [ "arg1" ]
}) ",
};
2026-03-28 20:41:32 +01:00
2026-03-06 21:01:00 +01:00
static common_chat_tool empty_args_tool {
/* .name = */ "empty_args" ,
/* .description = */ "A tool that takes no arguments" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {}
}) ",
};
2026-03-28 20:41:32 +01:00
static common_chat_tool empty_args_tool_no_properties {
/* .name = */ "empty_args_no_props" ,
/* .description = */ "A tool that takes no arguments and has no properties" ,
/* .parameters = */ R "({
"type" : "object"
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool python_tool {
2025-02-18 18:03:23 +00:00
/* .name = */ "python" ,
/* .description = */ "an ipython interpreter" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"code" : {
"type" : "string" ,
"description" : "Python code to execute."
}
},
"required" : [ "code" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool html_tool {
/* .name = */ "html" ,
/* .description = */ "an html validator" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"markup" : {
"type" : "string" ,
"description" : "HTML markup to validate."
}
},
"required" : [ "markup" ]
}) ",
};
static common_chat_tool get_time_tool {
/* .name = */ "get_time" ,
/* .description = */ "Get the current time in a city" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"city" : {
"type" : "string" ,
"description" : "City name"
}
},
"required" : [ "city" ]
}) ",
};
static common_chat_tool get_weather_tool {
/* .name = */ "get_weather" ,
/* .description = */ "Get the current weather in a city" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"city" : {
"type" : "string" ,
"description" : "City name"
}
},
"required" : [ "city" ]
}) ",
};
static common_chat_tool todo_list {
2026-02-20 16:22:22 -06:00
/* .name = */ "todo_list" ,
/* .description = */ "Create or update the todo list" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"todos" : {
"type" : "array" ,
"description" : "List of TODO list items"
}
},
"required" : [ "todos" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool edit_tool {
/* .name = */ "edit" ,
/* .description = */ "Edit file" ,
2025-02-18 18:03:23 +00:00
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
2026-03-06 21:01:00 +01:00
"filename" : {
2025-02-18 18:03:23 +00:00
"type" : "string" ,
2026-03-06 21:01:00 +01:00
"description" : "Path of file to edit"
},
"oldString" : {
"type" : "string" ,
"description" : "String to replace"
},
"newString" : {
"type" : "string" ,
"description" : "New (replacement) value"
2025-02-18 18:03:23 +00:00
}
},
2026-03-06 21:01:00 +01:00
"required" : [ "filename" , "oldString" , "newString" ]
2025-02-18 18:03:23 +00:00
}) ",
};
2026-03-06 21:01:00 +01:00
2026-05-04 13:18:11 +02:00
static common_chat_tool manage_todo_list_tool {
/* .name = */ "manage_todo_list" ,
/* .description = */ "Create or update the todo list" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"todos" : {
"type" : "array" ,
"description" : "List of TODO list items"
}
},
"required" : [ "todos" ]
}) ",
};
static common_chat_tool run_in_terminal_tool {
/* .name = */ "run_in_terminal" ,
/* .description = */ "Run a shell command." ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"command" : {
"type" : "string" ,
"description" : "Shell command to run"
}
},
"required" : [ "command" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool magic_tool {
/* .name = */ "magic" ,
/* .description = */ "Magic tool that takes a hash" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"name" : {
"type" : "string"
},
"ref" : {
"type" : "string"
}
},
"required" : [ "name" , "ref" ]
}) ",
};
static common_chat_tool magic_int_tool {
/* .name = */ "magic_int" ,
/* .description = */ "Magic tool that takes a hash" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"ref" : {
"type" : "integer"
},
"name" : {
"type" : "string"
}
},
"required" : [ "ref" ]
}) ",
};
static common_chat_tool amount_tool {
/* .name = */ "amount" ,
/* .description = */ "Amount converter" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"orig" : {
"type" : "number"
}
},
"required" : [ "orig" ]
}) ",
};
2026-04-02 23:31:02 +02:00
static common_chat_tool toggle_tool {
/* .name = */ "toggle" ,
/* .description = */ "Toggle a feature" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"enabled" : {
"type" : "boolean" ,
"description" : "Whether to enable the feature"
}
},
"required" : [ "enabled" ]
}) ",
};
static common_chat_tool nullable_tool {
/* .name = */ "set_nullable" ,
/* .description = */ "Set a nullable value" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"value" : {
"type" : "null" ,
"description" : "A null value"
}
},
"required" : [ "value" ]
}) ",
};
static common_chat_tool config_tool {
/* .name = */ "set_config" ,
/* .description = */ "Set configuration" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"config" : {
"type" : "object" ,
"description" : "Configuration dict"
}
},
"required" : [ "config" ]
}) ",
};
2026-06-05 21:31:56 +02:00
static common_chat_tool calendar_create_event_tool {
/* .name = */ "Calendar.create_event" ,
/* .description = */ "Create a calendar event" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"title" : { "type" : "string" },
"participants" : { "type" : "array" , "items" : { "type" : "string" } },
"metadata" : { "type" : "object" }
},
"required" : [ "title" , "participants" , "metadata" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool imaginary_number_tool {
/* .name = */ "imaginary_number" ,
/* .description = */ "Imaginary number converter" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"number" : {
"type" : "object" ,
"properties" : {
"real" : {
"type" : "number"
},
"imaginary" : {
"type" : "number"
}
},
"required" : [ "real" , "imaginary" ]
}
},
"required" : [ "number" ]
}) ",
};
2026-04-03 11:51:23 -04:00
static common_chat_tool nullable_string_tool {
/* .name = */ "set_nullable_str" ,
/* .description = */ "Set a nullable string value" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"name" : {
"type" : [ "string" , "null" ],
"description" : "A nullable string"
}
},
"required" : [ "name" ]
}) ",
};
static common_chat_tool nullable_string_null_first_tool {
/* .name = */ "set_nullable_str_nf" ,
/* .description = */ "Set a nullable string value with null first in type array" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"name" : {
"type" : [ "null" , "string" ],
"description" : "A nullable string with null first"
}
},
"required" : [ "name" ]
}) ",
};
static common_chat_tool nullable_int_tool {
/* .name = */ "set_nullable_int" ,
/* .description = */ "Set a nullable integer value" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"count" : {
"type" : [ "integer" , "null" ],
"description" : "A nullable integer"
}
},
"required" : [ "count" ]
}) ",
};
static common_chat_tool enum_no_type_tool {
/* .name = */ "set_unit" ,
/* .description = */ "Set a temperature unit" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"unit" : {
"enum" : [ "celsius" , "fahrenheit" ],
"description" : "Temperature unit"
}
},
"required" : [ "unit" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static common_chat_tool string_param_tool {
/* .name = */ "string_param" ,
/* .description = */ "Tool with string parameter for testing" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"text" : {
"type" : "string" ,
"description" : "A text parameter"
}
},
"required" : []
}) ",
};
static common_chat_tool quoted_unquoted_tool {
/* .name = */ "quoted_unquoted" ,
/* .description = */ "Tool with two string parameters, one for quoted string, one for unquoted" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"quoted" : {
"type" : "string" ,
"description" : "Quoted value"
},
"unquoted" : {
"type" : "string" ,
"description" : "Unquoted value"
}
},
"required" : [ "quoted" , "unquoted" ]
}) ",
};
2026-03-06 22:34:15 +01:00
static common_chat_tool tool_2req_4opt {
/* .name = */ "tool_2req_4opt" ,
/* .description = */ "Tool with 2 required and 4 optional params" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"req1" : { "type" : "string" , "description" : "Required string" },
"req2" : { "type" : "integer" , "description" : "Required int" },
"opt1" : { "type" : "string" , "description" : "Optional string 1" },
"opt2" : { "type" : "integer" , "description" : "Optional int 1" },
"opt3" : { "type" : "string" , "description" : "Optional string 2" },
"opt4" : { "type" : "integer" , "description" : "Optional int 2" }
},
"required" : [ "req1" , "req2" ]
}) ",
};
static common_chat_tool tool_2req_5opt {
/* .name = */ "tool_2req_5opt" ,
/* .description = */ "Tool with 2 required and 5 optional params" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"req1" : { "type" : "string" , "description" : "Required string" },
"req2" : { "type" : "integer" , "description" : "Required int" },
"opt1" : { "type" : "string" , "description" : "Optional string 1" },
"opt2" : { "type" : "integer" , "description" : "Optional int 1" },
"opt3" : { "type" : "string" , "description" : "Optional string 2" },
"opt4" : { "type" : "integer" , "description" : "Optional int 2" },
"opt5" : { "type" : "string" , "description" : "Optional string 3" }
},
"required" : [ "req1" , "req2" ]
}) ",
};
2026-03-06 21:01:00 +01:00
static std :: vector < common_chat_tool > tools { special_function_tool , special_function_tool_with_optional_param ,
python_tool , html_tool , todo_list };
const common_chat_msg message_user {
"user" ,
"Hey there!" ,
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "" ,
/* .tool_name = */ "" ,
/* .tool_call_id = */ "" ,
};
const common_chat_msg message_user_parts {
"user" ,
/* .content = */ "" ,
/* .content_parts = */
{
{ "text" , "Hey" },
{ "text" , "there" },
},
/* .tool_calls = */
{ },
/* .reasoning_content = */
"" ,
/* .tool_name = */ "" ,
/* .tool_call_id = */ "" ,
};
static common_chat_msg simple_assist_msg ( const std :: string & content ,
const std :: string & reasoning_content = "" ,
const std :: string & tool_name = "" ,
const std :: string & arguments = "" ,
const std :: string & id = "" ) {
common_chat_msg msg ;
msg . role = "assistant" ;
msg . content = content ;
msg . reasoning_content = reasoning_content ;
if ( ! tool_name . empty () || ! id . empty ()) {
msg . tool_calls . push_back ({ tool_name , arguments , id });
}
return msg ;
}
static common_chat_msg message_with_tool_calls ( const std :: string & tool_name , const std :: string & arguments ) {
return simple_assist_msg ( "" , "" , tool_name , arguments );
}
static common_chat_msg message_with_tool_calls_and_reasoning ( const std :: string & tool_name ,
const std :: string & arguments ,
const std :: string & reasoning ) {
return simple_assist_msg ( "" , reasoning , tool_name , arguments );
}
static common_chat_msg message_with_reasoning_content_and_multiple_tool_calls (
const std :: string & reasoning ,
const std :: string & content ,
const std :: vector < std :: pair < std :: string , std :: string >> & tool_calls ) {
common_chat_msg msg ;
msg . role = "assistant" ;
msg . content = content ;
msg . reasoning_content = reasoning ;
for ( const auto & [ name , args ] : tool_calls ) {
msg . tool_calls . push_back ({ name , args , "" });
}
return msg ;
}
static common_chat_msg message_with_content_and_tool_call ( const std :: string & content ,
const std :: string & tool_name ,
const std :: string & arguments ) {
return simple_assist_msg ( content , "" , tool_name , arguments );
}
static common_chat_msg message_with_reasoning_and_tool_call ( const std :: string & reasoning ,
const std :: string & tool_name ,
const std :: string & arguments ) {
return simple_assist_msg ( "" , reasoning , tool_name , arguments );
}
const common_chat_msg message_assist = simple_assist_msg ( "Hello, world! \n What's up?" );
const common_chat_msg message_assist_empty = simple_assist_msg ( "" );
const common_chat_msg message_assist_thoughts_unparsed_deepseek =
simple_assist_msg ( "<think>I'm \n thinking</think>Hello, world! \n What's up?" );
const common_chat_msg message_assist_thoughts_unparsed_md =
simple_assist_msg ( "<think>I'm \n thinking</think>Hello, world! \n What's up? \n ```json \n {}```" );
const common_chat_msg message_assist_thoughts_unparsed_md_partial =
simple_assist_msg ( "<think>I'm \n thinking</think>Hello, world! \n What's up? \n ```json \n {}" );
const common_chat_msg message_assist_thoughts_unparsed_r7b =
simple_assist_msg ( "<|START_THINKING|>I'm \n thinking<|END_THINKING|>Hello, world! \n What's up?" );
const common_chat_msg message_assist_thoughts_unparsed_magistral =
simple_assist_msg ( "[THINK]raisonnement[/THINK]Réponse" );
const common_chat_msg message_assist_thoughts = simple_assist_msg ( "Hello, world! \n What's up?" , "I'm \n thinking" );
const common_chat_msg message_assist_thoughts_unopened_unparsed =
simple_assist_msg ( "I'm \n thinking</think>Hello, world! \n What's up?" );
const common_chat_msg message_assist_thoughts_no_content = simple_assist_msg ( "" , "I'm \n thinking" );
const common_chat_msg message_assist_call = simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : 1}" );
const common_chat_msg message_assist_call_noopt =
simple_assist_msg ( "" , "" , "special_function_with_opt" , "{ \" arg1 \" : 1}" );
const common_chat_msg message_assist_call_withopt =
simple_assist_msg ( "" , "" , "special_function_with_opt" , "{ \" arg1 \" : 1, \" arg2 \" : 2}" );
const common_chat_msg message_assist_call_content =
simple_assist_msg ( "Hello, world! \n What's up?" , "" , "special_function" , "{ \" arg1 \" :1}" );
const common_chat_msg message_assist_call_empty_args = simple_assist_msg ( "" , "" , "special_function" );
const common_chat_msg message_assist_call_cutoff_args = simple_assist_msg ( "" , "" , "special_function" , "{ \" arg" );
const common_chat_msg message_assist_call_thoughts =
simple_assist_msg ( "" , "I'm \n thinking" , "special_function" , "{ \" arg1 \" :1}" );
const common_chat_msg message_assist_call_thoughts_unparsed =
simple_assist_msg ( "<think>I'm \n thinking</think> \n\n " , "" , "special_function" , "{ \" arg1 \" : 1}" );
const common_chat_msg message_assist_call_thoughts_content =
simple_assist_msg ( "Hello, world! \n What's up?" , "I'm \n thinking" , "special_function" , "{ \" arg1 \" : 1}" );
const common_chat_msg message_assist_call_id =
simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" :1}" , /* .id = */ "123456789" );
const common_chat_msg message_assist_call_idx =
simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" :1}" , /* .id = */ "0" );
const common_chat_msg message_assist_thoughts_call_idx =
simple_assist_msg ( "" , "I'm \n thinking" , "special_function" , "{ \" arg1 \" : 1}" , /* id = */ "0" );
const common_chat_msg message_assist_thoughts_partial_call =
simple_assist_msg ( "" , "I'm \n thinking" , "special_function" , "" , /* id = */ "0" );
const common_chat_msg message_assist_call_python = simple_assist_msg ( "" , "" , "python" , "{ \" code \" : \" print('hey') \" }" );
const common_chat_msg message_assist_call_python_lines =
simple_assist_msg ( "" , "" , "python" , "{ \" code \" : \" # This is a program: \\ nprint('hey') \" }" );
const common_chat_msg message_assist_call_python_lines_unclosed =
simple_assist_msg ( "" , "" , "python" , "{ \" code \" : \" # This is a program: \\ nprint('hey')" );
const common_chat_msg message_assist_json_content =
simple_assist_msg ( "{ \n \" response \" : \" Hello, world! \\ nWhat's up? \"\n }" );
2026-05-17 07:36:05 -04:00
const common_chat_msg message_assist_prefill_content = simple_assist_msg ( "Hello, " , "I'm thinking" );
const common_chat_msg message_assist_prefill_reasoning = simple_assist_msg ( "" , "I'm" );
2025-01-30 19:13:58 +00:00
2026-03-06 21:01:00 +01:00
// Use for PEG parser implementations
struct peg_test_case {
common_chat_templates_inputs params ;
std :: string input ;
common_chat_msg expect ;
2026-03-28 07:29:26 +01:00
bool is_partial = false ;
bool expect_reconstruction = false ;
2026-03-06 21:01:00 +01:00
};
struct make_peg_parser {
common_chat_params params_ ;
common_peg_arena arena_ ;
bool detailed_debug_ ;
make_peg_parser ( common_chat_templates * tmpls ,
const common_chat_templates_inputs & inputs ,
bool detailed_debug = false ) {
detailed_debug_ = detailed_debug ;
params_ = common_chat_templates_apply ( tmpls , inputs );
arena_ . load ( params_ . parser );
}
common_chat_msg parse ( const std :: string & msg , bool is_partial ) const {
2026-03-19 16:58:21 +01:00
common_chat_parser_params parser_params ( params_ );
2026-03-06 21:01:00 +01:00
parser_params . debug = detailed_debug_ ;
return common_chat_peg_parse ( arena_ , msg , is_partial , parser_params );
}
};
2026-03-28 07:29:26 +01:00
// Global template filter for --template flag
static std :: string g_template_filter ;
// When true, run reconstruction test on every non-partial test and report results
static bool g_force_reconstruction_test = false ;
2026-03-06 21:01:00 +01:00
static void test_peg_parser ( common_chat_templates * tmpls ,
const std :: function < void ( peg_test_case & ) > & init ,
bool detailed_debug ) {
// UTF-8-safe truncation helper (same as in test_parser_with_streaming)
constexpr auto utf8_truncate_safe_len = []( const std :: string_view s ) -> size_t {
auto len = s . size ();
if ( len == 0 ) {
return 0 ;
}
auto i = len ;
for ( size_t back = 0 ; back < 4 && i > 0 ; ++ back ) {
-- i ;
unsigned char c = s [ i ];
if (( c & 0x80 ) == 0 ) {
return len ;
}
if (( c & 0xC0 ) == 0xC0 ) {
size_t expected_len = 0 ;
if (( c & 0xE0 ) == 0xC0 ) {
expected_len = 2 ;
} else if (( c & 0xF0 ) == 0xE0 ) {
expected_len = 3 ;
} else if (( c & 0xF8 ) == 0xF0 ) {
expected_len = 4 ;
} else {
return i ;
}
if ( len - i >= expected_len ) {
return len ;
}
return i ;
}
}
return len - std :: min ( len , size_t ( 3 ));
};
peg_test_case tc ;
init ( tc );
if ( tc . params . messages . empty ()) {
tc . params . messages = { message_user };
}
if ( tc . expect . role . empty ()) {
tc . expect . role = "assistant" ;
}
auto parser = make_peg_parser ( tmpls , tc . params , detailed_debug );
if ( detailed_debug ) {
LOG_DBG ( "Using parser: \n %s \n " , parser . arena_ . dump ( parser . arena_ . root ()). c_str ());
2026-04-08 12:47:25 +02:00
LOG_DBG ( "Generation prompt: '%s' \n " , parser . params_ . generation_prompt . c_str ());
2026-03-06 21:01:00 +01:00
}
common_chat_msg msg_accum ;
common_chat_msg msg_prev ;
msg_accum . role = msg_prev . role = "assistant" ;
for ( size_t i = 1 ; i <= tc . input . size (); ++ i ) {
auto is_partial = i < tc . input . size () || tc . is_partial ;
// Use UTF-8 safe truncation to avoid corrupting multi-byte characters
size_t safe_len = utf8_truncate_safe_len ( std :: string_view ( tc . input ). substr ( 0 , i ));
std :: string prefix = tc . input . substr ( 0 , safe_len );
common_chat_msg msg_current = parser . parse ( prefix , is_partial );
for ( const auto & diff : common_chat_msg_diff :: compute_diffs ( msg_prev , msg_current )) {
if ( ! diff . reasoning_content_delta . empty ()) {
msg_accum . reasoning_content += diff . reasoning_content_delta ;
}
if ( ! diff . content_delta . empty ()) {
msg_accum . content += diff . content_delta ;
}
if ( diff . tool_call_index != std :: string :: npos ) {
// During partial parsing, a new tool call may appear with empty name initially
// The name gets filled in as more input is parsed
while ( msg_accum . tool_calls . size () <= diff . tool_call_index ) {
msg_accum . tool_calls . push_back ({ "" , "" , "" });
}
// Always update name and id from diff (may change during incremental parsing), but only if the delta
// actually contains them
if ( ! diff . tool_call_delta . name . empty ()) {
msg_accum . tool_calls [ diff . tool_call_index ]. name = diff . tool_call_delta . name ;
}
if ( ! diff . tool_call_delta . id . empty ()) {
msg_accum . tool_calls [ diff . tool_call_index ]. id = diff . tool_call_delta . id ;
}
if ( ! diff . tool_call_delta . arguments . empty ()) {
msg_accum . tool_calls [ diff . tool_call_index ]. arguments += diff . tool_call_delta . arguments ;
}
}
}
try {
assert_msg_equals ( msg_current , msg_accum , true );
} catch ( std :: exception & e ) {
throw std :: runtime_error (( std :: string ( "Error comparing accumulated message to current: " ) + e . what ()). c_str ());
}
msg_prev = msg_current ;
}
if ( ! tc . is_partial ) {
assert_msg_equals ( tc . expect , parser . parse ( tc . input , false ), true );
}
assert_msg_equals ( tc . expect , msg_accum , true );
// Test grammar if present in params
if ( ! parser . params_ . grammar . empty ()) {
auto grammar = build_grammar ( parser . params_ . grammar );
if ( ! grammar ) {
throw std :: runtime_error ( "Failed to build grammar: " + parser . params_ . grammar );
}
2026-03-27 12:30:40 -05:00
// In production, grammar triggers match against the full generated text
// including the generation prompt. All positions are in full_input coordinates.
const auto & gen_prompt = parser . params_ . generation_prompt ;
std :: string full_input = gen_prompt + tc . input ;
// Determine whether the reasoning-budget sampler path applies: tool-call grammar
// with all WORD triggers and thinking tags present. In production, the reasoning
// budget sampler inhibits grammar application while inside thinking blocks —
// triggers inside <think>...</think> are suppressed.
bool use_reasoning_budget_path = false ;
if ( parser . params_ . grammar_lazy && ! parser . params_ . thinking_end_tag . empty ()) {
use_reasoning_budget_path = true ;
for ( const auto & trigger : parser . params_ . grammar_triggers ) {
if ( trigger . type != COMMON_GRAMMAR_TRIGGER_TYPE_WORD ) {
use_reasoning_budget_path = false ;
break ;
2025-02-02 09:25:38 +00:00
}
}
2025-01-30 19:13:58 +00:00
}
2026-03-27 12:30:40 -05:00
// Find the earliest trigger position to determine the constrained portion
auto earliest_trigger_pos = std :: string :: npos ;
if ( use_reasoning_budget_path ) {
// Reasoning-budget path: simulate thinking-aware trigger detection.
// Walk through full_input tracking thinking state; only match triggers
// when outside thinking blocks.
const auto & think_start = parser . params_ . thinking_start_tag ;
const auto & think_end = parser . params_ . thinking_end_tag ;
bool in_thinking = false ;
for ( size_t i = 0 ; i < full_input . size (); ++ i ) {
if ( ! in_thinking && ! think_start . empty ()
&& full_input . compare ( i , think_start . size (), think_start ) == 0 ) {
in_thinking = true ;
i += think_start . size () - 1 ;
continue ;
}
if ( in_thinking && full_input . compare ( i , think_end . size (), think_end ) == 0 ) {
in_thinking = false ;
i += think_end . size () - 1 ;
continue ;
}
if ( in_thinking ) {
continue ;
}
// Outside thinking — check if any trigger word starts here
for ( const auto & trigger : parser . params_ . grammar_triggers ) {
if ( full_input . compare ( i , trigger . value . size (), trigger . value ) == 0 ) {
if ( earliest_trigger_pos == std :: string :: npos || i < earliest_trigger_pos ) {
earliest_trigger_pos = i ;
}
}
}
if ( earliest_trigger_pos != std :: string :: npos ) {
break ; // found the earliest
}
}
// If the reasoning-budget path found no trigger outside thinking but the test
// expects tool calls, this template nests tool calls inside thinking
// blocks (e.g. Kimi). Fall back to the legacy path for this case.
if ( earliest_trigger_pos == std :: string :: npos && ! tc . expect . tool_calls . empty ()) {
use_reasoning_budget_path = false ;
}
}
if ( ! use_reasoning_budget_path ) {
// Legacy path: find triggers without thinking-awareness
for ( const auto & trigger : parser . params_ . grammar_triggers ) {
size_t pos = std :: string :: npos ;
std :: smatch match ;
switch ( trigger . type ) {
case COMMON_GRAMMAR_TRIGGER_TYPE_WORD :
{
const auto & word = trigger . value ;
pos = full_input . find ( word );
break ;
}
case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN :
{
const auto & compiled = std :: regex ( trigger . value );
if ( std :: regex_search ( full_input , match , compiled )) {
pos = match . position ( compiled . mark_count ());
}
break ;
}
case COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL :
{
// In production, PATTERN_FULL triggers are checked against
// the text generated so far, growing token by token. Simulate
// by trying every prefix of full_input.
const auto & compiled = std :: regex ( trigger . value );
for ( size_t end = gen_prompt . size (); end <= full_input . size (); ++ end ) {
std :: string prefix = full_input . substr ( 0 , end );
if ( std :: regex_match ( prefix , match , compiled )) {
pos = std :: string :: npos ;
for ( size_t gi = 1 ; gi < match . size (); ++ gi ) {
if ( match [ gi ]. length () > 0 ) {
pos = match . position ( gi );
break ;
}
}
if ( pos == std :: string :: npos ) {
pos = match . position ( 0 );
}
break ;
}
}
break ;
}
default :
throw std :: runtime_error ( "Unknown trigger type" );
}
if ( pos != std :: string :: npos ) {
if ( earliest_trigger_pos == std :: string :: npos || pos < earliest_trigger_pos ) {
earliest_trigger_pos = pos ;
}
}
}
}
// If the test expects tool calls and the grammar is lazy, the trigger must fire.
// Otherwise the grammar would never activate in production and tool calls wouldn't
// be constrained. A silent skip here would hide broken triggers.
if ( parser . params_ . grammar_lazy && ! tc . expect . tool_calls . empty () && ! tc . is_partial
&& earliest_trigger_pos == std :: string :: npos ) {
std :: string trigger_desc ;
for ( const auto & trigger : parser . params_ . grammar_triggers ) {
trigger_desc += " \n [type=" + std :: to_string ( trigger . type ) + "] " + trigger . value ;
}
throw std :: runtime_error (
"Grammar trigger did not fire, but test expects tool calls (lazy grammar). \n "
">>> Input: " + full_input + " \n "
">>> Triggers (" + std :: to_string ( parser . params_ . grammar_triggers . size ()) + "):" + trigger_desc );
}
// Determine the constrained portion of input to test against grammar.
// If the trigger position falls inside the generation prompt, the grammar
// sampler was already active before model output began — constrain from the
// start of the model output (i.e. tc.input).
std :: string constrained = full_input ;
2026-03-06 21:01:00 +01:00
bool grammar_triggered = false ;
if ( earliest_trigger_pos != std :: string :: npos ) {
2026-03-27 12:30:40 -05:00
auto constrain_from = std :: max ( earliest_trigger_pos , gen_prompt . size ());
constrained = full_input . substr ( constrain_from );
2026-03-06 21:01:00 +01:00
grammar_triggered = true ;
} else if ( ! parser . params_ . grammar_lazy ) {
// For non-lazy grammars, the entire input should match
grammar_triggered = true ;
}
// Test the constrained portion against the grammar
if ( grammar_triggered && ! tc . is_partial ) {
auto result = match_string_detailed ( constrained , grammar . get ());
if ( ! result . success ) {
std :: string error_msg ;
if ( result . incomplete ) {
error_msg =
"Grammar matched all input but expects more: \n\n "
">>> Input: " + tc . input +
" \n\n >>> Constrained: " + constrained +
" \n\n >>> Matched prefix (" + std :: to_string ( result . matched_bytes ) + " bytes, " +
std :: to_string ( result . matched_codepoints ) + " codepoints): " +
( result . matched_prefix . size () > 100 ? result . matched_prefix . substr ( 0 , 100 ) + "..." : result . matched_prefix ) +
" \n\n >>> Expected next: " + result . expected_description +
" \n\n >>> Grammar: " + parser . params_ . grammar ;
2025-12-09 00:32:04 +11:00
} else {
2026-03-06 21:01:00 +01:00
error_msg =
"Grammar match failed: \n\n "
">>> Input: " + tc . input +
" \n\n >>> Constrained: " + constrained +
" \n\n >>> Matched prefix (" + std :: to_string ( result . matched_bytes ) + " bytes, " +
std :: to_string ( result . matched_codepoints ) + " codepoints): " +
( result . matched_prefix . size () > 100 ? result . matched_prefix . substr ( 0 , 100 ) + "..." : result . matched_prefix ) +
" \n\n >>> Failing character: " + result . failing_char +
" \n\n >>> Expected: " + result . expected_description +
" \n\n >>> Grammar: " + parser . params_ . grammar ;
2025-12-09 00:32:04 +11:00
}
2026-03-06 21:01:00 +01:00
throw std :: runtime_error ( error_msg );
2025-12-09 00:32:04 +11:00
}
}
2025-11-19 04:54:15 +11:00
}
2026-03-28 07:29:26 +01:00
// Reconstruction test: verify that appending the parsed message to the original
// messages and re-rendering the template (without generation prompt) reproduces
// the original prompt + input exactly, or as a proper prefix (the template may
// append end-of-turn tokens after the assistant message).
if (( tc . expect_reconstruction || g_force_reconstruction_test ) && ! tc . is_partial ) {
// Start from tc.expect but copy tool call arguments from the actual parser
// output, which preserves original JSON formatting (e.g. {"arg1":1} vs {"arg1": 1}).
auto reconstruction_msg = tc . expect ;
auto parsed_msg = parser . parse ( tc . input , false );
for ( size_t i = 0 ; i < reconstruction_msg . tool_calls . size () && i < parsed_msg . tool_calls . size (); i ++ ) {
reconstruction_msg . tool_calls [ i ]. arguments = parsed_msg . tool_calls [ i ]. arguments ;
}
common_chat_templates_inputs reconstruction_inputs = tc . params ;
reconstruction_inputs . messages . push_back ( reconstruction_msg );
reconstruction_inputs . add_generation_prompt = false ;
auto reconstruction_params = common_chat_templates_apply ( tmpls , reconstruction_inputs );
std :: string expected_text = parser . params_ . prompt + tc . input ;
bool match = reconstruction_params . prompt == expected_text ||
( reconstruction_params . prompt . size () > expected_text . size () &&
reconstruction_params . prompt . compare ( 0 , expected_text . size (), expected_text ) == 0 );
if ( ! match && g_force_reconstruction_test && ! tc . expect_reconstruction ) {
// In forced mode, report mismatch but don't fail
// Find the first difference position
size_t diff_pos = 0 ;
size_t min_len = std :: min ( expected_text . size (), reconstruction_params . prompt . size ());
while ( diff_pos < min_len && expected_text [ diff_pos ] == reconstruction_params . prompt [ diff_pos ]) {
diff_pos ++ ;
}
size_t ctx_start = diff_pos > 60 ? diff_pos - 60 : 0 ;
size_t ctx_end_e = std :: min ( expected_text . size (), diff_pos + 40 );
size_t ctx_end_r = std :: min ( reconstruction_params . prompt . size (), diff_pos + 40 );
LOG_ERR ( " \x1b [31m[RECONSTRUCTION FAIL] \x1b [0m "
"first diff at byte %zu (expected len=%zu, reconstructed len=%zu) \n "
" expected: ...%s... \n "
" reconstructed: ...%s... \n " ,
diff_pos , expected_text . size (), reconstruction_params . prompt . size (),
expected_text . substr ( ctx_start , ctx_end_e - ctx_start ). c_str (),
reconstruction_params . prompt . substr ( ctx_start , ctx_end_r - ctx_start ). c_str ());
} else if ( ! match ) {
std :: string error_msg =
"Reconstruction mismatch: \n\n "
">>> Expected (prompt + input): \n " + expected_text +
" \n\n >>> Reconstructed: \n " + reconstruction_params . prompt ;
throw std :: runtime_error ( error_msg );
} else if ( g_force_reconstruction_test ) {
LOG_INF ( " \x1b [32m[RECONSTRUCTION OK] \x1b [0m \n " );
}
}
}
2026-03-06 21:01:00 +01:00
// Fluent builder for PEG parser tests
class peg_test_builder ;
class peg_tester {
common_chat_templates_ptr tmpls_ ;
std :: string template_path_ ;
bool detailed_debug_ ;
friend class peg_test_builder ;
public :
explicit peg_tester ( const std :: string & template_path , const bool detailed_debug = false ) :
tmpls_ ( read_templates ( template_path )),
template_path_ ( template_path ),
detailed_debug_ ( detailed_debug ) {}
const std :: string & template_path () const { return template_path_ ; }
peg_test_builder test ( const std :: string & input );
2025-02-18 18:03:23 +00:00
};
2026-03-06 21:01:00 +01:00
class peg_test_builder {
peg_tester & tester_ ;
peg_test_case tc_ ;
2025-11-19 04:54:15 +11:00
2026-03-06 21:01:00 +01:00
public :
2026-05-17 07:36:05 -04:00
peg_test_builder ( peg_tester & tester , const std :: string & input ) : tester_ ( tester ) {
tc_ . input = input ;
tc_ . params . add_generation_prompt = true ;
}
2025-05-26 08:03:57 -07:00
2026-03-06 21:01:00 +01:00
// Parameter setters
peg_test_builder & reasoning_format ( common_reasoning_format fmt ) {
tc_ . params . reasoning_format = fmt ;
return * this ;
2025-12-09 17:31:04 -06:00
}
2026-03-06 21:01:00 +01:00
peg_test_builder & tools ( std :: vector < common_chat_tool > tools ) {
tc_ . params . tools = std :: move ( tools );
return * this ;
2025-12-09 17:31:04 -06:00
}
2026-03-06 21:01:00 +01:00
peg_test_builder & enable_thinking ( bool val ) {
tc_ . params . enable_thinking = val ;
return * this ;
}
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
peg_test_builder & parallel_tool_calls ( bool val ) {
tc_ . params . parallel_tool_calls = val ;
return * this ;
}
2025-12-09 17:31:04 -06:00
2026-05-17 07:36:05 -04:00
peg_test_builder & add_generation_prompt ( bool val ) {
tc_ . params . add_generation_prompt = val ;
return * this ;
}
peg_test_builder & continue_final_message ( common_chat_continuation cont ) {
tc_ . params . continue_final_message = cont ;
return * this ;
}
2026-03-06 21:01:00 +01:00
peg_test_builder & json_schema ( const std :: string & schema ) {
tc_ . params . json_schema = schema ;
return * this ;
}
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
peg_test_builder & is_partial ( bool val ) {
tc_ . is_partial = val ;
return * this ;
}
2026-03-28 07:29:26 +01:00
peg_test_builder & expect_reconstruction ( bool val = true ) {
tc_ . expect_reconstruction = val ;
return * this ;
}
2026-03-06 21:01:00 +01:00
// Expect setters
peg_test_builder & expect ( const common_chat_msg & msg ) {
tc_ . expect = msg ;
return * this ;
}
peg_test_builder & expect_content ( const std :: string & content ) {
tc_ . expect . content = content ;
return * this ;
}
peg_test_builder & expect_reasoning ( const std :: string & reasoning ) {
tc_ . expect . reasoning_content = reasoning ;
return * this ;
}
peg_test_builder & expect_tool_calls ( std :: vector < common_chat_tool_call > calls ) {
tc_ . expect . tool_calls = std :: move ( calls );
return * this ;
}
2026-05-04 13:18:11 +02:00
peg_test_builder & tool_choice ( common_chat_tool_choice choice ) {
tc_ . params . tool_choice = choice ;
return * this ;
}
peg_test_builder & messages ( std :: vector < common_chat_msg > messages ) {
tc_ . params . messages = std :: move ( messages );
return * this ;
}
2026-03-06 21:01:00 +01:00
// Execute the test
void run () {
// Check template filter
if ( ! g_template_filter . empty ()) {
// Case-insensitive substring match
std :: string template_path_lower = tester_ . template_path ();
std :: string filter_lower = g_template_filter ;
std :: transform ( template_path_lower . begin (), template_path_lower . end (), template_path_lower . begin (),
:: tolower );
std :: transform ( filter_lower . begin (), filter_lower . end (), filter_lower . begin (), :: tolower );
if ( template_path_lower . find ( filter_lower ) == std :: string :: npos ) {
// Skip this test
return ;
2025-12-09 17:31:04 -06:00
}
}
2026-03-06 21:01:00 +01:00
LOG_INF ( " \n\x1b [38;5;126m[%s] \x1b [0m \n %s \n\n " , tester_ . template_path (). c_str (), tc_ . input . c_str ());
test_peg_parser ( tester_ . tmpls_ . get (), [ this ]( peg_test_case & t ) { t = tc_ ; }, tester_ . detailed_debug_ );
2025-12-09 17:31:04 -06:00
}
2026-03-06 21:01:00 +01:00
};
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
peg_test_builder peg_tester :: test ( const std :: string & input ) {
return peg_test_builder ( * this , input );
2025-12-09 17:31:04 -06:00
}
2025-02-18 18:03:23 +00:00
static void test_msgs_oaicompat_json_conversion () {
2026-03-06 21:01:00 +01:00
LOG_DBG ( "%s \n " , __func__ );
2025-02-18 18:03:23 +00:00
std :: vector < common_chat_msg > msgs {
message_user ,
message_user_parts ,
message_assist_call ,
message_assist_call_thoughts ,
message_assist_call_thoughts_unparsed ,
2025-09-05 01:22:22 +02:00
message_assist_call_thoughts_content ,
2025-02-18 18:03:23 +00:00
message_assist_call_id ,
message_assist_call_idx ,
message_assist_call_python ,
};
for ( const auto & msg : msgs ) {
2026-03-06 21:01:00 +01:00
auto oai_json = common_chat_msgs_to_json_oaicompat ({ msg });
auto msgs2 = common_chat_msgs_parse_oaicompat ( oai_json );
2025-02-18 18:03:23 +00:00
assert_equals (( size_t ) 1 , msgs2 . size ());
2026-03-06 21:01:00 +01:00
const auto & msg2 = msgs2 [ 0 ];
2025-02-18 18:03:23 +00:00
assert_msg_equals ( msg , msg2 );
}
2026-03-06 21:01:00 +01:00
assert_equals ( std :: string ( "[ \n "
" { \n "
" \" role \" : \" user \" , \n "
" \" content \" : [ \n "
" { \n "
" \" type \" : \" text \" , \n "
" \" text \" : \" Hey \"\n "
" }, \n "
" { \n "
" \" type \" : \" text \" , \n "
" \" text \" : \" there \"\n "
" } \n "
" ] \n "
" } \n "
"]" ),
common_chat_msgs_to_json_oaicompat ({ message_user_parts }). dump ( 2 ));
2025-02-18 18:03:23 +00:00
2026-03-06 21:01:00 +01:00
// Note: content is "" instead of null due to workaround for templates that render null as "None"
assert_equals ( std :: string ( "[ \n "
" { \n "
" \" role \" : \" assistant \" , \n "
" \" content \" : \"\" , \n "
" \" tool_calls \" : [ \n "
" { \n "
" \" type \" : \" function \" , \n "
" \" function \" : { \n "
" \" name \" : \" python \" , \n "
2026-03-08 11:33:03 +01:00
" \" arguments \" : \" { \\\" code \\\" : \\\" print('hey') \\\" } \"\n "
2026-03-06 21:01:00 +01:00
" } \n "
" } \n "
" ] \n "
" } \n "
"]" ),
common_chat_msgs_to_json_oaicompat ({ message_assist_call_python }). dump ( 2 ));
2025-03-10 09:45:07 +00:00
auto res = common_chat_msgs_parse_oaicompat ( json :: parse ( "[{ \" role \" : \" assistant \" , \" tool_calls \" : []}]" ));
assert_equals < size_t > ( 1 , res . size ());
assert_equals < std :: string > ( res [ 0 ]. role , "assistant" );
assert_equals ( true , res [ 0 ]. content . empty ());
assert_equals ( true , res [ 0 ]. tool_calls . empty ());
try {
common_chat_msgs_parse_oaicompat ( json :: parse ( "[{ \" role \" : \" assistant \" }]" ));
throw std :: runtime_error ( "Expected exception" );
} catch ( const std :: exception & e ) {
if ( std :: string ( e . what ()). find ( "'content'" ) == std :: string :: npos ) {
throw std :: runtime_error ( "Expected exception about missing 'content'" );
}
}
2025-02-18 18:03:23 +00:00
}
2026-05-25 07:56:18 +02:00
static void test_split_by_role () {
LOG_DBG ( "%s \n " , __func__ );
// Empty inputs
assert_equals < size_t > ( 0 , common_chat_split_by_role ( "" , {}). size ());
assert_equals < size_t > ( 0 , common_chat_split_by_role ( "hello" , {}). size ());
assert_equals < size_t > ( 0 , common_chat_split_by_role ( "" , { { "user" , "<|user|>" } }). size ());
// Multi-role conversation, no leading/trailing content
{
const std :: string prompt = "<|user|>Hi<|assistant|>Hello<|user|>Bye" ;
const auto splits = common_chat_split_by_role ( prompt , {
{ "user" , "<|user|>" },
{ "assistant" , "<|assistant|>" },
});
assert_equals < size_t > ( 3 , splits . size ());
assert_equals < std :: string > ( "user" , splits [ 0 ]. role );
assert_equals < size_t > ( 0 , splits [ 0 ]. pos );
assert_equals < size_t > ( 10 , splits [ 0 ]. len );
assert_equals < std :: string > ( "<|user|>Hi" , prompt . substr ( splits [ 0 ]. pos , splits [ 0 ]. len ));
assert_equals < std :: string > ( "assistant" , splits [ 1 ]. role );
assert_equals < size_t > ( 10 , splits [ 1 ]. pos );
assert_equals < size_t > ( 18 , splits [ 1 ]. len );
assert_equals < std :: string > ( "<|assistant|>Hello" , prompt . substr ( splits [ 1 ]. pos , splits [ 1 ]. len ));
assert_equals < std :: string > ( "user" , splits [ 2 ]. role );
assert_equals < size_t > ( 28 , splits [ 2 ]. pos );
assert_equals < size_t > ( 11 , splits [ 2 ]. len );
assert_equals < std :: string > ( "<|user|>Bye" , prompt . substr ( splits [ 2 ]. pos , splits [ 2 ]. len ));
}
}
2025-02-18 18:03:23 +00:00
static void test_tools_oaicompat_json_conversion () {
2026-03-06 21:01:00 +01:00
LOG_DBG ( "%s \n " , __func__ );
2025-02-18 18:03:23 +00:00
std :: vector < common_chat_tool > tools {
special_function_tool ,
python_tool ,
};
for ( const auto & tool : tools ) {
2026-03-06 21:01:00 +01:00
auto oai_json = common_chat_tools_to_json_oaicompat ({ tool });
auto tools2 = common_chat_tools_parse_oaicompat ( oai_json );
2025-02-18 18:03:23 +00:00
assert_equals (( size_t ) 1 , tools2 . size ());
auto tool2 = tools2 [ 0 ];
assert_equals ( tool . name , tool2 . name );
assert_equals ( tool . description , tool2 . description );
assert_equals ( json :: parse ( tool . parameters ). dump ( 2 ), json :: parse ( tool2 . parameters ). dump ( 2 ));
}
2026-03-06 21:01:00 +01:00
assert_equals ( std :: string ( "[ \n "
" { \n "
" \" type \" : \" function \" , \n "
" \" function \" : { \n "
" \" name \" : \" special_function \" , \n "
" \" description \" : \" I'm special \" , \n "
" \" parameters \" : { \n "
" \" type \" : \" object \" , \n "
" \" properties \" : { \n "
" \" arg1 \" : { \n "
" \" type \" : \" integer \" , \n "
" \" description \" : \" The arg. \"\n "
" } \n "
" }, \n "
" \" required \" : [ \n "
" \" arg1 \"\n "
" ] \n "
" } \n "
" } \n "
" } \n "
"]" ),
common_chat_tools_to_json_oaicompat ({ special_function_tool }). dump ( 2 ));
2025-02-18 18:03:23 +00:00
}
2026-04-22 10:28:45 +02:00
static void test_convert_responses_to_chatcmpl () {
LOG_DBG ( "%s \n " , __func__ );
// Test basic conversion with input messages (user/assistant alternating)
{
json input = json :: parse ( R "({
"input" : [
{
"type" : "message" ,
"role" : "user" ,
"content" : "hi wassup"
},
{
"type" : "message" ,
"role" : "assistant" ,
"content" : "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?"
},
{
"type" : "message" ,
"role" : "user" ,
"content" : "hi"
}
],
"model" : "gpt-5-mini" ,
"stream" : false ,
"text" : {},
"reasoning" : {
"effort" : "medium"
}
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
// Verify messages were converted correctly
assert_equals ( true , result . contains ( "messages" ));
assert_equals ( true , result . at ( "messages" ). is_array ());
assert_equals (( size_t ) 3 , result . at ( "messages" ). size ());
// Check first message (user)
const auto & msg0 = result . at ( "messages" )[ 0 ];
assert_equals ( std :: string ( "user" ), msg0 . at ( "role" ). get < std :: string > ());
assert_equals ( true , msg0 . at ( "content" ). is_array ());
assert_equals ( std :: string ( "text" ), msg0 . at ( "content" )[ 0 ]. at ( "type" ). get < std :: string > ());
assert_equals ( std :: string ( "hi wassup" ), msg0 . at ( "content" )[ 0 ]. at ( "text" ). get < std :: string > ());
// Check second message (assistant)
const auto & msg1 = result . at ( "messages" )[ 1 ];
assert_equals ( std :: string ( "assistant" ), msg1 . at ( "role" ). get < std :: string > ());
assert_equals ( true , msg1 . at ( "content" ). is_array ());
assert_equals ( std :: string ( "text" ), msg1 . at ( "content" )[ 0 ]. at ( "type" ). get < std :: string > ());
assert_equals ( std :: string ( "Hey! 👋 Not much, just here ready to chat. What's up with you? Anything I can help you with today?" ), msg1 . at ( "content" )[ 0 ]. at ( "text" ). get < std :: string > ());
// Check third message (user)
const auto & msg2 = result . at ( "messages" )[ 2 ];
assert_equals ( std :: string ( "user" ), msg2 . at ( "role" ). get < std :: string > ());
assert_equals ( true , msg2 . at ( "content" ). is_array ());
assert_equals ( std :: string ( "text" ), msg2 . at ( "content" )[ 0 ]. at ( "type" ). get < std :: string > ());
assert_equals ( std :: string ( "hi" ), msg2 . at ( "content" )[ 0 ]. at ( "text" ). get < std :: string > ());
// Verify other fields preserved
assert_equals ( std :: string ( "gpt-5-mini" ), result . at ( "model" ). get < std :: string > ());
assert_equals ( false , result . at ( "stream" ). get < bool > ());
}
// Test string input
{
json input = json :: parse ( R "({
"input" : "Hello, world!" ,
"model" : "test-model"
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
assert_equals (( size_t ) 1 , result . at ( "messages" ). size ());
const auto & msg = result . at ( "messages" )[ 0 ];
assert_equals ( std :: string ( "user" ), msg . at ( "role" ). get < std :: string > ());
assert_equals ( std :: string ( "Hello, world!" ), msg . at ( "content" ). get < std :: string > ());
}
// Test with instructions (system message)
{
json input = json :: parse ( R "({
"input" : "Hello" ,
"instructions" : "You are a helpful assistant." ,
"model" : "test-model"
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
assert_equals (( size_t ) 2 , result . at ( "messages" ). size ());
const auto & sys_msg = result . at ( "messages" )[ 0 ];
assert_equals ( std :: string ( "system" ), sys_msg . at ( "role" ). get < std :: string > ());
assert_equals ( std :: string ( "You are a helpful assistant." ), sys_msg . at ( "content" ). get < std :: string > ());
}
// Test with max_output_tokens conversion
{
json input = json :: parse ( R "({
"input" : "Hello" ,
"model" : "test-model" ,
"max_output_tokens" : 100
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
assert_equals ( true , result . contains ( "max_tokens" ));
assert_equals ( false , result . contains ( "max_output_tokens" ));
assert_equals ( 100 , result . at ( "max_tokens" ). get < int > ());
}
2026-05-15 00:03:24 -07:00
// Test mixed Responses tools: convert only function tools
{
json input = json :: parse ( R "({
"input" : "Hello" ,
"model" : "test-model" ,
"tools" : [
{
"type" : "web_search"
},
{
"type" : "function" ,
"name" : "get_weather" ,
"description" : "Get weather for a location" ,
"parameters" : {
"type" : "object" ,
"properties" : {
"location" : {
"type" : "string"
}
},
"required" : [ "location" ]
}
},
{
"type" : "image_generation"
},
{
"type" : "mcp" ,
"server_label" : "test-server"
},
{
"type" : "namespace" ,
"name" : "browser"
}
]
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
assert_equals ( true , result . contains ( "tools" ));
assert_equals ( true , result . at ( "tools" ). is_array ());
assert_equals (( size_t ) 1 , result . at ( "tools" ). size ());
const auto & tool = result . at ( "tools" )[ 0 ];
assert_equals ( std :: string ( "function" ), tool . at ( "type" ). get < std :: string > ());
assert_equals ( std :: string ( "get_weather" ), tool . at ( "function" ). at ( "name" ). get < std :: string > ());
assert_equals ( true , tool . at ( "function" ). at ( "strict" ). get < bool > ());
}
// Test non-function Responses tools are ignored
{
json input = json :: parse ( R "({
"input" : "Hello" ,
"model" : "test-model" ,
"tools" : [
{
"type" : "web_search"
},
{
"type" : "image_generation"
},
{
"type" : "mcp" ,
"server_label" : "test-server"
},
{
"type" : "namespace" ,
"name" : "browser"
}
]
}) ");
json result = server_chat_convert_responses_to_chatcmpl ( input );
assert_equals ( false , result . contains ( "tools" ));
}
2026-04-22 10:28:45 +02:00
}
2026-06-06 22:39:21 +02:00
// Shared LFM2 parser cases - all variants use one output format and parser
static void test_lfm2_parser ( const std :: string & template_path , bool detailed_debug ) {
auto tst = peg_tester ( template_path , detailed_debug );
// Basic content only
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
// Single tool call without reasoning
tst . test ( "<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call with string argument
tst . test ( "<|tool_call_start|>[get_time(city= \" XYZCITY \" )]<|tool_call_end|>" )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls ( "get_time" , "{ \" city \" : \" XYZCITY \" }" ))
. run ();
// Python literals become JSON
tst . test ( "<|tool_call_start|>[toggle(enabled=True)]<|tool_call_end|>" )
. tools ({ toggle_tool })
. expect ( message_with_tool_calls ( "toggle" , R "({" enabled ": true})" ))
. run ();
tst . test ( "<|tool_call_start|>[set_nullable(value=None)]<|tool_call_end|>" )
. tools ({ nullable_tool })
. expect ( message_with_tool_calls ( "set_nullable" , R "({" value ": null})" ))
. run ();
// Nested Python literal
tst . test ( "<|tool_call_start|>[set_config(config={ \" enabled \" : True, \" count \" : 3})]<|tool_call_end|>" )
. tools ({ config_tool })
. expect ( message_with_tool_calls ( "set_config" , R "({" config ": {" enabled ": true, " count ": 3}})" ))
. run ();
// JSON literals are accepted too
tst . test ( "<|tool_call_start|>[set_config(config={ \" enabled \" : true, \" note \" : null})]<|tool_call_end|>" )
. tools ({ config_tool })
. expect ( message_with_tool_calls ( "set_config" , R "({" config ": {" enabled ": true, " note ": null}})" ))
. run ();
// Dotted function name with structured args
tst . test ( "<|tool_call_start|>[Calendar.create_event(title= \" demo \" , participants=[ \" Alice \" , \" Bob \" ], "
"metadata={ \" priority \" : \" high \" , \" reminder \" : true})]<|tool_call_end|>" )
. tools ({ calendar_create_event_tool })
. expect ( message_with_tool_calls (
"Calendar.create_event" ,
R "({" title ": " demo ", " participants ": [" Alice ", " Bob "], " metadata ": {" priority ": " high ", " reminder ": true}})" ))
. run ();
// Markdown links stay content
tst . test ( "Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org)." )
. tools ({ get_time_tool })
. expect ( simple_assist_msg ( "Use this format: [link text](url). Example: [Wikipedia](https://www.wikipedia.org)." ))
. run ();
2026-06-15 22:10:09 +02:00
// Python tool with multiline code in string: the \n in the literal decodes to a real
// newline, emitted as a JSON \n escape (not a doubled backslash).
2026-06-06 22:39:21 +02:00
tst . test ( "<|tool_call_start|>[python(code= \" def hello(): \\ n print('hey') \" )]<|tool_call_end|>" )
. tools ({ python_tool })
. expect_tool_calls ({
2026-06-15 22:10:09 +02:00
{ "python" , R "#({" code ": " def hello () : \ n print ( ' hey ' ) "})#" , "" }
})
. run ();
// String escape sequences decode to their actual characters (newline + tab here),
// so a "write a two line file" style call produces real line breaks, not literal "\n".
tst . test ( "<|tool_call_start|>[python(code= \" First line \\ nSecond line \\ tindented \" )]<|tool_call_end|>" )
. tools ({ python_tool })
. expect_tool_calls ({
{ "python" , R "#({" code ": " First line \ nSecond line \ tindented "})#" , "" }
})
. run ();
// Escaped quotes inside a string argument survive the round-trip.
tst . test ( "<|tool_call_start|>[python(code= \" print( \\\" hi \\\" ) \" )]<|tool_call_end|>" )
. tools ({ python_tool })
. expect_tool_calls ({
{ "python" , R "#({" code ": " print ( \ "hi \" )" }) # ", "" }
2026-06-06 22:39:21 +02:00
})
. run ();
// Content before tool call (no reasoning)
tst . test ( "Let me check the time.<|tool_call_start|>[get_time(city= \" Paris \" )]<|tool_call_end|>" )
. tools ({ get_time_tool })
. expect ( message_with_reasoning_content_and_multiple_tool_calls (
"" , "Let me check the time." , { { "get_time" , "{ \" city \" : \" Paris \" }" } }
))
. run ();
// Multiple tool calls (parallel)
tst . test ( "<|tool_call_start|>[special_function(arg1=1), special_function_with_opt(arg1=1, arg2=2)]<|tool_call_end|>" )
. parallel_tool_calls ( true )
. tools ({ special_function_tool , special_function_tool_with_optional_param })
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
// Partial tool call (streaming)
tst . test ( "<|tool_call_start|>[special_function(arg1=" )
. tools ({ special_function_tool })
. is_partial ( true )
. expect ( simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : " ))
. run ();
// Tool call with empty arguments
tst . test ( "<|tool_call_start|>[empty_args()]<|tool_call_end|>" )
. tools ({ empty_args_tool })
. expect ( simple_assist_msg ( "" , "" , "empty_args" , "{}" ))
. run ();
}
2026-03-06 21:01:00 +01:00
static void test_template_output_peg_parsers ( bool detailed_debug ) {
LOG_DBG ( "%s \n " , __func__ );
2025-12-09 17:31:04 -06:00
// JSON schemas
const char * invoice_schema = R "({
"type" : "object" ,
"properties" : {
"amount" : { "type" : "number" },
"date" : { "type" : "string" }
}
}) ";
2026-06-15 08:27:10 +02:00
const char * const_schema = R "({
"const" : "42"
}) ";
2026-03-28 20:41:32 +01:00
{
// Qwen3.5 (basically same as Nemotron, but keeping separate tests just in case)
auto tst = peg_tester ( "models/templates/Qwen3.5-4B.jinja" , detailed_debug );
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n Hello, world! \n What's up?" )
2026-03-28 20:41:32 +01:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. expect ( message_assist_thoughts )
. run ();
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n Hello, world! \n What's up?" )
2026-03-28 20:41:32 +01:00
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_NONE )
2026-04-25 21:24:13 +02:00
. expect_content ( "<think> \n I'm \n thinking \n </think> \n\n Hello, world! \n What's up?" )
2026-03-28 20:41:32 +01:00
. run ();
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
tst . test (
2026-04-25 21:24:13 +02:00
"I'm \n thinking \n </think> \n\n "
2026-03-28 20:41:32 +01:00
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call> \n "
"<tool_call> \n "
"<function=special_function_with_opt> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"<parameter=arg2> \n 2 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
tst . test (
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Hello, world! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
python_tool
})
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , {} },
})
. run ();
2026-06-15 15:27:47 +02:00
tst . test (
"<tool_call> \n "
"<function=edit> \n "
"<parameter=filename> \n "
"foo.c \n "
"</parameter> \n "
"<parameter=oldString> \n "
"#iclunde \n "
"</parameter> \n "
"<parameter=newString> \n "
"#include \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
edit_tool
})
. expect_tool_calls ({
{ "edit" , "{ \" filename \" : \" foo.c \" , \" oldString \" : \" #iclunde \" , \" newString \" : \" #include \" }" , {} },
})
. run ();
// a parameter value that itself ends in a newline (e.g. a source file with a
// trailing newline). The structural delimiter is "\n</parameter>\n", so the value
// "#include\n" renders as "...#include\n\n</parameter>\n". The trailing newline must
// be preserved faithfully (no stripping), and the generated grammar must admit a
// value ending on a delimiter prefix. Regression test for gbnf_excluding_pattern.
tst . test (
"<tool_call> \n "
"<function=edit> \n "
"<parameter=filename> \n "
"foo.c \n "
"</parameter> \n "
"<parameter=oldString> \n "
"#iclunde \n "
"</parameter> \n "
"<parameter=newString> \n "
"#include \n "
" \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
edit_tool
})
. expect_tool_calls ({
{ "edit" , "{ \" filename \" : \" foo.c \" , \" oldString \" : \" #iclunde \" , \" newString \" : \" #include \\ n \" }" , {} },
})
. run ();
2026-06-15 08:27:10 +02:00
// test code that starts with indent
tst . test (
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
" print( \" Hello, world! \" ) \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
python_tool
})
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" print( \\\" Hello, world! \\\" ) \" }" , {} },
})
. run ();
2026-03-28 20:41:32 +01:00
tst . test (
"I need to output the invoice details in JSON \n "
2026-04-25 21:24:13 +02:00
"</think> \n\n "
2026-03-28 20:41:32 +01:00
R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. json_schema ( invoice_schema )
. expect_reasoning ( "I need to output the invoice details in JSON" )
. expect_content ( R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
. run ();
// tool call segment in reasoning
tst . test (
"Let's call a tool: <tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Not the real call! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-04-25 21:24:13 +02:00
"</tool_call> \n </think> \n\n "
2026-03-28 20:41:32 +01:00
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Hello, world! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-05-04 13:18:11 +02:00
"</tool_call>" )
2026-03-28 20:41:32 +01:00
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
python_tool
})
2026-05-04 13:18:11 +02:00
. expect_reasoning (
"Let's call a tool: <tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Not the real call! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
2026-03-28 20:41:32 +01:00
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , {} },
})
. run ();
// No args tool
tst . test (
"<tool_call> \n "
"<function=empty_args> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ empty_args_tool })
. expect ( message_with_tool_calls ( "empty_args" , "{}" ))
. run ();
// No args tool with no properties defined
tst . test (
"<tool_call> \n "
"<function=empty_args_no_props> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ empty_args_tool_no_properties })
. expect ( message_with_tool_calls ( "empty_args_no_props" , "{}" ))
. run ();
2026-05-04 13:18:11 +02:00
// Edge cases when reasoning traces are not sent
tst . test (
"<think> \n\n </think> \n\n "
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
special_function_tool
})
. expect_reasoning ( "<think> \n\n " )
. expect_tool_calls ({ { "special_function" , "{ \" arg1 \" : 1}" , "" } })
. run ();
tst . test (
"</think> \n\n "
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
special_function_tool
})
. expect_reasoning ( "" )
. expect_tool_calls ({ { "special_function" , "{ \" arg1 \" : 1}" , "" } })
. run ();
tst . test (
"</think> \n\n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
run_in_terminal_tool
})
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"</think> \n\n "
"Let me inspect the current directory. \n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
run_in_terminal_tool
})
. expect_content ( "Let me inspect the current directory. \n " )
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"</think> \n\n "
"Let me inspect the current directory. \n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
run_in_terminal_tool
})
. tool_choice ( COMMON_CHAT_TOOL_CHOICE_REQUIRED )
. expect_content ( "Let me inspect the current directory. \n " )
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"I should inspect the directory. \n "
"</think> \n\n "
"Let me inspect it now. \n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
run_in_terminal_tool
})
. expect_reasoning ( "I should inspect the directory." )
. expect_content ( "Let me inspect it now. \n " )
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"I might call <tool_call> later, but I am still thinking. \n "
"</think> \n\n "
"Final answer without tools." )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({ run_in_terminal_tool })
. expect_reasoning ( "I might call <tool_call> later, but I am still thinking." )
. expect_content ( "Final answer without tools." )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n\n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-05-04 13:18:11 +02:00
{
common_chat_msg user_start ;
user_start . role = "user" ;
user_start . content = "Create a todo list, then inspect the repository." ;
common_chat_msg assistant_todos =
simple_assist_msg ( "" , "" , "manage_todo_list" ,
R "({" todos ":[{" item ":" Inspect repository "," selected ":false}]})" , "call_todos" );
common_chat_msg tool_result ;
tool_result . role = "tool" ;
tool_result . content = "Successfully wrote todo list" ;
tool_result . tool_call_id = "call_todos" ;
common_chat_msg user_continue ;
user_continue . role = "user" ;
user_continue . content = "Proceed." ;
tst . test (
"I need to run a terminal command. \n "
"</think> \n\n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
manage_todo_list_tool , run_in_terminal_tool
})
. messages ({ user_start , assistant_todos , tool_result , user_continue })
. expect_reasoning ( "I need to run a terminal command." )
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"I need to run a terminal command. \n "
"</think> \n\n "
"Let me inspect the current directory. \n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
manage_todo_list_tool , run_in_terminal_tool
})
. tool_choice ( COMMON_CHAT_TOOL_CHOICE_REQUIRED )
. messages ({ user_start , assistant_todos , tool_result , user_continue })
. expect_reasoning ( "I need to run a terminal command." )
. expect_content ( "Let me inspect the current directory. \n " )
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
tst . test (
"</think> \n\n "
"<tool_call> \n "
"<function=run_in_terminal> \n "
"<parameter=command> \n "
"pwd \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({
manage_todo_list_tool , run_in_terminal_tool
})
. messages ({ user_start , assistant_todos , tool_result , user_continue })
. expect_tool_calls ({
{ "run_in_terminal" , R "({" command ": " pwd "})" , {} },
})
. run ();
}
2026-03-28 20:41:32 +01:00
}
2025-12-09 17:31:04 -06:00
{
// Ministral-3-14B-Reasoning-2512
2026-03-06 21:01:00 +01:00
auto tst = peg_tester ( "models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja" , detailed_debug );
2025-12-09 17:31:04 -06:00
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "[THINK]I'm \n thinking[/THINK]Hello, world! \n What's up?" )
. expect_content ( "[THINK]I'm \n thinking[/THINK]Hello, world! \n What's up?" )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "[THINK]I'm \n thinking[/THINK]Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
2026-03-06 21:01:00 +01:00
. expect ( message_assist_thoughts )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( R "([TOOL_CALLS]special_function[ARGS]{" arg1 ":1})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"[THINK]I'm \n thinking[/THINK]"
R "([TOOL_CALLS]special_function[ARGS]{" arg1 ":1})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( R "([TOOL_CALLS]special_function[ARGS]{" arg1 ": 1})"
R "([TOOL_CALLS]special_function_with_opt[ARGS]{" arg1 ": 1, " arg2 ": 2})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2025-12-09 17:31:04 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"[THINK]I need to output the invoice details in JSON[/THINK]"
"```json \n "
R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})"
" \n ```" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. json_schema ( invoice_schema )
. expect_reasoning ( "I need to output the invoice details in JSON" )
. expect_content ( R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
. run ();
2026-03-27 12:30:40 -05:00
// fake tool call marker in reasoning
tst . test (
"[THINK]Let me think about [TOOL_CALLS]special_function[ARGS]{ \" arg1 \" :1} and more[/THINK]"
R "([TOOL_CALLS]special_function[ARGS]{" arg1 ": 1})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. tools ({ special_function_tool })
. expect_reasoning ( "Let me think about [TOOL_CALLS]special_function[ARGS]{ \" arg1 \" :1} and more" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-27 12:30:40 -05:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking[/THINK]Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-02-20 16:22:22 -06:00
}
2025-12-16 04:05:23 -06:00
{
// NVIDIA Nemotron-3 Nano
2026-03-06 21:01:00 +01:00
auto tst = peg_tester ( "models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja" , detailed_debug );
2025-12-16 04:05:23 -06:00
2026-03-19 16:58:21 +01:00
tst . test ( "Hello, world! \n What's up?" ).
enable_thinking ( false ).
reasoning_format ( COMMON_REASONING_FORMAT_AUTO ).
expect ( message_assist ). run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "I'm \n thinking \n </think> \n Hello, world! \n What's up?" )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
2026-03-06 21:01:00 +01:00
. reasoning_format ( COMMON_REASONING_FORMAT_NONE )
2026-03-21 00:19:04 +01:00
. expect_content ( "<think> \n I'm \n thinking \n </think> \n Hello, world! \n What's up?" )
2026-03-06 21:01:00 +01:00
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "I'm \n thinking \n </think> \n Hello, world! \n What's up?" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist_thoughts )
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n " )
2026-03-06 21:01:00 +01:00
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"I'm \n thinking \n </think> \n "
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n " )
2026-03-06 21:01:00 +01:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"</function> \n "
"</tool_call> \n "
"<tool_call> \n "
"<function=special_function_with_opt> \n "
"<parameter=arg1> \n 1 \n </parameter> \n "
"<parameter=arg2> \n 2 \n </parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n " )
2026-03-06 21:01:00 +01:00
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Hello, world! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n " )
2026-03-06 21:01:00 +01:00
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
python_tool
})
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , {} },
})
. run ();
2025-12-16 04:05:23 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"I need to output the invoice details in JSON \n "
"</think> \n "
R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. json_schema ( invoice_schema )
. expect_reasoning ( "I need to output the invoice details in JSON" )
. expect_content ( R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
. run ();
2026-03-27 12:30:40 -05:00
// tool call segment in reasoning
tst . test (
"Let's call a tool: <tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Not the real call! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-04-25 21:24:13 +02:00
"</tool_call> \n </think> \n "
2026-03-27 12:30:40 -05:00
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Hello, world! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n "
2026-03-27 12:30:40 -05:00
)
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
python_tool
})
. expect_reasoning ( "Let's call a tool: <tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Not the real call! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
2026-04-19 06:28:35 -05:00
"</tool_call> \n " )
2026-03-27 12:30:40 -05:00
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , {} },
})
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2025-12-16 04:05:23 -06:00
}
2026-02-19 13:40:52 -08:00
{
2026-03-06 21:01:00 +01:00
// CohereForAI Command-R 7B (2024-tool_use)
auto tst = peg_tester ( "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" , detailed_debug );
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test ( "<|START_RESPONSE|>Hello, world! \n What's up?<|END_RESPONSE|>" ). expect ( message_assist ). run ();
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test (
"<|START_THINKING|>I'm \n thinking<|END_THINKING|>"
"<|START_RESPONSE|>Hello, world! \n What's up?<|END_RESPONSE|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test (
"<|START_THINKING|>I'm \n thinking<|END_THINKING|>"
"<|START_RESPONSE|>Hello, world! \n What's up?<|END_RESPONSE|>" )
. expect ( message_assist_thoughts_unparsed_r7b )
. run ();
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test (
"<|START_THINKING|>I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_thoughts_call_idx )
. run ();
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test (
"<|START_THINKING|>I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , " )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. is_partial ( true )
. expect ( message_assist_thoughts_partial_call )
. run ();
2026-02-19 13:40:52 -08:00
2026-03-06 21:01:00 +01:00
tst . test (
"<|START_THINKING|><|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_call_idx )
. run ();
2026-02-19 13:40:52 -08:00
}
2026-06-14 20:17:40 +02:00
{
// Cohere2 MoE (North Code) - dedicated parser.
// Marker-wrapped format: <|START_THINKING|>...<|END_THINKING|> then either
// <|START_TEXT|>...<|END_TEXT|> (content) or <|START_ACTION|>[json]<|END_ACTION|> (tools).
// The generation prompt forces a leading <|START_THINKING|>, so model output begins inside
// the thinking block: test inputs start with the reasoning body, not the <|START_THINKING|> tag.
auto tst = peg_tester ( "models/templates/Cohere2MoE.jinja" , detailed_debug );
// Content with reasoning, extracted.
tst . test ( "I'm \n thinking<|END_THINKING|><|START_TEXT|>Hello, world! \n What's up?<|END_TEXT|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
// Content with reasoning, reasoning_format=NONE -> thinking kept inline in content (markers preserved).
tst . test ( "I'm \n thinking<|END_THINKING|><|START_TEXT|>Hello, world! \n What's up?<|END_TEXT|>" )
. expect ( message_assist_thoughts_unparsed_r7b )
. run ();
// Content with empty thinking block.
tst . test ( "<|END_THINKING|><|START_TEXT|>Hello, world! \n What's up?<|END_TEXT|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist )
. run ();
// Single tool call with reasoning.
tst . test (
"I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_thoughts_call_idx )
. run ();
// Single tool call, empty thinking block (no reasoning content).
tst . test (
"<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_call_idx )
. run ();
// Tool call with an array argument (todo_list).
tst . test (
"<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" todo_list \" , \" parameters \" : { \" todos \" : [ \" buy milk \" , \" walk dog \" ]}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ todo_list })
. expect ( simple_assist_msg ( "" , "" , "todo_list" , "{ \" todos \" : [ \" buy milk \" , \" walk dog \" ]}" , "0" ))
. run ();
// Parallel tool calls with reasoning.
tst . test (
"I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}}, \n "
" { \" tool_call_id \" : \" 1 \" , \" tool_name \" : \" python \" , \" parameters \" : { \" code \" : \" print('hey') \" }} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. parallel_tool_calls ( true )
. tools ({ special_function_tool , python_tool })
. expect_reasoning ( "I'm \n thinking" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "0" },
{ "python" , "{ \" code \" : \" print('hey') \" }" , "1" },
})
. run ();
// Tools available but the model answers with content instead of calling a tool.
tst . test ( "I'm \n thinking<|END_THINKING|><|START_TEXT|>Hello, world! \n What's up?<|END_TEXT|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_thoughts )
. run ();
// Partial tool call (streaming): name/id resolved before arguments arrive.
tst . test (
"I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , " )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. is_partial ( true )
. expect ( message_assist_thoughts_partial_call )
. run ();
}
2026-01-29 09:06:15 -06:00
{
2026-03-06 21:01:00 +01:00
// Google Gemma 2 2B - does not support tool calling
auto tst = peg_tester ( "models/templates/google-gemma-2-2b-it.jinja" );
2026-01-29 09:06:15 -06:00
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world!" ). expect ( simple_assist_msg ( "Hello, world!" )). expect_reconstruction (). run ();
2026-01-29 09:06:15 -06:00
2026-03-28 07:29:26 +01:00
tst . test ( "Line 1 \n Line 2 \n Line 3" ). expect ( simple_assist_msg ( "Line 1 \n Line 2 \n Line 3" )). expect_reconstruction (). run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
2026-01-29 09:06:15 -06:00
2026-04-02 23:31:02 +02:00
{
// Google Gemma 4 (tool calling with Gemma4 dict format)
2026-04-04 13:39:00 -05:00
auto tst = peg_tester ( "models/templates/google-gemma-4-31B-it.jinja" );
2026-04-02 23:31:02 +02:00
tst . test ( "Hello, world!" ). expect ( simple_assist_msg ( "Hello, world!" )). run ();
2026-04-04 13:39:00 -05:00
// Reasoning and content
tst . test (
"<|channel>thought \n I'm \n thinking<channel|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist_thoughts )
. run ();
2026-04-10 05:49:14 -04:00
// Empty reasoning (budget=0: sampler forces end tag before newline)
tst . test (
"<|channel>thought<channel|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( simple_assist_msg ( "Hello, world! \n What's up?" , "" ))
. run ();
2026-04-04 13:39:00 -05:00
// Reasoning and content with reasoning_format = none
tst . test (
"<|channel>thought \n I'm \n thinking<channel|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_NONE )
. expect_content ( "<|channel>thought \n I'm \n thinking<channel|>Hello, world! \n What's up?" )
. run ();
2026-04-02 23:31:02 +02:00
// Simple tool call with string argument
tst . test (
"<|tool_call>call:get_time{city:<| \" |>London<| \" |>}<tool_call|>" )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls ( "get_time" , R "({" city ": " London "})" ))
. run ();
// Tool call with string argument containing special chars
tst . test (
"<|tool_call>call:get_time{city:<| \" |>San Francisco<| \" |>}<tool_call|>" )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls ( "get_time" , R "({" city ": " San Francisco "})" ))
. run ();
// Tool call with empty args
tst . test (
"<|tool_call>call:empty_args{}<tool_call|>" )
. tools ({ empty_args_tool })
. expect ( message_with_tool_calls ( "empty_args" , "{}" ))
. run ();
// Tool call with string and content
tst . test (
"Hello, world! \n What's up?<|tool_call>call:get_time{city:<| \" |>Paris<| \" |>}<tool_call|>" )
. tools ({ get_time_tool })
. expect ( message_with_content_and_tool_call ( "Hello, world! \n What's up?" , "get_time" , R "({" city ": " Paris "})" ))
. run ();
// Parallel tool calls
tst . test (
"<|tool_call>call:get_time{city:<| \" |>London<| \" |>}<tool_call|>"
"<|tool_call>call:get_weather{city:<| \" |>Paris<| \" |>}<tool_call|>" )
. tools ({ get_time_tool , get_weather_tool })
. parallel_tool_calls ( true )
. expect_tool_calls ({
{ "get_time" , R "({" city ": " London "})" , "" },
{ "get_weather" , R "({" city ": " Paris "})" , "" },
})
. run ();
// Tool call with integer argument (number type)
tst . test (
"<|tool_call>call:special_function{arg1:42}<tool_call|>" )
. tools ({ special_function_tool })
. expect ( message_with_tool_calls ( "special_function" , R "({" arg1 ": 42})" ))
. run ();
// Tool call with negative number argument
tst . test (
"<|tool_call>call:special_function{arg1:-7}<tool_call|>" )
. tools ({ special_function_tool })
. expect ( message_with_tool_calls ( "special_function" , R "({" arg1 ": -7})" ))
. run ();
// Tool call with decimal number argument
tst . test (
"<|tool_call>call:amount{orig:3.14}<tool_call|>" )
. tools ({ amount_tool })
. expect ( message_with_tool_calls ( "amount" , R "({" orig ": 3.14})" ))
. run ();
// Tool call with boolean argument (true)
tst . test (
"<|tool_call>call:toggle{enabled:true}<tool_call|>" )
. tools ({ toggle_tool })
. expect ( message_with_tool_calls ( "toggle" , R "({" enabled ": true})" ))
. run ();
// Tool call with boolean argument (false)
tst . test (
"<|tool_call>call:toggle{enabled:false}<tool_call|>" )
. tools ({ toggle_tool })
. expect ( message_with_tool_calls ( "toggle" , R "({" enabled ": false})" ))
. run ();
// Tool call with null argument
tst . test (
"<|tool_call>call:set_nullable{value:null}<tool_call|>" )
. tools ({ nullable_tool })
. expect ( message_with_tool_calls ( "set_nullable" , R "({" value ": null})" ))
. run ();
// Tool call with array argument (todo list)
tst . test (
"<|tool_call>call:todo_list{todos:[<| \" |>buy milk<| \" |>,<| \" |>walk dog<| \" |>]}<tool_call|>" )
. tools ({ todo_list })
. expect ( message_with_tool_calls ( "todo_list" , R "({" todos ":[" buy milk "," walk dog "]})" ))
. run ();
// Tool call with object/dict argument
tst . test (
"<|tool_call>call:set_config{config:{theme:<| \" |>dark<| \" |>,count:3}}<tool_call|>" )
. tools ({ config_tool })
. expect ( message_with_tool_calls ( "set_config" , R "({" config ":{" theme ":" dark "," count ":3}})" ))
. run ();
// Tool call with empty array
tst . test (
"<|tool_call>call:todo_list{todos:[]}<tool_call|>" )
. tools ({ todo_list })
. expect ( message_with_tool_calls ( "todo_list" , R "({" todos ":[]})" ))
. run ();
// Tool call with empty dict
tst . test (
"<|tool_call>call:set_config{config:{}}<tool_call|>" )
. tools ({ config_tool })
. expect ( message_with_tool_calls ( "set_config" , R "({" config ":{}})" ))
. run ();
// Tool call with scientific notation number
tst . test (
"<|tool_call>call:amount{orig:1.5e10}<tool_call|>" )
. tools ({ amount_tool })
. expect ( message_with_tool_calls ( "amount" , R "({" orig ": 1.5e10})" ))
. run ();
2026-04-13 18:18:18 -05:00
// Edge cases
tst . test (
"<|channel>thought \n <channel|>Hello, world! \n What's up?<channel|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist )
. run ();
tst . test (
"<|channel>thought \n <channel|>Hello, world! \n What's up?<|channel>thought \n <channel|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist )
. run ();
tst . test (
"<|channel>thought \n <channel|>Hello, world! \n What's up?<|channel>thought \n <channel|><channel|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist )
. run ();
tst . test (
"<|channel><|channel>thought \n <channel|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist )
. run ();
2026-04-27 17:36:59 +03:00
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking<channel|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-27 17:36:59 +03:00
{
// additional tests for https://github.com/ggml-org/llama.cpp/pull/21760
auto tmpls = read_templates ( "models/templates/google-gemma-4-31B-it.jinja" );
common_chat_msg tool_call_msg = simple_assist_msg (
"Let me check." , "" , "special_function" , "{ \" arg1 \" : 1}" , "c0" );
common_chat_msg tool_msg ;
tool_msg . role = "tool" ;
tool_msg . tool_name = "special_function" ;
tool_msg . tool_call_id = "c0" ;
tool_msg . content = "{ \" r \" : \" ok \" }" ;
{
common_chat_templates_inputs inputs ;
inputs . messages = { message_user , tool_call_msg , tool_msg };
inputs . tools = { special_function_tool };
inputs . add_generation_prompt = true ;
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
if ( ! string_ends_with ( params . prompt , "<turn|> \n <|turn>model \n " )) {
throw std :: runtime_error ( "Missing generation prompt for Gemma 4" );
}
}
{
common_chat_templates_inputs inputs ;
inputs . messages = { message_user , tool_call_msg , tool_msg };
inputs . tools = { special_function_tool };
inputs . add_generation_prompt = false ;
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
if ( string_ends_with ( params . prompt , "<|turn>model \n " )) {
throw std :: runtime_error ( "Gemma 4: generation prompt was modified despite add_generation_prompt=false" );
}
}
}
2026-04-02 23:31:02 +02:00
}
2026-03-06 21:01:00 +01:00
{
// Qwen-QwQ-32B (reasoning model)
auto tst = peg_tester ( "models/templates/Qwen-QwQ-32B.jinja" );
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
// QwQ always has thinking forced open - input starts after the <think>\n in the prompt
tst . test ( "Let me think about this... \n </think> \n The answer is 42." )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( simple_assist_msg ( "The answer is 42." , "Let me think about this..." ))
. run ();
2026-01-29 09:06:15 -06:00
2026-03-19 16:58:21 +01:00
tst . test ( "</think>Hello, world!" ). reasoning_format ( COMMON_REASONING_FORMAT_AUTO ). expect ( simple_assist_msg ( "Hello, world!" )). run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
// NousResearch-Hermes-2-Pro and Hermes-3 (tool calling models)
auto tst = peg_tester ( "models/templates/NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja" , detailed_debug );
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<tool_call> \n "
2026-04-15 10:51:50 +02:00
"{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}"
2026-03-06 21:01:00 +01:00
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"Hello, world! \n What's up?<tool_call> \n "
2026-04-15 10:51:50 +02:00
"{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}"
2026-03-06 21:01:00 +01:00
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call_content )
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
// Note: Hermes template doesn't support thinking/reasoning natively
// Note: We only support one tool calling format per template, no alternate formats
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
// Test simple content-only template
auto tst = peg_tester ( "models/templates/google-gemma-2-2b-it.jinja" , detailed_debug );
2026-01-29 09:06:15 -06:00
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
}
{
// IBM Granite (reasoning and tool calling model)
auto tst = peg_tester ( "models/templates/ibm-granite-granite-3.3-2B-Instruct.jinja" , detailed_debug );
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "<think>I'm \n thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
// TODO: pending support for WRAPPED_WITH_REASONING
// tst.test("<think>I'm\nthinking</think><response>Hello, world!\nWhat's up?</response>")
// .reasoning_format(COMMON_REASONING_FORMAT_DEEPSEEK)
// .expect(message_assist_thoughts)
// .run();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
2026-01-29 09:06:15 -06:00
2026-04-02 11:28:56 +02:00
{
// IBM Granite 4.0 (production template shared by h-tiny, h-small, micro)
// Uses <tool_call> XML tags for tool calls, tools in system message
auto tst = peg_tester ( "models/templates/ibm-granite-granite-4.0.jinja" , detailed_debug );
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
tst . test (
"<tool_call> \n "
"{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n "
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-02 11:28:56 +02:00
}
2026-05-28 13:13:33 +02:00
{
// IBM Granite 4.1 (same format as 4.0)
auto tst = peg_tester ( "models/templates/ibm-granite-granite-4.1.jinja" , detailed_debug );
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
tst . test (
"<tool_call> \n "
"{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n "
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
}
2026-03-06 21:01:00 +01:00
{
// ByteDance-Seed-OSS (reasoning and tool calling model)
auto tst = peg_tester ( "models/templates/ByteDance-Seed-OSS.jinja" , detailed_debug );
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test ( "<seed:think>I'm thinking about the answer</seed:think> \n Hello, world!" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( simple_assist_msg ( "Hello, world!" , "I'm thinking about the answer" ))
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<seed:tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1>1</parameter> \n "
"</function> \n "
"</seed:tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<seed:tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1>1</parameter> \n "
"</function> \n "
"</seed:tool_call> \n "
"<seed:tool_call> \n "
"<function=special_function_with_opt> \n "
"<parameter=arg1>1</parameter> \n "
"<parameter=arg2>2</parameter> \n "
"</function> \n "
"</seed:tool_call>" )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
tst . test (
"<seed:tool_call> \n "
"<function=todo_list> \n "
"<parameter=todos>[{ \" item \" : \" Check stuff \" , \" selected \" : false}, { \" item \" : \" Prepare stuff \" , \" selected \" : true}]</parameter> \n "
"</function> \n "
"</seed:tool_call>" )
. tools ({
todo_list
})
. expect_tool_calls ({
{ "todo_list" , "{ \" todos \" : [{ \" item \" : \" Check stuff \" , \" selected \" : false}, { \" item \" : \" Prepare stuff \" , \" selected \" : true}]}" , {} },
})
. run ();
2026-01-29 09:06:15 -06:00
2026-03-06 21:01:00 +01:00
// tool call with inside quotes
tst . test (
"<seed:tool_call> \n "
"<function=edit> \n "
2026-06-15 08:27:10 +02:00
"<parameter=filename>"
"foo.cpp"
2026-03-06 21:01:00 +01:00
"</parameter> \n "
"<parameter=oldString>"
"def foo(arg = \" 14 \" ): \n "
" return arg + \" bar \"\n "
"</parameter> \n "
"<parameter=newString>"
"def foo(arg = \" 15 \" ): \n "
" pass \n "
"</parameter> \n "
"</function> \n "
"</seed:tool_call>" )
. tools ({
edit_tool
})
. expect_tool_calls ({
{ "edit" , "{ \" filename \" : \" foo.cpp \" , "
" \" oldString \" : \" def foo(arg = \\\" 14 \\\" ): \\ n return arg + \\\" bar \\\"\\ n \" , "
" \" newString \" : \" def foo(arg = \\\" 15 \\\" ): \\ n pass \\ n \" }" , {}
}
})
. run ();
2026-04-03 11:51:23 -04:00
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</seed:think> \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
// Qwen3-Coder (tool calling with XML-style format)
auto tst = peg_tester ( "models/templates/Qwen3-Coder.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n "
"1 \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
tst . test (
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n "
"1 \n "
"</parameter> \n "
"</function> \n "
"</tool_call> \n "
"<tool_call> \n "
"<function=special_function_with_opt> \n "
"<parameter=arg1> \n "
"1 \n "
"</parameter> \n "
"<parameter=arg2> \n "
"2 \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Test with code content (multiline)
tst . test (
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"def hello(): \n "
" print( \" Hello, world! \" ) \n "
" \n "
"hello() \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({
python_tool
})
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Test with code content (asian unicode chars)
tst . test (
"<tool_call> \n "
"<function=python> \n "
"<parameter=code> \n "
"格 \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({
python_tool
})
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" 格 \" }" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Test with HTML tag content
tst . test (
"<tool_call> \n "
"<function=html> \n "
"<parameter=markup> \n "
"<html> \n "
" <head> \n "
" <title>Hello!</title> \n "
" </head> \n "
"</html> \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({
html_tool
})
. expect_tool_calls ({
{ "html" , "{ \" markup \" : \" <html> \\ n <head> \\ n <title>Hello!</title> \\ n </head> \\ n</html> \" }" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Test with TODO list (array of objects)
tst . test (
"<tool_call> \n "
"<function=todo_list> \n "
"<parameter=todos> \n "
"[{ \" item \" : \" Check stuff \" , \" selected \" : false}, { \" item \" : \" Prepare stuff \" , \" selected \" : true}] \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({
todo_list
})
. expect_tool_calls ({
{ "todo_list" , "{ \" todos \" : [{ \" item \" : \" Check stuff \" , \" selected \" : false}, { \" item \" : \" Prepare stuff \" , \" selected \" : true}]}" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-03-06 22:34:15 +01:00
// Test flexible optional argument ordering (2 required + 4 optional, reversed optional order)
tst . test (
"<tool_call> \n "
"<function=tool_2req_4opt> \n "
"<parameter=req1> \n hello \n </parameter> \n "
"<parameter=req2> \n 42 \n </parameter> \n "
"<parameter=opt4> \n 100 \n </parameter> \n "
"<parameter=opt2> \n 200 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ tool_2req_4opt })
. expect_tool_calls ({
{ "tool_2req_4opt" , R "({" req1 ": " hello ", " req2 ": 42, " opt4 ": 100, " opt2 ": 200})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 22:34:15 +01:00
. run ();
// Test flexible optional argument ordering (2 required + 5 optional, reversed optional order)
tst . test (
"<tool_call> \n "
"<function=tool_2req_5opt> \n "
"<parameter=req1> \n world \n </parameter> \n "
"<parameter=req2> \n 7 \n </parameter> \n "
"<parameter=opt5> \n last \n </parameter> \n "
"<parameter=opt3> \n middle \n </parameter> \n "
"<parameter=opt1> \n first \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ tool_2req_5opt })
. expect_tool_calls ({
{ "tool_2req_5opt" , R "({" req1 ": " world ", " req2 ": 7, " opt5 ": " last ", " opt3 ": " middle ", " opt1 ": " first "})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 22:34:15 +01:00
. run ();
// Test flexible optional argument ordering (2 required + 5 optional, all 5 in shuffled order)
tst . test (
"<tool_call> \n "
"<function=tool_2req_5opt> \n "
"<parameter=req1> \n test \n </parameter> \n "
"<parameter=req2> \n 99 \n </parameter> \n "
"<parameter=opt3> \n c \n </parameter> \n "
"<parameter=opt1> \n a \n </parameter> \n "
"<parameter=opt5> \n e \n </parameter> \n "
"<parameter=opt4> \n 4 \n </parameter> \n "
"<parameter=opt2> \n 2 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ tool_2req_5opt })
. expect_tool_calls ({
{ "tool_2req_5opt" , R "({" req1 ": " test ", " req2 ": 99, " opt3 ": " c ", " opt1 ": " a ", " opt5 ": " e ", " opt4 ": 4, " opt2 ": 2})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 22:34:15 +01:00
. run ();
2026-04-03 11:51:23 -04:00
// nullable string type ["string", "null"]
tst . test (
"<tool_call> \n "
"<function=set_nullable_str> \n "
"<parameter=name> \n hello world \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ nullable_string_tool })
. expect_tool_calls ({
{ "set_nullable_str" , R "({" name ": " hello world "})" , {} },
})
. run ();
// nullable string with null first in type array ["null", "string"]
tst . test (
"<tool_call> \n "
"<function=set_nullable_str_nf> \n "
"<parameter=name> \n hello world \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ nullable_string_null_first_tool })
. expect_tool_calls ({
{ "set_nullable_str_nf" , R "({" name ": " hello world "})" , {} },
})
. run ();
// nullable integer type ["integer", "null"] - should use JSON value path, not string
tst . test (
"<tool_call> \n "
"<function=set_nullable_int> \n "
"<parameter=count> \n 42 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ nullable_int_tool })
. expect_tool_calls ({
{ "set_nullable_int" , R "({" count ": 42})" , {} },
})
. run ();
// enum without explicit type key - should infer string from enum values
tst . test (
"<tool_call> \n "
"<function=set_unit> \n "
"<parameter=unit> \n celsius \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. tools ({ enum_no_type_tool })
. expect_tool_calls ({
{ "set_unit" , R "({" unit ": " celsius "})" , {} },
})
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.1.jinja" , detailed_debug );
tst . test (
"<| tool▁calls▁begin| ><| tool▁call▁begin| >get_time<| tool▁sep| >{ \" city \" : "
" \" XYZCITY \" }<| tool▁call▁end| ><| tool▁calls▁end| >" )
. tools ({ get_time_tool })
2026-03-19 16:58:21 +01:00
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
2026-03-06 21:01:00 +01:00
. expect ( message_with_tool_calls ( "get_time" , "{ \" city \" : \" XYZCITY \" }" ))
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.1.jinja" , detailed_debug );
tst . test (
"REASONING</think><| tool▁calls▁begin| ><| tool▁call▁begin| >get_time<| tool▁sep| >{ \" city \" : "
" \" Tokyo \" }<| tool▁call▁end| ><| tool▁calls▁end| >" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls_and_reasoning ( "get_time" , "{ \" city \" : \" Tokyo \" }" , "REASONING" ))
. run ();
}
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.1.jinja" , detailed_debug );
tst . test (
"REASONING</think>CONTENT<| tool▁calls▁begin| ><| tool▁call▁begin| >get_time<| tool▁sep| >{ \" city \" : "
" \" Paris \" }<| tool▁call▁end| ><| tool▁call▁begin| >get_weather<| tool▁sep| >{ \" city \" : "
" \" Paris \" }<| tool▁call▁end| ><| tool▁calls▁end| >" )
. tools ({
get_time_tool , get_weather_tool
})
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. parallel_tool_calls ( true )
. expect ( message_with_reasoning_content_and_multiple_tool_calls (
"REASONING" , "CONTENT" ,
{ { "get_time" , "{ \" city \" : \" Paris \" }" }, { "get_weather" , "{ \" city \" : \" Paris \" }" } }))
. run ();
}
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.1.jinja" , detailed_debug );
tst . test ( "REASONING</think> \n CONTENT" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( simple_assist_msg ( "CONTENT" , "REASONING \n " ))
. run ();
}
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.1.jinja" , detailed_debug );
2026-03-19 16:58:21 +01:00
tst . test ( "CONTENT" ). enable_thinking ( false ). reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK ).
expect ( simple_assist_msg ( "CONTENT" , "" )). run ();
2026-03-06 21:01:00 +01:00
}
2026-04-13 22:23:53 +02:00
// DeepSeek V3.2 tests - format uses DSML markup:
// <| DSML| function_calls>
// <| DSML| invoke name="foo">
// <| DSML| parameter name="bar" string="true|false">value</| DSML| parameter>
// </| DSML| invoke>
// </| DSML| function_calls>
// Reasoning uses <think>...</think>. The generation prompt ends in <think> (thinking mode)
// or <think></think> (non-thinking mode).
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-V3.2.jinja" , detailed_debug );
// Pure content (non-thinking mode)
tst . test ( "Hello, world! \n What's up?" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist )
. run ();
// Thinking + content
tst . test ( "I'm \n thinking</think>Hello, world! \n What's up?" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
// Thinking + tool call (single, string param)
tst . test (
"Let me check the time</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" get_time \" > \n "
"<| DSML| parameter name= \" city \" string= \" true \" >Tokyo</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls_and_reasoning ( "get_time" , R "({" city ": " Tokyo "})" , "Let me check the time" ))
. run ();
// Tool call without reasoning (non-thinking mode), integer param (string="false")
tst . test (
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" special_function \" > \n "
"<| DSML| parameter name= \" arg1 \" string= \" false \" >1</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Multiple parallel tool calls with reasoning
tst . test (
"Calling both</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" get_time \" > \n "
"<| DSML| parameter name= \" city \" string= \" true \" >Paris</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"<| DSML| invoke name= \" get_weather \" > \n "
"<| DSML| parameter name= \" city \" string= \" true \" >Paris</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. parallel_tool_calls ( true )
. tools ({ get_time_tool , get_weather_tool })
. expect ( message_with_reasoning_content_and_multiple_tool_calls (
"Calling both" , "" ,
{ { "get_time" , R "({" city ": " Paris "})" }, { "get_weather" , R "({" city ": " Paris "})" } }))
. run ();
// Tool call with content before tool calls
tst . test (
"Thinking about it</think>"
"Let me call the function. \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" special_function \" > \n "
"<| DSML| parameter name= \" arg1 \" string= \" false \" >1</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect_reasoning ( "Thinking about it" )
. expect_content ( "Let me call the function." )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
})
. run ();
// Tool call with negative number
tst . test (
"Test negative</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" magic_int \" > \n "
"<| DSML| parameter name= \" ref \" string= \" false \" >-14</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ magic_int_tool })
. expect_reasoning ( "Test negative" )
. expect_tool_calls ({
{ "magic_int" , R "({" ref ": -14})" , {} },
})
. run ();
// Tool call with decimal number
tst . test (
"Test decimal</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" amount \" > \n "
"<| DSML| parameter name= \" orig \" string= \" false \" >3.14</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ amount_tool })
. expect_reasoning ( "Test decimal" )
. expect_tool_calls ({
{ "amount" , R "({" orig ": 3.14})" , {} },
})
. run ();
// Tool call with boolean
tst . test (
"Test boolean</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" toggle \" > \n "
"<| DSML| parameter name= \" enabled \" string= \" false \" >true</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ toggle_tool })
. expect_reasoning ( "Test boolean" )
. expect_tool_calls ({
{ "toggle" , R "({" enabled ": true})" , {} },
})
. run ();
// Tool call with array parameter (JSON-formatted)
tst . test (
"Test array</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" todo_list \" > \n "
"<| DSML| parameter name= \" todos \" string= \" false \" >[ \" buy milk \" , \" walk dog \" ]</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ todo_list })
. expect_reasoning ( "Test array" )
. expect_tool_calls ({
{ "todo_list" , R "({" todos ": [" buy milk ", " walk dog "]})" , {} },
})
. run ();
// Tool call with object parameter (JSON-formatted)
tst . test (
"Test object</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" set_config \" > \n "
"<| DSML| parameter name= \" config \" string= \" false \" >{ \" theme \" : \" dark \" , \" level \" :2}</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ config_tool })
. expect_reasoning ( "Test object" )
. expect_tool_calls ({
{ "set_config" , R "({" config ": {" theme ": " dark ", " level ": 2}})" , {} },
})
. run ();
// Edge case: empty reasoning
tst . test (
"</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" get_time \" > \n "
"<| DSML| parameter name= \" city \" string= \" true \" >XYZCITY</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls ( "get_time" , R "({" city ": " XYZCITY "})" ))
. run ();
// Edge case: tool call with multiple params (mixed types, string first)
tst . test (
"Multi-arg call</think> \n\n "
"<| DSML| function_calls> \n "
"<| DSML| invoke name= \" magic_int \" > \n "
"<| DSML| parameter name= \" ref \" string= \" false \" >42</| DSML| parameter> \n "
"<| DSML| parameter name= \" name \" string= \" true \" >foo bar</| DSML| parameter> \n "
"</| DSML| invoke> \n "
"</| DSML| function_calls>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ magic_int_tool })
. expect_reasoning ( "Multi-arg call" )
. expect_tool_calls ({
{ "magic_int" , R "({" ref ": 42, " name ": " foo bar "})" , {} },
})
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-13 22:23:53 +02:00
}
2026-03-06 21:01:00 +01:00
// GLM-4.6 tests - format: <tool_call>function_name\n<arg_key>...</arg_key>\n<arg_value>...</arg_value>\n</tool_call>
{
auto tst = peg_tester ( "models/templates/GLM-4.6.jinja" , detailed_debug );
tst . test (
"<tool_call>special_function \n "
"<arg_key>arg1</arg_key> \n <arg_value>1</arg_value> \n "
"</tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
}
// GLM-4.7-Flash tests - format: <tool_call>function_name<arg_key>...</arg_key><arg_value>...</arg_value></tool_call>
// Note: Template uses forced-open thinking mode (prompt ends with <think>)
{
auto tst = peg_tester ( "models/templates/GLM-4.7-Flash.jinja" , detailed_debug );
// Pure content (no reasoning)
tst . test ( "Hello, world! \n What's up?" )
. enable_thinking ( false )
. expect ( message_assist )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Reasoning with content (forced-open mode - input starts after <think>)
tst . test ( "I'm \n thinking</think>Hello, world! \n What's up?" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Tool call without reasoning
tst . test (
"<tool_call>special_function"
"<arg_key>arg1</arg_key><arg_value>1</arg_value>"
"</tool_call>" )
. enable_thinking ( false )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Tool call with reasoning (forced-open mode)
tst . test (
"I'm \n thinking</think>"
"<tool_call>special_function"
"<arg_key>arg1</arg_key><arg_value>1</arg_value>"
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
tst . test (
"<tool_call>special_function"
"<arg_key>arg1</arg_key><arg_value>1</arg_value>"
"</tool_call>"
"<tool_call>special_function_with_opt"
"<arg_key>arg1</arg_key><arg_value>1</arg_value>"
"<arg_key>arg2</arg_key><arg_value>2</arg_value>"
"</tool_call>" )
2026-03-19 16:58:21 +01:00
. enable_thinking ( false )
2026-03-06 21:01:00 +01:00
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-03-19 09:53:11 -07:00
// #20650: tool with no required args, model emits <tool_call>name</tool_call> with no arg tags.
{
static common_chat_tool no_args_tool {
"read_file_diff_md" , "Reads a file diff" ,
R "({" type ":" object "," properties ":{" review_id ":{" type ":" string "}," file_id ":{" type ":" string "}}})" ,
};
tst . test (
"Let me read the diff content."
"</think>"
"<tool_call>read_file_diff_md</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ no_args_tool })
. expect_reasoning ( "Let me read the diff content." )
. expect_tool_calls ({{ "read_file_diff_md" , "{}" , {} }})
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-19 09:53:11 -07:00
. run ();
}
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
2026-03-19 18:37:22 -07:00
// Verify the throw path produces a readable error message, not std::out_of_range.
// #20424 introduced effective_input = generation_prompt + input, but the throw
// uses input.substr(result.end) where result.end is in effective_input space.
{
2026-04-03 17:51:52 +02:00
if ( ! g_template_filter . empty () && std :: string ( "models/templates/GLM-4.7-Flash.jinja" ). find ( g_template_filter ) != std :: string :: npos ) {
auto tmpls = common_chat_templates_ptr (
common_chat_templates_init ( nullptr , read_file ( "models/templates/GLM-4.7-Flash.jinja" )));
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
static common_chat_tool weather_tool {
"get_weather" , "Get weather" ,
R "({" type ":" object "," properties ":{" city ":{" type ":" string "}}," required ":[" city "]})" ,
};
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
common_chat_templates_inputs inputs ;
inputs . tools = { weather_tool };
inputs . enable_thinking = true ;
inputs . reasoning_format = COMMON_REASONING_FORMAT_AUTO ;
inputs . add_generation_prompt = true ;
inputs . use_jinja = true ;
common_chat_msg msg ;
msg . role = "user" ;
msg . content = "get_weather" ;
inputs . messages = { msg };
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
common_peg_arena arena ;
arena . load ( params . parser );
common_chat_parser_params pp ( params );
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
// generation_prompt is non-empty for thinking models, so result.end
// will be offset by generation_prompt.size() into effective_input space.
assert ( ! pp . generation_prompt . empty ());
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
std :: string bad_input =
"Thinking. \n "
"</think>"
"<tool_call>get_weather"
"<arg_key>city</arg_key><arg_value>Tokyo</arg_value>"
"</tool_call> \n " ;
2026-03-19 18:37:22 -07:00
2026-04-03 17:51:52 +02:00
bool got_runtime_error = false ;
bool got_out_of_range = false ;
std :: string error_msg ;
try {
common_chat_peg_parse ( arena , bad_input , /*is_partial=*/ false , pp );
} catch ( const std :: out_of_range & e ) {
got_out_of_range = true ;
error_msg = e . what ();
} catch ( const std :: runtime_error & e ) {
got_runtime_error = true ;
error_msg = e . what ();
}
GGML_ASSERT ( ! got_out_of_range && "throw path crashed with out_of_range (input.substr in effective_input space)" );
GGML_ASSERT ( got_runtime_error && "throw path should produce std::runtime_error with parse position" );
2026-03-19 18:37:22 -07:00
}
}
2026-03-06 21:01:00 +01:00
// Kimi-K2-Thinking tests - custom parser
// Unique feature: tool call ID embeds function name as functions.<name>:<counter>
{
auto tst = peg_tester ( "models/templates/Kimi-K2-Thinking.jinja" , detailed_debug );
// Basic content only
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
// Single tool call
tst . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : 1}<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ special_function_tool })
. expect ( simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : 1}" , "functions.special_function:0" ))
. run ();
// Single tool call with reasoning
tst . test (
"<think>I'm thinking about this</think>"
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : 1}<|tool_call_end|><|tool_calls_section_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( simple_assist_msg ( "" , "I'm thinking about this" , "special_function" , "{ \" arg1 \" : 1}" , "functions.special_function:0" ))
. run ();
// Tool call with content
tst . test (
"Hello, world! \n What's up?"
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : 1}<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ special_function_tool })
. expect ( simple_assist_msg ( "Hello, world! \n What's up?" , "" , "special_function" , "{ \" arg1 \" : 1}" , "functions.special_function:0" ))
. run ();
// Multiple tool calls (parallel) - tests the indexing behavior
tst . test (
"<|tool_calls_section_begin|>"
"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{ \" arg1 \" : 1}<|tool_call_end|>"
"<|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{ \" arg1 \" : 1, \" arg2 \" : 2}<|tool_call_end|>"
"<|tool_calls_section_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "functions.special_function:0" },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , "functions.special_function_with_opt:1" },
})
. run ();
// Multiple tool calls with reasoning
tst . test (
"<think>I need to call two functions</think>"
"<|tool_calls_section_begin|>"
"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{ \" arg1 \" : 1}<|tool_call_end|>"
"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{ \" code \" : \" print('hey') \" }<|tool_call_end|>"
"<|tool_calls_section_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , python_tool
})
. expect_reasoning ( "I need to call two functions" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "functions.special_function:0" },
{ "python" , "{ \" code \" : \" print('hey') \" }" , "functions.python:1" },
})
. run ();
// Python tool with multiline code
tst . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.python:0<|tool_call_argument_begin|>"
"{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ python_tool })
. expect_tool_calls ({
{ "python" , "{ \" code \" : \" def hello(): \\ n print( \\\" Hello, world! \\\" ) \\ n \\ nhello() \" }" , "functions.python:0" },
})
. run ();
// Tool call with empty arguments
tst . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.empty_args:0<|tool_call_argument_begin|>"
"{}<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ empty_args_tool })
. expect ( simple_assist_msg ( "" , "" , "empty_args" , "{}" , "functions.empty_args:0" ))
. run ();
// Partial tool call (streaming)
tst . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : " )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. is_partial ( true )
. expect ( simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : " , "functions.special_function:0" ))
. run ();
// Three tool calls to verify counter continues incrementing
tst . test (
"<|tool_calls_section_begin|>"
"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{ \" arg1 \" : 1}<|tool_call_end|>"
"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{ \" code \" : \" print(1) \" }<|tool_call_end|>"
"<|tool_call_begin|>functions.html:2<|tool_call_argument_begin|>{ \" markup \" : \" <p>test</p> \" }<|tool_call_end|>"
"<|tool_calls_section_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , python_tool , html_tool
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "functions.special_function:0" },
{ "python" , "{ \" code \" : \" print(1) \" }" , "functions.python:1" },
{ "html" , "{ \" markup \" : \" <p>test</p> \" }" , "functions.html:2" },
})
. run ();
// Multiple tool calls with reasoning, call *inside thinking block*
tst . test (
"<think>I need to call two functions"
"<|tool_calls_section_begin|>"
"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{ \" arg1 \" : 1}<|tool_call_end|>"
"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{ \" code \" : \" print('hey') \" }<|tool_call_end|>"
"<|tool_calls_section_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , python_tool
})
. expect_reasoning ( "I need to call two functions" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "functions.special_function:0" },
{ "python" , "{ \" code \" : \" print('hey') \" }" , "functions.python:1" },
})
. run ();
// Multiple tool calls with reasoning, call *inside thinking block* and *without section markers or end markers
tst . test (
"<think>I need to call two functions"
"<|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{ \" arg1 \" : 1}"
"<|tool_call_begin|>functions.python:1<|tool_call_argument_begin|>{ \" code \" : \" print('hey') \" }" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , python_tool
})
. expect_reasoning ( "I need to call two functions" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "functions.special_function:0" },
{ "python" , "{ \" code \" : \" print('hey') \" }" , "functions.python:1" },
})
. run ();
// Real life test - execute_command
tst . test ( "<|tool_call_begin|>functions.execute_command:0<|tool_call_argument_begin|>{ \" command \" : \" ls -lah \" "
", \" cwd \" : \" /home/jarvis/development/exllamav3 \" , \" timeout \" : 10}" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. parallel_tool_calls ( true )
. tools ({
{
/* .name = */ "execute_command" ,
/* .description = */ "Execute shell command" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"command" : {
"type" : "string" ,
"description" : "Shell command to execute"
},
"cwd" : {
"type" : "string" ,
"description" : "Working directory"
},
"timeout" : {
"type" : "integer" ,
"description" : "The timeout in seconds"
}
},
"required" : [ "command" ]
}) "
}
}).
expect_tool_calls ({
{
"execute_command" ,
R "({" command ": " ls - lah ", " cwd ": " / home / jarvis / development / exllamav3 ", " timeout ": 10})" ,
"functions.execute_command:0"
}
})
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
auto kimi_id_special_func_tool_call =
simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : 1}" , "functions.special_function:0" );
// Kimi-K2 old template
auto tst = peg_tester ( "models/templates/moonshotai-Kimi-K2.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : 1}<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ special_function_tool })
. expect ( kimi_id_special_func_tool_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Kimi-K2-Instruct
auto tst2 = peg_tester ( "models/templates/Kimi-K2-Instruct.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst2 . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst2 . test (
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>"
"{ \" arg1 \" : 1}<|tool_call_end|><|tool_calls_section_end|>" )
. tools ({ special_function_tool })
. expect ( kimi_id_special_func_tool_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
2026-06-06 22:39:21 +02:00
for ( const char * tmpl : {
"models/templates/LFM2-8B-A1B.jinja" ,
"models/templates/LFM2.5-Instruct.jinja" ,
"models/templates/LFM2.5-8B-A1B.jinja" ,
}) {
test_lfm2_parser ( tmpl , detailed_debug );
}
// Thinking cases only apply to LFM2.5-8B-A1B, the one LFM2 template that emits <think>
2026-03-09 01:11:22 +01:00
{
2026-06-06 22:39:21 +02:00
auto tst = peg_tester ( "models/templates/LFM2.5-8B-A1B.jinja" , detailed_debug );
2026-03-09 01:11:22 +01:00
2026-06-06 22:39:21 +02:00
// Reasoning is parsed independent of enable_thinking
2026-03-09 01:11:22 +01:00
2026-06-06 22:39:21 +02:00
// Tool call with reasoning
2026-03-09 01:11:22 +01:00
tst . test ( "<think>I'm \n thinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
// Tool call with reasoning and content
tst . test ( "<think>I need to call a function</think>"
"Let me check the time.<|tool_call_start|>[get_time(city= \" Paris \" )]<|tool_call_end|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ get_time_tool })
. expect ( message_with_reasoning_content_and_multiple_tool_calls (
"I need to call a function" , "Let me check the time." , { { "get_time" , "{ \" city \" : \" Paris \" }" } }
))
. run ();
2026-06-06 22:39:21 +02:00
// Fake tool call marker inside reasoning is not parsed as a call
tst . test ( "<think>Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm</think>"
"<|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" )
2026-03-27 12:30:40 -05:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect_reasoning ( "Let me think about <|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|> hmm" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
})
. run ();
2026-05-17 07:36:05 -04:00
2026-06-06 22:39:21 +02:00
// enable_thinking=false still captures emitted reasoning
tst . test ( "<think>I'm \n thinking</think>Hello, world! \n What's up?" )
. enable_thinking ( false )
2026-05-17 07:36:05 -04:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-06-06 22:39:21 +02:00
. expect ( message_assist_thoughts )
2026-05-17 07:36:05 -04:00
. run ();
2026-06-05 21:31:56 +02:00
tst . test ( "<think>I'm \n thinking</think><|tool_call_start|>[special_function(arg1=1)]<|tool_call_end|>" )
2026-06-06 22:39:21 +02:00
. enable_thinking ( false )
2026-04-01 07:22:44 -07:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
2026-06-06 22:39:21 +02:00
// Continuation: prefill content
2026-05-17 07:36:05 -04:00
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-06-06 22:39:21 +02:00
// Continuation: prefill reasoning
2026-05-17 07:36:05 -04:00
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-01 07:22:44 -07:00
}
2026-04-15 10:51:50 +02:00
// Reka-Edge tests - uses native JSON format with per-call wrapper
{
auto tst = peg_tester ( "models/templates/Reka-Edge.jinja" , detailed_debug );
// Basic content only
tst . test ( "Hello, world! \n What's up?" ). enable_thinking ( false ). expect ( message_assist ). run ();
// Single tool call without reasoning
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}</tool_call>" )
. enable_thinking ( false )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call with string argument
tst . test ( "<tool_call> \n { \" name \" : \" get_time \" , \" arguments \" : { \" city \" : \" XYZCITY \" }}</tool_call>" )
. enable_thinking ( false )
. tools ({ get_time_tool })
. expect ( message_with_tool_calls ( "get_time" , "{ \" city \" : \" XYZCITY \" }" ))
. run ();
// Tool call with reasoning (enable_thinking=true)
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n <tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}</tool_call>" )
2026-04-15 10:51:50 +02:00
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
// Multiple tool calls (parallel)
tst . test (
"<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}</tool_call>"
"<tool_call> \n { \" name \" : \" special_function_with_opt \" , \" arguments \" : { \" arg1 \" : 1, \" arg2 \" : 2}}</tool_call>"
)
. enable_thinking ( false )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
// Tool call with reasoning and content
2026-04-25 21:24:13 +02:00
tst . test ( "I need to call a function \n </think> \n\n "
2026-04-15 10:51:50 +02:00
"Let me check the time.<tool_call> \n { \" name \" : \" get_time \" , \" arguments \" : { \" city \" : \" XYZCITY \" }}</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ get_time_tool })
. expect ( message_with_reasoning_content_and_multiple_tool_calls (
"I need to call a function" , "Let me check the time." , { { "get_time" , "{ \" city \" : \" XYZCITY \" }" } }
))
. run ();
// Partial tool call (streaming)
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" :" )
. tools ({ special_function_tool })
. enable_thinking ( false )
. is_partial ( true )
. expect ( simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : " ))
. run ();
// Tool call with empty arguments
tst . test ( "<tool_call> \n { \" name \" : \" empty_args \" , \" arguments \" : {}}</tool_call>" )
. enable_thinking ( false )
. tools ({ empty_args_tool })
. expect ( simple_assist_msg ( "" , "" , "empty_args" , "{}" ))
. run ();
// fake tool call marker in reasoning
tst . test (
2026-04-25 21:24:13 +02:00
"Let me think about <tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 2}}</tool_call> hmm \n </think> \n\n "
2026-04-15 10:51:50 +02:00
"<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect_reasoning ( "Let me think about <tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 2}}</tool_call> hmm" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
})
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n\n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-15 10:51:50 +02:00
}
2026-03-06 21:01:00 +01:00
// Apertus-8B-Instruct tests - FUNC_NAME_AS_KEY format
// Format: <|tools_prefix|>[{"function_name": {...arguments...}}]<|tools_suffix|>
{
auto tst = peg_tester ( "models/templates/Apertus-8B-Instruct.jinja" , detailed_debug );
tst . test ( "<|tools_prefix|>[{ \" special_function \" : { \" arg1 \" : 1}}]<|tools_suffix|>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// MiniMax-M2 tests - XML invoke format with parameter tags
// Format: <minimax:tool_call><invoke name="func"><parameter name="key">value</parameter></invoke></minimax:tool_call>
{
auto tst = peg_tester ( "models/templates/MiniMax-M2.jinja" , detailed_debug );
2026-04-25 21:24:13 +02:00
tst . test ( " \n </think> \n\n Hello, world! \n What's up?" ). enable_thinking ( true ). reasoning_format ( COMMON_REASONING_FORMAT_AUTO ). expect ( message_assist ). run ();
2026-04-08 12:47:25 +02:00
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n Hello, world! \n What's up?" ). enable_thinking ( true ). reasoning_format ( COMMON_REASONING_FORMAT_AUTO ). expect ( message_assist_thoughts ). run ();
2026-04-08 12:47:25 +02:00
2026-04-25 21:24:13 +02:00
tst . test ( "Let's call a tool: \n </think> \n\n <minimax:tool_call> \n <invoke name= \" empty_args \" > \n </invoke> \n </minimax:tool_call>" ).
2026-04-08 12:47:25 +02:00
enable_thinking ( true ).
reasoning_format ( COMMON_REASONING_FORMAT_AUTO ).
tools ({ empty_args_tool }).
expect ( message_with_reasoning_and_tool_call ( "Let's call a tool:" , "empty_args" , "{}" )).
run ();
2026-03-06 21:01:00 +01:00
tst . test (
2026-04-25 21:24:13 +02:00
" \n </think> \n\n <minimax:tool_call> \n <invoke name= \" special_function \" > \n <parameter "
2026-03-06 21:01:00 +01:00
"name= \" arg1 \" >1</parameter> \n </invoke> \n </minimax:tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-19 16:58:21 +01:00
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n\n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// NVIDIA-Nemotron-Nano-v2 tests - <TOOLCALL>...</TOOLCALL> format
// Format: <TOOLCALL>[{"name": "func", "arguments": {...}}]</TOOLCALL>
{
auto tst = peg_tester ( "models/templates/NVIDIA-Nemotron-Nano-v2.jinja" , detailed_debug );
2026-05-25 07:56:18 +02:00
tst . test ( "<TOOLCALL>[{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}]</TOOLCALL>" )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
2026-05-25 07:56:18 +02:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
2026-05-17 07:36:05 -04:00
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
2026-05-25 07:56:18 +02:00
. expect_reasoning ( "I'm thinking" )
2026-05-17 07:36:05 -04:00
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// CohereForAI-c4ai-command-r7b (uses START_RESPONSE/END_RESPONSE, START_THINKING/END_THINKING, START_ACTION/END_ACTION)
{
auto tst = peg_tester ( "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja" , detailed_debug );
tst . test ( "<|START_RESPONSE|>Hello, world! \n What's up?<|END_RESPONSE|>" ). expect ( message_assist ). run ();
tst . test (
"<|START_THINKING|>I'm \n thinking<|END_THINKING|>"
"<|START_ACTION|>[ \n "
" { \" tool_call_id \" : \" 0 \" , \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}} \n "
"]<|END_ACTION|>" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_thoughts_call_idx )
. run ();
}
// CohereForAI-c4ai-command-r-plus (uses markdown code block format)
{
auto tst = peg_tester ( "models/templates/CohereForAI-c4ai-command-r-plus-tool_use.jinja" , detailed_debug );
tst . test ( "<|CHATBOT_TOKEN|>Hello, world! \n What's up?<|END_OF_TURN_TOKEN|>" ). expect ( message_assist ). run ();
// Tool calls: Action: followed by JSON code block
tst . test (
"Action: \n "
"```json \n "
"[{ \" tool_name \" : \" special_function \" , \" parameters \" : { \" arg1 \" : 1}}] \n "
"```" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// mistralai-Mistral-Nemo-Instruct-2407.jinja
{
auto tst = peg_tester ( "models/templates/mistralai-Mistral-Nemo-Instruct-2407.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "[TOOL_CALLS][{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}, \" id \" : \" 123456789 \" }]" )
. tools ({ special_function_tool })
. expect ( message_assist_call_id )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
auto tst = peg_tester ( "models/templates/meetkai-functionary-medium-v3.1.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "<function=special_function>{ \" arg1 \" : 1}</function>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// Functionary v3.2 - recipient-based format: >>>recipient\n{content}
{
auto tst = peg_tester ( "models/templates/meetkai-functionary-medium-v3.2.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "all \n Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-19 16:58:21 +01:00
tst . test ( "special_function \n { \" arg1 \" : 1}" )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// FireFunction
{
auto tst = peg_tester ( "models/templates/fireworks-ai-llama-3-firefunction-v2.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( " functools[{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}]" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// DeepSeek R1 Distill Llama 8B - reasoning tests only (forced open thinking)
// Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja" , detailed_debug );
2026-03-19 16:58:21 +01:00
tst . test ( "</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
2026-03-06 21:01:00 +01:00
. expect ( message_assist )
. run ();
tst . test ( "I'm \n thinking</think>Hello, world! \n What's up?" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// llama-cpp DeepSeek R1 template (always forced-open thinking)
{
auto tst = peg_tester ( "models/templates/llama-cpp-deepseek-r1.jinja" , detailed_debug );
2026-03-19 16:58:21 +01:00
tst . test ( "</think>Hello, world! \n What's up?" ). expect ( message_assist ). reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK ). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "I'm \n thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
tst . test (
2026-03-19 16:58:21 +01:00
"</think><| tool▁calls▁begin| ><| tool▁call▁begin| >function<| tool▁sep| >special_function \n "
2026-03-06 21:01:00 +01:00
"```json \n { \" arg1 \" : 1}```<| tool▁call▁end| ><| tool▁calls▁end| >" )
2026-03-19 16:58:21 +01:00
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. parallel_tool_calls ( true )
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// DeepSeek R1 Distill Qwen 32B - reasoning tests only (forced open thinking)
// Note: Template uses forced-open mode (prompt ends with <think>), so input shouldn't include opening tag
{
auto tst = peg_tester ( "models/templates/deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja" , detailed_debug );
2026-03-19 16:58:21 +01:00
tst . test ( "</think>Hello, world! \n What's up?" ). enable_thinking ( true ).
reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK ).
expect ( message_assist ). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "I'm \n thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
tst . test (
"<| tool▁calls▁begin| ><| tool▁call▁begin| >function<| tool▁sep| >special_function \n "
"```json \n { \" arg1 \" : 1}```<| tool▁call▁end| ><| tool▁calls▁end| >" )
. tools ({ special_function_tool })
2026-03-19 16:58:21 +01:00
. enable_thinking ( false )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
2026-03-06 21:01:00 +01:00
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking</think>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// MiMo-VL / Hermes 3 / Qwen 2.5 (Common <tool_call> JSON format)
for ( const auto & path :
{ "models/templates/MiMo-VL.jinja" , "models/templates/NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja" ,
"models/templates/Qwen-Qwen2.5-7B-Instruct.jinja" }) {
auto tst = peg_tester ( path , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n </tool_call>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
2026-04-22 02:02:49 +08:00
// Reka Edge
{
auto tst = peg_tester ( "models/templates/Reka-Edge.jinja" , detailed_debug );
tst . test ( "Hello, world! \n What's up?" )
. enable_thinking ( false )
. expect ( message_assist )
. run ();
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n Hello, world! \n What's up?" )
2026-04-22 02:02:49 +08:00
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. expect ( message_assist_thoughts )
. run ();
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n </tool_call>" )
. enable_thinking ( false )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
tst . test ( "Hello, world! \n What's up? \n <tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n </tool_call>" )
. enable_thinking ( false )
. tools ({ special_function_tool })
. expect ( message_assist_call_content )
. run ();
2026-04-25 21:24:13 +02:00
tst . test ( "I'm \n thinking \n </think> \n\n <tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n </tool_call>" )
2026-04-22 02:02:49 +08:00
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}} \n </tool_call> \n <tool_call> \n { \" name \" : \" special_function_with_opt \" , \" arguments \" : { \" arg1 \" : 1, \" arg2 \" : 2}} \n </tool_call>" )
. enable_thinking ( false )
. parallel_tool_calls ( true )
. tools ({ special_function_tool , special_function_tool_with_optional_param })
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , {} },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , {} },
})
. run ();
tst . test ( "<tool_call> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg" )
. enable_thinking ( false )
. tools ({ special_function_tool })
. is_partial ( true )
. expect ( message_assist_call_cutoff_args )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n\n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-04-22 02:02:49 +08:00
}
2026-03-06 21:01:00 +01:00
// Apriel 1.5
{
auto tst = peg_tester ( "models/templates/unsloth-Apriel-1.5.jinja" , detailed_debug );
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). run ();
tst . test ( "<tool_calls>[{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}]</tool_calls>" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// Apriel 1.6 Thinker (reasoning-only support)
{
auto tst = peg_tester ( "models/templates/Apriel-1.6-15b-Thinker-fixed.jinja" , detailed_debug );
// Implicit reasoning start (forced open)
tst . test ( "I'm \n thinking \n [BEGIN FINAL RESPONSE] \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
. expect ( simple_assist_msg ( "Hello, world! \n What's up?" , "Here are my reasoning steps: \n I'm \n thinking" ))
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
// Reasoning + Tool calls
tst . test (
"I'm \n thinking \n [BEGIN FINAL RESPONSE] \n <tool_calls>[{ \" name \" : \" special_function \" , \" arguments \" : "
"{ \" arg1 \" : 1}}]</tool_calls>" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-19 16:58:21 +01:00
. enable_thinking ( true )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
2026-03-19 16:58:21 +01:00
. expect ( simple_assist_msg ( "" , "Here are my reasoning steps: \n I'm \n thinking" , "special_function" , "{ \" arg1 \" :1}" ))
2026-03-06 21:01:00 +01:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n [BEGIN FINAL RESPONSE] \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// Mistral Small 3.2 - FUNC_BRACKET_TAG format: [TOOL_CALLS]func_name[CALL_ID]id[ARGS]{...}
{
auto tst = peg_tester ( "models/templates/Mistral-Small-3.2-24B-Instruct-2506.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "[TOOL_CALLS]special_function[CALL_ID]123456789[ARGS]{ \" arg1 \" : 1}" )
. tools ({ special_function_tool })
. expect ( message_assist_call_id )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
2026-04-03 17:51:52 +02:00
tst . test ( "[TOOL_CALLS]special_function[CALL_ID]000000001[ARGS]{ \" arg1 \" : 1}"
"[TOOL_CALLS]special_function_with_opt[CALL_ID]000000002[ARGS]{ \" arg1 \" : 1, \" arg2 \" : 2}" )
. parallel_tool_calls ( true )
. tools ({
special_function_tool , special_function_tool_with_optional_param
})
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 1})" , "000000001" },
{ "special_function_with_opt" , R "({" arg1 ": 1, " arg2 ": 2})" , "000000002" },
})
. expect_reconstruction ()
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// Devstral
{
auto tst = peg_tester ( "models/templates/unsloth-mistral-Devstral-Small-2507.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-06 21:01:00 +01:00
tst . test ( "[TOOL_CALLS]special_function[ARGS]{ \" arg1 \" : 1}" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
tst . test ( "Hello, world! \n What's up?[TOOL_CALLS]special_function[ARGS]{ \" arg1 \" : 1}" )
. tools ({ special_function_tool })
. expect ( message_assist_call_content )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-06 21:01:00 +01:00
. run ();
}
{
// Llama 3.1
auto tst = peg_tester ( "models/templates/meta-llama-Llama-3.1-8B-Instruct.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). tools ({ special_function_tool }). expect ( message_assist ). expect_reconstruction (). run ();
2026-05-17 07:36:05 -04:00
2026-06-15 08:27:10 +02:00
tst . test (
2026-06-20 17:43:04 -05:00
"```json \n\" 42 \"\n ```" )
2026-06-15 08:27:10 +02:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. json_schema ( const_schema )
. expect_content ( R "(" 42 ")" )
. run ();
tst . test (
2026-06-20 17:43:04 -05:00
" \" 42 \"\n " )
2026-06-15 08:27:10 +02:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. json_schema ( const_schema )
. expect_content ( R "(" 42 ")" )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
// Llama 3.2
auto tst = peg_tester ( "models/templates/meta-llama-Llama-3.2-3B-Instruct.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). tools ({ special_function_tool }). expect ( message_assist ). expect_reconstruction (). run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
// Llama 3.3
auto tst = peg_tester ( "models/templates/meta-llama-Llama-3.3-70B-Instruct.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). tools ({ python_tool }). expect ( message_assist ). expect_reconstruction (). run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
// GPT-OSS format tests
{
auto tst = peg_tester ( "models/templates/openai-gpt-oss-120b.jinja" , detailed_debug );
// Basic content only - final channel
tst . test ( "<|channel|>final<|message|>Hello, world! \n What's up?" ). expect ( message_assist ). run ();
// Basic content only - commentary channel
tst . test ( "<|channel|>commentary<|message|>Hello, world! \n What's up?" ). expect ( message_assist ). run ();
// Analysis channel (reasoning) with final channel (content)
tst . test (
2026-03-18 04:41:25 -05:00
"<|channel|>analysis<|message|>I'm \n thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world! \n What's "
2026-03-06 21:01:00 +01:00
"up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist_thoughts )
. run ();
2026-03-28 09:33:39 -05:00
// Analysis channel (reasoning) with final channel (content) with reasoning_format = none
tst . test (
"<|channel|>analysis<|message|>I'm \n thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world! \n What's "
"up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_NONE )
. expect_content ( "<|channel|>analysis<|message|>I'm \n thinking<|end|>Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
// Analysis channel only (partial) - still works when reasoning format is set
tst . test ( "<|channel|>analysis<|message|>I'm \n thinking" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. is_partial ( true )
. expect_reasoning ( "I'm \n thinking" )
. run ();
// Tool call with recipient in role header: " to=functions.NAME<|channel|>analysis<|message|>JSON"
tst . test ( " to=functions.special_function<|channel|>analysis<|message|>{ \" arg1 \" : 1}" )
2026-03-28 09:33:39 -05:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call with recipient in channel header: "<|channel|>analysis to=functions.NAME<|message|>JSON"
tst . test ( "<|channel|>analysis to=functions.special_function<|message|>{ \" arg1 \" : 1}" )
2026-03-28 09:33:39 -05:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call with constraint: " to=functions.NAME<|channel|>analysis <|constrain|>json<|message|>JSON"
tst . test ( " to=functions.special_function<|channel|>analysis <|constrain|>json<|message|>{ \" arg1 \" : 1}" )
2026-03-28 09:33:39 -05:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call in commentary channel (channel header variant)
tst . test ( "<|channel|>commentary to=functions.special_function<|message|>{ \" arg1 \" : 1}" )
2026-03-28 09:33:39 -05:00
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
2026-03-06 21:01:00 +01:00
. tools ({ special_function_tool })
. expect ( message_assist_call )
. run ();
// Tool call with reasoning + content (analysis first, then tool call)
tst . test (
2026-03-18 04:41:25 -05:00
"<|channel|>analysis<|message|>I'm \n thinking<|end|>"
2026-03-06 21:01:00 +01:00
"<|start|>assistant to=functions.special_function<|channel|>analysis<|message|>{ \" arg1 \" : 1}" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({ special_function_tool })
. expect ( message_assist_call_thoughts )
. run ();
2026-03-18 04:41:25 -05:00
// Complex tool calling
2026-03-06 21:01:00 +01:00
tst . test (
2026-03-18 04:41:25 -05:00
"<|channel|>analysis<|message|>Thinking about edit...<|end|>"
2026-03-06 21:01:00 +01:00
"<|start|>assistant<|channel|>commentary to=functions.edit <|constrain|>json"
"<|message|>{ \" oldString \" : \" if (part < railCount - 1) { \" , \" newString \" : \" if (part < 4) { \" , \" replaceAll \" : false}"
)
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. tools ({
{
/* .name = */ "edit" ,
/* .description = */ "Edit a file" ,
/* .parameters = */ R "({
"type" : "object" ,
"properties" : {
"oldString" : {
"type" : "string" ,
"description" : "Old string to replace."
},
"newString" : {
"type" : "string" ,
"description" : "New replacement string."
},
"replaceAll" : {
"type" : "boolean" ,
2026-04-08 10:29:03 -04:00
"description" : "Whether to replace all occurrences."
2026-03-06 21:01:00 +01:00
}
},
"required" : [ "oldString" , "newString" ]
}) ",
}
})
. expect_reasoning ( "Thinking about edit..." )
. expect_tool_calls ({
{ "edit" , R "({" oldString ": " if ( part < railCount - 1 ) { ", " newString ": " if ( part < 4 ) { ", " replaceAll ": false})" , {} }
})
. run ();
2026-03-18 04:41:25 -05:00
// Structured output
2026-03-06 21:01:00 +01:00
tst . test (
2026-03-18 04:41:25 -05:00
"<|channel|>analysis<|message|>I need to output the invoice details in JSON<|end|>"
"<|start|>assistant<|channel|>final <|constrain|>json"
"<|message|>"
R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})"
)
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. json_schema ( invoice_schema )
. expect_reasoning ( "I need to output the invoice details in JSON" )
. expect_content ( R "({" amount ": 123.45, " date ": " 2025 - 12 - 03 "})" )
2026-03-06 21:01:00 +01:00
. run ();
2026-03-31 06:52:42 -05:00
// Unsolicited tool calls. There is no good way to handle these, so we return empty content.
// Builtin function - recipient in role
tst . test (
"<|channel|>analysis<|message|>I will execute python to say hello<|end|>"
"<|start|>assistant to=container.exec<|channel|>commentary<|message|>python3 -c 'print( \" hello \" )'" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect_reasoning ( "I will execute python to say hello" )
. expect_content ( "" )
. run ();
// Builtin function - recipient in channel
tst . test (
"<|channel|>analysis<|message|>I will execute python to say hello<|end|>"
"<|start|>assistant<|channel|>commentary to=python <|constrain|>code<|message|>print( \" hello \" )" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect_reasoning ( "I will execute python to say hello" )
. expect_content ( "" )
. run ();
2026-04-02 08:59:59 -05:00
// Edge cases
// "<|channel|>commentary to=assistant" before reasoning
tst . test (
"<|channel|>commentary to=assistant<|channel|>analysis<|message|>I'm \n thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world! \n What's "
"up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist_thoughts )
. run ();
// "<|channel|>commentary to=assistant" before final message
tst . test (
"<|channel|>analysis<|message|>I'm \n thinking<|end|><|start|>assistant<|channel|>commentary to=assistant<|channel|>final<|message|>Hello, world! \n What's "
"up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. expect ( message_assist_thoughts )
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_AUTO )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-06 21:01:00 +01:00
}
{
auto tst = peg_tester ( "models/templates/StepFun3.5-Flash.jinja" , detailed_debug );
2026-04-25 21:24:13 +02:00
tst . test ( "I was thinking \n </think> \n Now I'm not." ).
2026-03-06 21:01:00 +01:00
enable_thinking ( true ).
reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK ).
expect_reasoning ( "I was thinking" ).
expect_content ( "Now I'm not." )
. run ();
// Test that numeric-looking string values are coerced to strings per the schema
tst . test (
"Let me call the magic tool \n "
"</think> \n "
"<tool_call> \n "
"<function=magic> \n "
"<parameter=name> \n fooBar \n </parameter> \n "
"<parameter=ref> \n 5123123 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ magic_tool })
. expect_reasoning ( "Let me call the magic tool" )
. expect_tool_calls ({
{ "magic" , R "({" name ": " fooBar ", " ref ": " 5123123 "})" , {} },
})
. run ();
// Test that numeric values are correctly interpreted as numbers when schema calls for number
tst . test (
"Let me call the special function \n "
"</think> \n "
"<tool_call> \n "
"<function=special_function> \n "
"<parameter=arg1> \n 42555916 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool })
. expect_reasoning ( "Let me call the special function" )
. expect_tool_calls ({
{ "special_function" , R "({" arg1 ": 42555916})" , {} },
})
. run ();
tst . test (
"Let me call the special function with opt \n "
"</think> \n "
"<tool_call> \n "
"<function=special_function_with_opt> \n "
"<parameter=arg1> \n 42555916 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ special_function_tool_with_optional_param })
. expect_reasoning ( "Let me call the special function with opt" )
. expect_tool_calls ({
{ "special_function_with_opt" , R "({" arg1 ": 42555916})" , {} },
})
. run ();
tst . test (
"Let me call the magic_int function \n "
"</think> \n "
"<tool_call> \n "
"<function=magic_int> \n "
"<parameter=ref> \n 42555916 \n </parameter> \n "
"<parameter=name> \n baz \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ magic_int_tool })
. expect_reasoning ( "Let me call the magic_int function" )
. expect_tool_calls ({
{ "magic_int" , R "({" ref ": 42555916, " name ": " baz "})" , {} },
})
. run ();
tst . test (
"Call string_param with empty text \n "
"</think> \n "
"<tool_call> \n "
"<function=string_param> \n "
"<parameter=text> \n\n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ string_param_tool })
. expect_reasoning ( "Call string_param with empty text" )
. expect_tool_calls ({
{ "string_param" , R "({" text ": ""})" , {} },
})
. run ();
tst . test (
"Test simple quoted unquoted \n "
"</think> \n "
"<tool_call> \n "
"<function=quoted_unquoted> \n "
"<parameter=quoted> \n\" foo \"\n </parameter> \n "
"<parameter=unquoted> \n foo \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ quoted_unquoted_tool })
. expect_reasoning ( "Test simple quoted unquoted" )
. expect_tool_calls ({
{ "quoted_unquoted" , R "({" quoted ": " \ "foo \" " , "unquoted" : "foo" }) ", {} },
})
. run ();
tst . test (
"Test complex quoted unquoted \n "
"</think> \n "
"<tool_call> \n "
"<function=quoted_unquoted> \n "
"<parameter=quoted> \n\" printf( \\\" foo \\\" ); \"\n </parameter> \n "
"<parameter=unquoted> \n printf( \" foo \" ); \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ quoted_unquoted_tool })
. expect_reasoning ( "Test complex quoted unquoted" )
. expect_tool_calls ({
{ "quoted_unquoted" , R "({ " quoted " : " \ "printf( \\\" foo \\\" ); \" " , "unquoted" : "printf( \" foo \" );" }) ", {} }
})
. run ();
tst . test (
"Test negative number \n "
"</think> \n "
"<tool_call> \n "
"<function=magic_int> \n "
"<parameter=ref> \n -14 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ magic_int_tool })
. expect_reasoning ( "Test negative number" )
. expect_tool_calls ({
{ "magic_int" , R "({ " ref " : -14 })" , {} }
})
. run ();
tst . test (
"Test decimal number \n "
"</think> \n "
"<tool_call> \n "
"<function=amount> \n "
"<parameter=orig> \n 3.14 \n </parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ amount_tool })
. expect_reasoning ( "Test decimal number" )
. expect_tool_calls ({
{ "amount" , R "({ " orig " : 3.14 })" , {} }
})
. run ();
tst . test (
"Test imaginary number \n "
"</think> \n "
"<tool_call> \n "
"<function=imaginary_number> \n "
"<parameter=number> \n "
"{ \" real \" : 3.14, \" imaginary \" : 2.71 } \n "
"</parameter> \n "
"</function> \n "
"</tool_call>" )
. enable_thinking ( true )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. tools ({ imaginary_number_tool })
. expect_reasoning ( "Test imaginary number" )
. expect_tool_calls ({
{ "imaginary_number" , R "({ " number " : {" real ":3.14," imaginary ":2.71 } })" , {} }
})
. run ();
2026-01-29 09:06:15 -06:00
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
tst . test ( " thinking \n </think> \n Hello, world! \n What's up?" )
. reasoning_format ( COMMON_REASONING_FORMAT_DEEPSEEK )
. enable_thinking ( true )
. messages ({ message_user , message_assist_prefill_reasoning })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_REASONING )
. expect_reasoning ( "I'm thinking" )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-01-29 09:06:15 -06:00
}
2026-03-12 03:22:25 +03:00
// GigaChat V3
{
auto tst = peg_tester ( "models/templates/GigaChat3-10B-A1.8B.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-12 03:22:25 +03:00
tst . test ( "<|message_sep|> \n\n function call<|role_sep|> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-12 03:22:25 +03:00
. run ();
tst . test (
"Hello, world! \n What's up?"
"<|message_sep|> \n\n function call<|role_sep|> \n { \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}"
)
. tools ({ special_function_tool })
. expect ( message_assist_call_content )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-12 03:22:25 +03:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-12 03:22:25 +03:00
}
// GigaChat V3.1
{
auto tst = peg_tester ( "models/templates/GigaChat3.1-10B-A1.8B.jinja" , detailed_debug );
2026-03-28 07:29:26 +01:00
tst . test ( "Hello, world! \n What's up?" ). expect ( message_assist ). expect_reconstruction (). run ();
2026-03-12 03:22:25 +03:00
tst . test ( "<|function_call|>{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}" )
. tools ({ special_function_tool })
. expect ( message_assist_call )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-12 03:22:25 +03:00
. run ();
tst . test (
"Hello, world! \n What's up?"
"<|function_call|>{ \" name \" : \" special_function \" , \" arguments \" : { \" arg1 \" : 1}}"
)
. tools ({ special_function_tool })
. expect ( message_assist_call_content )
2026-03-28 07:29:26 +01:00
. expect_reconstruction ()
2026-03-12 03:22:25 +03:00
. run ();
2026-05-17 07:36:05 -04:00
// Continuation tests
tst . test ( "world! \n What's up?" )
. messages ({ message_user , message_assist_prefill_content })
. add_generation_prompt ( false )
. continue_final_message ( COMMON_CHAT_CONTINUATION_CONTENT )
. expect_content ( "Hello, world! \n What's up?" )
. run ();
2026-03-12 03:22:25 +03:00
}
2025-01-30 19:13:58 +00:00
}
2026-05-17 07:36:05 -04:00
static void test_template_generation_prompt () {
common_chat_msg system_msg ;
system_msg . role = "system" ;
system_msg . content = "You are a helpful assistant." ;
2026-04-22 02:02:49 +08:00
2026-05-17 07:36:05 -04:00
common_chat_msg tool_call_msg = simple_assist_msg ( "" , "" , "special_function" , "{ \" arg1 \" : 1}" );
common_chat_msg tool_msg ;
tool_msg . role = "tool" ;
tool_msg . tool_name = "special_function" ;
tool_msg . tool_call_id = "call0" ;
tool_msg . content = "Sunny" ;
struct test_case_options {
std :: vector < common_chat_msg > messages ;
bool add_generation_prompt = true ;
common_chat_continuation continue_final_message = COMMON_CHAT_CONTINUATION_NONE ;
};
auto basic = [ & ]() {
test_case_options opts ;
opts . messages = { system_msg , message_user };
return opts ;
};
auto continuation_content = [ & ]() {
test_case_options opts ;
opts . messages = { system_msg , message_user , message_assist_prefill_content };
opts . add_generation_prompt = false ;
opts . continue_final_message = COMMON_CHAT_CONTINUATION_CONTENT ;
return opts ;
};
auto continuation_reasoning = [ & ]() {
test_case_options opts ;
opts . messages = { system_msg , message_user , message_assist_prefill_reasoning };
opts . add_generation_prompt = false ;
opts . continue_final_message = COMMON_CHAT_CONTINUATION_REASONING ;
return opts ;
};
auto check = [ & ]( const common_chat_templates_ptr & tmpls ,
const test_case_options & opts ,
const std :: string & expected_generation_prompt ) {
2026-04-22 02:02:49 +08:00
common_chat_templates_inputs inputs ;
2026-05-17 07:36:05 -04:00
inputs . messages = opts . messages ;
inputs . add_generation_prompt = opts . add_generation_prompt ;
inputs . continue_final_message = opts . continue_final_message ;
2026-04-22 02:02:49 +08:00
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
2026-05-17 07:36:05 -04:00
assert_contains ( params . prompt , system_msg . content );
assert_contains ( params . prompt , message_user . content );
assert_equals ( expected_generation_prompt , params . generation_prompt );
assert_ends_with ( params . prompt , expected_generation_prompt );
};
{
auto tmpls = read_templates ( "models/templates/Qwen3.5-4B.jinja" );
check ( tmpls , basic (), "<|im_start|>assistant \n <think> \n " );
check ( tmpls , continuation_content (), "<|im_start|>assistant \n <think> \n I'm thinking \n </think> \n\n Hello, " );
check ( tmpls , continuation_reasoning (), "<|im_start|>assistant \n <think> \n I'm" );
2026-04-22 02:02:49 +08:00
}
{
2026-05-17 07:36:05 -04:00
auto tmpls = read_templates ( "models/templates/openai-gpt-oss-120b.jinja" );
check ( tmpls , basic (), "<|start|>assistant" );
check ( tmpls , continuation_content (), "<|start|>assistant<|channel|>analysis<|message|>I'm thinking<|end|><|start|>assistant<|channel|>final<|message|>Hello, " );
check ( tmpls , continuation_reasoning (), "<|start|>assistant<|channel|>analysis<|message|>I'm" );
}
2026-04-22 02:02:49 +08:00
2026-05-17 07:36:05 -04:00
{
auto tmpls = read_templates ( "models/templates/mistralai-Ministral-3-14B-Reasoning-2512.jinja" );
check ( tmpls , basic (), "" );
check ( tmpls , continuation_content (), "[THINK]I'm thinking[/THINK]Hello, " );
check ( tmpls , continuation_reasoning (), "[THINK]I'm" );
}
{
auto tmpls = read_templates ( "models/templates/google-gemma-4-31B-it.jinja" );
check ( tmpls , basic (), "<|turn>model \n " );
check ( tmpls , continuation_content (), "<|turn>model \n <|channel>thought \n I'm thinking<channel|>Hello, " );
check ( tmpls , continuation_reasoning (), "<|turn>model \n <|channel>thought \n I'm" );
// Special case when last message is a tool response
test_case_options after_tool_call = continuation_reasoning ();
after_tool_call . messages = { system_msg , message_user , tool_call_msg , tool_msg , message_assist_prefill_reasoning };
check ( tmpls , after_tool_call , "<|channel>thought \n I'm" );
}
{
auto tmpls = read_templates ( "models/templates/meetkai-functionary-medium-v3.2.jinja" );
check ( tmpls , basic (), "<|start_header_id|>assistant<|end_header_id|> \n\n >>>" );
check ( tmpls , continuation_content (), "<|start_header_id|>assistant<|end_header_id|> \n\n >>>all \n Hello, " );
check ( tmpls , continuation_reasoning (), "<|start_header_id|>assistant<|end_header_id|> \n\n >>>all \n " );
}
{
auto tmpls = read_templates ( "models/templates/Reka-Edge.jinja" );
check ( tmpls , basic (), "assistant: <think> \n " );
check ( tmpls , continuation_content (), "assistant: <think> \n I'm thinking \n </think> \n\n Hello, " );
check ( tmpls , continuation_reasoning (), "assistant: <think> \n I'm" );
}
{
auto tmpls = read_templates ( "models/templates/moonshotai-Kimi-K2.jinja" );
check ( tmpls , basic (), "<|im_assistant|>assistant<|im_middle|>" );
check ( tmpls , continuation_content (), "<|im_assistant|>assistant<|im_middle|><think>I'm thinking</think>Hello, " );
check ( tmpls , continuation_reasoning (), "<|im_assistant|>assistant<|im_middle|><think>I'm" );
}
2026-06-06 22:39:21 +02:00
for ( const char * tmpl : {
"models/templates/LFM2-8B-A1B.jinja" ,
"models/templates/LFM2.5-Instruct.jinja" ,
"models/templates/LFM2.5-8B-A1B.jinja" ,
}) {
auto tmpls = read_templates ( tmpl );
2026-05-17 07:36:05 -04:00
check ( tmpls , basic (), "<|im_start|>assistant \n " );
check ( tmpls , continuation_content (), "<|im_start|>assistant \n <think>I'm thinking</think>Hello, " );
check ( tmpls , continuation_reasoning (), "<|im_start|>assistant \n <think>I'm" );
}
{
2026-06-06 22:39:21 +02:00
// 8B-A1B renders prior-turn reasoning via the "thinking" field
auto tmpls = read_templates ( "models/templates/LFM2.5-8B-A1B.jinja" );
common_chat_templates_inputs inputs ;
inputs . messages = { message_user , message_assist_call_thoughts , tool_msg };
inputs . add_generation_prompt = true ;
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
assert_contains ( params . prompt , "<think>I'm \n thinking</think>" );
2026-05-17 07:36:05 -04:00
}
{
auto tmpls = read_templates ( "models/templates/GigaChat3-10B-A1.8B.jinja" );
check ( tmpls , basic (), "assistant<|role_sep|> \n " );
check ( tmpls , continuation_content (), "assistant<|role_sep|> \n Hello, " );
check ( tmpls , continuation_reasoning (), "assistant<|role_sep|> \n " );
}
{
auto tmpls = read_templates ( "models/templates/deepseek-ai-DeepSeek-V3.2.jinja" );
check ( tmpls , basic (), "<| Assistant| ><think>" );
check ( tmpls , continuation_content (), "<| Assistant| ><think>I'm thinking</think>Hello, " );
check ( tmpls , continuation_reasoning (), "<| Assistant| ><think>I'm" );
2026-04-22 02:02:49 +08:00
}
}
2026-03-09 14:25:11 +01:00
// Test the developer role to system workaround with a simple mock template
static void test_developer_role_to_system_workaround () {
LOG_DBG ( "%s \n " , __func__ );
// Simple mock template that supports system role
const std :: string mock_template =
"{%- for message in messages -%} \n "
" {{- '<|' + message.role + '|>' + message.content + '<|end|>' -}} \n "
"{%- endfor -%} \n "
"{%- if add_generation_prompt -%} \n "
" {{- '<|assistant|>' -}} \n "
"{%- endif -%}" ;
auto tmpls = common_chat_templates_ptr ( common_chat_templates_init ( /* model= */ nullptr , mock_template ));
// Test case 1: Developer message - should be changed to system
// After simplification we only test this case
{
common_chat_templates_inputs inputs ;
common_chat_msg developer_msg ;
developer_msg . role = "developer" ;
developer_msg . content = "You are a helpful developer assistant." ;
inputs . messages = { developer_msg };
inputs . add_generation_prompt = false ;
auto params = common_chat_templates_apply ( tmpls . get (), inputs );
// The developer role should have been changed to system
if ( params . prompt . find ( "<|developer|>" ) != std :: string :: npos ) {
throw std :: runtime_error ( "Test failed: developer role was not changed to system" );
}
if ( params . prompt . find ( "<|system|>You are a helpful developer assistant.<|end|>" ) == std :: string :: npos ) {
throw std :: runtime_error ( "Test failed: system message not found in output" );
}
LOG_ERR ( "Test 1 passed: developer role changed to system \n " );
}
}
2025-05-25 01:48:08 +01:00
static void test_msg_diffs_compute () {
2026-03-06 21:01:00 +01:00
LOG_DBG ( "%s \n " , __func__ );
2025-05-25 01:48:08 +01:00
{
common_chat_msg msg1 ;
common_chat_msg msg2 ;
msg2 . content = "Hello, world!" ;
common_chat_msg_diff diff ;
diff . content_delta = "Hello, world!" ;
2026-03-06 21:01:00 +01:00
assert_equals ({ diff }, common_chat_msg_diff :: compute_diffs ( msg1 , msg2 ));
2025-05-25 01:48:08 +01:00
}
{
common_chat_msg msg1 ;
msg1 . content = "Hello," ;
common_chat_msg msg2 ;
msg2 . content = "Hello, world!" ;
common_chat_msg_diff diff ;
diff . content_delta = " world!" ;
2026-03-06 21:01:00 +01:00
assert_equals ({ diff }, common_chat_msg_diff :: compute_diffs ( msg1 , msg2 ));
2025-05-25 01:48:08 +01:00
}
{
common_chat_msg msg0 ;
common_chat_msg msg1 ;
2026-03-06 21:01:00 +01:00
msg1 . tool_calls = {
{ "special_function" , "{ \" ar" , /* .id = */ "123" }
};
2025-05-25 01:48:08 +01:00
common_chat_msg msg2 ;
2026-03-06 21:01:00 +01:00
msg2 . tool_calls = {
{ "special_function" , "{ \" arg1 \" : 1}" , /* .id = */ "123" }
};
2025-05-25 01:48:08 +01:00
common_chat_msg_diff diff01 ;
2026-03-06 21:01:00 +01:00
diff01 . tool_call_index = 0 ;
diff01 . tool_call_delta . name = "special_function" ;
diff01 . tool_call_delta . id = "123" ;
2025-05-25 01:48:08 +01:00
diff01 . tool_call_delta . arguments = "{ \" ar" ;
2026-03-06 21:01:00 +01:00
assert_equals ({ diff01 }, common_chat_msg_diff :: compute_diffs ( msg0 , msg1 ));
2025-05-25 01:48:08 +01:00
common_chat_msg_diff diff12 ;
2026-03-06 21:01:00 +01:00
diff12 . tool_call_index = 0 ;
2025-05-26 06:56:49 -07:00
// Note: neither id nor name change here.
2025-05-25 01:48:08 +01:00
diff12 . tool_call_delta . arguments = "g1 \" : 1}" ;
2026-03-06 21:01:00 +01:00
assert_equals ({ diff12 }, common_chat_msg_diff :: compute_diffs ( msg1 , msg2 ));
2025-05-25 01:48:08 +01:00
}
{
common_chat_msg msg0 ;
common_chat_msg msg2 ;
msg2 . tool_calls = {
{ "f1" , "{ \" arg1 \" : 1}" , /* .id = */ "123" },
{ "f2" , "{ \" arg2 \" : 2}" , /* .id = */ "222" },
};
common_chat_msg_diff diff1 ;
2026-03-06 21:01:00 +01:00
diff1 . tool_call_index = 0 ;
diff1 . tool_call_delta . name = "f1" ;
diff1 . tool_call_delta . id = "123" ;
2025-05-25 01:48:08 +01:00
diff1 . tool_call_delta . arguments = "{ \" arg1 \" : 1}" ;
common_chat_msg_diff diff2 ;
2026-03-06 21:01:00 +01:00
diff2 . tool_call_index = 1 ;
diff2 . tool_call_delta . name = "f2" ;
diff2 . tool_call_delta . id = "222" ;
2025-05-25 01:48:08 +01:00
diff2 . tool_call_delta . arguments = "{ \" arg2 \" : 2}" ;
2026-03-06 21:01:00 +01:00
assert_equals ({ diff1 , diff2 }, common_chat_msg_diff :: compute_diffs ( msg0 , msg2 ));
2025-05-25 01:48:08 +01:00
}
}
2025-01-30 19:13:58 +00:00
int main ( int argc , char ** argv ) {
2026-03-06 21:01:00 +01:00
bool detailed_debug = false ;
bool only_run_filtered = false ;
2025-06-09 11:03:09 -07:00
2026-04-22 10:28:45 +02:00
// Check for --template and --detailed flags
2026-03-06 21:01:00 +01:00
for ( int i = 1 ; i < argc ; i ++ ) {
std :: string arg = argv [ i ];
if ( arg == "--template" && i + 1 < argc ) {
g_template_filter = argv [ ++ i ];
// Only run PEG parser tests with the filter
only_run_filtered = true ;
2025-02-18 18:03:23 +00:00
}
2026-03-06 21:01:00 +01:00
if ( arg == "--detailed" ) {
detailed_debug = true ;
common_log_set_verbosity_thold ( 999 );
}
2026-03-28 07:29:26 +01:00
if ( arg == "--force-reconstruction-test" ) {
g_force_reconstruction_test = true ;
only_run_filtered = true ;
}
2026-03-06 21:01:00 +01:00
}
if ( only_run_filtered ) {
test_template_output_peg_parsers ( detailed_debug );
std :: cout << " \n [chat] All template tests passed!" << '\n' ;
2025-02-18 18:03:23 +00:00
return 0 ;
2026-03-06 21:01:00 +01:00
}
#ifndef _WIN32
2026-04-22 10:28:45 +02:00
// Check if any argument is a .jinja file (for template format detection mode)
bool has_jinja_files = false ;
for ( int i = 1 ; i < argc ; i ++ ) {
std :: string arg = argv [ i ];
if ( arg == "--detailed" ) {
continue ;
}
if ( arg . size () >= 6 && arg . rfind ( ".jinja" ) == arg . size () - 6 ) {
has_jinja_files = true ;
break ;
}
}
if ( has_jinja_files ) {
2026-03-06 21:01:00 +01:00
common_chat_templates_inputs inputs ;
common_chat_msg msg ;
msg . role = "user" ;
msg . content = "Hey" ;
inputs . messages = { msg };
inputs . tools = { special_function_tool };
std :: cout << "| Template | Format | \n " ;
std :: cout << "|----------|--------| \n " ;
for ( int i = 1 ; i < argc ; i ++ ) {
try {
std :: string path = argv [ i ];
if ( path . rfind ( ".jinja" ) != path . size () - 6 ) {
std :: cerr << "Skipping non-jinja file: " << path << '\n' ;
continue ;
}
auto tmpls = read_templates ( path );
auto parts = string_split ( path , "/" );
const auto & name = parts [ parts . size () - 1 ];
const auto * format = common_chat_format_name ( common_chat_templates_apply ( tmpls . get (), inputs ). format );
std :: cout << "| " << name << " | " << format << " | \n " ;
} catch ( const std :: exception & e ) {
std :: cerr << "Failed to process " << argv [ i ] << ": " << e . what () << '\n' ;
}
}
} else
#endif
{
test_msg_diffs_compute ();
test_msgs_oaicompat_json_conversion ();
2026-05-25 07:56:18 +02:00
test_split_by_role ();
2026-03-06 21:01:00 +01:00
test_tools_oaicompat_json_conversion ();
2026-04-22 10:28:45 +02:00
test_convert_responses_to_chatcmpl ();
2026-03-09 14:25:11 +01:00
test_developer_role_to_system_workaround ();
2026-05-17 07:36:05 -04:00
test_template_generation_prompt ();
2026-03-06 21:01:00 +01:00
test_template_output_peg_parsers ( detailed_debug );
std :: cout << " \n [chat] All tests passed!" << '\n' ;
}
return 0 ;
2025-01-30 19:13:58 +00:00
}