From 22248e01afc3c051c7738d445296a0460a8c2c1e Mon Sep 17 00:00:00 2001 From: Piotr Wilkin Date: Tue, 31 Mar 2026 16:33:25 +0200 Subject: [PATCH 1/3] Fix call ID detection (Mistral parser mostly) + atomicity for tag-json parsers --- common/chat-auto-parser-generator.cpp | 120 +++++++++++++++---------- common/chat-auto-parser.h | 7 ++ common/chat-diff-analyzer.cpp | 48 +++++++--- common/chat.cpp | 2 +- common/chat.h | 5 ++ tests/test-chat.cpp | 101 ++++++++++++--------- tools/parser/debug-template-parser.cpp | 110 ++++++++++++----------- 7 files changed, 241 insertions(+), 152 deletions(-) diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index 60b269c42d..4494184561 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -6,6 +6,7 @@ #include "json-schema-to-grammar.h" #include "log.h" #include "nlohmann/json.hpp" +#include "peg-parser.h" #include #include @@ -317,6 +318,44 @@ common_peg_parser analyze_tools::build_tool_parser_json_native(parser_build_cont p.end(); } +common_peg_parser analyze_tools::build_func_parser(common_chat_peg_builder & p, const std::string & name, + const common_peg_parser & call_id_section, bool have_call_id, + const common_peg_parser & args, + std::optional atomic_peek) const { + auto open = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix); + bool matched_atomic = false; + common_peg_parser func_parser = p.eps(); + + if (!function.name_suffix.empty()) { + func_parser = open + call_id_section + p.space() + args; + matched_atomic = true; + } else if (have_call_id) { + func_parser = p.atomic(open + call_id_section) + p.space() + args; + matched_atomic = true; + } else if (atomic_peek.has_value()) { + func_parser = p.atomic(open + call_id_section + p.space() + *atomic_peek) + args; + matched_atomic = true; + } else { + func_parser = open + call_id_section + p.space() + args; + } + + if (!function.close.empty()) { + func_parser = func_parser + p.space() + p.tool_close(p.literal(function.close)); + } else if (!format.per_call_end.empty()) { + // When there's no func_close but there is a per_call_end marker, use peek() to ensure + // we only emit tool_close when we can actually see the closing marker. This prevents + // premature closing during partial parsing when we've seen e.g. "" (end) or "" prefix that failed to match. + func_parser = func_parser + p.tool_close(p.peek(p.literal(format.per_call_end))); + } else { + func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper + } + if (!matched_atomic) { + func_parser = p.atomic(func_parser); + } + return func_parser; +} + common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context & ctx) const { auto & p = ctx.p; const auto & inputs = ctx.inputs; @@ -330,17 +369,27 @@ common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context const auto & schema = func.contains("parameters") ? func.at("parameters") : json::object(); // Build call_id parser based on position (if supported) + bool have_call_id = false; common_peg_parser call_id_section = p.eps(); if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() && - !call_id.suffix.empty()) { - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix))) + call_id.suffix; + (!call_id.suffix.empty() || !arguments.start.empty())) { + if (!call_id.suffix.empty()) { + call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix))) + call_id.suffix; + } else { + call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start))); + } + have_call_id = true; + } + auto args_parser = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)); + if (!arguments.start.empty()) { + args_parser = p.literal(arguments.start) + args_parser; + } + if (!arguments.end.empty()) { + args_parser = args_parser + p.literal(arguments.end); } - auto func_parser = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) + - call_id_section + p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)); - if (!function.close.empty()) { - func_parser = func_parser + function.close; - } + auto atomic_peek = !arguments.start.empty() ? std::optional(p.peek(p.literal(arguments.start))) : std::nullopt; + auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_parser, atomic_peek); tool_choice |= p.rule("tool-" + name, func_parser); }); @@ -448,52 +497,31 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte args_seq = args_seq + p.repeat(p.space() + any_opt, 0, (int) optional_parsers.size()); } + if (!arguments.start.empty()) { + args_seq = p.literal(arguments.start) + args_seq; + } + if (!arguments.end.empty()) { + args_seq = args_seq + p.literal(arguments.end); + } + // Build call_id parser based on position (if supported) common_peg_parser call_id_section = p.eps(); bool have_call_id = false; if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() && - !call_id.suffix.empty()) { + (!call_id.suffix.empty() || !arguments.start.empty())) { have_call_id = true; - call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix)) + call_id.suffix); - } - - bool matched_atomic = false; - common_peg_parser func_parser = p.eps(); - if (!function.name_suffix.empty()) { - func_parser = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) + - call_id_section + p.space() + args_seq; - matched_atomic = true; - } else if (have_call_id) { - func_parser = p.atomic(p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) + - call_id_section) + p.space() + args_seq; - matched_atomic = true; - } else if (!arguments.name_prefix.empty() && !required_parsers.empty()) { - // Only peek for an arg tag when there are required args that must follow. - // When all args are optional, the model may emit no arg tags at all (#20650). - func_parser = p.atomic(p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) + - call_id_section + p.space() + p.peek(p.literal(arguments.name_prefix))) + args_seq; - matched_atomic = true; - } else { - func_parser = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix) + - call_id_section + p.space() + args_seq; - } - - if (!function.close.empty()) { - func_parser = func_parser + p.space() + p.tool_close(p.literal(function.close)); - } else if (!format.per_call_end.empty()) { - // When there's no func_close but there is a per_call_end marker, use peek() to ensure - // we only emit tool_close when we can actually see the closing marker. This prevents - // premature closing during partial parsing when we've seen e.g. "" (end) or "" prefix that failed to match. - func_parser = func_parser + p.tool_close(p.peek(p.literal(format.per_call_end))); - } else { - func_parser = - func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper - } - if (!matched_atomic) { - func_parser = p.atomic(func_parser); + if (!call_id.suffix.empty()) { + call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix)) + call_id.suffix); + } else { + call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start))); + } } + // Only peek for an arg tag when there are required args that must follow. + // When all args are optional, the model may emit no arg tags at all (#20650). + auto atomic_peek = (!arguments.name_prefix.empty() && !required_parsers.empty()) ? + std::optional(p.peek(p.literal(arguments.name_prefix))) : std::nullopt; + auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_seq, atomic_peek); tool_choice |= p.rule("tool-" + name, func_parser); }); diff --git a/common/chat-auto-parser.h b/common/chat-auto-parser.h index 8886c330dd..2168bb05ed 100644 --- a/common/chat-auto-parser.h +++ b/common/chat-auto-parser.h @@ -356,6 +356,13 @@ struct analyze_tools : analyze_base { common_peg_parser build_tool_parser_json_native(parser_build_context & ctx) const; common_peg_parser build_tool_parser_tag_json(parser_build_context & ctx) const; common_peg_parser build_tool_parser_tag_tagged(parser_build_context & ctx) const; + + // Shared helper: builds func_parser from open+call_id+args, handling atomic wrapping and close. + // atomic_peek: if present, used as the peek expression in the third atomicity branch. + common_peg_parser build_func_parser(common_chat_peg_builder & p, const std::string & name, + const common_peg_parser & call_id_section, bool have_call_id, + const common_peg_parser & args, + std::optional atomic_peek) const; common_peg_parser build_tool_parser_tag_gemma4_dict(parser_build_context & ctx) const; }; diff --git a/common/chat-diff-analyzer.cpp b/common/chat-diff-analyzer.cpp index aadade60fa..8288296631 100644 --- a/common/chat-diff-analyzer.cpp +++ b/common/chat-diff-analyzer.cpp @@ -25,6 +25,9 @@ static const std::string ARG_SECOND = "BB_ARG_SND_BB"; static const std::string USER_MSG = "U_USER_MSG Hello END_U"; static const std::string ASSISTANT_MSG = "A_ASST_MSG I can help END_A"; static const std::string THINKING_CONTENT = "REASON_PART I am thinking END_R"; +static const std::string CALL_ID_001 = "call00001"; +static const std::string CALL_ID_002 = "call00002"; +static const std::string CALL_ID_999 = "call99999"; static std::vector> workarounds( { // Old reasoning Qwen templates - they don't really display reasoning content, but we still want to @@ -131,6 +134,7 @@ static std::vector static std::string mode_to_str(T mode) { @@ -215,6 +219,11 @@ void autoparser::analyze_template(const common_chat_template & tmpl) { LOG_DBG("func_name_prefix: '%s'\n", tools.function.name_prefix.c_str()); LOG_DBG("func_name_suffix: '%s'\n", tools.function.name_suffix.c_str()); LOG_DBG("func_close: '%s'\n", tools.function.close.c_str()); + LOG_DBG("call_id_prefix: '%s'\n", tools.call_id.prefix.c_str()); + LOG_DBG("call_id_suffix: '%s'\n", tools.call_id.suffix.c_str()); + LOG_DBG("call_id_pos: '%s'\n", mode_to_str(tools.call_id.pos).c_str()); + LOG_DBG("args_start: '%s'\n", tools.arguments.start.c_str()); + LOG_DBG("args_end: '%s'\n", tools.arguments.end.c_str()); LOG_DBG("arg_name_prefix: '%s'\n", tools.arguments.name_prefix.c_str()); LOG_DBG("arg_name_suffix: '%s'\n", tools.arguments.name_suffix.c_str()); LOG_DBG("arg_value_prefix: '%s'\n", tools.arguments.value_prefix.c_str()); @@ -583,12 +592,15 @@ analyze_tools::analyze_tools(const common_chat_template & tmpl, if (caps.supports_parallel_tool_calls) { check_per_call_markers(); } + LOG_DBG(ANSI_ORANGE "Phase 3a: Function call analysis\n" ANSI_RESET); extract_function_markers(); + LOG_DBG(ANSI_ORANGE "Phase 3b: Argument analysis\n" ANSI_RESET); if (format.mode == tool_format::TAG_WITH_TAGGED) { analyze_arguments(); } extract_argument_separator(); extract_args_markers(); + LOG_DBG(ANSI_ORANGE "Phase 3c: Call id analysis\n" ANSI_RESET); extract_call_id_markers(); } } @@ -979,8 +991,6 @@ void analyze_tools::extract_function_markers() { } void analyze_tools::analyze_arguments() { - LOG_DBG(ANSI_ORANGE "Phase 4: Argument analysis\n" ANSI_RESET); - extract_argument_name_markers(); extract_argument_value_markers(); } @@ -1189,7 +1199,7 @@ void analyze_tools::extract_args_markers() { const auto & diff = comparison->diff; - if (format.mode != tool_format::JSON_NATIVE) { + if (format.mode == tool_format::JSON_NATIVE) { std::string prefix_marker = !format.section_start.empty() ? format.section_start : format.per_call_start; std::string suffix_marker = !format.section_end.empty() ? format.section_end : format.per_call_end; // these might happen earlier in the tools section as an example or somewhere else, so we need to find the closest ones @@ -1211,6 +1221,10 @@ void analyze_tools::extract_args_markers() { if (find_fun != std::string::npos) { args_start = args_start.substr(find_fun + FUN_FIRST.size(), args_start.size() - find_fun - FUN_FIRST.size()); } + size_t find_call_id = args_start.find(CALL_ID_001); + if (find_call_id != std::string::npos) { + args_start = args_start.substr(find_call_id + CALL_ID_001.size(), args_start.size() - find_call_id - CALL_ID_001.size()); + } arguments.start = args_start; arguments.end = args_end; } @@ -1250,8 +1264,8 @@ void analyze_tools::extract_call_id_markers() { return; } - std::string id_value_1 = "call00001"; - std::string id_value_2 = "call99999"; + std::string id_value_1 = CALL_ID_001; + std::string id_value_2 = CALL_ID_999; size_t common_id_prefix_len = 0; for (size_t i = 0; i < std::min(id_value_1.length(), id_value_2.length()); i++) { @@ -1350,6 +1364,14 @@ void analyze_tools::extract_call_id_markers() { call_id.suffix = find_first_marker(before_func); } + if (call_id.prefix == arguments.end) { + call_id.prefix = ""; + } + + if (call_id.suffix == arguments.start) { + call_id.suffix = ""; + } + // When call_id is detected, per_call_end may have been incorrectly set to include // the call_id_suffix and sample args. Clear it if it starts with call_id_suffix. if (call_id.pos != call_id_position::NONE && !call_id.suffix.empty() && diff --git a/common/chat.cpp b/common/chat.cpp index c0670496b3..f2a1e699d5 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1631,7 +1631,7 @@ static json common_chat_extra_context() { return ctx; } -static std::optional try_specialized_template( +std::optional try_specialized_template( const common_chat_template & tmpl, const std::string & src, const autoparser::generation_params & params) { diff --git a/common/chat.h b/common/chat.h index a60a9228bd..8febed7b2a 100644 --- a/common/chat.h +++ b/common/chat.h @@ -270,3 +270,8 @@ std::map common_chat_templates_get_caps(const common_chat_tem std::string common_chat_template_direct_apply( const common_chat_template & tmpl, const autoparser::generation_params & inputs); + +std::optional try_specialized_template( + const common_chat_template & tmpl, + const std::string & src, + const autoparser::generation_params & params); diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 34d50124c4..ea8acb671d 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -2541,55 +2541,57 @@ static void test_template_output_peg_parsers(bool detailed_debug) { // #20424 introduced effective_input = generation_prompt + input, but the throw // uses input.substr(result.end) where result.end is in effective_input space. { - auto tmpls = common_chat_templates_ptr( - common_chat_templates_init(nullptr, read_file("models/templates/GLM-4.7-Flash.jinja"))); + if (!g_template_filter.empty() && std::string("models/templates/GLM-4.7-Flash.jinja").find(g_template_filter) != std::string::npos) { + auto tmpls = common_chat_templates_ptr( + common_chat_templates_init(nullptr, read_file("models/templates/GLM-4.7-Flash.jinja"))); - static common_chat_tool weather_tool{ - "get_weather", "Get weather", - R"({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})", - }; + static common_chat_tool weather_tool{ + "get_weather", "Get weather", + R"({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})", + }; - common_chat_templates_inputs inputs; - inputs.tools = { weather_tool }; - inputs.enable_thinking = true; - inputs.reasoning_format = COMMON_REASONING_FORMAT_AUTO; - inputs.add_generation_prompt = true; - inputs.use_jinja = true; - common_chat_msg msg; - msg.role = "user"; - msg.content = "get_weather"; - inputs.messages = { msg }; + common_chat_templates_inputs inputs; + inputs.tools = { weather_tool }; + inputs.enable_thinking = true; + inputs.reasoning_format = COMMON_REASONING_FORMAT_AUTO; + inputs.add_generation_prompt = true; + inputs.use_jinja = true; + common_chat_msg msg; + msg.role = "user"; + msg.content = "get_weather"; + inputs.messages = { msg }; - auto params = common_chat_templates_apply(tmpls.get(), inputs); - common_peg_arena arena; - arena.load(params.parser); - common_chat_parser_params pp(params); + auto params = common_chat_templates_apply(tmpls.get(), inputs); + common_peg_arena arena; + arena.load(params.parser); + common_chat_parser_params pp(params); - // generation_prompt is non-empty for thinking models, so result.end - // will be offset by generation_prompt.size() into effective_input space. - assert(!pp.generation_prompt.empty()); + // generation_prompt is non-empty for thinking models, so result.end + // will be offset by generation_prompt.size() into effective_input space. + assert(!pp.generation_prompt.empty()); - std::string bad_input = - "Thinking.\n" - "" - "get_weather" - "cityTokyo" - "\n"; + std::string bad_input = + "Thinking.\n" + "" + "get_weather" + "cityTokyo" + "\n"; - bool got_runtime_error = false; - bool got_out_of_range = false; - std::string error_msg; - try { - common_chat_peg_parse(arena, bad_input, /*is_partial=*/false, pp); - } catch (const std::out_of_range & e) { - got_out_of_range = true; - error_msg = e.what(); - } catch (const std::runtime_error & e) { - got_runtime_error = true; - error_msg = e.what(); + bool got_runtime_error = false; + bool got_out_of_range = false; + std::string error_msg; + try { + common_chat_peg_parse(arena, bad_input, /*is_partial=*/false, pp); + } catch (const std::out_of_range & e) { + got_out_of_range = true; + error_msg = e.what(); + } catch (const std::runtime_error & e) { + got_runtime_error = true; + error_msg = e.what(); + } + GGML_ASSERT(!got_out_of_range && "throw path crashed with out_of_range (input.substr in effective_input space)"); + GGML_ASSERT(got_runtime_error && "throw path should produce std::runtime_error with parse position"); } - GGML_ASSERT(!got_out_of_range && "throw path crashed with out_of_range (input.substr in effective_input space)"); - GGML_ASSERT(got_runtime_error && "throw path should produce std::runtime_error with parse position"); } // Kimi-K2-Thinking tests - custom parser @@ -3169,6 +3171,21 @@ static void test_template_output_peg_parsers(bool detailed_debug) { .expect(message_assist_call_id) .expect_reconstruction() .run(); + + tst.test("[TOOL_CALLS]special_function[CALL_ID]000000001[ARGS]{\"arg1\": 1}" + "[TOOL_CALLS]special_function_with_opt[CALL_ID]000000002[ARGS]{\"arg1\": 1, \"arg2\": 2}") + .parallel_tool_calls(true) + .tools({ + special_function_tool, special_function_tool_with_optional_param + }) + .expect_tool_calls({ + { "special_function", R"({"arg1": 1})", "000000001" }, + { "special_function_with_opt", R"({"arg1": 1, "arg2": 2})", "000000002" }, + }) + .expect_reconstruction() + .run(); + + } // Devstral { diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp index a837971571..eedac5a661 100644 --- a/tools/parser/debug-template-parser.cpp +++ b/tools/parser/debug-template-parser.cpp @@ -5,15 +5,15 @@ #include "gguf.h" #include "jinja/runtime.h" #include "log.h" +#include "nlohmann/json.hpp" +#include "peg-parser.h" #include #include +#include #include #include -#include "nlohmann/json.hpp" -#include "peg-parser.h" - using json = nlohmann::ordered_json; enum class output_mode { @@ -34,14 +34,14 @@ enum class input_message_type { }; 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; - output_mode mode = output_mode::BOTH; - input_message_type input_message = input_message_type::NONE; + 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; + output_mode mode = output_mode::BOTH; + input_message_type input_message = input_message_type::NONE; }; static std::string read_file(const std::string & path) { @@ -274,7 +274,7 @@ static void render_scenario(const common_chat_template & tmpl, json final_messages = messages; if (add_generation_prompt && !messages.empty() && messages.back().value("role", "") == "assistant") { final_messages.push_back(json{ - { "role", "user" }, + { "role", "user" }, { "content", "Now please continue with another response." } }); } @@ -305,7 +305,7 @@ 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) { + input_message_type message_type) { json user_msg = build_user_message(); auto render_if = [&](input_message_type type, const std::string & name, const json & assistant_msg) { @@ -335,6 +335,24 @@ static void render_all_scenarios(const common_chat_template & tmpl, } } +static autoparser::generation_params prepare_params(const debug_options & opts, const json & tools) { + autoparser::generation_params params; + params.messages = json::array({ build_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 = false; + return params; +} + int main(int argc, char ** argv) { // Set log level to most verbose to capture all debug output common_log_set_verbosity_thold(99); @@ -369,49 +387,41 @@ int main(int argc, char ** argv) { try { common_chat_template chat_template(template_source, "", ""); - // Build tools definition json tools = opts.with_tools ? build_tools_definition() : json(); - // Render template scenarios if requested - if (opts.input_message != input_message_type::NONE && - (opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) { + autoparser::generation_params params = prepare_params(opts, tools); + common_chat_params parser_data; + if (std::optional spec_tmpl = + try_specialized_template(chat_template, template_source, params)) { LOG_ERR("\n"); - LOG_ERR("================================================================================\n"); - LOG_ERR(" TEMPLATE RENDERING OUTPUT\n"); - LOG_ERR("================================================================================\n"); + LOG_ERR("This template uses a specialized parser, analysis results will not be available."); + parser_data = *spec_tmpl; + } else { + // Render template scenarios if requested + if (opts.input_message != input_message_type::NONE && + (opts.mode == output_mode::TEMPLATE || opts.mode == output_mode::BOTH)) { + LOG_ERR("\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR(" TEMPLATE RENDERING OUTPUT\n"); + LOG_ERR("================================================================================\n"); - render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning, - opts.input_message); - } - - // Output analysis if requested - if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) { - LOG_ERR("\n"); - LOG_ERR("================================================================================\n"); - LOG_ERR(" TEMPLATE ANALYSIS\n"); - LOG_ERR("================================================================================\n"); - - autoparser::autoparser analysis; - analysis.analyze_template(chat_template); - - // Generate Parser - autoparser::generation_params params; - params.messages = json::array({ build_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; + render_all_scenarios(chat_template, tools, opts.generation_prompt, opts.enable_reasoning, + opts.input_message); } - params.parallel_tool_calls = false; - auto parser_data = autoparser::peg_generator::generate_parser(chat_template, params, analysis); + // Output analysis if requested + if (opts.mode == output_mode::ANALYSIS || opts.mode == output_mode::BOTH) { + LOG_ERR("\n"); + LOG_ERR("================================================================================\n"); + LOG_ERR(" TEMPLATE ANALYSIS\n"); + LOG_ERR("================================================================================\n"); + + autoparser::autoparser analysis; + analysis.analyze_template(chat_template); + + // Generate Parser + parser_data = autoparser::peg_generator::generate_parser(chat_template, params, analysis); + } LOG_ERR("\n=== Generated Parser ===\n"); common_peg_arena arena; From ed9aa135135687dc50f21ec31286c1c2dbd62acb Mon Sep 17 00:00:00 2001 From: Piotr Wilkin Date: Fri, 3 Apr 2026 12:50:24 +0200 Subject: [PATCH 2/3] Rename --- common/chat.cpp | 4 ++-- common/chat.h | 2 +- tools/parser/debug-template-parser.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/chat.cpp b/common/chat.cpp index f2a1e699d5..e93ee6b230 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1631,7 +1631,7 @@ static json common_chat_extra_context() { return ctx; } -std::optional try_specialized_template( +std::optional common_chat_try_specialized_template( const common_chat_template & tmpl, const std::string & src, const autoparser::generation_params & params) { @@ -1778,7 +1778,7 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_ return data; } - if (auto result = try_specialized_template(tmpl, src, params)) { + if (auto result = common_chat_try_specialized_template(tmpl, src, params)) { result->generation_prompt = params.generation_prompt; return *result; } diff --git a/common/chat.h b/common/chat.h index 8febed7b2a..d5328379cc 100644 --- a/common/chat.h +++ b/common/chat.h @@ -271,7 +271,7 @@ std::string common_chat_template_direct_apply( const common_chat_template & tmpl, const autoparser::generation_params & inputs); -std::optional try_specialized_template( +std::optional common_chat_try_specialized_template( const common_chat_template & tmpl, const std::string & src, const autoparser::generation_params & params); diff --git a/tools/parser/debug-template-parser.cpp b/tools/parser/debug-template-parser.cpp index eedac5a661..9c591a1f11 100644 --- a/tools/parser/debug-template-parser.cpp +++ b/tools/parser/debug-template-parser.cpp @@ -392,7 +392,7 @@ int main(int argc, char ** argv) { autoparser::generation_params params = prepare_params(opts, tools); common_chat_params parser_data; if (std::optional spec_tmpl = - try_specialized_template(chat_template, template_source, params)) { + 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."); parser_data = *spec_tmpl; From 620b2c05d16decdb60e48aa8bd3df583d8465b88 Mon Sep 17 00:00:00 2001 From: "Piotr Wilkin (ilintar)" Date: Fri, 3 Apr 2026 14:34:12 +0200 Subject: [PATCH 3/3] Update common/chat-auto-parser-generator.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sigbjørn Skjæret --- common/chat-auto-parser-generator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/chat-auto-parser-generator.cpp b/common/chat-auto-parser-generator.cpp index 4494184561..c1f0330e92 100644 --- a/common/chat-auto-parser-generator.cpp +++ b/common/chat-auto-parser-generator.cpp @@ -322,9 +322,9 @@ common_peg_parser analyze_tools::build_func_parser(common_chat_peg_builder & p, const common_peg_parser & call_id_section, bool have_call_id, const common_peg_parser & args, std::optional atomic_peek) const { - auto open = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix); + auto open = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix); bool matched_atomic = false; - common_peg_parser func_parser = p.eps(); + common_peg_parser func_parser = p.eps(); if (!function.name_suffix.empty()) { func_parser = open + call_id_section + p.space() + args;