Felipe97/llama-cpp-compiled
01.1k
1#include "chat-auto-parser-helpers.h"2#include "chat-auto-parser.h"3#include "chat-peg-parser.h"4#include "chat.h"5#include "common.h"6#include "json-schema-to-grammar.h"7#include "log.h"8#include "parsers/parsers.h"9#include "peg-parser.h"10 11#include <stdexcept>12#include <string>13 14using json = common_json;15 16namespace autoparser {17 18parser_build_context::parser_build_context(common_chat_peg_builder & p, const generation_params & inputs) :19 p(p),20 inputs(inputs),21 reasoning_parser(p.eps()) {}22 23common_chat_params peg_generator::generate_parser(const common_chat_template & tmpl,24 const struct generation_params & inputs) {25 // Run differential analysis to extract template structure26 struct autoparser autoparser;27 autoparser.analyze_template(tmpl);28 return generate_parser(tmpl, inputs, autoparser);29}30 31common_chat_params peg_generator::generate_parser(const common_chat_template & tmpl,32 const struct generation_params & inputs,33 const autoparser & autoparser) {34 // Create the result structure35 common_chat_params data;36 data.prompt = common_chat_template_direct_apply(tmpl, inputs);37 data.generation_prompt = common_chat_template_generation_prompt(tmpl, inputs);38 data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;39 data.preserved_tokens = autoparser.preserved_tokens;40 data.additional_stops.insert(data.additional_stops.end(),41 autoparser.additional_stops.begin(), autoparser.additional_stops.end());42 43 std::string parser_generation_prompt = data.generation_prompt;44 45 if (inputs.continue_final_message != COMMON_CHAT_CONTINUATION_NONE && !inputs.continue_msg.empty()) {46 // Build up generation prompt manually47 const auto & msg = inputs.continue_msg;48 49 if (!autoparser.reasoning.start.empty()) {50 data.generation_prompt = data.generation_prompt.substr(0, data.generation_prompt.find(autoparser.reasoning.start));51 data.generation_prompt += autoparser.reasoning.start + msg.reasoning_content;52 if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {53 data.generation_prompt += autoparser.reasoning.end;54 }55 }56 57 if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {58 data.generation_prompt += msg.render_content();59 }60 61 data.prompt += data.generation_prompt;62 }63 64 auto parser = autoparser.build_parser(inputs, parser_generation_prompt);65 data.parser = parser.save();66 67 // Build grammar if tools are present68 bool has_tools =69 autoparser.tools.format.mode != tool_format::NONE && inputs.tools.is_array() && !inputs.tools.empty();70 std::string trigger_marker = !autoparser.tools.format.section_start.empty() ? autoparser.tools.format.section_start :71 autoparser.tools.format.per_call_start;72 73 bool has_response_format = !inputs.json_schema.empty() && inputs.json_schema.is_object();74 bool include_grammar = has_response_format || (has_tools &&75 ((inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO && !trigger_marker.empty()) ||76 inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));77 78 if (include_grammar) {79 data.grammar_lazy = !has_response_format && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;80 data.grammar = build_grammar([&](const common_grammar_builder & builder) {81 parser.build_grammar(builder, data.grammar_lazy);82 });83 84 // Set grammar triggers based on tool section markers (fall back to per-call markers)85 if (data.grammar_lazy) {86 data.grammar_triggers = {87 { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, trigger_marker }88 };89 if (autoparser.tools.format.openai_wrapper_trigger) {90 // model emits the OpenAI function wrapper, trigger on it91 data.grammar_triggers.push_back({ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "{\"type\": \"function\"," });92 }93 }94 }95 96 return data;97}98 99common_peg_arena autoparser::build_parser(const generation_params & inputs, const std::string & generation_prompt) const {100 if (!analysis_complete) {101 throw std::invalid_argument("Cannot call build_parser on autoparser without performing analysis first, call analyze_template(...)");102 }103 return build_chat_peg_parser([&](common_chat_peg_builder & p) {104 parser_build_context ctx(p, inputs);105 bool extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;106 107 ctx.extracting_reasoning = extract_reasoning && reasoning.mode != reasoning_mode::NONE;108 ctx.content = &content;109 ctx.reasoning = &reasoning;110 111 // Build reasoning parser112 ctx.reasoning_parser = reasoning.build_parser(ctx);113 114 auto parser = p.eps();115 116 bool has_tools = inputs.tools.is_array() && !inputs.tools.empty();117 bool has_response_format = inputs.json_schema.is_object() && !inputs.json_schema.empty();118 bool pure_content = reasoning.mode == reasoning_mode::NONE;119 120 if (has_response_format) {121 auto response_format = p.rule("response-format", p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)));122 parser = ctx.reasoning_parser + p.space() + p.choice({123 p.literal("```json") + p.space() + response_format + p.space() + p.literal("```"),124 p.space() + response_format + p.space()125 }) + p.end();126 pure_content = false;127 } else if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE && jinja_caps.supports_tool_calls) {128 parser = tools.build_parser(ctx);129 pure_content = false;130 } else {131 parser = content.build_parser(ctx);132 }133 const std::string reasoning_start = trim_whitespace(reasoning.start);134 return pure_content ? p.prefix(generation_prompt, reasoning_start) + parser : p.prefix(generation_prompt, reasoning_start) << parser;135 });136}137 138common_peg_parser analyze_reasoning::build_parser(parser_build_context & ctx) const {139 auto & p = ctx.p;140 141 if (!ctx.extracting_reasoning) {142 return p.eps();143 }144 145 if (mode == reasoning_mode::TAG_BASED || mode == reasoning_mode::TOOLS_ONLY) {146 if (!end.empty()) {147 if (!start.empty()) {148 // Standard tag-based: optional(<think>reasoning</think>)149 return p.optional(p.optspace(start) + p.reasoning(p.until(trim_whitespace(end))) + p.optspace(end));150 }151 // Delimiter-style (empty start)152 return p.optional(p.reasoning(p.until(trim_whitespace(end))) + p.optspace(end));153 }154 }155 156 return p.eps();157}158 159common_peg_parser analyze_content::build_parser(parser_build_context & ctx) const {160 auto & p = ctx.p;161 162 if (is_always_wrapped()) {163 if (ctx.extracting_reasoning) {164 return ctx.reasoning_parser + start + p.content(p.until(end)) + end + p.end();165 }166 return p.content(p.until(start)) + start + p.content(p.until(end)) + end + p.end();167 }168 return ctx.reasoning_parser + p.content(p.rest()) + p.end();169}170 171common_peg_parser analyze_content::build_optional_wrapped(parser_build_context & ctx) const {172 auto & p = ctx.p;173 174 if (is_always_wrapped()) {175 return p.optional(start + p.content(p.until(end)) + end);176 }177 return p.eps();178}179 180common_peg_parser analyze_tools::build_parser(parser_build_context & ctx) const {181 switch (format.mode) {182 case tool_format::JSON_NATIVE:183 return build_tool_parser_json_native(ctx);184 case tool_format::TAG_WITH_JSON:185 return build_tool_parser_tag_json(ctx);186 case tool_format::TAG_WITH_TAGGED:187 return build_tool_parser_tag_tagged(ctx);188 default:189 LOG_ERR("[ERROR] Template seems to support tool calls, but failed to determine tool format. Tool calling will not work properly. "190 "Check for a fixed template for your model in the models/templates directory of your llama.cpp installation or "191 "report an issue at https://github.com/ggml-org/llama.cpp/issues\n");192 return ctx.p.eps();193 }194}195 196common_peg_parser analyze_tools::build_tool_parser_json_native(parser_build_context & ctx) const {197 auto & p = ctx.p;198 const auto & inputs = ctx.inputs;199 200 // Build effective field names with dot notation if function_field is set201 std::string name_field = format.name_field;202 std::string args_field = format.args_field;203 204 if (!format.function_field.empty() && format.function_field != "function" &&205 name_field.find('.') == std::string::npos) {206 name_field = format.function_field + "." + name_field;207 args_field = format.function_field + "." + args_field;208 }209 210 auto tools_parser = p.eps();211 if (format.section_start.empty() && !format.per_call_start.empty()) {212 auto single_tool_parser = p.standard_json_tools(213 format.per_call_start, format.per_call_end, inputs.tools, inputs.parallel_tool_calls,214 inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED, name_field, args_field, format.tools_array_wrapped,215 format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order, format.openai_wrapper_trigger);216 tools_parser = p.trigger_rule("tool-calls", p.one_or_more(single_tool_parser + p.space()));217 } else {218 tools_parser = p.standard_json_tools(219 format.section_start, format.section_end, inputs.tools, inputs.parallel_tool_calls,220 inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED, name_field, args_field, format.tools_array_wrapped,221 format.fun_name_is_key, format.id_field, format.gen_id_field, format.parameter_order, format.openai_wrapper_trigger);222 }223 224 // Handle content wrappers if present225 if (ctx.content && ctx.content->is_always_wrapped()) {226 auto wrapped_content = ctx.content->build_optional_wrapped(ctx);227 return ctx.reasoning_parser + wrapped_content + tools_parser + p.end();228 }229 230 std::string tool_start = "{";231 if (!format.section_start.empty()) {232 tool_start = format.section_start;233 } else if (!format.per_call_start.empty()) {234 tool_start = format.per_call_start;235 }236 237 return ctx.reasoning_parser + p.optional(p.content(p.until(tool_start))) + tools_parser + p.end();238}239 240common_peg_parser analyze_tools::build_func_parser(common_chat_peg_builder & p, const std::string & name,241 const common_peg_parser & call_id_section, bool have_call_id,242 const common_peg_parser & args,243 std::optional<common_peg_parser> atomic_peek) const {244 auto open = p.tool_open(function.name_prefix + p.tool_name(p.literal(name)) + function.name_suffix);245 bool matched_atomic = false;246 common_peg_parser func_parser = p.eps();247 248 if (!function.args_separator.empty()) {249 open = open + p.space() + p.literal(function.args_separator);250 }251 252 if (!function.name_suffix.empty()) {253 func_parser = open + call_id_section + p.space() + args;254 matched_atomic = true;255 } else if (have_call_id) {256 func_parser = p.atomic(open + call_id_section) + p.space() + args;257 matched_atomic = true;258 } else if (atomic_peek.has_value()) {259 func_parser = p.atomic(open + call_id_section + p.space() + *atomic_peek) + args;260 matched_atomic = true;261 } else {262 func_parser = open + call_id_section + p.space() + args;263 }264 265 if (!function.close.empty()) {266 func_parser = func_parser + p.space() + p.tool_close(p.literal(function.close));267 } else if (!format.per_call_end.empty()) {268 // When there's no func_close but there is a per_call_end marker, use peek() to ensure269 // we only emit tool_close when we can actually see the closing marker. This prevents270 // premature closing during partial parsing when we've seen e.g. "</" which could be271 // either "</tool_call>" (end) or "<arg_key>" prefix that failed to match.272 // Laguna (v4): the model may emit whitespace between the last </arg_value> and273 // </tool_call> even though the template renders them tight. Tolerate optional274 // leading space in the close lookahead so the tool call still closes.275 auto close_peek = arguments.tolerate_intertag_whitespace276 ? p.peek(p.space() + p.literal(format.per_call_end))277 : p.peek(p.literal(format.per_call_end));278 func_parser = func_parser + p.tool_close(close_peek);279 } else {280 func_parser = func_parser + p.tool_close(p.space()); // force this to process tool closing callbacks in mapper281 }282 if (!matched_atomic) {283 func_parser = p.atomic(func_parser);284 }285 return func_parser;286}287 288common_peg_parser analyze_tools::build_tool_parser_tag_json(parser_build_context & ctx) const {289 auto & p = ctx.p;290 const auto & inputs = ctx.inputs;291 292 common_peg_parser tool_choice = p.choice();293 294 foreach_function(inputs.tools, [&](const json & tool) {295 const auto & func = tool.at("function");296 std::string name = func.at("name");297 const auto schema = common_chat_tool_parameters(func);298 299 // Build call_id parser based on position (if supported)300 bool have_call_id = false;301 common_peg_parser call_id_section = p.eps();302 if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() &&303 (!call_id.suffix.empty() || !arguments.start.empty())) {304 if (!call_id.suffix.empty()) {305 call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix))) + call_id.suffix;306 } else {307 call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start)));308 }309 have_call_id = true;310 }311 auto args_parser = p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema));312 if (!arguments.start.empty()) {313 args_parser = p.literal(arguments.start) + args_parser;314 }315 if (!arguments.end.empty()) {316 args_parser = args_parser + p.literal(arguments.end);317 }318 319 auto atomic_peek = !arguments.start.empty() ? std::optional(p.peek(p.literal(arguments.start))) : std::nullopt;320 auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_parser, atomic_peek);321 tool_choice |= p.rule("tool-" + name, func_parser);322 });323 324 auto require_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;325 326 common_peg_parser tool_calls = p.eps();327 328 if (!format.per_call_start.empty()) {329 auto wrapped_call = format.per_call_start + tool_choice + format.per_call_end;330 if (inputs.parallel_tool_calls) {331 tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call));332 } else {333 tool_calls = p.trigger_rule("tool-call", wrapped_call);334 }335 if (!format.section_start.empty()) {336 tool_calls = p.trigger_rule("tool-calls",337 p.literal(format.section_start) + p.space() + tool_calls + p.space() +338 (format.section_end.empty() ? p.end() : p.literal(format.section_end)));339 }340 } else {341 std::string separator = ", "; // Default342 if (inputs.parallel_tool_calls) {343 tool_calls = p.trigger_rule("tool-call", format.section_start + tool_choice +344 p.zero_or_more(separator + tool_choice) + format.section_end);345 } else {346 tool_calls = p.trigger_rule("tool-call", format.section_start + tool_choice + format.section_end);347 }348 }349 350 if (!require_calls) {351 tool_calls = p.optional(tool_calls);352 }353 354 std::string trigger_marker = !format.section_start.empty() ? format.section_start : format.per_call_start;355 auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker);356 return ctx.reasoning_parser + p.optional(p.content(content_before_tools)) + tool_calls + p.end();357}358 359common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_context & ctx) const {360 auto & p = ctx.p;361 const auto & inputs = ctx.inputs;362 363 auto until_suffix = p.rule("until-suffix", p.until(arguments.value_suffix));364 365 common_peg_parser tool_choice = p.choice();366 367 foreach_function(inputs.tools, [&](const json & tool) {368 const auto & func = tool.at("function");369 std::string name = func.at("name");370 371 // Build parser for each argument, separating required and optional372 std::vector<common_peg_parser> required_parsers;373 std::vector<common_peg_parser> optional_parsers;374 foreach_parameter(func, [&](const common_chat_schema_property & param, const common_chat_schema_document_ptr & doc) {375 auto arg =376 p.tool_arg(p.tool_arg_open(arguments.name_prefix + p.tool_arg_name(p.literal(param.name)) +377 arguments.name_suffix) +378 arguments.value_prefix +379 (param.schema->may_be_string() ?380 p.ac(p.tool_arg_string_value(until_suffix) +381 p.tool_arg_close(p.literal(arguments.value_suffix)), arguments.value_suffix) :382 (p.tool_arg_json_value(p.schema(383 p.json(), "tool-" + name + "-arg-" + param.name + "-schema", doc, *param.schema)) +384 p.tool_arg_close(p.literal(arguments.value_suffix)))));385 386 auto named_arg = p.rule("tool-" + name + "-arg-" + param.name, arg);387 if (param.required) {388 required_parsers.push_back(named_arg);389 } else {390 optional_parsers.push_back(named_arg);391 }392 });393 394 // Build required arg sequence in definition order395 common_peg_parser args_seq = p.eps();396 for (size_t i = 0; i < required_parsers.size(); i++) {397 if (i > 0) {398 args_seq = args_seq + p.space();399 }400 args_seq = args_seq + required_parsers[i];401 }402 403 // Build optional args with flexible ordering404 if (!optional_parsers.empty()) {405 common_peg_parser any_opt = p.choice();406 for (const auto & opt : optional_parsers) {407 any_opt |= opt;408 }409 args_seq = args_seq + p.repeat(p.space() + any_opt, 0, -1);410 }411 412 if (!arguments.start.empty()) {413 args_seq = p.literal(arguments.start) + args_seq;414 }415 if (!arguments.end.empty()) {416 args_seq = args_seq + p.literal(arguments.end);417 }418 419 // Build call_id parser based on position (if supported)420 common_peg_parser call_id_section = p.eps();421 bool have_call_id = false;422 if (call_id.pos == call_id_position::BETWEEN_FUNC_AND_ARGS && !call_id.prefix.empty() &&423 (!call_id.suffix.empty() || !arguments.start.empty())) {424 have_call_id = true;425 if (!call_id.suffix.empty()) {426 call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(call_id.suffix)) + call_id.suffix);427 } else {428 call_id_section = p.optional(call_id.prefix + p.tool_id(p.until(arguments.start)));429 }430 }431 432 // Only peek for an arg tag when there are required args that must follow.433 // When all args are optional, the model may emit no arg tags at all (#20650).434 auto atomic_peek = (!arguments.name_prefix.empty() && !required_parsers.empty()) ?435 std::optional(p.peek(p.literal(arguments.name_prefix))) : std::nullopt;436 auto func_parser = build_func_parser(p, name, call_id_section, have_call_id, args_seq, atomic_peek);437 tool_choice |= p.rule("tool-" + name, func_parser);438 });439 440 auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;441 442 common_peg_parser tool_calls = p.eps();443 444 if (!format.per_call_start.empty()) {445 auto wrapped_call = format.per_call_start + p.space() + tool_choice + p.space() + format.per_call_end;446 if (inputs.parallel_tool_calls) {447 tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call) + p.space());448 } else {449 tool_calls = p.trigger_rule("tool-call", wrapped_call + p.space());450 }451 if (!format.section_start.empty()) {452 tool_calls = p.trigger_rule("tool-calls",453 p.literal(format.section_start) + p.space() + tool_calls + p.space() +454 (format.section_end.empty() ? p.end() : p.literal(format.section_end) + p.space()));455 }456 } else {457 std::string separator = ", "; // Default458 459 if (inputs.parallel_tool_calls) {460 tool_calls = p.trigger_rule("tool-call", format.section_start + p.space() + tool_choice +461 p.zero_or_more(separator + tool_choice) + p.space() +462 format.section_end);463 } else {464 tool_calls = p.trigger_rule(465 "tool-call", format.section_start + p.space() + tool_choice + p.space() + format.section_end);466 }467 }468 469 if (!require_tools) {470 tool_calls = p.optional(tool_calls);471 }472 473 std::string trigger_marker = !format.section_start.empty() ? format.section_start : format.per_call_start;474 auto content_before_tools = trigger_marker.empty() ? p.eps() : p.until(trigger_marker);475 return ctx.reasoning_parser + p.optional(p.content(content_before_tools)) + tool_calls + p.end();476}477 478} // namespace autoparser479 