| #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); |
|
|
| |
| static 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); |
|
|
| |
| static 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); |
|
|
| |
| static void test_cohere_reasoning_detection(testing & t); |
| static void test_cohere_analysis(testing & t); |
|
|
| |
| static void test_smollm3_analysis(testing & t); |
|
|
| |
| static void test_marker_separation(testing & t); |
|
|
| |
| static 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); |
|
|
| |
| static void test_normalize_quotes_to_json(testing & t); |
| static void test_normalize_quotes_with_embedded_quotes(testing & t); |
|
|
| |
| static 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(); |
|
|
| |
| |
| |
|
|
| enum class output_mode { |
| ANALYSIS, |
| TEMPLATE, |
| BOTH |
| }; |
|
|
| enum class input_message_type { |
| NONE, |
| CONTENT_ONLY, |
| REASONING_CONTENT, |
| TOOL_CALL_ONLY, |
| CONTENT_TOOL_CALL, |
| REASONING_TOOL_CALL, |
| CONTENT_FAKE_TOOL_CALL, |
| ALL |
| }; |
|
|
| 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 = { true, |
| 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() { |
| |
| |
| 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"); |
|
|
| |
| 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()); |
|
|
| |
| 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); |
|
|
| |
| 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 { |
| |
| 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 { |
| |
| 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); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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; |
|
|
| |
|
|
| 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); |
|
|
| |
| 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) { |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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", ""); |
| 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); |
|
|
| |
| 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"); |
| |
| |
| 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) { |
| |
| 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); |
|
|
| |
| result = calculate_diff_split("Xtest", "Ytest"); |
| |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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) { |
| |
| diff_split result = calculate_diff_split("test<tag", "test>content"); |
| |
| 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 != ""); |
|
|
| |
| result = calculate_diff_split("test[", "test]value"); |
| t.assert_true("should handle bracket boundaries", result.left != "" || result.right != "" || result.suffix != ""); |
|
|
| |
| result = calculate_diff_split("prefix<tag>", "prefix</tag>suffix"); |
| |
| 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); |
|
|
| |
| result = calculate_diff_split("prefix<div>content</div>", "prefix<div>different</div>"); |
| |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| { |
| |
| 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); |
| } |
|
|
| { |
| |
| |
| 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); |
| |
| 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) { |
| |
| |
| |
| { |
| |
| 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); |
| } |
|
|
| { |
| |
| 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) { |
| |
| 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); |
|
|
| |
| |
| result = until_common_prefix("prefix<test>suffix", "<test>different", "<test>other"); |
| t.assert_equal("should return 'prefix'", "prefix", result); |
|
|
| |
| result = until_common_prefix("<common>rest", "<common>left", "<common>right"); |
| t.assert_equal("should return empty string when common prefix at start", "", result); |
|
|
| |
| result = until_common_prefix("something", "left", "right"); |
| t.assert_equal("should return empty string when no common prefix", "", result); |
|
|
| |
| result = until_common_prefix("test", "", "right"); |
| t.assert_equal("should return empty string when left is empty", "", result); |
|
|
| |
| 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) { |
| |
| 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); |
|
|
| |
| result = after_common_suffix("rest<common>", "left<common>", "right<common>"); |
| t.assert_equal("should return empty string when common suffix at end", "", result); |
|
|
| |
| result = after_common_suffix("test", "left", ""); |
| t.assert_equal("should return empty string when right is empty", "", result); |
|
|
| |
| result = after_common_suffix("<outer><inner>value</inner></outer>", |
| "<inner>value</inner>", |
| "<inner>different</inner>"); |
| t.assert_equal("should return '</outer>'", "</outer>", result); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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) { |
| |
| 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; |
| } |
| |
| |
| 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) { |
| |
| 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) { |
| |
| 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) { |
| |
| 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) { |
| |
| |
| 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) { |
| |
| common_chat_template tmpl("{{ messages[0]['content'] }}", "", ""); |
|
|
| template_params params; |
| params.messages = json::array({ |
| json {{"role", "user"}, {"content", "Hello"}} |
| }); |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
|
|
| 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); |
| } |
|
|
| |
| static 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; |
| } |
|
|
| |
| static 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} |
| }} |
| }; |
| } |
|
|
| |
| static 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} |
| }} |
| } |
| }); |
| } |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("T1 left should contain 'Let me help you.'", "Let me help you.", diff.left); |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("T1 suffix should be '<seed:eos>'", "<seed:eos>", diff.suffix); |
| } |
|
|
| |
| static 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; |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("T2 left should be empty", "", diff.left); |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("T2 suffix should end with '<seed:eos>'", "<seed:eos>", diff.suffix.substr(diff.suffix.length() - 10, 10)); |
| } |
|
|
| |
| static void test_seed_oss_function_names(testing & t) { |
| common_chat_template tmpl = load_seed_oss_template(t); |
|
|
| |
| 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; |
|
|
| |
| t.assert_true("T3 left should contain func_alpha (or prefix)", func_alpha_in_left || func_alpha_in_prefix); |
|
|
| |
| t.assert_true("T3 right should contain func_beta", func_beta_in_right || func_beta_in_prefix || func_beta_in_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); |
| } |
|
|
| |
| static void test_seed_oss_argument_count(testing & t) { |
| common_chat_template tmpl = load_seed_oss_template(t); |
|
|
| |
| 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} |
| }} |
| } |
| }); |
|
|
| |
| 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"} |
| }; |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
|
|
| |
| static 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; |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
|
|
| |
| static 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; |
|
|
| |
| t.assert_equal("T6 left should be empty", "", diff.left); |
|
|
| |
| 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); |
|
|
| |
| t.assert_true("T6 prefix should contain assistant", diff.prefix.find("assistant") != std::string::npos); |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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); |
|
|
| |
| 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; |
|
|
| |
| struct autoparser analysis; |
| analysis.analyze_template(tmpl); |
|
|
| |
| 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); |
|
|
| |
| |
| t.assert_equal("reasoning should be TAG_BASED", reasoning_mode::TAG_BASED, analysis.reasoning.mode); |
|
|
| |
| 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); |
|
|
| |
| struct autoparser analysis; |
| analysis.analyze_template(tmpl); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| t.assert_true("tool format should be TAG_WITH_TAGGED", analysis.tools.format.mode == tool_format::TAG_WITH_TAGGED); |
|
|
| |
| t.assert_true("should support tools", analysis.jinja_caps.supports_tools); |
| } |
|
|
| |
| |
| |
| 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); |
| |
| |
| 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) { |
| |
| |
| 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); |
|
|
| |
| struct autoparser analysis; |
| analysis.analyze_template(tmpl); |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("reasoning should be TOOLS_ONLY", reasoning_mode::TOOLS_ONLY, analysis.reasoning.mode); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| struct autoparser analysis; |
| analysis.analyze_template(tmpl); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| t.assert_equal("args_start should be empty", "", analysis.tools.arguments.start); |
| t.assert_equal("args_end should be empty", "", analysis.tools.arguments.end); |
|
|
| |
| 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); |
|
|
| |
| 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); |
| |
| t.assert_equal("id_field should be 'tool_call_id'", "", analysis.tools.format.id_field); |
|
|
| |
| t.assert_equal("tool format should be JSON_NATIVE", tool_format::JSON_NATIVE, analysis.tools.format.mode); |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| |
| |
| 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); |
|
|
| |
| struct autoparser analysis; |
| analysis.analyze_template(tmpl); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
|
|
| |
| static 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); |
| } |
|
|
| |
| |
| 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, |
| true, |
| false, |
| "function.name", |
| "function.arguments", |
| false, |
| false, |
| "id", |
| "", |
| {} |
| ); |
| 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); |
| } |
|
|
| |
| |
| 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, |
| true, |
| false, |
| "tool_name", |
| "parameters", |
| false, |
| false, |
| "", |
| "tool_call_id", |
| {"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); |
| } |
|
|
| |
| |
| 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, |
| true, |
| false, |
| "", |
| "args", |
| false, |
| true, |
| "id", |
| "", |
| {} |
| ); |
| 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); |
| } |
|
|
| |
| |
| |
|
|
| |
| 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); |
| } |
|
|
| |
| static void test_normalize_quotes_with_embedded_quotes(testing & t) { |
| |
| |
| std::string input = "{'filename': 'foo.cpp', 'oldString': 'def foo(arg = \"14\"):\\n return arg + \"bar\"\\n', 'newString': 'def foo(arg = \"15\"):\\n pass\\n'}"; |
|
|
| |
| 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); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| |
| |
|
|
| |
| static 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} |
| }} |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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) { |
| |
| |
| |
| |
| |
| const std::vector<role_marker_case> cases = { |
| |
| { "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-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|>" }, |
|
|
| |
| { "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-llama-3-firefunction-v2.jinja", "<|start_header_id|>user<|end_header_id|>", "<|start_header_id|>assistant<|end_header_id|>" }, |
|
|
| |
| { "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|>" }, |
|
|
| |
| { "google-gemma-2-2b-it.jinja", "<start_of_turn>user", "<start_of_turn>model" }, |
|
|
| |
| { "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|>" }, |
|
|
| |
| { "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|>" }, |
|
|
| |
| { "mistralai-Mistral-Nemo-Instruct-2407.jinja", "[INST]", "" }, |
| { "Mistral-Small-3.2-24B-Instruct-2506.jinja", "[INST]", "" }, |
|
|
| |
| |
| { "Apertus-8B-Instruct.jinja", "<|user_start|>", "<|assistant_start|>" }, |
|
|
| |
| |
| |
| { "Apriel-1.6-15b-Thinker-fixed.jinja", "<|begin_user|>", "<|begin_assistant|>" }, |
|
|
| |
| { "ByteDance-Seed-OSS.jinja", "<seed:bos>user", "<seed:bos>assistant" }, |
|
|
| |
| { "GigaChat3.1-10B-A1.8B.jinja", "user<|role_sep|>", "assistant<|role_sep|>" }, |
|
|
| |
| { "MiniMax-M2.jinja", "]~b]user", "]~b]ai" }, |
|
|
| |
| { "tencent-Hy3.jinja", "<|hy_User:opensource|>", "<|hy_Assistant:opensource|>" }, |
|
|
| |
| |
| { "NVIDIA-Nemotron-Nano-v2.jinja", "<SPECIAL_11>User", "<SPECIAL_11>Assistant" }, |
|
|
| |
| |
| |
| { "Reka-Edge.jinja", "human:", "assistant:" }, |
|
|
| |
| { "llama-cpp-rwkv-world.jinja", "User:", "Assistant:" }, |
|
|
| |
| |
| { "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()); |
| } |
|
|
| |
| static void test_tagged_args_with_embedded_quotes(testing & t) { |
| json tools = build_edit_tool(); |
|
|
| |
| auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) { |
| |
| 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"); |
|
|
| |
| 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))); |
| } |
|
|
| |
| 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); |
|
|
| |
| std::string args = msg.tool_calls[0].arguments; |
|
|
| try { |
| json parsed = json::parse(args); |
| t.assert_true("arguments is valid JSON", true); |
|
|
| |
| 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); |
| } |
| } |
| } |
|
|