Something went wrong. Try again.
llama.cpp fork for Ternary Bonsai 2 27B: MTP speculation, suffix cache reuse (SCR), q8 KV, two-slot fairness, fused DeltaNet kernels
Something went wrong. Try again.
125 kB · 2707 lines
C++
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708#include "chat-auto-parser-helpers.h"#include "chat-auto-parser.h"#include "chat-peg-parser.h"#include "chat.h"#include "gguf.h"#include "jinja/runtime.h"#include "log.h"#include "peg-parser.h"#include "testing.h"
#include <cstdlib>#include <filesystem>#include <fstream>#include <iostream>#include <iterator>#include <optional>#include <sstream>#include <string>
using namespace autoparser;
static void test_calculate_diff_split_basic(testing & t);static void test_calculate_diff_split_identical(testing & t);static void test_calculate_diff_split_common_prefix(testing & t);static void test_calculate_diff_split_common_suffix(testing & t);static void test_calculate_diff_split_common_both(testing & t);static void test_calculate_diff_split_empty_cases(testing & t);static void test_calculate_diff_split_no_common(testing & t);static void test_calculate_diff_split_single_char(testing & t);static void test_calculate_diff_split_overlaps(testing & t);static void test_calculate_diff_split_tag_boundaries(testing & t);static void test_calculate_diff_split_generation_prompt(testing & t);static void test_calculate_diff_split(testing & t);
static void test_until_common_prefix_basic(testing & t);static void test_until_common_prefix(testing & t);
static void test_after_common_suffix_basic(testing & t);static void test_after_common_suffix(testing & t);
static void test_analyze_tool_call_pure_json(testing & t);static void test_analyze_tool_call_function_name_markers(testing & t);static void test_analyze_tool_call_full_markers(testing & t);static void test_analyze_tool_call_edge_cases(testing & t);
static void test_compare_variants_basic(testing & t);static void test_compare_variants_messages_modifier(testing & t);static void test_compare_variants_tools_modifier(testing & t);static void test_compare_variants_both_modifiers(testing & t);static void test_compare_variants_template_failure(testing & t);static void test_compare_variants_identity(testing & t);static void test_compare_variants(testing & t);
// Seed-OSS template tool calling analysis testsstatic void test_seed_oss_tool_analysis(testing & t);static void test_seed_oss_tool_presence(testing & t);static void test_seed_oss_call_count(testing & t);static void test_seed_oss_function_names(testing & t);static void test_seed_oss_argument_count(testing & t);static void test_seed_oss_args_presence(testing & t);static void test_seed_oss_tool_with_reasoning(testing & t);
// Nemotron template analysis testsstatic void test_nemotron_analysis(testing & t);static void test_nemotron_reasoning_detection(testing & t);static void test_nemotron_tool_format(testing & t);static void test_laguna_analysis(testing & t);static void test_laguna_reasoning_detection(testing & t);static void test_laguna_tool_format(testing & t);static void test_laguna_s_analysis(testing & t);static void test_laguna_s_reasoning_detection(testing & t);static void test_laguna_s_tool_format(testing & t);static void test_laguna_s_preserve_reasoning(testing & t);static void test_laguna_xs2_analysis(testing & t);static void test_laguna_xs2_reasoning_detection(testing & t);static void test_laguna_xs2_tool_format(testing & t);
// CohereForAI template analysis testsstatic void test_cohere_reasoning_detection(testing & t);static void test_cohere_analysis(testing & t);
// SmolLM3 template analysis testsstatic void test_smollm3_analysis(testing & t);
// Marker separationstatic void test_marker_separation(testing & t);
// standard_json_tools format testsstatic void test_standard_json_tools_formats(testing & t);static void test_standard_json_tools_openai(testing & t);static void test_standard_json_tools_cohere(testing & t);static void test_standard_json_tools_function_key(testing & t);
// normalize_quotes_to_json testsstatic void test_normalize_quotes_to_json(testing & t);static void test_normalize_quotes_with_embedded_quotes(testing & t);
// TAG_WITH_TAGGED argument parsing testsstatic void test_tagged_args_with_embedded_quotes(testing & t);static void test_bailing_v3_tool_format(testing & t);
static void test_role_markers_all_templates(testing & t);
static json build_tools_definition();
//// debug mode: analyze a single template and dump the generated parser and grammar//
enum class output_mode { ANALYSIS, // Only output analysis results (default) TEMPLATE, // Only output rendered template BOTH // Output both};
enum class input_message_type { NONE, // Don't render any message scenarios (only analysis) CONTENT_ONLY, // Simple assistant message with content REASONING_CONTENT, // Message with reasoning_content + content TOOL_CALL_ONLY, // Message with tool_calls only CONTENT_TOOL_CALL, // Message with content + tool_calls REASONING_TOOL_CALL, // Message with reasoning_content + tool_calls CONTENT_FAKE_TOOL_CALL, // Message with content but no actual tool_calls (for testing) ALL // Render all scenarios};
struct debug_options { std::string template_path; bool with_tools = true; bool generation_prompt = true; bool enable_reasoning = true; bool debug_jinja = false; bool force_tool_call = false; bool parallel_tool_calls = true; output_mode mode = output_mode::BOTH; input_message_type input_message = input_message_type::NONE;};
static std::string read_file(const std::string & path) { std::ifstream fin(path, std::ios::binary); if (!fin.is_open()) { throw std::runtime_error("Could not open file: " + path); } std::ostringstream buf; buf << fin.rdbuf(); return buf.str();}
static std::string read_gguf_chat_template(const std::string & path) { struct gguf_init_params params = { /*no_alloc =*/true, // We only need metadata, not tensor data /*ctx=*/nullptr };
struct gguf_context * ctx = gguf_init_from_file(path.c_str(), params); if (ctx == nullptr) { throw std::runtime_error("Could not open GGUF file: " + path); }
const char * key = "tokenizer.chat_template"; int64_t key_id = gguf_find_key(ctx, key);
if (key_id == -1) { gguf_free(ctx); throw std::runtime_error("GGUF file does not contain chat template key: " + std::string(key)); }
const char * template_str = gguf_get_val_str(ctx, key_id); if (template_str == nullptr) { gguf_free(ctx); throw std::runtime_error("GGUF file contains chat template key but value is null"); }
std::string result = template_str; gguf_free(ctx); return result;}
static void print_usage(const char * program_name) { LOG_ERR("Test the chat template auto-parser; also usable as a debug tool that shows the generated PEG parser, GBNF grammar and triggers for a given template.\n"); LOG_ERR("\nUsage: %s [filter_regex] run the automated tests (default)\n", program_name); LOG_ERR(" %s <template_or_gguf_path> [options] debug a single template\n", program_name); LOG_ERR("\nDebug mode options:\n"); LOG_ERR(" --no-tools Disable tool definitions\n"); LOG_ERR(" --force-tool-call Set tool calls to forced\n"); LOG_ERR(" --parallel-tool-calls=0|1 Set parallel_tool_calls (default: 1)\n"); LOG_ERR(" --generation-prompt=0|1 Set add_generation_prompt (default: 1)\n"); LOG_ERR(" --enable-reasoning=0|1 Enable reasoning parsing (default: 1)\n"); LOG_ERR(" --output=MODE Output mode: analysis, template, both (default: both)\n"); LOG_ERR(" --debug-jinja Enable Jinja fine-grained debug\n"); LOG_ERR(" --input-message=TYPE Message type to render:\n"); LOG_ERR(" content_only, reasoning_content, tool_call_only,\n"); LOG_ERR(" content_tool_call, reasoning_tool_call,\n"); LOG_ERR(" content_fake_tool_call, all\n"); LOG_ERR("\nExamples:\n"); LOG_ERR(" %s template.jinja --input-message=all --generation-prompt=1\n", program_name); LOG_ERR(" %s template.jinja --output=template --input-message=tool_call_only\n", program_name);}
static bool parse_bool_option(const std::string & value) { return value == "1" || value == "true" || value == "yes";}
static bool parse_debug_options(int argc, char ** argv, debug_options & opts) { opts.template_path = argv[1];
for (int i = 2; i < argc; ++i) { std::string arg = argv[i];
if (arg == "--force-tool-call") { opts.force_tool_call = true; } else if (arg == "--debug-jinja") { opts.debug_jinja = true; } else if (arg == "--no-tools") { opts.with_tools = false; } else if (arg.rfind("--parallel-tool-calls=", 0) == 0) { opts.parallel_tool_calls = parse_bool_option(arg.substr(22)); } else if (arg.rfind("--generation-prompt=", 0) == 0) { opts.generation_prompt = parse_bool_option(arg.substr(20)); } else if (arg.rfind("--enable-reasoning=", 0) == 0) { opts.enable_reasoning = parse_bool_option(arg.substr(19)); } else if (arg.rfind("--output=", 0) == 0) { std::string mode = arg.substr(9); if (mode == "analysis") { opts.mode = output_mode::ANALYSIS; } else if (mode == "template") { opts.mode = output_mode::TEMPLATE; } else if (mode == "both") { opts.mode = output_mode::BOTH; } else { LOG_ERR("Unknown output mode: %s\n", mode.c_str()); return false; } } else if (arg.rfind("--input-message=", 0) == 0) { std::string type = arg.substr(16); if (type == "content_only") { opts.input_message = input_message_type::CONTENT_ONLY; } else if (type == "reasoning_content") { opts.input_message = input_message_type::REASONING_CONTENT; } else if (type == "tool_call_only") { opts.input_message = input_message_type::TOOL_CALL_ONLY; } else if (type == "content_tool_call") { opts.input_message = input_message_type::CONTENT_TOOL_CALL; } else if (type == "reasoning_tool_call") { opts.input_message = input_message_type::REASONING_TOOL_CALL; } else if (type == "content_fake_tool_call") { opts.input_message = input_message_type::CONTENT_FAKE_TOOL_CALL; } else if (type == "all") { opts.input_message = input_message_type::ALL; } else { LOG_ERR("Unknown input message type: %s\n", type.c_str()); return false; } } else { LOG_ERR("Unknown option: %s\n", arg.c_str()); print_usage(argv[0]); return false; } }
return true;}
static json build_debug_user_message() { return json{ { "role", "user" }, { "content", "Hello, please help me with a task." } };}
static json build_content_only_message() { return json{ { "role", "assistant" }, { "content", "Hello! I'm here to help you with your task." } };}
static json build_reasoning_content_message() { return json{ { "role", "assistant" }, { "content", "Hello! I'm here to help you with your task." }, { "reasoning_content", "The user is greeting me and asking for help. I should respond politely." } };}
static json build_tool_call_only_message() { return json{ { "role", "assistant" }, { "content", nullptr }, { "tool_calls", json::array({ json{ { "type", "function" }, { "function", json{ { "name", "test_function_name" }, { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } }, { "id", "123456789" } } }) } };}
static json build_content_tool_call_message() { return json{ { "role", "assistant" }, { "content", "I'll help you by calling a function." }, { "tool_calls", json::array({ json{ { "type", "function" }, { "function", json{ { "name", "test_function_name" }, { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } };}
static json build_reasoning_tool_call_message() { return json{ { "role", "assistant" }, { "content", nullptr }, { "reasoning_content", "I need to call a function to help with this task." }, { "tool_calls", json::array({ json{ { "type", "function" }, { "function", json{ { "name", "test_function_name" }, { "arguments", json::object({ { "param1", "value1" }, { "param2", "value2" } }) } } } } }) } };}
static json build_content_fake_tool_call_message() { // This message has content but NO tool_calls field // It's used to test if a template renders tool definitions but not tool calls return json{ { "role", "assistant" }, { "content", "I'll help you by calling a function." } };}
static void render_scenario(const common_chat_template & tmpl, const std::string & scenario_name, const json & messages, const json & tools, bool add_generation_prompt, bool enable_thinking) { LOG_ERR("\n=== Scenario: %s ===\n", scenario_name.c_str()); LOG_ERR("add_generation_prompt: %s, enable_thinking: %s\n", add_generation_prompt ? "true" : "false", enable_thinking ? "true" : "false");
// When add_generation_prompt is true, add a trailing user message to trigger the prompt json final_messages = messages; if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") { final_messages.push_back(json{ { "role", "user" }, { "content", "Now please continue with another response." } }); }
LOG_ERR("Messages:\n%s\n", final_messages.dump(2).c_str());
try { generation_params inputs; inputs.messages = final_messages; inputs.add_generation_prompt = add_generation_prompt; inputs.extra_context["enable_thinking"] = enable_thinking;
if (!tools.is_null() && tools.is_array() && !tools.empty()) { inputs.tools = tools; }
std::string output = common_chat_template_direct_apply(tmpl, inputs);
LOG_ERR("\n--- Rendered Output ---\n"); LOG_ERR("%s\n", output.c_str()); LOG_ERR("--- End Output (length: %zu) ---\n", output.length()); } catch (const std::exception & e) { LOG_ERR("Rendering failed: %s\n", e.what()); }}
static void render_all_scenarios(const common_chat_template & tmpl, const json & tools, bool add_generation_prompt, bool enable_thinking, input_message_type message_type) { json user_msg = build_debug_user_message();
auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) { if (message_type == input_message_type::ALL || message_type == type) { json messages = json::array({ user_msg, assistant_msg }); render_scenario(tmpl, name, messages, tools, add_generation_prompt, enable_thinking); } };
render_if(input_message_type::CONTENT_ONLY, "content_only", build_content_only_message()); render_if(input_message_type::REASONING_CONTENT, "reasoning_content", build_reasoning_content_message()); render_if(input_message_type::TOOL_CALL_ONLY, "tool_call_only", build_tool_call_only_message()); render_if(input_message_type::CONTENT_TOOL_CALL, "content_tool_call", build_content_tool_call_message()); render_if(input_message_type::REASONING_TOOL_CALL, "reasoning_tool_call", build_reasoning_tool_call_message()); render_if(input_message_type::CONTENT_FAKE_TOOL_CALL, "content_fake_tool_call", build_content_fake_tool_call_message());
// Also render with add_generation_prompt=true to show the prompt ending if (message_type == input_message_type::ALL) { LOG_ERR("\n\n=== Generation Prompt Scenarios (add_generation_prompt=true) ===\n");
json prompt_messages = json::array({ user_msg }); render_scenario(tmpl, "generation_prompt_only", prompt_messages, tools, true, enable_thinking);
// With enable_thinking toggled render_scenario(tmpl, "generation_prompt_thinking_disabled", prompt_messages, tools, true, false); }}
static generation_params prepare_debug_params(const debug_options & opts, const json & tools) { generation_params params; params.messages = json::array({ build_debug_user_message() }); params.reasoning_format = opts.enable_reasoning ? COMMON_REASONING_FORMAT_DEEPSEEK : COMMON_REASONING_FORMAT_NONE; params.enable_thinking = opts.enable_reasoning; params.add_generation_prompt = opts.generation_prompt;
if (opts.with_tools) { params.tools = tools; params.tool_choice = opts.force_tool_call ? COMMON_CHAT_TOOL_CHOICE_REQUIRED : COMMON_CHAT_TOOL_CHOICE_AUTO; } else { params.tools = json(); params.tool_choice = COMMON_CHAT_TOOL_CHOICE_NONE; } params.parallel_tool_calls = opts.parallel_tool_calls; return params;}
static int debug_single_template(const debug_options & opts) { std::string template_source; try { // Check if the file is a GGUF file if (opts.template_path.size() >= 5 && opts.template_path.compare(opts.template_path.size() - 5, 5, ".gguf") == 0) { template_source = read_gguf_chat_template(opts.template_path); } else { template_source = read_file(opts.template_path); } } catch (const std::exception & e) { LOG_ERR("Error reading template: %s\n", e.what()); return 1; }
LOG_ERR("Analyzing template: %s\n", opts.template_path.c_str()); LOG_ERR("Options: with_tools=%s, generation_prompt=%s, enable_reasoning=%s\n", opts.with_tools ? "true" : "false", opts.generation_prompt ? "true" : "false", opts.enable_reasoning ? "true" : "false");
try { common_chat_template chat_template(template_source, "", "");
json tools = opts.with_tools ? build_tools_definition() : json();
generation_params params = prepare_debug_params(opts, tools); common_chat_params parser_data; if (std::optional<common_chat_params> spec_tmpl = common_chat_try_specialized_template(chat_template, template_source, params)) { LOG_ERR("\n"); LOG_ERR("This template uses a specialized parser, analysis results will not be available.\n"); parser_data = *spec_tmpl; } else { // Render template scenarios if requested if (opts.input_message != input_message_type::NONE && (opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) { LOG_ERR("\n"); LOG_ERR("================================================================================\n"); LOG_ERR(" TEMPLATE RENDERING OUTPUT\n"); LOG_ERR("================================================================================\n");
render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning, opts.input_message); }
// Output analysis if requested if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) { LOG_ERR("\n"); LOG_ERR("================================================================================\n"); LOG_ERR(" TEMPLATE ANALYSIS\n"); LOG_ERR("================================================================================\n");
struct autoparser analysis; analysis.analyze_template(chat_template);
// Generate Parser parser_data = peg_generator::generate_parser(chat_template, params, analysis); } }
if (!std::empty(parser_data.parser)) { LOG_ERR("\n=== Generated Parser ===\n"); common_peg_arena arena; arena.load(parser_data.parser); LOG_ERR("%s\n", arena.dump(arena.root()).c_str());
LOG_ERR("\n=== Generated Grammar ===\n"); LOG_ERR("%s\n", parser_data.grammar.c_str());
LOG_ERR("\n=== Generated Lazy Grammar ===\n"); LOG_ERR("%d\n", parser_data.grammar_lazy);
LOG_ERR("\n=== Generated Grammar Triggers ===\n"); for (const common_grammar_trigger & cgt : parser_data.grammar_triggers) { LOG_ERR("Token: %d | Type: %d | Value: %s\n", cgt.token, cgt.type, cgt.value.c_str()); }
LOG_ERR("\n=== Preserved Tokens ===\n"); for (const std::string & token : parser_data.preserved_tokens) { LOG_ERR(" '%s'\n", token.c_str()); } } } catch (const std::exception & e) { LOG_ERR("Analysis failed: %s\n", e.what()); return 1; }
return 0;}
int main(int argc, char * argv[]) { if (argc > 1) { std::string arg = argv[1]; if (arg == "-h" || arg == "--help") { common_log_set_verbosity_thold(99); print_usage(argv[0]); return 0; }
// debug mode: if the first argument is an existing file, analyze that template instead of running the automated tests if (std::filesystem::is_regular_file(arg)) { common_log_set_verbosity_thold(99);
debug_options opts; if (!parse_debug_options(argc, argv, opts)) { return 1; }
if (opts.debug_jinja || std::getenv("LLAMA_DEBUG_JINJA") != nullptr) { jinja::enable_debug(true); }
return debug_single_template(opts); } }
testing t(std::cout); t.verbose = true;
// usage: test-chat-auto-parser [filter_regex]
if (argc > 1) { t.set_filter(argv[1]); }
t.test("diff_split", test_calculate_diff_split); t.test("common_prefix", test_until_common_prefix); t.test("common_suffix", test_after_common_suffix); t.test("compare_variants", test_compare_variants); t.test("segments", test_marker_separation); t.test("seed_oss_diffs", test_seed_oss_tool_analysis); t.test("cohere", test_cohere_analysis); t.test("nemotron", test_nemotron_analysis); t.test("laguna", test_laguna_analysis); t.test("laguna-s", test_laguna_s_analysis); t.test("laguna-xs2", test_laguna_xs2_analysis); t.test("smollm3", test_smollm3_analysis); t.test("standard_json_tools", test_standard_json_tools_formats); t.test("normalize_quotes_to_json", test_normalize_quotes_to_json); t.test("tagged_args_embedded_quotes", test_tagged_args_with_embedded_quotes); t.test("bailing_v3", test_bailing_v3_tool_format); t.test("role_markers_all_templates", test_role_markers_all_templates);
return t.summary();}
static void test_marker_separation(testing & t) { auto single_square_marker = segmentize_markers("pre_marker[marker]post_marker"); auto single_diag_marker = segmentize_markers("pre_marker<marker>post_marker"); auto paired_markers = segmentize_markers("<hello>world</hello>"); auto double_different_markers = segmentize_markers("<hello>[hello]<world>[world]"); auto in_between = segmentize_markers("im<blue>daba<dee>da[hey]");
t.test("single_square_marker", [&] (testing & t) { t.assert_equal("first is text", segment_type::TEXT, single_square_marker[0].type); t.assert_equal("second is marker", segment_type::MARKER, single_square_marker[1].type); t.assert_equal("last is text", segment_type::TEXT, single_square_marker[2].type);
t.assert_equal("first is 'pre_marker'", "pre_marker", single_square_marker[0].value); t.assert_equal("second is '[marker]'", "[marker]", single_square_marker[1].value); t.assert_equal("last is 'post_marker'", "post_marker", single_square_marker[2].value); });
t.test("single_diagonal_marker", [&] (testing & t) { t.assert_equal("first is text", segment_type::TEXT, single_diag_marker[0].type); t.assert_equal("second is marker", segment_type::MARKER, single_diag_marker[1].type); t.assert_equal("last is text", segment_type::TEXT, single_diag_marker[2].type);
t.assert_equal("first is 'pre_marker'", "pre_marker", single_diag_marker[0].value); t.assert_equal("second is '<marker>'", "<marker>", single_diag_marker[1].value); t.assert_equal("last is 'post_marker'", "post_marker", single_diag_marker[2].value); });
t.test("paired_markers", [&] (testing & t) { t.assert_equal("first is marker", segment_type::MARKER, paired_markers[0].type); t.assert_equal("second is text", segment_type::TEXT, paired_markers[1].type); t.assert_equal("third is marker", segment_type::MARKER, paired_markers[2].type);
t.assert_equal("first is '<hello>'", "<hello>", paired_markers[0].value); t.assert_equal("second is 'world'", "world", paired_markers[1].value); t.assert_equal("third is '</hello>'", "</hello>", paired_markers[2].value); });
t.test("double_different_markers", [&] (testing & t) { t.assert_equal("first is marker", segment_type::MARKER, double_different_markers[0].type); t.assert_equal("second is marker", segment_type::MARKER, double_different_markers[1].type); t.assert_equal("third is marker", segment_type::MARKER, double_different_markers[2].type); t.assert_equal("fourth is marker", segment_type::MARKER, double_different_markers[3].type);
t.assert_equal("first is '<hello>'", "<hello>", double_different_markers[0].value); t.assert_equal("second is '[hello]'", "[hello]", double_different_markers[1].value); t.assert_equal("third is '<world>'", "<world>", double_different_markers[2].value); t.assert_equal("fourth is '[world]'", "[world]", double_different_markers[3].value); });
t.test("in_between", [&] (testing & t) { t.assert_equal("first is text", segment_type::TEXT, in_between[0].type); t.assert_equal("second is marker", segment_type::MARKER, in_between[1].type); t.assert_equal("third is text", segment_type::TEXT, in_between[2].type); t.assert_equal("fourth is marker", segment_type::MARKER, in_between[3].type); t.assert_equal("fifth is text", segment_type::TEXT, in_between[4].type); t.assert_equal("sixth is marker", segment_type::MARKER, in_between[5].type);
t.assert_equal("first is 'im'", "im", in_between[0].value); t.assert_equal("second is '<blue>'", "<blue>", in_between[1].value); t.assert_equal("third is 'daba'", "daba", in_between[2].value); t.assert_equal("fourth is '<dee>'", "<dee>", in_between[3].value); t.assert_equal("fifth is 'da'", "da", in_between[4].value); t.assert_equal("sixth is '[hey]'", "[hey]", in_between[5].value); });}
static void test_calculate_diff_split(testing & t) { t.test("calculate_diff_split basic", test_calculate_diff_split_basic); t.test("calculate_diff_split identical", test_calculate_diff_split_identical); t.test("calculate_diff_split common prefix", test_calculate_diff_split_common_prefix); t.test("calculate_diff_split common suffix", test_calculate_diff_split_common_suffix); t.test("calculate_diff_split common both", test_calculate_diff_split_common_both); t.test("calculate_diff_split empty cases", test_calculate_diff_split_empty_cases); t.test("calculate_diff_split no common", test_calculate_diff_split_no_common); t.test("calculate_diff_split single char", test_calculate_diff_split_single_char); t.test("calculate_diff_split overlaps", test_calculate_diff_split_overlaps); t.test("calculate_diff_split tag boundaries", test_calculate_diff_split_tag_boundaries); t.test("calculate_diff_split generation prompt", test_calculate_diff_split_generation_prompt);}
static void test_calculate_diff_split_basic(testing & t) { diff_split result = calculate_diff_split("hello world", "hello test"); t.assert_equal("prefix should be 'hello '", "hello ", result.prefix); t.assert_equal("left should be 'world'", "world", result.left); t.assert_equal("right should be 'test'", "test", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("abc", "xyz"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'abc'", "abc", result.left); t.assert_equal("right should be 'xyz'", "xyz", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("prefixA suffix", "prefixB suffix"); t.assert_equal("prefix should be 'prefix'", "prefix", result.prefix); t.assert_equal("left should be 'A'", "A", result.left); t.assert_equal("right should be 'B'", "B", result.right); t.assert_equal("suffix should be ' suffix'", " suffix", result.suffix);}
static void test_calculate_diff_split_identical(testing & t) { diff_split result = calculate_diff_split("hello", "hello"); t.assert_equal("prefix should be 'hello'", "hello", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("", ""); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("a", "a"); t.assert_equal("prefix should be 'a'", "a", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("<row><row><row><your><boat><gently>", "<row><row><row><your><boat><gently>"); t.assert_equal("prefix should be '<row><row><row><your><boat><gently>'", "<row><row><row><your><boat><gently>", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_common_prefix(testing & t) { diff_split result = calculate_diff_split("abcdef", "abcxyz"); t.assert_equal("prefix should be 'abc'", "abc", result.prefix); t.assert_equal("left should be 'def'", "def", result.left); t.assert_equal("right should be 'xyz'", "xyz", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("same", "sameagain"); t.assert_equal("prefix should be 'same'", "same", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'again'", "again", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("test", "testing"); t.assert_equal("prefix should be 'test'", "test", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'ing'", "ing", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_common_suffix(testing & t) { diff_split result = calculate_diff_split("123end", "456end"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be '123'", "123", result.left); t.assert_equal("right should be '456'", "456", result.right); t.assert_equal("suffix should be 'end'", "end", result.suffix);
result = calculate_diff_split("start", "end"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'start'", "start", result.left); t.assert_equal("right should be 'end'", "end", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("abcsuffix", "xyzsuffix"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'abc'", "abc", result.left); t.assert_equal("right should be 'xyz'", "xyz", result.right); t.assert_equal("suffix should be 'suffix'", "suffix", result.suffix);}
static void test_calculate_diff_split_common_both(testing & t) { diff_split result = calculate_diff_split("helloXworld", "helloYworld"); t.assert_equal("prefix should be 'hello'", "hello", result.prefix); t.assert_equal("left should be 'X'", "X", result.left); t.assert_equal("right should be 'Y'", "Y", result.right); t.assert_equal("suffix should be 'world'", "world", result.suffix);
result = calculate_diff_split("ABCmiddleXYZ", "ABCdifferentXYZ"); t.assert_equal("prefix should be 'ABC'", "ABC", result.prefix); t.assert_equal("left should be 'middle'", "middle", result.left); t.assert_equal("right should be 'different'", "different", result.right); t.assert_equal("suffix should be 'XYZ'", "XYZ", result.suffix);
result = calculate_diff_split("startAend", "startBend"); t.assert_equal("prefix should be 'start'", "start", result.prefix); t.assert_equal("left should be 'A'", "A", result.left); t.assert_equal("right should be 'B'", "B", result.right); t.assert_equal("suffix should be 'end'", "end", result.suffix);
// Edge case: common prefix and suffix overlap result = calculate_diff_split("aa", "ab"); t.assert_equal("prefix should be 'a'", "a", result.prefix); t.assert_equal("left should be 'a'", "a", result.left); t.assert_equal("right should be 'b'", "b", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_empty_cases(testing & t) { // Empty left, non-empty right diff_split result = calculate_diff_split("", "hello"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'hello'", "hello", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Non-empty left, empty right result = calculate_diff_split("hello", ""); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'hello'", "hello", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Both empty result = calculate_diff_split("", ""); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Left single char, empty right result = calculate_diff_split("a", ""); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'a'", "a", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Empty left, right single char result = calculate_diff_split("", "a"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'a'", "a", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_no_common(testing & t) { diff_split result = calculate_diff_split("abc", "xyz"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'abc'", "abc", result.left); t.assert_equal("right should be 'xyz'", "xyz", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("left", "right"); // The algorithm finds "t" as a common suffix since both strings end with 't' // This is the algorithm's actual behavior - it finds maximal common suffix t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'lef'", "lef", result.left); t.assert_equal("right should be 'righ'", "righ", result.right); t.assert_equal("suffix should be 't'", "t", result.suffix);
result = calculate_diff_split("123", "456"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be '123'", "123", result.left); t.assert_equal("right should be '456'", "456", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_single_char(testing & t) { diff_split result = calculate_diff_split("a", "b"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'a'", "a", result.left); t.assert_equal("right should be 'b'", "b", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("a", "a"); t.assert_equal("prefix should be 'a'", "a", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("a", "ab"); t.assert_equal("prefix should be 'a'", "a", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'b'", "b", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("ab", "a"); t.assert_equal("prefix should be 'a'", "a", result.prefix); t.assert_equal("left should be 'b'", "b", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_overlaps(testing & t) { // One string is substring of another diff_split result = calculate_diff_split("test", "testing"); t.assert_equal("prefix should be 'test'", "test", result.prefix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be 'ing'", "ing", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
result = calculate_diff_split("testing", "test"); t.assert_equal("prefix should be 'test'", "test", result.prefix); t.assert_equal("left should be 'ing'", "ing", result.left); t.assert_equal("right should be empty", "", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Similar strings with one extra char at start result = calculate_diff_split("Xtest", "Ytest"); // The algorithm finds "test" as a common suffix since both strings end with "test" // This is the algorithm's actual behavior - it finds maximal common suffix t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'X'", "X", result.left); t.assert_equal("right should be 'Y'", "Y", result.right); t.assert_equal("suffix should be 'test'", "test", result.suffix);
// Similar strings with one extra char at end result = calculate_diff_split("testX", "testY"); t.assert_equal("prefix should be 'test'", "test", result.prefix); t.assert_equal("left should be 'X'", "X", result.left); t.assert_equal("right should be 'Y'", "Y", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Strings that are reverses result = calculate_diff_split("abc", "cba"); t.assert_equal("prefix should be empty", "", result.prefix); t.assert_equal("left should be 'abc'", "abc", result.left); t.assert_equal("right should be 'cba'", "cba", result.right); t.assert_equal("suffix should be empty", "", result.suffix);}
static void test_calculate_diff_split_tag_boundaries(testing & t) { // Test with unclosed XML tags diff_split result = calculate_diff_split("test<tag", "test>content"); // The fix_tag_boundaries should move incomplete tags appropriately t.assert_true("prefix should start with 'test'", result.prefix.find("test") == 0); t.assert_true("should handle tag boundaries", result.left != "" || result.right != "" || result.suffix != "");
// Test with unclosed brackets result = calculate_diff_split("test[", "test]value"); t.assert_true("should handle bracket boundaries", result.left != "" || result.right != "" || result.suffix != "");
// Test with partial tags on both sides result = calculate_diff_split("prefix<tag>", "prefix</tag>suffix"); // fix_tag_boundaries moves the incomplete '<' from prefix to left/right t.assert_equal("prefix should be 'prefix'", "prefix", result.prefix); t.assert_equal("left should be '<tag>'", "<tag>", result.left); t.assert_equal("right should be '</tag>suffix'", "</tag>suffix", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Test with complex nested tags result = calculate_diff_split("prefix<div>content</div>", "prefix<div>different</div>"); // Algorithm finds "ent</div>" as a common suffix because both strings end with it // This is the actual algorithm behavior, though not semantically ideal t.assert_equal("prefix should be 'prefix<div>'", "prefix<div>", result.prefix); t.assert_equal("left should be 'cont'", "cont", result.left); t.assert_equal("right should be 'differ'", "differ", result.right); t.assert_equal("suffix should be 'ent</div>'", "ent</div>", result.suffix);
// Test with unclosed angle bracket result = calculate_diff_split("Hello <world>", "Hello test"); t.assert_equal("prefix should be 'Hello '", "Hello ", result.prefix); t.assert_true("left should contain '<world>'", result.left.find("<world>") != std::string::npos); t.assert_equal("right should be 'test'", "test", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Test with unclosed square bracket result = calculate_diff_split("test [array]", "test other"); t.assert_equal("prefix should be 'test '", "test ", result.prefix); t.assert_true("left should contain '[array]'", result.left.find("[array]") != std::string::npos); t.assert_equal("right should be 'other'", "other", result.right); t.assert_equal("suffix should be empty", "", result.suffix);
// Test empty prefix and suffix with tags result = calculate_diff_split("<tag>left</tag>", "<tag>righ</tag>"); t.assert_equal("prefix should be '<tag>'", "<tag>", result.prefix); t.assert_equal("left should be 'left'", "left", result.left); t.assert_equal("right should be 'righ'", "righ", result.right); t.assert_equal("suffix should be '</tag>'", "</tag>", result.suffix);
{ // real case from template tests, simplified std::string left = "PREFIX</think>Sure"; std::string right = "PREFIX<think>Lemme think</think>Sure"; result = calculate_diff_split(left, right); t.assert_equal("prefix should be PREFIX", "PREFIX", result.prefix); t.assert_equal("suffix should be </think>Sure", "</think>Sure", result.suffix); t.assert_equal("left should be empty", "", result.left); t.assert_equal("right should be <think>Lemme think", "<think>Lemme think", result.right); }
{ // Real case: special tokens with |> boundary issue // The suffix starts with |> which should be moved to complete <|END_RESPONSE and <|END_ACTION std::string prefix = "SOME_PREFIX"; std::string suffix = "|><|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>"; std::string left_diff = "<|START_RESPONSE|>Let me help you.<|END_RESPONSE"; std::string right_diff = "<|START_THINKING|><|END_THINKING|><|START_ACTION|>[\n" " {\"tool_call_id\": \"0\", \"tool_name\": \"test_function_name\", " "\"parameters\": {\"param1\": \"value1\", \"param2\": \"value2\"}}\n" "]<|END_ACTION";
std::string left = prefix + left_diff + suffix; std::string right = prefix + right_diff + suffix; result = calculate_diff_split(left, right);
t.assert_equal("special token prefix", prefix, result.prefix); // The |> should be moved from suffix to complete the tokens t.assert_equal("special token left", "<|START_RESPONSE|>Let me help you.<|END_RESPONSE|>", result.left); t.assert_true("special token right ends with |>", result.right.find("<|END_ACTION|>") != std::string::npos); t.assert_equal("special token suffix", "<|END_OF_TURN_TOKEN|><|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>", result.suffix); }}
static void test_calculate_diff_split_generation_prompt(testing & t) { // ChatML thinking template: left is a prefix of right, generation_prompt is the appended part. // The trailing \n in left matches the trailing \n in the generation_prompt, causing // the suffix matcher to steal it and rotate the diff result. { // Simplified reproduction: left ends with \n, right = left + "<|im_start|>assistant\n<think>\n" std::string left = "<|im_start|>user\nHello<|im_end|>\n"; std::string right = left + "<|im_start|>assistant\n<think>\n"; diff_split result = calculate_diff_split(left, right); t.assert_equal("chatml prefix", left, result.prefix); t.assert_equal("chatml left", "", result.left); t.assert_equal("chatml right should be generation prompt", "<|im_start|>assistant\n<think>\n", result.right); t.assert_equal("chatml suffix", "", result.suffix); }
{ // More realistic: longer conversation ending with tool_response std::string common = "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n" "<|im_start|>user\nSearch for files<|im_end|>\n" "<|im_start|>assistant\n<think>\nLet me search.\n</think>\n\n" "<tool_call>\n<function=search>\n</function>\n</tool_call><|im_end|>\n" "<|im_start|>user\n<tool_response>\nNo files found\n</tool_response><|im_end|>\n"; std::string left = common; std::string right = common + "<|im_start|>assistant\n<think>\n"; diff_split result = calculate_diff_split(left, right); t.assert_equal("tool_response left", "", result.left); t.assert_equal("tool_response right should be generation prompt", "<|im_start|>assistant\n<think>\n", result.right); }}
static void test_until_common_prefix(testing & t) { t.test("until_common_prefix basic", test_until_common_prefix_basic);}
static void test_until_common_prefix_basic(testing & t) { // Test case from the user request std::string result = until_common_prefix("<function name=foo><arg name=bar>", "<arg name=bar>", "<arg name=baz>"); t.assert_equal("untilCommonPrefix should return '<function name=foo>'", "<function name=foo>", result);
// Additional test cases to ensure robustness // Test with different common prefix lengths result = until_common_prefix("prefix<test>suffix", "<test>different", "<test>other"); t.assert_equal("should return 'prefix'", "prefix", result);
// Test when common prefix is at the start result = until_common_prefix("<common>rest", "<common>left", "<common>right"); t.assert_equal("should return empty string when common prefix at start", "", result);
// Test when there's no common prefix result = until_common_prefix("something", "left", "right"); t.assert_equal("should return empty string when no common prefix", "", result);
// Test with empty strings result = until_common_prefix("test", "", "right"); t.assert_equal("should return empty string when left is empty", "", result);
// Test with longer common prefix result = until_common_prefix("abcXYZ<shared_prefix>rest", "<shared_prefix>left", "<shared_prefix>right"); t.assert_equal("should return 'abcXYZ'", "abcXYZ", result);}
static void test_after_common_suffix(testing & t) { t.test("after_common_suffix basic", test_after_common_suffix_basic);}
static void test_after_common_suffix_basic(testing & t) { // Test case from the user request std::string result = after_common_suffix("<function name=foo><arg name=bar>100</arg></function>", "<arg name=bar>100</arg>", "<arg name=baz>535</arg>"); t.assert_equal("afterCommonSuffix should return '</function>'", "</function>", result);
// Test when common suffix is at the end result = after_common_suffix("rest<common>", "left<common>", "right<common>"); t.assert_equal("should return empty string when common suffix at end", "", result);
// Test with empty strings result = after_common_suffix("test", "left", ""); t.assert_equal("should return empty string when right is empty", "", result);
// Test case with XML-like structure similar to the main example result = after_common_suffix("<outer><inner>value</inner></outer>", "<inner>value</inner>", "<inner>different</inner>"); t.assert_equal("should return '</outer>'", "</outer>", result);
// Test with longer common suffix appearing at the end of full result = after_common_suffix("prefix<shared>rest</shared>", "prefix<shared>left</shared>", "prefix<shared>right</shared>"); t.assert_equal("should return '' when common suffix is at end of full", "", result);
// Test with common suffix appearing in middle but not at end result = after_common_suffix("<tag>content</tag><extra>", "<tag>value</tag>", "<tag>other</tag>"); t.assert_equal("should return '<extra>' when common suffix appears before end", "<extra>", result);
// Test with multi-character common suffix at the very end of full result = after_common_suffix("start<middle>end</middle>", "prefix<middle>left</middle>", "prefix<middle>right</middle>"); t.assert_equal("should return '' when common suffix </middle> is at end of full", "", result);}
static void test_compare_variants(testing & t) { t.test("compare_variants basic", test_compare_variants_basic); t.test("compare_variants messages modifier", test_compare_variants_messages_modifier); t.test("compare_variants tools modifier", test_compare_variants_tools_modifier); t.test("compare_variants both modifiers", test_compare_variants_both_modifiers); t.test("compare_variants template failure", test_compare_variants_template_failure); t.test("compare_variants identity", test_compare_variants_identity);}
static void test_compare_variants_basic(testing & t) { // Create a simple template that just echoes messages common_chat_template tmpl("{{ messages[0]['content'] }}", "", "");
template_params params; params.messages = json::array({ json {{"role", "user"}, {"content", "Hello"}} });
auto modifier = [](template_params & p) { p.messages[0]["content"] = "World"; };
auto result = ::compare_variants(tmpl, params, modifier);
if (!t.assert_true("result should have value", result.has_value())) { return; } // The template might not output anything if messages is empty or format is different // Check that we get a valid result t.assert_true("prefix or left should have content", !result->diff.prefix.empty() || !result->diff.left.empty());}
static void test_compare_variants_messages_modifier(testing & t) { // Test with messages modifier only common_chat_template tmpl("{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}", "", "");
template_params params; params.messages = json::array({ json {{"role", "user"}, {"content", "A"}} });
auto modifier = [](template_params & p) { p.messages[0]["content"] = "B"; };
std::optional<compare_variants_result> result = ::compare_variants(tmpl, params, modifier);
if (!t.assert_true("result should have value", result.has_value())) { return; } t.assert_equal("left should be 'A'", "A", result->diff.left); t.assert_equal("right should be 'B'", "B", result->diff.right);}
static void test_compare_variants_tools_modifier(testing & t) { // Test with tools modifier only common_chat_template tmpl( "{% for tool in tools %}{{ tool['name'] }}{% endfor %}", "", "");
template_params params; params.tools = json::array({ json {{"name", "foo"}} });
auto modifier = [](template_params & p) { p.tools[0]["name"] = "bar"; };
auto result = ::compare_variants(tmpl, params, modifier);
if (!t.assert_true("result should have value", result.has_value())) { return; } t.assert_equal("left should be 'foo'", "foo", result->diff.left); t.assert_equal("right should be 'bar'", "bar", result->diff.right);}
static void test_compare_variants_both_modifiers(testing & t) { // Test with both messages and tools modifiers using the for loop approach common_chat_template tmpl( "{% for message in messages %}{{ message['role'] }}:{{ message['content'] }}{% endfor %}", "", "");
template_params params; params.messages = json::array({ json {{"role", "user"}, {"content", "A"}} });
auto modifier = [](template_params & p) { p.messages[0]["content"] = "B"; p.messages[0]["role"] = "newuser"; };
auto result = ::compare_variants(tmpl, params, modifier);
if (!t.assert_true("result should have value", result.has_value())) { return; } t.assert_equal("left should be 'user:A'", "user:A", result->diff.left); t.assert_equal("right should be 'newuser:B'", "newuser:B", result->diff.right);}
static void test_compare_variants_template_failure(testing & t) { // Test with template that causes failure during application (not construction) // We use a valid template syntax but one that will fail during application common_chat_template tmpl("{{ messages.cahoot()[0]['nonexistent_field'] }}", "", "");
template_params params; params.messages = json::array({ json {{"role", "user"}, {"content", "Hello"}} });
auto modifier = [](template_params & p) { p.messages[0]["content"] = "World"; };
auto result = ::compare_variants(tmpl, params, modifier);
t.assert_true("result should be nullopt on template failure", !result.has_value());}
static void test_compare_variants_identity(testing & t) { // Test with identity modifier (no change) common_chat_template tmpl("{{ messages[0]['content'] }}", "", "");
template_params params; params.messages = json::array({ json {{"role", "user"}, {"content", "Hello"}} });
// No modifier - should use identity auto result = ::compare_variants(tmpl, params, nullptr);
if (!t.assert_true("result should have value", result.has_value())) { return; } t.assert_equal("prefix should be 'Hello'", "Hello", result->diff.prefix); t.assert_equal("left should be empty", "", result->diff.left); t.assert_equal("right should be empty", "", result->diff.right); t.assert_equal("suffix should be empty", "", result->diff.suffix);}
// ============================================================================// Seed-OSS Template Tool Calling Analysis Tests// ============================================================================
static void test_seed_oss_tool_analysis(testing & t) { t.test("Seed-OSS tool presence", test_seed_oss_tool_presence); t.test("Seed-OSS call count", test_seed_oss_call_count); t.test("Seed-OSS function names", test_seed_oss_function_names); t.test("Seed-OSS argument count", test_seed_oss_argument_count); t.test("Seed-OSS args presence", test_seed_oss_args_presence); t.test("Seed-OSS tool with reasoning", test_seed_oss_tool_with_reasoning);}
// Helper to load Seed-OSS templatestatic common_chat_template load_seed_oss_template(testing & t) { std::string template_path = "models/templates/ByteDance-Seed-OSS.jinja"; std::ifstream fin(template_path, std::ios::binary); std::ostringstream buf; if (fin.is_open()) { buf << fin.rdbuf(); } std::string template_source = buf.str(); common_chat_template tmpl(template_source, "", ""); t.assert_true("Seed-OSS template loaded successfully", template_source.length() > 0); return tmpl;}
// Helper to build tool call JSONstatic json build_tool_call(const std::string & name, const json & args, const std::string & id = "call_001") { return json{ {"id", id}, {"type", "function"}, {"function", json{ {"name", name}, {"arguments", args} }} };}
// Helper to build tools definitionstatic json build_tools_definition() { json parameters_schema = json::object(); parameters_schema["type"] = "object"; parameters_schema["properties"] = json::object(); parameters_schema["properties"]["param1"] = json::object({ {"type", "string"}, {"description", "First parameter"} }); parameters_schema["properties"]["param2"] = json::object({ {"type", "string"}, {"description", "Second parameter"} }); parameters_schema["required"] = json::array({"param1", "param2"});
return json::array({ json{ {"type", "function"}, {"function", json{ {"name", "test_function_name"}, {"description", "A test function for debugging"}, {"parameters", parameters_schema} }} } });}
// T1: Compare with/without tool call (user, assistant)static void test_seed_oss_tool_presence(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
json assistant_no_tools = json{ {"role", "assistant"}, {"content", "Let me help you."} };
json assistant_with_tools = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) })} };
json user_msg = json{ {"role", "user"}, {"content", "Hello, please help me."} };
template_params params_no_tools; params_no_tools.messages = json::array({user_msg, assistant_no_tools}); params_no_tools.tools = build_tools_definition(); params_no_tools.add_generation_prompt = false; params_no_tools.enable_thinking = true;
template_params params_with_tools; params_with_tools.messages = json::array({user_msg, assistant_with_tools}); params_with_tools.tools = build_tools_definition(); params_with_tools.add_generation_prompt = false; params_with_tools.enable_thinking = true;
auto result = ::compare_variants(tmpl, params_no_tools, [&](template_params & p) { p.messages = params_with_tools.messages; });
if (!t.assert_true("T1 result should have value", result.has_value())) { return; }
const auto & diff = result->diff; t.assert_true("T1 prefix should contain system", diff.prefix.find("system") != std::string::npos); t.assert_true("T1 prefix should contain user", diff.prefix.find("user") != std::string::npos); t.assert_true("T1 prefix should contain assistant", diff.prefix.find("assistant") != std::string::npos);
// Left should be the assistant content without tool t.assert_equal("T1 left should contain 'Let me help you.'", "Let me help you.", diff.left);
// Right should contain the tool call markers t.assert_true("T1 right should contain tool_call begin", diff.right.find("<seed:tool_call>") != std::string::npos); t.assert_true("T1 right should contain function tag", diff.right.find("<function=test_function_name>") != std::string::npos); t.assert_true("T1 right should contain parameter=param1", diff.right.find("<parameter=param1>") != std::string::npos); t.assert_true("T1 right should contain parameter=param2", diff.right.find("<parameter=param2>") != std::string::npos); t.assert_true("T1 right should contain value1", diff.right.find("value1") != std::string::npos); t.assert_true("T1 right should contain value2", diff.right.find("value2") != std::string::npos); t.assert_true("T1 right should contain tool_call end", diff.right.find("</seed:tool_call>") != std::string::npos);
// Suffix should be the eos token t.assert_equal("T1 suffix should be '<seed:eos>'", "<seed:eos>", diff.suffix);}
// T2: Compare one vs two tool callsstatic void test_seed_oss_call_count(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
json assistant_one_call = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) })} };
json assistant_two_calls = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})), build_tool_call("test_function_name", json::object({{"param1", "value3"}, {"param2", "value4"}}), "call_002") })} };
json user_msg = json{ {"role", "user"}, {"content", "Hello, please help me."} };
template_params params_one; params_one.messages = json::array({user_msg, assistant_one_call}); params_one.tools = build_tools_definition(); params_one.add_generation_prompt = false; params_one.enable_thinking = true;
auto result = ::compare_variants(tmpl, params_one, [&](template_params & p) { p.messages = json::array({user_msg, assistant_two_calls}); });
if (!t.assert_true("T2 result should have value", result.has_value())) { return; }
const auto & diff = result->diff;
// Prefix should include the first tool call t.assert_true("T2 prefix should contain first tool_call begin", diff.prefix.find("<seed:tool_call>") != std::string::npos); t.assert_true("T2 prefix should contain first function", diff.prefix.find("<function=test_function_name>") != std::string::npos); t.assert_true("T2 prefix should contain value1", diff.prefix.find("value1") != std::string::npos); t.assert_true("T2 prefix should contain value2", diff.prefix.find("value2") != std::string::npos); t.assert_true("T2 prefix should contain first tool_call end", diff.prefix.find("</seed:tool_call>") != std::string::npos);
// Left should be empty (no second tool call in variant A) t.assert_equal("T2 left should be empty", "", diff.left);
// Right should contain the second tool call t.assert_true("T2 right should contain second tool_call begin", diff.right.find("<seed:tool_call>") != std::string::npos); t.assert_true("T2 right should contain second function", diff.right.find("<function=test_function_name>") != std::string::npos); t.assert_true("T2 right should contain value3", diff.right.find("value3") != std::string::npos); t.assert_true("T2 right should contain value4", diff.right.find("value4") != std::string::npos); t.assert_true("T2 right should contain second tool_call end", diff.right.find("</seed:tool_call>") != std::string::npos);
// Suffix should end with the eos token t.assert_equal("T2 suffix should end with '<seed:eos>'", "<seed:eos>", diff.suffix.substr(diff.suffix.length() - 10, 10));}
// T3: Compare different function namesstatic void test_seed_oss_function_names(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
// Build tools with two different function names json parameters_schema = json::object(); parameters_schema["type"] = "object"; parameters_schema["properties"] = json::object(); parameters_schema["properties"]["arg1"] = json::object({ {"type", "string"}, {"description", "Argument 1"} }); parameters_schema["required"] = json::array({"arg1"});
json tools = json::array({ json{ {"type", "function"}, {"function", json{ {"name", "func_alpha"}, {"description", "First function"}, {"parameters", parameters_schema} }} }, json{ {"type", "function"}, {"function", json{ {"name", "func_beta"}, {"description", "Second function"}, {"parameters", parameters_schema} }} } });
json assistant_func_alpha = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("func_alpha", json::object({{"arg1", "test_value"}})) })} };
json assistant_func_beta = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("func_beta", json::object({{"arg1", "test_value"}})) })} };
json user_msg = json{ {"role", "user"}, {"content", "Hello"} };
template_params params_alpha; params_alpha.messages = json::array({user_msg, assistant_func_alpha}); params_alpha.tools = tools; params_alpha.add_generation_prompt = false; params_alpha.enable_thinking = true;
auto result = ::compare_variants(tmpl, params_alpha, [&](template_params & p) { p.messages = json::array({user_msg, assistant_func_beta}); });
if (!t.assert_true("T3 result should have value", result.has_value())) { return; }
const auto & diff = result->diff;
bool func_alpha_in_left = diff.left.find("func_alpha") != std::string::npos; bool func_alpha_in_prefix = diff.prefix.find("func_alpha") != std::string::npos; bool func_beta_in_right = diff.right.find("func_beta") != std::string::npos; bool func_beta_in_prefix = diff.prefix.find("func_beta") != std::string::npos; bool func_beta_in_suffix = diff.suffix.find("func_beta") != std::string::npos;
// Left should contain func_alpha (or be in prefix) t.assert_true("T3 left should contain func_alpha (or prefix)", func_alpha_in_left || func_alpha_in_prefix);
// Right should contain func_beta t.assert_true("T3 right should contain func_beta", func_beta_in_right || func_beta_in_prefix || func_beta_in_suffix);
// Both should have the same parameter value (in common parts, not in diffs) // Since both have same args, test_value will be in prefix/suffix t.assert_true("T3 diff should contain test_value (in prefix or suffix)", diff.prefix.find("test_value") != std::string::npos || diff.suffix.find("test_value") != std::string::npos);}
// T4: Compare different argument counts (zero, one, two parameters)static void test_seed_oss_argument_count(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
// Build tools with 0, 1, or 2 required parameters json params_2_required = json::object(); params_2_required["type"] = "object"; params_2_required["properties"] = json::object(); params_2_required["properties"]["arg1"] = json::object({ {"type", "string"}, {"description", "Argument 1"} }); params_2_required["properties"]["arg2"] = json::object({ {"type", "string"}, {"description", "Argument 2"} }); params_2_required["required"] = json::array({"arg1", "arg2"});
json params_1_required = json::object(); params_1_required["type"] = "object"; params_1_required["properties"] = json::object(); params_1_required["properties"]["arg1"] = json::object({ {"type", "string"}, {"description", "Argument 1"} }); params_1_required["required"] = json::array({"arg1"});
json tools = json::array({ json{ {"type", "function"}, {"function", json{ {"name", "test_func"}, {"description", "Test function"}, {"parameters", params_2_required} }} } });
// Test: zero args vs one arg json assistant_zero_args = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_func", json::object()) })} };
json assistant_one_arg = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_func", json::object({{"arg1", "value1"}})) })} };
json assistant_two_args = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_func", json::object({{"arg1", "value1"}, {"arg2", "value2"}})) })} };
json user_msg = json{ {"role", "user"}, {"content", "Hello"} };
// Test zero vs one template_params params_zero; params_zero.messages = json::array({user_msg, assistant_zero_args}); params_zero.tools = tools; params_zero.add_generation_prompt = false; params_zero.enable_thinking = true;
auto result_zero_one = ::compare_variants(tmpl, params_zero, [&](template_params & p) { p.messages = json::array({user_msg, assistant_one_arg}); });
if (!t.assert_true("T4 zero vs one result should have value", result_zero_one.has_value())) { return; } t.assert_true("T4 zero vs one left should be empty or minimal", result_zero_one->diff.left.empty() || result_zero_one->diff.left == ""); t.assert_true("T4 zero vs one right should contain arg1", result_zero_one->diff.right.find("arg1") != std::string::npos);
// Test one vs two template_params params_one; params_one.messages = json::array({user_msg, assistant_one_arg}); params_one.tools = tools; params_one.add_generation_prompt = false; params_one.enable_thinking = true;
auto result_one_two = ::compare_variants(tmpl, params_one, [&](template_params & p) { p.messages = json::array({user_msg, assistant_two_args}); });
if (!t.assert_true("T4 one vs two result should have value", result_one_two.has_value())) { return; }
const auto & diff4 = result_one_two->diff; t.assert_true("T4 one vs two left should contain arg1 (or prefix)", diff4.left.find("arg1") != std::string::npos || diff4.prefix.find("arg1") != std::string::npos); t.assert_true("T4 one vs two right should contain arg1 (or prefix)", diff4.right.find("arg1") != std::string::npos || diff4.prefix.find("arg1") != std::string::npos); t.assert_true("T4 one vs two right should contain arg2 (or prefix/suffix)", diff4.right.find("arg2") != std::string::npos || diff4.prefix.find("arg2") != std::string::npos || diff4.suffix.find("arg2") != std::string::npos);}
// T5: Compare different argument valuesstatic void test_seed_oss_args_presence(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
json assistant_same_arg = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}})) })} };
json assistant_other_arg = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param2", "value2"}})) })} };
json assistant_both_args = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) })} };
json user_msg = json{ {"role", "user"}, {"content", "Hello"} };
template_params params_same; params_same.messages = json::array({user_msg, assistant_same_arg}); params_same.tools = build_tools_definition(); params_same.add_generation_prompt = false; params_same.enable_thinking = true;
// Test same arg vs other arg auto result_same_other = ::compare_variants(tmpl, params_same, [&](template_params & p) { p.messages = json::array({user_msg, assistant_other_arg}); });
if (!t.assert_true("T5 same vs other result should have value", result_same_other.has_value())) { return; } const auto & diff5a = result_same_other->diff; t.assert_true("T5 same vs other left should contain param1 (or prefix/suffix)", diff5a.left.find("param1") != std::string::npos || diff5a.prefix.find("param1") != std::string::npos || diff5a.suffix.find("param1") != std::string::npos); t.assert_true("T5 same vs other left should contain value1 (or prefix/suffix)", diff5a.left.find("value1") != std::string::npos || diff5a.prefix.find("value1") != std::string::npos); t.assert_true("T5 same vs other right should contain param2 (or prefix/suffix)", diff5a.right.find("param2") != std::string::npos || diff5a.prefix.find("param2") != std::string::npos || diff5a.suffix.find("param2") != std::string::npos); t.assert_true("T5 same vs other right should contain value2 (or prefix/suffix)", diff5a.right.find("value2") != std::string::npos || diff5a.prefix.find("value2") != std::string::npos || diff5a.suffix.find("value2") != std::string::npos);
// Test same arg vs both args auto result_same_both = ::compare_variants(tmpl, params_same, [&](template_params & p) { p.messages = json::array({user_msg, assistant_both_args}); });
if (!t.assert_true("T5 same vs both result should have value", result_same_both.has_value())) { return; } const auto & diff5b = result_same_both->diff; t.assert_true("T5 same vs both left should contain param1 (or prefix/suffix)", diff5b.left.find("param1") != std::string::npos || diff5b.prefix.find("param1") != std::string::npos || diff5b.suffix.find("param1") != std::string::npos); t.assert_true("T5 same vs both right should contain param1 (or prefix/suffix)", diff5b.right.find("param1") != std::string::npos || diff5b.prefix.find("param1") != std::string::npos || diff5b.suffix.find("param1") != std::string::npos); t.assert_true("T5 same vs both right should contain param2 (or prefix/suffix)", diff5b.right.find("param2") != std::string::npos || diff5b.prefix.find("param2") != std::string::npos || diff5b.suffix.find("param2") != std::string::npos);}
// T6: Tool call with vs without reasoning_contentstatic void test_seed_oss_tool_with_reasoning(testing & t) { common_chat_template tmpl = load_seed_oss_template(t);
json assistant_tool_only = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) })} };
json assistant_tool_with_reasoning = json{ {"role", "assistant"}, {"content", nullptr}, {"tool_calls", json::array({ build_tool_call("test_function_name", json::object({{"param1", "value1"}, {"param2", "value2"}})) })}, {"reasoning_content", "I need to call the tool first."} };
json user_msg = json{ {"role", "user"}, {"content", "Hello, please help me."} };
template_params params_tool_only; params_tool_only.messages = json::array({user_msg, assistant_tool_only}); params_tool_only.tools = build_tools_definition(); params_tool_only.add_generation_prompt = false; params_tool_only.enable_thinking = true;
auto result = ::compare_variants(tmpl, params_tool_only, [&](template_params & p) { p.messages = json::array({user_msg, assistant_tool_with_reasoning}); });
if (!t.assert_true("T6 result should have value", result.has_value())) { return; }
const auto & diff = result->diff;
// Left should be empty (no reasoning in variant A) t.assert_equal("T6 left should be empty", "", diff.left);
// Right should contain the thinking token with reasoning content t.assert_true("T6 right should contain think begin", diff.right.find("<seed:think>") != std::string::npos); t.assert_true("T6 right should contain reasoning content", diff.right.find("I need to call the tool first.") != std::string::npos); t.assert_true("T6 right should contain think end", diff.right.find("</seed:think>") != std::string::npos);
// Prefix should contain the assistant role t.assert_true("T6 prefix should contain assistant", diff.prefix.find("assistant") != std::string::npos);
// Suffix should contain the tool call t.assert_true("T6 suffix should contain tool_call begin", diff.suffix.find("<seed:tool_call>") != std::string::npos); t.assert_true("T6 suffix should contain function name", diff.suffix.find("test_function_name") != std::string::npos); t.assert_true("T6 suffix should contain eos", diff.suffix.find("<seed:eos>") != std::string::npos);}
static common_chat_template load_template(testing & t, const std::string & template_path) { std::ifstream fin(template_path, std::ios::binary); std::ostringstream buf; if (fin.is_open()) { buf << fin.rdbuf(); } std::string template_source = buf.str(); common_chat_template tmpl(template_source, "", ""); t.assert_true("Nemotron template loaded successfully", template_source.length() > 0); return tmpl;}
// ============================================================================// Nemotron Template Analysis Tests// ============================================================================static common_chat_template load_nemotron_template(testing & t) { return load_template(t, "models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja");}
static void test_nemotron_analysis(testing & t) { t.test("Nemotron reasoning detection", test_nemotron_reasoning_detection); t.test("Nemotron tool format", test_nemotron_tool_format);}
static void test_nemotron_reasoning_detection(testing & t) { common_chat_template tmpl = load_nemotron_template(t);
// Test the comparison manually to see what's happening json user_msg = json{ { "role", "user" }, { "content", "Hello" } }; json assistant_no_reasoning = json{ { "role", "assistant" }, { "content", "I can help." } }; json assistant_with_reasoning = json{ { "role", "assistant" }, { "content", "I can help." }, { "reasoning_content", "Let me think about this." } };
template_params params; params.messages = json::array({ user_msg, assistant_no_reasoning }); params.add_generation_prompt = false; params.enable_thinking = true;
// Run differential analysis struct autoparser analysis; analysis.analyze_template(tmpl);
// Check reasoning markers t.assert_equal("reasoning_start should be '<think>\\n'", "<think>\n", analysis.reasoning.start); t.assert_equal("reasoning_end should be '\\n</think>\\n'", "\n</think>\n", analysis.reasoning.end);
// Check reasoning mode detection // Nemotron uses tag-based reasoning; prefill handles the template's forced markers t.assert_equal("reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);
// Make sure reasoning markers don't spill over to content markers t.assert_equal("content start should be empty", "", analysis.content.start); t.assert_equal("content end should be empty", "", analysis.content.end);
t.assert_equal("content should be PLAIN", content_mode::PLAIN, analysis.content.mode);}
static void test_nemotron_tool_format(testing & t) { common_chat_template tmpl = load_nemotron_template(t);
// Run differential analysis struct autoparser analysis; analysis.analyze_template(tmpl);
// Check tool markers - Nemotron uses per-call wrapping (each call individually wrapped) t.assert_equal("tool_section_start should be empty (per-call format)", "", analysis.tools.format.section_start); t.assert_equal("tool_section_end should be empty (per-call format)", "", analysis.tools.format.section_end); t.assert_equal("per_call_start should be '<tool_call>\\n'", "<tool_call>\n", analysis.tools.format.per_call_start); t.assert_equal("per_call_end should be '</tool_call>'", "</tool_call>", analysis.tools.format.per_call_end); t.assert_true("should support parallel calls", analysis.jinja_caps.supports_parallel_tool_calls);
// Check function markers t.assert_equal("func_name_prefix should be '<function='", "<function=", analysis.tools.function.name_prefix); t.assert_equal("func_name_suffix should be '>\\n'", ">\n", analysis.tools.function.name_suffix); t.assert_equal("func_close should be '</function>\\n'", "</function>\n", analysis.tools.function.close);
// Check argument markers (note: markers retain trailing newlines for proper parsing) t.assert_equal("arg_name_prefix should be '<parameter='", "<parameter=", analysis.tools.arguments.name_prefix); t.assert_equal("arg_name_suffix should be '>\\n'", ">\n", analysis.tools.arguments.name_suffix); t.assert_equal("arg_value_suffix should be '\\n</parameter>\\n'", "\n</parameter>\n", analysis.tools.arguments.value_suffix);
// Check format classification t.assert_true("tool format should be TAG_WITH_TAGGED", analysis.tools.format.mode == tool_format::TAG_WITH_TAGGED);
// Verify tool support t.assert_true("should support tools", analysis.jinja_caps.supports_tools);}
// ============================================================================// Laguna Template Analysis Tests// ============================================================================static common_chat_template load_laguna_template(testing & t) { return load_template(t, "models/templates/poolside-Laguna-XS-2.1.jinja");}
static void test_laguna_reasoning_detection(testing & t) { common_chat_template tmpl = load_laguna_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); // Laguna's template renders reasoning delimiters with formatting whitespace // ("<think>\n") that the model does not emit; the Laguna patch trims them. t.assert_equal("reasoning_start should be '<think>'", "<think>", analysis.reasoning.start); t.assert_equal("reasoning_end should be '</think>'", "</think>", analysis.reasoning.end); t.assert_equal("reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);}
static void test_laguna_tool_format(testing & t) { common_chat_template tmpl = load_laguna_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); t.assert_equal("arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);}
static void test_laguna_stop_string(testing & t) { // The </assistant> turn terminator can be emitted as ordinary text tokens // (not the single eot token), so it must also be a literal stop string. common_chat_template tmpl = load_laguna_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); bool has_stop = false; for (const auto & stop : analysis.additional_stops) { if (stop == "</assistant>") { has_stop = true; break; } } t.assert_true("Laguna additional_stops contains </assistant>", has_stop);}
static void test_laguna_analysis(testing & t) { t.test("Laguna reasoning detection", test_laguna_reasoning_detection); t.test("Laguna tool format", test_laguna_tool_format); t.test("Laguna stop string", test_laguna_stop_string);}
static common_chat_template load_laguna_s_template(testing & t) { return load_template(t, "models/templates/poolside-Laguna-S-2.1.jinja");}static void test_laguna_s_reasoning_detection(testing & t) { common_chat_template tmpl = load_laguna_s_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); t.assert_equal("Laguna-S(v8) reasoning_start should be '<think>'", "<think>", analysis.reasoning.start); t.assert_equal("Laguna-S(v8) reasoning_end should be '</think>'", "</think>", analysis.reasoning.end); t.assert_equal("Laguna-S(v8) reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);}static void test_laguna_s_tool_format(testing & t) { common_chat_template tmpl = load_laguna_s_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); t.assert_equal("Laguna-S(v8) arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);}static void test_laguna_s_preserve_reasoning(testing & t) { common_chat_template tmpl = load_laguna_s_template(t); t.assert_true("Laguna-S(v8) supports preserving reasoning", tmpl.original_caps().supports_preserve_reasoning);}static void test_laguna_s_analysis(testing & t) { t.test("Laguna-S(v8) reasoning detection", test_laguna_s_reasoning_detection); t.test("Laguna-S(v8) tool format", test_laguna_s_tool_format); t.test("Laguna-S(v8) preserve reasoning", test_laguna_s_preserve_reasoning);}
static common_chat_template load_laguna_xs2_template(testing & t) { return load_template(t, "models/templates/poolside-Laguna-XS.2.jinja");}static void test_laguna_xs2_reasoning_detection(testing & t) { common_chat_template tmpl = load_laguna_xs2_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); t.assert_equal("Laguna-XS.2(v5) reasoning_start should be '<think>'", "<think>", analysis.reasoning.start); t.assert_equal("Laguna-XS.2(v5) reasoning_end should be '</think>'", "</think>", analysis.reasoning.end); t.assert_equal("Laguna-XS.2(v5) reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);}static void test_laguna_xs2_tool_format(testing & t) { common_chat_template tmpl = load_laguna_xs2_template(t); struct autoparser analysis; analysis.analyze_template(tmpl); t.assert_equal("Laguna-XS.2(v5) arg_value_suffix should be '</arg_value>'", "</arg_value>", analysis.tools.arguments.value_suffix);}static void test_laguna_xs2_analysis(testing & t) { t.test("Laguna-XS.2(v5) reasoning detection", test_laguna_xs2_reasoning_detection); t.test("Laguna-XS.2(v5) tool format", test_laguna_xs2_tool_format);}
static common_chat_template load_cohere_template(testing & t) { return load_template(t, "models/templates/CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja");}
static void test_cohere_analysis(testing & t) { t.test("Cohere reasoning detection", test_cohere_reasoning_detection);}
static void test_cohere_reasoning_detection(testing & t) { common_chat_template tmpl = load_cohere_template(t);
// Run differential analysis struct autoparser analysis; analysis.analyze_template(tmpl);
// Check reasoning markers - Cohere uses special token format t.assert_equal("reasoning_start should be '<|START_THINKING|>'", "<|START_THINKING|>", analysis.reasoning.start); t.assert_equal("reasoning_end should be '<|END_THINKING|>'", "<|END_THINKING|>", analysis.reasoning.end);
// Check reasoning mode - Cohere only shows reasoning with tool calls (TOOLS_ONLY) t.assert_equal("reasoning should be TOOLS_ONLY", reasoning_mode::TOOLS_ONLY, analysis.reasoning.mode);
// Check content markers - Cohere wraps all content with START/END_RESPONSE t.assert_equal("content_start should be '<|START_RESPONSE|>'", "<|START_RESPONSE|>", analysis.content.start); t.assert_equal("content_end should be '<|END_RESPONSE|>'", "<|END_RESPONSE|>", analysis.content.end);
// Content is always wrapped (both with and without tools) t.assert_equal("content should be ALWAYS_WRAPPED", content_mode::ALWAYS_WRAPPED, analysis.content.mode);}
static void test_tool_format_cohere(testing & t) { common_chat_template tmpl = load_cohere_template(t);
// Run differential analysis struct autoparser analysis; analysis.analyze_template(tmpl);
// Check tool section markers - Cohere uses ACTION markers t.assert_equal("tool_section_start should be '<|START_ACTION|>'", "<|START_ACTION|>", analysis.tools.format.section_start); t.assert_equal("tool_section_end should be '<|END_ACTION|>'", "<|END_ACTION|>", analysis.tools.format.section_end);
// JSON_NATIVE format has no per-call markers t.assert_equal("per_call_start should be empty", "", analysis.tools.format.per_call_start); t.assert_equal("per_call_end should be empty", "", analysis.tools.format.per_call_end);
// JSON_NATIVE format has empty function markers (no XML-style markers) t.assert_equal("func_name_prefix should be empty", "", analysis.tools.function.name_prefix); t.assert_equal("func_name_suffix should be empty", "", analysis.tools.function.name_suffix); t.assert_equal("func_close should be empty", "", analysis.tools.function.close);
// JSON_NATIVE format has empty args markers t.assert_equal("args_start should be empty", "", analysis.tools.arguments.start); t.assert_equal("args_end should be empty", "", analysis.tools.arguments.end);
// JSON_NATIVE format has empty argument markers t.assert_equal("arg_name_prefix should be empty", "", analysis.tools.arguments.name_prefix); t.assert_equal("arg_name_suffix should be empty", "", analysis.tools.arguments.name_suffix); t.assert_equal("arg_value_prefix should be empty", "", analysis.tools.arguments.value_prefix); t.assert_equal("arg_value_suffix should be empty", "", analysis.tools.arguments.value_suffix); t.assert_equal("arg_separator should be empty", "", analysis.tools.arguments.separator);
// Check JSON field names - Cohere uses non-standard names t.assert_equal("name_field should be 'tool_name'", "tool_name", analysis.tools.format.name_field); t.assert_equal("args_field should be 'parameters'", "parameters", analysis.tools.format.args_field); // This isn't a real tool call id field, i.e. with the OpenAI tool call ID format t.assert_equal("id_field should be 'tool_call_id'", "", analysis.tools.format.id_field);
// Check format classification t.assert_equal("tool format should be JSON_NATIVE", tool_format::JSON_NATIVE, analysis.tools.format.mode);
// Check flags t.assert_true("should support tools", analysis.jinja_caps.supports_tools); t.assert_true("should support parallel calls", analysis.jinja_caps.supports_parallel_tool_calls); t.assert_true("should not require nonnull content", !analysis.content.requires_nonnull_content); t.assert_true("tools_array_wrapped should be true", analysis.tools.format.tools_array_wrapped);}
// ============================================================================// SmolLM3 Template Analysis Tests// Tests for templates that change system message when enable_thinking flips// and prefill an empty <think></think> block in no-think mode.// ============================================================================static common_chat_template load_smollm3_template(testing & t) { return load_template(t, "models/templates/HuggingFaceTB-SmolLM3-3B.jinja");}
static void test_smollm3_reasoning_detection(testing & t);
static void test_smollm3_analysis(testing & t) { t.test("SmolLM3 reasoning detection", test_smollm3_reasoning_detection);}
static void test_smollm3_reasoning_detection(testing & t) { common_chat_template tmpl = load_smollm3_template(t);
// Run differential analysis struct autoparser analysis; analysis.analyze_template(tmpl);
// SmolLM3 uses <think>/<think> reasoning tags. // The template changes the entire system message when enable_thinking flips, // so the analyzer must compare isolated generation prompts (not full outputs). t.assert_equal("reasoning_start should be '<think>'", "<think>", analysis.reasoning.start); t.assert_equal("reasoning_end should be '</think>'", "</think>", analysis.reasoning.end); t.assert_equal("reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode);
// Content should remain plain (no wrappers) t.assert_equal("content start should be empty", "", analysis.content.start); t.assert_equal("content end should be empty", "", analysis.content.end); t.assert_equal("content should be PLAIN", content_mode::PLAIN, analysis.content.mode);
// Preserved tokens should include the reasoning markers bool has_think_start = std::find(analysis.preserved_tokens.begin(), analysis.preserved_tokens.end(), "<think>") != analysis.preserved_tokens.end(); bool has_think_end = std::find(analysis.preserved_tokens.begin(), analysis.preserved_tokens.end(), "</think>") != analysis.preserved_tokens.end(); t.assert_true("preserved_tokens should contain '<think>'", has_think_start); t.assert_true("preserved_tokens should contain '</think>'", has_think_end);}
// ============================================================================// standard_json_tools Format Tests// ============================================================================
// Helper to build tools definition for testsstatic json build_test_tools() { json parameters_schema = json::object(); parameters_schema["type"] = "object"; parameters_schema["properties"] = json::object(); parameters_schema["properties"]["location"] = json::object({ {"type", "string"}, {"description", "The city and state"} }); parameters_schema["properties"]["unit"] = json::object({ {"type", "string"}, {"description", "Temperature unit"}, {"enum", json::array({"celsius", "fahrenheit"})} }); parameters_schema["required"] = json::array({"location"});
return json::array({ json{ {"type", "function"}, {"function", json{ {"name", "get_current_weather"}, {"description", "Get the current weather in a given location"}, {"parameters", parameters_schema} }} } });}
static void test_standard_json_tools_formats(testing & t) { t.test("OpenAI format", test_standard_json_tools_openai); t.test("Cohere format", test_standard_json_tools_cohere); t.test("function-as-key format", test_standard_json_tools_function_key);}
// Test 1: OpenAI Standard Format// {"id": "call_abc", "function": {"name": "get_weather", "arguments": {"location": "NYC"}}}static void test_standard_json_tools_openai(testing & t) { json tools = build_test_tools();
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { auto tool_call = p.standard_json_tools( "<tool_call>", "</tool_call>", tools, /* parallel */ true, /* force */ false, /* name_key */ "function.name", /* args_key */ "function.arguments", /* array_wrapped */ false, /* function_is_key */ false, /* call_id_key */ "id", /* gen_call_id_key */ "", /* parameters_order */ {} ); return p.content(p.until("<tool_call>")) + p.optional(tool_call) + p.end(); });
std::string input = "Let me check the weather." "<tool_call>" R"({"id": "call_abc123", "function": {"name": "get_current_weather", "arguments": {"location": "NYC"}}})" "</tool_call>";
common_peg_parse_context ctx(input); auto result = parser.parse(ctx);
if (!t.assert_true("parse success", result.success())) { return; }
common_chat_msg msg; auto mapper = common_chat_peg_mapper(msg); mapper.from_ast(ctx.ast, result);
t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); if (!msg.tool_calls.empty()) { t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); t.assert_equal("tool id", "call_abc123", msg.tool_calls[0].id); } t.assert_true("content present", msg.content.find("Let me check the weather") != std::string::npos);}
// Test 2: Cohere Format// {"tool_call_id": 0, "tool_name": "get_weather", "parameters": {"location": "NYC"}}static void test_standard_json_tools_cohere(testing & t) { json tools = build_test_tools();
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { auto tool_call = p.standard_json_tools( "<|START_ACTION|>[", "]<|END_ACTION|>", tools, /* parallel */ true, /* force */ false, /* name_key */ "tool_name", /* args_key */ "parameters", /* array_wrapped */ false, // Brackets are part of section markers /* function_is_key */ false, /* call_id_key */ "", /* gen_call_id_key */ "tool_call_id", /* parameters_order */ {"tool_call_id", "tool_name", "parameters"} ); return p.content(p.until("<|START_ACTION|>")) + p.optional(tool_call) + p.end(); });
std::string input = "Let me search for that." "<|START_ACTION|>[" R"({"tool_call_id": 0, "tool_name": "get_current_weather", "parameters": {"location": "NYC", "unit": "celsius"}})" "]<|END_ACTION|>";
common_peg_parse_context ctx(input); auto result = parser.parse(ctx);
if (!t.assert_true("parse success", result.success())) { return; }
common_chat_msg msg; auto mapper = common_chat_peg_mapper(msg); mapper.from_ast(ctx.ast, result);
t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); if (!msg.tool_calls.empty()) { t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); t.assert_equal("tool id", "0", msg.tool_calls[0].id); } t.assert_true("content present", msg.content.find("Let me search") != std::string::npos);}
// Test 3: Function-as-Key Format// {"get_current_weather": {"id": "call-0001", "args": {"location": "NYC"}}}static void test_standard_json_tools_function_key(testing & t) { json tools = build_test_tools();
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { auto tool_call = p.standard_json_tools( "<tool_calls>[", "]</tool_calls>", tools, /* parallel */ true, /* force */ false, /* name_key */ "", // Name is the key itself /* args_key */ "args", /* array_wrapped */ false, /* function_is_key */ true, /* call_id_key */ "id", /* gen_call_id_key */ "", /* parameters_order */ {} ); return p.content(p.until("<tool_calls>")) + p.optional(tool_call) + p.end(); });
std::string input = "I'll call the weather function." "<tool_calls>[" R"({"get_current_weather": {"id": "call-0001", "args": {"location": "NYC", "unit": "celsius"}}})" "]</tool_calls>";
common_peg_parse_context ctx(input); auto result = parser.parse(ctx);
if (!t.assert_true("parse success", result.success())) { return; }
common_chat_msg msg; auto mapper = common_chat_peg_mapper(msg); mapper.from_ast(ctx.ast, result);
t.assert_equal("tool calls count", 1u, msg.tool_calls.size()); if (!msg.tool_calls.empty()) { t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name); t.assert_equal("tool id", "call-0001", msg.tool_calls[0].id); } t.assert_true("content present", msg.content.find("I'll call the weather") != std::string::npos);}
// ============================================================================// normalize_quotes_to_json Tests// ============================================================================
// Copy of the function for isolated testing (original is static in chat-peg-parser.cpp)static std::string normalize_quotes_to_json(const std::string & input) { std::string result; result.reserve(input.size() + 16);
bool in_single_quoted = false; bool in_double_quoted = false;
for (size_t i = 0; i < input.size(); ++i) { char c = input[i];
if (c == '\\' && i + 1 < input.size()) { char next = input[i + 1];
if (in_single_quoted) { if (next == '\'') { result += '\''; ++i; continue; } if (next == '"') { result += "\\\""; ++i; continue; } result += c; result += next; ++i; continue; }
if (in_double_quoted) { result += c; result += next; ++i; continue; }
result += c; continue; }
if (c == '"') { if (in_single_quoted) { result += "\\\""; } else { in_double_quoted = !in_double_quoted; result += c; } } else if (c == '\'') { if (in_double_quoted) { result += c; } else if (in_single_quoted) { in_single_quoted = false; result += '"'; } else { in_single_quoted = true; result += '"'; } } else { result += c; } }
return result;}
static void test_normalize_quotes_to_json(testing & t) { t.test("basic single to double quotes", [](testing & t) { std::string input = "{'key': 'value'}"; std::string expected = "{\"key\": \"value\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("basic conversion", expected, result); });
t.test("escaped single quote inside single-quoted string", [](testing & t) { std::string input = "{'code': 'print(\\'hello\\')'}"; std::string expected = "{\"code\": \"print('hello')\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("escaped single quote", expected, result); });
t.test("double quote inside single-quoted string", [](testing & t) { std::string input = "{'msg': 'He said \"hi\"'}"; std::string expected = "{\"msg\": \"He said \\\"hi\\\"\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("double quote escaping", expected, result); });
t.test("nested backslash escapes", [](testing & t) { std::string input = "{'path': 'C:\\\\Users\\\\test'}"; std::string expected = "{\"path\": \"C:\\\\Users\\\\test\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("backslash escaping", expected, result); });
t.test("newline escapes", [](testing & t) { std::string input = "{'text': 'line1\\nline2'}"; std::string expected = "{\"text\": \"line1\\nline2\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("newline escaping", expected, result); });
t.test("mixed quotes", [](testing & t) { std::string input = "{\"already_double\": 'single_value'}"; std::string expected = "{\"already_double\": \"single_value\"}"; std::string result = normalize_quotes_to_json(input); t.assert_equal("mixed quotes", expected, result); });
t.test("embedded quotes - the test case", test_normalize_quotes_with_embedded_quotes);}
// Test case that mirrors the Seed-OSS failing test scenariostatic void test_normalize_quotes_with_embedded_quotes(testing & t) { // This is similar to the Seed-OSS template test case // The input has embedded double quotes like "14" and "bar" inside string values std::string input = "{'filename': 'foo.cpp', 'oldString': 'def foo(arg = \"14\"):\\n return arg + \"bar\"\\n', 'newString': 'def foo(arg = \"15\"):\\n pass\\n'}";
// Expected: Python single quotes -> JSON double quotes, internal double quotes escaped std::string expected = "{\"filename\": \"foo.cpp\", \"oldString\": \"def foo(arg = \\\"14\\\"):\\n return arg + \\\"bar\\\"\\n\", \"newString\": \"def foo(arg = \\\"15\\\"):\\n pass\\n\"}";
std::string result = normalize_quotes_to_json(input);
t.assert_equal("normalize quotes with embedded double quotes", expected, result);
// Also verify the result is valid JSON try { json parsed = json::parse(result); t.assert_true("result is valid JSON", true); t.assert_equal("filename field", "foo.cpp", parsed["filename"].get<std::string>()); t.assert_true("oldString contains embedded quotes", parsed["oldString"].get<std::string>().find("\"14\"") != std::string::npos); t.assert_true("newString contains embedded quotes", parsed["newString"].get<std::string>().find("\"15\"") != std::string::npos); } catch (const std::exception & e) { t.assert_true(std::string("JSON parse failed: ") + e.what(), false); }}
// ============================================================================// TAG_WITH_TAGGED Argument Parsing Tests// ============================================================================
// Build tools definition for edit functionstatic json build_edit_tool() { json parameters_schema = json::object(); parameters_schema["type"] = "object"; parameters_schema["properties"] = json::object(); parameters_schema["properties"]["filename"] = json::object({ {"type", "string"}, {"description", "Path of file to edit"} }); parameters_schema["properties"]["oldString"] = json::object({ {"type", "string"}, {"description", "String to replace"} }); parameters_schema["properties"]["newString"] = json::object({ {"type", "string"}, {"description", "New (replacement) value"} }); parameters_schema["required"] = json::array({"filename", "oldString", "newString"});
return json::array({ json{ {"type", "function"}, {"function", json{ {"name", "edit"}, {"description", "Edit a file"}, {"parameters", parameters_schema} }} } });}
// ============================================================================// Role marker detection tests for all autoparser-handled templates//// Verifies that detect_user_start_marker / detect_assistant_start_marker// return the correct boundary text between turns for every template that// falls through to the differential autoparser (i.e. is not handled by a// dedicated specialized template in common_chat_try_specialized_template).//// Markers were deduced manually from the jinja sources in models/templates/.// ============================================================================struct role_marker_case { std::string template_file; std::string expected_user_start; std::string expected_assistant_start;};
static void test_role_markers_all_templates(testing & t) { // Each entry is { template filename, user_start, assistant_start } as // produced when rendering the standard chatml-like sequences. The values // come from reading each jinja template and tracing what text precedes // a user/assistant message body once the autoparser strips any reasoning // markers it detected first. const std::vector<role_marker_case> cases = { // ChatML family: <|im_start|>{role} ... <|im_end|> { "Bielik-11B-v3.0-Instruct.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "HuggingFaceTB-SmolLM3-3B.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "MiMo-VL.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "NousResearch-Hermes-2-Pro-Llama-3-8B-tool_use.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "NousResearch-Hermes-3-Llama-3.1-8B-tool_use.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "Qwen3.5-4B.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "Qwen3-Coder.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "Qwen-Qwen2.5-7B-Instruct.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "Qwen-Qwen3-0.6B.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "Qwen-QwQ-32B.jinja", "<|im_start|>user", "<|im_start|>assistant" }, { "StepFun3.5-Flash.jinja", "<|im_start|>user", "<|im_start|>assistant" },
// DeepSeek family { "deepseek-ai-DeepSeek-R1-Distill-Llama-8B.jinja", "<|User|>", "<|Assistant|>" }, { "deepseek-ai-DeepSeek-R1-Distill-Qwen-32B.jinja", "<|User|>", "<|Assistant|>" }, { "deepseek-ai-DeepSeek-V3.1.jinja", "<|User|>", "<|Assistant|>" }, { "llama-cpp-deepseek-r1.jinja", "<|User|>", "<|Assistant|>" },
// Llama 3 header family { "meetkai-functionary-medium-v3.1.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" }, { "meta-llama-Llama-3.1-8B-Instruct.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" }, { "meta-llama-Llama-3.2-3B-Instruct.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" }, { "meta-llama-Llama-3.3-70B-Instruct.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" }, // fireworks-ai forces a trailing assistant header even without add_generation_prompt, // so the marker is absorbed into the common suffix and assistant_start is detected as empty. { "fireworks-ai-llama-3-firefunction-v2.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" },
// Phi/GLM/Apriel-style: <|user|> / <|assistant|> { "microsoft-Phi-3.5-mini-instruct.jinja", "<|user|>", "<|assistant|>" }, { "GLM-4.6.jinja", "<|user|>", "<|assistant|>" }, { "unsloth-Apriel-1.5.jinja", "<|user|>", "<|assistant|>" }, { "GLM-4.7-Flash.jinja", "<|user|>", "<|assistant|>" },
// Gemma 2: <start_of_turn>{user|model} { "google-gemma-2-2b-it.jinja", "<start_of_turn>user", "<start_of_turn>model" },
// IBM Granite { "ibm-granite-granite-3.3-2B-Instruct.jinja", "<|start_of_role|>user<|end_of_role|>", "<|start_of_role|>assistant<|end_of_role|>" }, { "ibm-granite-granite-4.0.jinja", "<|start_of_role|>user<|end_of_role|>", "<|start_of_role|>assistant<|end_of_role|>" },
// Cohere R-series { "CohereForAI-c4ai-command-r7b-12-2024-tool_use.jinja", "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>", "<|START_RESPONSE|>" }, { "CohereForAI-c4ai-command-r-plus-tool_use.jinja", "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>", "<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>" },
// Mistral: assistant content follows [/INST] immediately, no header { "mistralai-Mistral-Nemo-Instruct-2407.jinja", "[INST]", "" }, { "Mistral-Small-3.2-24B-Instruct-2506.jinja", "[INST]", "" },
// Apertus uses <|user_start|> / <|assistant_start|> but the user diff // carries the preceding <|assistant_end|> from the previous turn. { "Apertus-8B-Instruct.jinja", "<|user_start|>", "<|assistant_start|>" },
// Apriel 1.6 wraps the assistant body with <|begin_assistant|>, but // <|begin_assistant|> is also the detected reasoning start, so the // assistant_start is trimmed back to the preceding newline. { "Apriel-1.6-15b-Thinker-fixed.jinja", "<|begin_user|>", "<|begin_assistant|>" },
// ByteDance Seed-OSS: <seed:bos>{role} { "ByteDance-Seed-OSS.jinja", "<seed:bos>user", "<seed:bos>assistant" },
// GigaChat 3.1: {role}<|role_sep|> { "GigaChat3.1-10B-A1.8B.jinja", "user<|role_sep|>", "assistant<|role_sep|>" },
// MiniMax M2: ]~b]{user|ai} { "MiniMax-M2.jinja", "]~b]user", "]~b]ai" },
// HunYuan V3: <|hy_User:opensource|> / <|hy_Assistant:opensource|> { "tencent-Hy3.jinja", "<|hy_User:opensource|>", "<|hy_Assistant:opensource|>" },
// Nemotron Nano v2: <SPECIAL_11>{User|Assistant}; assistant marker // is followed by a prefilled <think> block that gets included. { "NVIDIA-Nemotron-Nano-v2.jinja", "<SPECIAL_11>User", "<SPECIAL_11>Assistant" },
// Reka Edge: "human: " / "assistant: " — but the rendered preamble // depends on enable_thinking, which currently confuses the user-start // diff and trims the marker down. Lock in the observed value. { "Reka-Edge.jinja", "human:", "assistant:" },
// RWKV-world chat preset: "User: " / "Assistant: " { "llama-cpp-rwkv-world.jinja", "User:", "Assistant:" },
// Upstage Solar 100B: <|begin|>{role}... but reasoning marker absorbs // the "<|begin|>assistant" prefix from assistant_start. { "upstage-Solar-Open-100B.jinja", "<|begin|>user<|content|>", "<|begin|>assistant" }, };
for (const auto & c : cases) { t.test(c.template_file, [&](testing & t) { common_chat_template tmpl = load_template(t, "models/templates/" + c.template_file); struct autoparser ap; ap.analyze_template(tmpl); t.assert_equal("user_start", c.expected_user_start, ap.user_start); t.assert_equal("assistant_start", c.expected_assistant_start, ap.assistant_start); }); }}
static void test_bailing_v3_tool_format(testing & t) { const std::string template_source = R"JINJA({# Bailing V3 chat template #}{%- if tools %}{{ tools | tojson }}{%- endif %}{%- for message in messages %} {%- if message.role == "user" %} {{- '<role>HUMAN</role>' + message.content + '<|role_end|>' }} {%- elif message.role == "assistant" %} {{- '<role>ASSISTANT</role>' }} {%- if message.tool_calls %} {%- for tool_call in message.tool_calls %} {%- set tc = tool_call.function %} {{- '<tool_call>' + tc.name }} {%- for k, v in tc.arguments.items() %} {{- '<arg_key>' + k + '</arg_key>' }} {{- '\n<arg_value>' + v + '</arg_value>' }} {%- endfor %} {{- '\n</tool_call>' }} {%- endfor %} {%- endif %} {{- '<|role_end|>' }} {%- endif %}{%- endfor %}{%- if add_generation_prompt %}{{- '<role>ASSISTANT</role>' }}{%- endif %})JINJA";
common_chat_template tmpl(template_source, "", ""); struct autoparser analysis; analysis.analyze_template(tmpl);
t.assert_equal("arg_value_suffix", "</arg_value>", analysis.tools.arguments.value_suffix); t.assert_true("intertag whitespace", analysis.tools.arguments.tolerate_intertag_whitespace);
generation_params inputs; inputs.tools = json::array({ { { "type", "function" }, { "function", { { "name", "test_function_name" }, { "parameters", { { "type", "object" }, { "properties", { { "param1", { { "type", "string" } } }, { "param2", { { "type", "string" } } }, } }, } }, } }, }, }); inputs.reasoning_format = COMMON_REASONING_FORMAT_NONE; auto parser = analysis.build_parser(inputs, ""); const std::string output = "<tool_call>test_function_name\n" "<arg_key>param1</arg_key>\n" "<arg_value>value1</arg_value>" "<arg_key>param2</arg_key>\n" "<arg_value>value2</arg_value>\n" "</tool_call>"; common_peg_parse_context ctx(output, COMMON_PEG_PARSE_FLAG_LENIENT); t.assert_true("multi-argument tool call", parser.parse(ctx).success());}
// Test that reproduces the Seed-OSS template issue with embedded quotesstatic void test_tagged_args_with_embedded_quotes(testing & t) { json tools = build_edit_tool();
// Build a parser for TAG_WITH_TAGGED format like Seed-OSS/Nemotron auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { // Build tool choice for the edit function auto tool_choice = p.choice();
for (const auto & tool_def : tools) { if (!tool_def.contains("function")) { continue; } const auto & function = tool_def.at("function"); std::string name = function.at("name"); const auto & params = function.at("parameters");
if (!params.contains("properties") || !params.at("properties").is_object()) { continue; }
const auto & properties = params.at("properties");
// Build argument parsers std::vector<common_peg_parser> arg_parsers; for (const auto & [param_name, param_schema] : properties.items()) { auto arg = p.tool_arg( p.tool_arg_open(p.literal("<parameter=") + p.tool_arg_name(p.literal(param_name)) + p.literal(">")) + p.space() + p.tool_arg_string_value(p.until("</parameter>")) + p.space() + p.tool_arg_close(p.literal("</parameter>")) ); arg_parsers.push_back(p.optional(p.rule("arg-" + param_name, arg))); }
// Build arg sequence with space() between common_peg_parser args_seq = p.eps(); for (size_t i = 0; i < arg_parsers.size(); i++) { if (i > 0) { args_seq = args_seq + p.space(); } args_seq = args_seq + arg_parsers[i]; }
auto func_parser = p.tool_open(p.literal("<function=") + p.tool_name(p.literal(name)) + p.literal(">")) + p.space() + args_seq + p.space() + p.tool_close(p.literal("</function>"));
tool_choice |= p.rule("tool-" + name, p.tool(func_parser)); }
auto tool_section = p.literal("<seed:tool_call>") + p.space() + tool_choice + p.space() + p.literal("</seed:tool_call>");
return p.content(p.until("<seed:tool_call>")) + p.optional(tool_section) + p.end(); });
std::string input = "<seed:tool_call>\n" "<function=edit>\n" "<parameter=filename>" "foo.cpp" "</parameter>\n" "<parameter=oldString>" "def foo(arg = \"14\"):\n" " return arg + \"bar\"\n" "\n" "</parameter>\n" "<parameter=newString>" "def foo(arg = \"15\"):\n" " pass\n" "\n" "</parameter>\n" "</function>\n" "</seed:tool_call>";
common_peg_parse_context ctx(input); auto result = parser.parse(ctx);
if (!t.assert_true("parse success", result.success())) { return; }
common_chat_msg msg; auto mapper = common_chat_peg_mapper(msg); mapper.from_ast(ctx.ast, result);
t.assert_equal("tool calls count", 1u, msg.tool_calls.size());
if (!msg.tool_calls.empty()) { t.assert_equal("tool name", "edit", msg.tool_calls[0].name);
// Parse the arguments as JSON to verify they're valid std::string args = msg.tool_calls[0].arguments;
try { json parsed = json::parse(args); t.assert_true("arguments is valid JSON", true);
// Verify each field has proper value t.assert_equal("filename", "foo.cpp", parsed.value("filename", ""));
std::string oldString = parsed.value("oldString", ""); t.assert_true("oldString contains embedded quotes", oldString.find("\"14\"") != std::string::npos); t.assert_true("oldString contains bar with quotes", oldString.find("\"bar\"") != std::string::npos);
std::string newString = parsed.value("newString", ""); t.assert_true("newString contains embedded quotes", newString.find("\"15\"") != std::string::npos);
} catch (const std::exception & e) { t.assert_true(std::string("arguments should be valid JSON: ") + e.what(), false); } }}