char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aldehir Rojascommon : implement common_schema internal representation for JSON schemas (#28736)acecd5603

char/aliceai-t5-moe
46.7 KiB1232 linesraw
1#include "chat-peg-parser.h"
2
3#include "chat-auto-parser.h"
4#include "ggml.h"
5#include "peg-parser.h"
6
7#include <cstdint>
8#include <functional>
9
10using ordered_json = common_json;
11
12static std::string_view trim_trailing_space(std::string_view sv, int max = -1) {
13    int count = 0;
14    while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.back()))) {
15        if (max != -1 && count >= max) {
16            break;
17        }
18        sv.remove_suffix(1);
19        count++;
20    }
21    return sv;
22}
23
24static std::string_view trim_leading_space(std::string_view sv, int max = -1) {
25    int count = 0;
26    while (!sv.empty() && std::isspace(static_cast<unsigned char>(sv.front()))) {
27        if (max != -1 && count >= max) {
28            break;
29        }
30        sv.remove_prefix(1);
31        count++;
32    }
33    return sv;
34}
35
36static std::string_view trim(std::string_view sv) {
37    return trim_trailing_space(trim_leading_space(sv, 1));
38}
39
40// Count the number of unclosed '{' braces in a JSON-like string,
41// properly skipping braces inside quoted strings.
42static int json_brace_depth(const std::string & s) {
43    int  depth     = 0;
44    bool in_string = false;
45    bool escaped   = false;
46    for (char c : s) {
47        if (escaped) {
48            escaped = false;
49            continue;
50        }
51        if (c == '\\' && in_string) {
52            escaped = true;
53            continue;
54        }
55        if (c == '"') {
56            in_string = !in_string;
57            continue;
58        }
59        if (!in_string) {
60            if (c == '{') {
61                depth++;
62            } else if (c == '}') {
63                depth--;
64            }
65        }
66    }
67    return depth;
68}
69
70// JSON-escape a string and return the inner content (without surrounding quotes).
71static std::string escape_json_string_inner(const std::string & s) {
72    std::string escaped = ordered_json(s).dump();
73    if (escaped.size() >= 2 && escaped.front() == '"' && escaped.back() == '"') {
74        return escaped.substr(1, escaped.size() - 2);
75    }
76    return escaped;
77}
78
79// Convert Python-style single-quoted strings to JSON double-quoted strings
80// Only converts outer string delimiters, properly handling escape sequences:
81// - {'key': 'value'} -> {"key": "value"}
82// - {'code': 'print(\'hello\')'} -> {"code": "print('hello')"}
83// - {'msg': 'He said "hi"'} -> {"msg": "He said \"hi\""}
84static std::string normalize_quotes_to_json(const std::string & input) {
85    std::string result;
86    result.reserve(input.size() + 16);  // May need extra space for escaping
87
88    bool in_single_quoted = false;
89    bool in_double_quoted = false;
90
91    auto is_word_char = [](char ch) { return std::isalnum(static_cast<unsigned char>(ch)) || ch == '_'; };
92
93    for (size_t i = 0; i < input.size(); ++i) {
94        char c = input[i];
95
96        // Handle escape sequences
97        if (c == '\\' && i + 1 < input.size()) {
98            char next = input[i + 1];
99
100            if (in_single_quoted) {
101                // Inside a single-quoted string being converted to double quotes
102                if (next == '\'') {
103                    // \' -> ' (escaped single quote becomes unescaped in double-quoted string)
104                    result += '\'';
105                    ++i;
106                    continue;
107                }
108                if (next == '"') {
109                    // \" stays as \" (already escaped, works in double-quoted string)
110                    result += "\\\"";
111                    ++i;
112                    continue;
113                }
114                // Other escapes (\n, \\, etc.): pass through both characters
115                result += c;
116                result += next;
117                ++i;
118                continue;
119            }
120
121            if (in_double_quoted) {
122                // Inside a double-quoted string - pass through escape sequences as-is
123                result += c;
124                result += next;
125                ++i;
126                continue;
127            }
128
129            // Outside any string - just pass through the backslash
130            result += c;
131            continue;
132        }
133
134        // Handle quote characters
135        if (c == '"') {
136            if (in_single_quoted) {
137                // Unescaped double quote inside single-quoted string -> must escape for JSON
138                result += "\\\"";
139            } else {
140                // Double quote as string delimiter or outside strings
141                in_double_quoted = !in_double_quoted;
142                result += c;
143            }
144        } else if (c == '\'') {
145            if (in_double_quoted) {
146                // Single quote inside double-quoted string -> pass through
147                result += c;
148            } else if (in_single_quoted) {
149                // Closing single quote -> convert to double quote
150                in_single_quoted = false;
151                result += '"';
152            } else {
153                // Opening single quote -> convert to double quote
154                in_single_quoted = true;
155                result += '"';
156            }
157        } else if (!in_single_quoted && !in_double_quoted && (c == 'T' || c == 'F' || c == 'N') &&
158                   (i == 0 || !is_word_char(input[i - 1]))) {
159            // Python literals -> JSON; prefix match keeps streamed partials monotonic.
160            static constexpr std::pair<std::string_view, std::string_view> literals[] = {
161                { "True", "true" }, { "False", "false" }, { "None", "null" },
162            };
163            size_t n = 0;
164            while (i + n < input.size() && is_word_char(input[i + n])) {
165                ++n;
166            }
167            std::string_view token(input.data() + i, n);
168            bool matched = false;
169            for (const auto & [py, js] : literals) {
170                if (py.substr(0, n) == token) {
171                    result += js.substr(0, n);
172                    i += n - 1;
173                    matched = true;
174                    break;
175                }
176            }
177            if (!matched) {
178                result += c;
179            }
180        } else {
181            result += c;
182        }
183    }
184
185    return result;
186}
187
188void tag_based_peg_mapper::from_ast(const common_peg_ast_arena & arena, const common_peg_parse_result & result) {
189    arena.visit(result, [this](const common_peg_ast_node & node) {
190        if (!node.tag.empty()) {
191            tags[node.tag] = std::string(node.text);
192        }
193    });
194}
195
196tagged_parse_result tagged_peg_parser::parse_and_extract(const std::string & input, common_peg_parse_flags extra_flags) const {
197    common_peg_parse_context ctx(input, flags | extra_flags);
198    auto parse_result = arena.parse(ctx);
199
200    tag_based_peg_mapper mapper;
201    mapper.from_ast(ctx.ast, parse_result);
202
203    return { std::move(parse_result), std::move(mapper.tags) };
204}
205
206tagged_parse_result tagged_peg_parser::parse_anywhere_and_extract(const std::string & input) const {
207    if (input.empty()) {
208        return parse_and_extract(input);
209    }
210    for (size_t i = 0; i < input.size(); i++) {
211        common_peg_parse_context ctx(input, flags);
212        auto parse_result = arena.parse(ctx, i);
213        if (parse_result.success() || i == input.size() - 1) {
214            tag_based_peg_mapper mapper;
215            mapper.from_ast(ctx.ast, parse_result);
216            return { std::move(parse_result), std::move(mapper.tags) };
217        }
218    }
219    GGML_ABORT("Should not happen");
220}
221
222tagged_peg_parser build_tagged_peg_parser(
223    const std::function<common_peg_parser(common_peg_parser_builder & builder)> & fn) {
224    common_peg_parser_builder builder;
225    builder.set_root(fn(builder));
226    return { builder.build() };
227}
228
229common_peg_parser common_chat_peg_builder::tag_with_safe_content(const std::string &       tag_name,
230                                                                 const std::string &       marker,
231                                                                 const common_peg_parser & p) {
232    if (marker.empty()) {
233        return zero_or_more(choice({ p, rule(tag_name, content(any())) }));
234    }
235    auto content_chunk = rule(tag_name, content(negate(literal(marker)) + any() + until(marker)));
236    return zero_or_more(choice({ p, content_chunk }));
237}
238
239common_peg_parser common_chat_peg_builder::permute(const std::string &                    rule_prefix,
240                                                   const std::vector<common_peg_parser> & parsers) {
241    if (parsers.empty()) {
242        return eps();
243    }
244
245    if (parsers.size() == 1 || parsers.size() > COMMON_CHAT_MAX_PERMUTE) {
246        return sequence(parsers);
247    }
248
249    std::map<uint32_t, common_peg_parser>      rules;
250    std::function<common_peg_parser(uint32_t)> remaining_of;
251
252    remaining_of = [&](uint32_t remaining) -> common_peg_parser {
253        if (remaining == 0) {
254            return eps();
255        }
256
257        auto cached = rules.find(remaining);
258        if (cached != rules.end()) {
259            return cached->second;
260        }
261
262        auto alternatives = choice();
263        for (size_t i = 0; i < parsers.size(); i++) {
264            const uint32_t bit = 1u << i;
265            if (remaining & bit) {
266                alternatives |= parsers[i] + remaining_of(remaining & ~bit);
267            }
268        }
269
270        return rules.emplace(remaining, rule(rule_prefix + "-" + std::to_string(remaining), alternatives)).first->second;
271    };
272
273    return remaining_of((1u << parsers.size()) - 1);
274}
275
276std::string & common_chat_peg_mapper::args_target() {
277    return (current_tool && !current_tool->name.empty()) ? current_tool->arguments : args_buffer;
278}
279
280std::string common_chat_peg_mapper::normalize_container_value(const std::string & input) {
281    return normalize_quotes_to_json(input);
282}
283
284void common_chat_peg_mapper::from_ast(const common_peg_ast_arena &    arena,
285                                      const common_peg_parse_result & parse_result_arg) {
286    arena.visit(parse_result_arg, [this](const common_peg_ast_node & node) { map(node); });
287    // Flush any pending tool call that was started but never got a name
288    // This happens during partial parsing when the tool call is incomplete
289    if (pending_tool_call.has_value() && !pending_tool_call->name.empty()) {
290        if (!args_buffer.empty()) {
291            pending_tool_call->arguments = args_buffer;
292        }
293        if (closing_quote_pending && !pending_tool_call->arguments.empty()) {
294            pending_tool_call->arguments += "\"";
295        }
296        result.tool_calls.push_back(pending_tool_call.value());
297        pending_tool_call.reset();
298    }
299
300    // Discard whitespace-only reasoning content (e.g. from <think></think> prefill)
301    if (!result.reasoning_content.empty()) {
302        bool all_whitespace = true;
303        for (char c : result.reasoning_content) {
304            if (c != ' ' && c != '\n' && c != '\r' && c != '\t') {
305                all_whitespace = false;
306                break;
307            }
308        }
309        if (all_whitespace) {
310            result.reasoning_content.clear();
311        }
312    }
313}
314
315void common_chat_peg_mapper::map(const common_peg_ast_node & node) {
316    // Handle reasoning/content tags
317    bool is_reasoning = node.tag == common_chat_peg_builder::REASONING;
318    bool is_content   = node.tag == common_chat_peg_builder::CONTENT;
319
320    if (is_reasoning) { // GPT OSS can have more than 1 reasoning block, so concatenate here
321        result.reasoning_content += std::string(node.text);
322    }
323
324    if (is_content) {
325        // Concatenate content from multiple content nodes (e.g., when reasoning markers
326        // are preserved before content markers in reasoning_format=NONE mode)
327        result.content += std::string(node.text);
328    }
329
330    // Handle tool-related tags (supporting both JSON and tagged formats)
331    bool is_tool_open  = node.tag == common_chat_peg_builder::TOOL_OPEN;
332    bool is_tool_close = node.tag == common_chat_peg_builder::TOOL_CLOSE;
333    bool is_tool_name  = node.tag == common_chat_peg_builder::TOOL_NAME;
334    bool is_tool_id    = node.tag == common_chat_peg_builder::TOOL_ID;
335    bool is_tool_args  = node.tag == common_chat_peg_builder::TOOL_ARGS;
336    bool is_arg_open   = node.tag == common_chat_peg_builder::TOOL_ARG_OPEN;
337    bool is_arg_close  = node.tag == common_chat_peg_builder::TOOL_ARG_CLOSE;
338    bool is_arg_name         = node.tag == common_chat_peg_builder::TOOL_ARG_NAME;
339    bool is_arg_value        = node.tag == common_chat_peg_builder::TOOL_ARG_VALUE;
340    bool is_arg_string_value = node.tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE;
341
342    if (is_tool_open) {
343        pending_tool_call     = common_chat_tool_call();
344        current_tool          = &pending_tool_call.value();
345        arg_count             = 0;
346        args_buffer.clear();
347        closing_quote_pending = false;
348    }
349
350    if (is_tool_id && current_tool) {
351        auto text = trim_trailing_space(node.text);
352        if (text.size() >= 2 && text.front() == '"' && text.back() == '"') {
353            text = text.substr(1, text.size() - 2);
354        }
355        current_tool->id = std::string(text);
356    }
357
358    if (is_tool_name && current_tool) {
359        current_tool->name = std::string(trim_trailing_space(node.text));
360        // Now that we have the name, populate the arguments from the buffer
361        if (!args_buffer.empty()) {
362            current_tool->arguments = args_buffer;
363            args_buffer.clear();
364        } else if (current_tool->arguments.empty()) {
365            current_tool->arguments = "{";
366        }
367        // Add the tool call to results so streaming can see it
368        if (pending_tool_call.has_value()) {
369            result.tool_calls.push_back(pending_tool_call.value());
370            pending_tool_call.reset();
371            current_tool = &result.tool_calls.back();
372        }
373    }
374
375    if (is_tool_args && current_tool) {
376        // For JSON format: arguments come as a complete JSON object
377        // For tagged format: built up from individual arg_name/arg_value nodes
378        auto text = trim_trailing_space(node.text);
379        if (!text.empty() && text.front() == '{') {
380            args_target() = std::string(text);
381        }
382    }
383
384    if (is_arg_open) {
385        closing_quote_pending = false;
386    }
387
388    if (is_arg_name && current_tool) {
389        std::string arg_entry;
390        if (arg_count > 0) {
391            arg_entry = ",";
392        }
393        arg_entry += ordered_json(trim(node.text)).dump() + ":";
394        ++arg_count;
395
396        auto & target = args_target();
397        if (target.empty()) {
398            target = "{";
399        }
400        target += arg_entry;
401    }
402
403    if ((is_arg_value || is_arg_string_value) && current_tool) {
404        std::string value_content = std::string(node.text);
405
406        std::string value_to_add;
407        if (value_content.empty() && is_arg_string_value) {
408            // Empty string value - arg_close will add the closing quote
409            value_to_add          = "\"";
410            closing_quote_pending = true;
411        } else if (!value_content.empty() && is_arg_string_value) {
412            // Schema declares this as string type - always treat as literal string value
413            if (!closing_quote_pending) {
414                value_to_add          = "\"";
415                closing_quote_pending = true;
416            }
417            value_to_add += escape_json_string_inner(value_content);
418        } else if (!value_content.empty()) {
419            // Pythonic scalars/containers -> JSON.
420            value_to_add += normalize_container_value(value_content);
421        }
422
423        args_target() += value_to_add;
424    }
425
426    if (is_arg_close && current_tool) {
427        if (closing_quote_pending) {
428            args_target() += "\"";
429            closing_quote_pending = false;
430        }
431    }
432
433    if (is_tool_close && current_tool) {
434        // Flush buffer to arguments if tool name was never seen
435        if (current_tool->name.empty() && !args_buffer.empty()) {
436            current_tool->arguments = args_buffer;
437            args_buffer.clear();
438        }
439        // Close any pending string quote
440        if (closing_quote_pending) {
441            current_tool->arguments += "\"";
442            closing_quote_pending = false;
443        }
444        // Close any unclosed braces (accounts for nested objects)
445        for (int d = json_brace_depth(current_tool->arguments); d > 0; d--) {
446            current_tool->arguments += "}";
447        }
448        // Add tool call to results if named; otherwise discard
449        if (pending_tool_call.has_value()) {
450            if (!current_tool->name.empty()) {
451                result.tool_calls.push_back(pending_tool_call.value());
452            }
453            pending_tool_call.reset();
454        }
455    }
456}
457
458common_peg_parser common_chat_peg_builder::standard_constructed_tools(
459    const std::map<std::string, std::string> & markers,
460    const ordered_json &                       tools,
461    bool                                       parallel_tool_calls,
462    bool                                       force_tool_calls) {
463    if (!tools.is_array() || tools.empty()) {
464        return eps();
465    }
466
467    // Extract markers with defaults
468    auto get_marker = [&markers](const std::string & key, const std::string & default_val = "") -> std::string {
469        auto it = markers.find(key);
470        return it != markers.end() ? it->second : default_val;
471    };
472
473    std::string section_start    = get_marker("tool_call_start_marker", "<tool_call>");
474    std::string section_end      = get_marker("tool_call_end_marker", "</tool_call>");
475    std::string func_opener      = get_marker("function_opener", "<function=");
476    std::string func_name_suffix = get_marker("function_name_suffix", ">");
477    std::string func_closer      = get_marker("function_closer", "</function>");
478    std::string param_key_prefix = get_marker("parameter_key_prefix", "<param=");
479    std::string param_key_suffix = get_marker("parameter_key_suffix", ">");
480    std::string param_closer     = get_marker("parameter_closer", "</param>");
481
482    // Build tool choices for tagged format
483    auto tool_choices = choice();
484
485    for (const auto & tool_def : tools) {
486        if (!tool_def.contains("function")) {
487            continue;
488        }
489        const auto &   function = tool_def.at("function");
490        std::string    name     = function.at("name");
491        ordered_json   params   = common_chat_tool_parameters(function);
492
493        // Build argument parsers
494        auto args = eps();
495        if (params.contains("properties") && !params["properties"].empty()) {
496            auto arg_choice = choice();
497            for (const auto & el : params["properties"].items()) {
498                const std::string & prop_name = el.key();
499
500                auto arg_name_parser =
501                    choice({ literal(prop_name), literal("\"" + prop_name + "\""), literal("'" + prop_name + "'") });
502
503                auto arg_rule = tool_arg(tool_arg_open(literal(param_key_prefix)) + tool_arg_name(arg_name_parser) +
504                                         literal(param_key_suffix) + tool_arg_value(until(param_closer)) +
505                                         tool_arg_close(literal(param_closer)));
506                arg_choice |= arg_rule;
507            }
508            args = zero_or_more(arg_choice + space());
509        }
510
511        // Build function parser: <function=name>args</function>
512        auto tool_parser = tool(tool_open(literal(func_opener) + tool_name(literal(name)) + literal(func_name_suffix)) +
513                                space() + tool_args(args) + space() + tool_close(literal(func_closer)));
514
515        tool_choices |= rule("tool-" + name, tool_parser);
516    }
517
518    // Build the section with markers
519    auto section =
520        parallel_tool_calls ?
521            trigger_rule("tool-call", literal(section_start) + space() + one_or_more(tool_choices + space()) +
522                                          literal(section_end)) :
523            trigger_rule("tool-call", literal(section_start) + space() + tool_choices + space() + literal(section_end));
524
525    return force_tool_calls ? section : optional(section);
526}
527
528// Like python_value(), but the leaf also accepts JSON-cased true/false/null, used by LFM2/LFM2.5
529common_peg_parser common_chat_peg_builder::python_or_json_value() {
530    return rule("python-or-json-value", [this]() {
531        auto ws    = space();
532        auto value = python_or_json_value();
533
534        auto member  = sequence({ python_string(), ws, literal(":"), ws, value });
535        auto members = sequence({ member, zero_or_more(sequence({ ws, literal(","), ws, member })) });
536        auto dict    = rule("python-or-json-dict", [&]() {
537            return sequence({ literal("{"), ws, choice({ literal("}"), sequence({ members, ws, literal("}") }) }), ws });
538        });
539
540        auto elements = sequence({ value, zero_or_more(sequence({ literal(","), ws, value })) });
541        auto array    = rule("python-or-json-array", [&]() {
542            return sequence({ literal("["), ws, choice({ literal("]"), sequence({ elements, ws, literal("]") }) }), ws });
543        });
544
545        return choice({ dict, array, python_string(), python_number(),
546                        python_bool(), python_null(), json_bool(), json_null() });
547    });
548}
549
550// Python-style tool calls: name(arg1="value1", arg2=123)
551// Used only by LFM2 for now, so we don't merge it into autoparser
552common_peg_parser common_chat_peg_builder::python_style_tool_calls(
553    const ordered_json & tools,
554    bool                 parallel_tool_calls,
555    bool                 allow_json_literals) {
556    if (!tools.is_array() || tools.empty()) {
557        return eps();
558    }
559
560    auto tool_choices = choice();
561
562    for (const auto & tool_def : tools) {
563        if (!tool_def.contains("function")) {
564            continue;
565        }
566        const auto &   function = tool_def.at("function");
567        std::string    name     = function.at("name");
568        ordered_json   params   = common_chat_tool_parameters(function);
569
570        auto args = eps();
571        if (params.contains("properties") && !params["properties"].empty()) {
572            auto arg_choice = choice();
573            for (const auto & el : params["properties"].items()) {
574                const std::string & prop_name = el.key();
575                const auto & prop_def = el.value();
576                bool is_string_type = (prop_def.contains("type") && prop_def["type"] == "string");
577
578                auto arg_name_parser = literal(prop_name);
579
580                common_peg_parser arg_value_parser = eps();
581                // Quoted literal as a value: normalize_quotes_to_json preserves escapes.
582                auto string_value_parser = tool_arg_value(choice({
583                    literal("\"") + string_content('"') + literal("\""),
584                    literal("'") + string_content('\'') + literal("'")
585                }));
586
587                if (is_string_type) {
588                    arg_value_parser = string_value_parser;
589                } else {
590                    arg_value_parser = tool_arg_value(allow_json_literals ? python_or_json_value() : python_value());
591                }
592
593                // Full argument: name="value" or name=value
594                auto arg_rule = tool_arg(
595                    tool_arg_open(tool_arg_name(arg_name_parser) + literal("=")) +
596                    arg_value_parser +
597                    tool_arg_close(eps())
598                );
599                arg_choice |= arg_rule;
600            }
601
602            args = arg_choice + zero_or_more("," + space() + arg_choice);
603        }
604
605        auto tool_parser = tool(tool_open(tool_name(literal(name)) + literal("(")) +
606            space() + tool_args(args) + space() + tool_close(literal(")"))
607        );
608
609        tool_choices |= rule("tool-" + name, tool_parser);
610    }
611
612    if (parallel_tool_calls) {
613        return "[" + space() + tool_choices + zero_or_more("," + space() + tool_choices) + space() + "]";
614    }
615    return "[" + space() + tool_choices + space() + "]";
616}
617
618// Helper: Parse dot notation key into prefix and field name
619static std::pair<std::string, std::string> parse_key_spec(const std::string & key) {
620    auto dot_pos = key.find('.');
621    if (dot_pos == std::string::npos) {
622        return {"", key};  // Top-level field
623    }
624    return {key.substr(0, dot_pos), key.substr(dot_pos + 1)};
625}
626
627// Mode 1: function_is_key — parse {"function_name": {...}}
628common_peg_parser common_chat_peg_builder::build_json_tools_function_is_key(
629    const ordered_json & tools,
630    const std::string &  args_key,
631    const std::string &  effective_args_key,
632    const std::string &  call_id_key,
633    const std::string &  gen_call_id_key) {
634
635    auto tool_choices = choice();
636
637    for (const auto & tool_def : tools) {
638        if (!tool_def.contains("function")) {
639            continue;
640        }
641        const auto &   function = tool_def.at("function");
642        std::string    name     = function.at("name");
643        ordered_json   params   = common_chat_tool_parameters(function);
644
645        // Build inner object fields
646        std::vector<common_peg_parser> inner_fields;
647
648        if (!call_id_key.empty()) {
649            auto id_parser = atomic(
650                literal("\"" + call_id_key + "\"") + space() + literal(":") + space() +
651                literal("\"") + tool_id(string_content('"')) + literal("\"")
652            );
653            inner_fields.push_back(optional(id_parser + space() + optional(literal(",") + space())));
654        }
655
656        if (!gen_call_id_key.empty()) {
657            auto gen_id_parser = atomic(
658                literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() +
659                choice({
660                    literal("\"") + tool_id(string_content('"')) + literal("\""),
661                    tool_id(json_number())
662                })
663            );
664            inner_fields.push_back(optional(gen_id_parser + space() + optional(literal(",") + space())));
665        }
666
667        // Arguments — either wrapped in args_key or parsed directly
668        common_peg_parser args_parser = eps();
669        if (args_key.empty()) {
670            args_parser = tool_args(schema(json(), "tool-" + name + "-schema", params));
671        } else {
672            args_parser = literal("\"" + effective_args_key + "\"") + space() + literal(":") + space() +
673                          tool_args(schema(json(), "tool-" + name + "-schema", params));
674        }
675        inner_fields.push_back(args_parser);
676
677        // Build inner object parser
678        common_peg_parser inner_object = eps();
679        if (args_key.empty() && inner_fields.size() == 1) {
680            inner_object = inner_fields[0];
681        } else {
682            inner_object = literal("{") + space();
683            for (size_t i = 0; i < inner_fields.size(); i++) {
684                inner_object = inner_object + inner_fields[i];
685                if (i < inner_fields.size() - 1) {
686                    inner_object = inner_object + space();
687                }
688            }
689            inner_object = inner_object + space() + literal("}");
690        }
691
692        auto tool_parser = tool(
693            tool_open(literal("{")) + space() +
694            literal("\"") + tool_name(literal(name)) + literal("\"") +
695            space() + literal(":") + space() +
696            inner_object +
697            space() + tool_close(literal("}"))
698        );
699
700        tool_choices |= rule("tool-" + name, tool_parser);
701    }
702
703    return tool_choices;
704}
705
706// Mode 2: Nested keys (dot notation like "function.name")
707common_peg_parser common_chat_peg_builder::build_json_tools_nested_keys(
708    const ordered_json & tools,
709    const std::string &  effective_name_key,
710    const std::string &  effective_args_key,
711    const std::string &  call_id_key,
712    const std::string &  gen_call_id_key) {
713
714    auto tool_choices = choice();
715
716    auto name_spec = parse_key_spec(effective_name_key);
717    auto args_spec = parse_key_spec(effective_args_key);
718
719    std::string nested_prefix     = !name_spec.first.empty() ? name_spec.first  : args_spec.first;
720    std::string nested_name_field = !name_spec.first.empty() ? name_spec.second  : effective_name_key;
721    std::string nested_args_field = !args_spec.first.empty() ? args_spec.second  : effective_args_key;
722
723    for (const auto & tool_def : tools) {
724        if (!tool_def.contains("function")) {
725            continue;
726        }
727        const auto &   function = tool_def.at("function");
728        std::string    name     = function.at("name");
729        ordered_json   params   = common_chat_tool_parameters(function);
730
731        auto nested_name = literal("\"" + nested_name_field + "\"") + space() + literal(":") + space() +
732                          atomic(literal("\"") + tool_name(literal(name)) + literal("\""));
733        auto nested_args = literal("\"" + nested_args_field + "\"") + space() + literal(":") + space() +
734                          tool_args(schema(json(), "tool-" + name + "-schema", params));
735
736        auto nested_object = literal("{") + space() +
737                            nested_name + space() + literal(",") + space() +
738                            nested_args +
739                            space() + literal("}");
740
741        // Format: { id?, "function": {...} }
742        auto tool_parser_body = tool_open(literal("{")) + space();
743
744        if (!call_id_key.empty()) {
745            auto id_spec = parse_key_spec(call_id_key);
746            if (id_spec.first.empty()) {
747                auto id_parser = atomic(
748                    literal("\"" + call_id_key + "\"") + space() + literal(":") + space() +
749                    literal("\"") + tool_id(string_content('"')) + literal("\"")
750                );
751                tool_parser_body = tool_parser_body + optional(id_parser + space() + literal(",") + space());
752            }
753        }
754
755        if (!gen_call_id_key.empty()) {
756            auto gen_id_spec = parse_key_spec(gen_call_id_key);
757            if (gen_id_spec.first.empty()) {
758                auto gen_id_parser = atomic(
759                    literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() +
760                    choice({
761                        literal("\"") + tool_id(string_content('"')) + literal("\""),
762                        tool_id(json_number())
763                    })
764                );
765                tool_parser_body = tool_parser_body + optional(gen_id_parser + space() + literal(",") + space());
766            }
767        }
768
769        auto nested_field = literal("\"" + nested_prefix + "\"") + space() + literal(":") + space() + nested_object;
770        tool_parser_body = tool_parser_body + nested_field + space() + tool_close(literal("}"));
771
772        tool_choices |= rule("tool-" + name, tool(tool_parser_body));
773    }
774
775    return tool_choices;
776}
777
778// Mode 3: Flat keys with optional ID fields and parameter ordering
779common_peg_parser common_chat_peg_builder::build_json_tools_flat_keys(
780    const ordered_json &             tools,
781    const std::string &              effective_name_key,
782    const std::string &              effective_args_key,
783    const std::string &              call_id_key,
784    const std::string &              gen_call_id_key,
785    const std::vector<std::string> & parameters_order,
786    bool                             accept_openai_wrapper) {
787
788    auto tool_choices    = choice();
789    auto name_key_parser = literal("\"" + effective_name_key + "\"");
790    auto args_key_parser = literal("\"" + effective_args_key + "\"");
791
792    for (const auto & tool_def : tools) {
793        if (!tool_def.contains("function")) {
794            continue;
795        }
796        const auto &   function = tool_def.at("function");
797        std::string    name     = function.at("name");
798        ordered_json   params   = common_chat_tool_parameters(function);
799
800        auto tool_name_ = name_key_parser + space() + literal(":") + space() +
801                         atomic(literal("\"") + tool_name(literal(name)) + literal("\""));
802        auto tool_args_ = args_key_parser + space() + literal(":") + space() +
803                         tool_args(schema(json(), "tool-" + name + "-schema", params));
804
805        // Build ID parsers if keys are provided
806        common_peg_parser id_parser = eps();
807        if (!call_id_key.empty()) {
808            id_parser = atomic(
809                literal("\"" + call_id_key + "\"") + space() + literal(":") + space() +
810                choice({
811                    literal("\"") + tool_id(string_content('"')) + literal("\""),
812                    tool_id(json_number())
813                })
814            );
815        }
816
817        common_peg_parser gen_id_parser = eps();
818        if (!gen_call_id_key.empty()) {
819            gen_id_parser = atomic(
820                literal("\"" + gen_call_id_key + "\"") + space() + literal(":") + space() +
821                choice({
822                    literal("\"") + tool_id(string_content('"')) + literal("\""),
823                    tool_id(json_number())
824                })
825            );
826        }
827
828        // Create (parser, key) pairs for all fields, then sort by parameters_order
829        std::vector<std::pair<common_peg_parser, std::string>> parser_pairs;
830        parser_pairs.emplace_back(tool_name_, effective_name_key);
831        parser_pairs.emplace_back(tool_args_, effective_args_key);
832        if (!call_id_key.empty()) {
833            parser_pairs.emplace_back(optional(id_parser), call_id_key);
834        }
835        if (!gen_call_id_key.empty()) {
836            parser_pairs.emplace_back(optional(gen_id_parser), gen_call_id_key);
837        }
838
839        std::sort(parser_pairs.begin(), parser_pairs.end(),
840            [&parameters_order](const auto & a, const auto & b) {
841                auto pos_a = std::find(parameters_order.begin(), parameters_order.end(), a.second);
842                auto pos_b = std::find(parameters_order.begin(), parameters_order.end(), b.second);
843                size_t idx_a = (pos_a == parameters_order.end()) ? parameters_order.size() : std::distance(parameters_order.begin(), pos_a);
844                size_t idx_b = (pos_b == parameters_order.end()) ? parameters_order.size() : std::distance(parameters_order.begin(), pos_b);
845                return idx_a < idx_b;
846            });
847
848        // accept an optional leading "type": "function" field when the model emits the OpenAI wrapper
849        common_peg_parser type_field = eps();
850        if (accept_openai_wrapper) {
851            type_field = optional(literal("\"type\"") + space() + literal(":") + space() +
852                                  literal("\"function\"") + space() + literal(",") + space());
853        }
854        auto ordered_body = tool_open(literal("{")) + space() + type_field;
855        for (size_t i = 0; i < parser_pairs.size(); i++) {
856            ordered_body = ordered_body + parser_pairs[i].first;
857            if (i < parser_pairs.size() - 1) {
858                ordered_body = ordered_body + space() + literal(",") + space();
859            }
860        }
861        ordered_body = ordered_body + space() + tool_close(literal("}"));
862
863        tool_choices |= rule("tool-" + name, tool(ordered_body));
864    }
865
866    return tool_choices;
867}
868
869common_peg_parser common_chat_peg_builder::prefix(const std::string & s, const std::string & delimiter) {
870    if (s.empty()) {
871        return eps();
872    }
873    if (delimiter.empty()) {
874        return literal(s);
875    }
876    return literal(s.substr(0, s.find(delimiter)));
877}
878
879common_peg_parser common_chat_peg_builder::optspace(const std::string & tag) {
880    auto parser = eps();
881    size_t end_of_prefix_space = tag.size();
882    size_t start_of_suffix_space = tag.size();
883    for (size_t i = 0; i < tag.size(); i++) {
884        if (!std::isspace(tag[i])) {
885            end_of_prefix_space = i;
886            break;
887        }
888    }
889    for (size_t i = tag.size(); i > 0; i--) {
890        if (!std::isspace(tag[i - 1])) {
891            start_of_suffix_space = i;
892            break;
893        }
894    }
895    for (size_t i = 0; i < end_of_prefix_space; i++) {
896        parser += optional(literal(std::string(1, tag[i])));
897    }
898    parser += literal(tag.substr(end_of_prefix_space, start_of_suffix_space - end_of_prefix_space));
899    for (size_t i = start_of_suffix_space; i < tag.size(); i++) {
900        parser += optional(literal(std::string(1, tag[i])));
901    }
902    return parser;
903}
904
905common_peg_parser common_chat_peg_builder::standard_json_tools(
906                                                       const std::string &              section_start,
907                                                       const std::string &              section_end,
908                                                       const ordered_json &             tools,
909                                                       bool                             parallel_tool_calls,
910                                                       bool                             force_tool_calls,
911                                                       const std::string &              name_key,
912                                                       const std::string &              args_key,
913                                                       bool                             array_wrapped,
914                                                       bool                             function_is_key,
915                                                       const std::string &              call_id_key,
916                                                       const std::string &              gen_call_id_key,
917                                                       const std::vector<std::string> & parameters_order,
918                                                       bool                             accept_openai_wrapper) {
919    if (!tools.is_array() || tools.empty()) {
920        return eps();
921    }
922
923    std::string effective_name_key = name_key.empty() ? "name" : name_key;
924    std::string effective_args_key = args_key.empty() ? "arguments" : args_key;
925
926    // Dispatch to the appropriate builder based on the JSON layout mode
927    common_peg_parser tool_choices = eps();
928    if (function_is_key) {
929        tool_choices = build_json_tools_function_is_key(tools, args_key, effective_args_key, call_id_key, gen_call_id_key);
930    } else {
931        auto name_spec = parse_key_spec(effective_name_key);
932        auto args_spec = parse_key_spec(effective_args_key);
933        if (!name_spec.first.empty() || !args_spec.first.empty()) {
934            tool_choices = build_json_tools_nested_keys(tools, effective_name_key, effective_args_key, call_id_key, gen_call_id_key);
935        } else {
936            tool_choices = build_json_tools_flat_keys(tools, effective_name_key, effective_args_key, call_id_key, gen_call_id_key, parameters_order, accept_openai_wrapper);
937        }
938    }
939
940    // Build the section with markers
941    auto tool_calls = tool_choices;
942    if (parallel_tool_calls) {
943        tool_calls = tool_calls + zero_or_more(space() + literal(",") + space() + tool_choices);
944    }
945
946    if (array_wrapped) {
947        tool_calls = literal("[") + space() + tool_calls + space() + literal("]");
948    }
949
950    auto section =
951        trigger_rule("tool-call", literal(section_start) + space() + tool_calls + space() + literal(section_end));
952
953    return force_tool_calls ? section : optional(section);
954}
955
956void common_chat_peg_gemma4_mapper::from_ast(const common_peg_ast_arena & arena, const common_peg_parse_result & result) {
957    for (const auto & node : result.nodes) {
958        visit(arena, node);
959    }
960}
961
962static std::string gemma4_to_json(const common_peg_ast_arena & arena, common_peg_ast_id id) {
963    const auto & node = arena.get(id);
964
965    if (node.text.empty()) {
966        return "";
967    }
968
969    if (node.rule == "gemma4-number" || node.rule == "gemma4-bool" || node.rule == "gemma4-null") {
970        return std::string(node.text);
971    }
972
973    if (node.rule == "gemma4-string-content") {
974        return escape_json_string_inner(std::string(node.text));
975    }
976
977    if (node.rule == "gemma4-string") {
978        std::string result = "\"";
979        if (!node.children.empty()) {
980            result += gemma4_to_json(arena, node.children[0]);
981            if (!node.is_partial) {
982                result += "\"";
983            }
984        }
985        return result;
986    }
987
988    if (node.rule == "gemma4-array") {
989        std::string result = "[";
990
991        bool add_comma = false;
992        for (auto child_id : node.children) {
993            if (add_comma) {
994                result += ',';
995            }
996            add_comma = true;
997            result += gemma4_to_json(arena, child_id);
998        }
999
1000        if (!node.is_partial) {
1001            result += ']';
1002        }
1003        return result;
1004    }
1005
1006    if (node.rule == "gemma4-dict-key-name") {
1007        return std::string(node.text);
1008    }
1009
1010    if (node.rule == "gemma4-dict-key") {
1011        std::string result = "\"";
1012        if (!node.children.empty()) {
1013            result += escape_json_string_inner(gemma4_to_json(arena, node.children[0]));
1014        }
1015        if (!node.is_partial) {
1016            result += "\":";
1017        }
1018        return result;
1019    }
1020
1021    if (node.rule == "gemma4-dict-kv") {
1022        std::string result;
1023        for (auto child_id : node.children) {
1024            result += gemma4_to_json(arena, child_id);
1025        }
1026        return result;
1027    }
1028
1029    if (node.rule == "gemma4-dict") {
1030        std::string result = "{";
1031
1032        bool add_comma = false;
1033        for (auto child_id : node.children) {
1034            if (add_comma) {
1035                result += ',';
1036            }
1037            add_comma = true;
1038            result += gemma4_to_json(arena, child_id);
1039        }
1040
1041        if (!node.is_partial) {
1042            result += '}';
1043        }
1044        return result;
1045    }
1046
1047    if (node.rule == "gemma4-value") {
1048        if (!node.children.empty()) {
1049            return gemma4_to_json(arena, node.children[0]);
1050        }
1051        return "";
1052    }
1053
1054    return "";
1055}
1056
1057void common_chat_peg_gemma4_mapper::visit(const common_peg_ast_arena & arena, common_peg_ast_id id) {
1058    const auto & node = arena.get(id);
1059
1060    if (node.tag == "reasoning") {
1061        result.reasoning_content += std::string(node.text);
1062        return;
1063    }
1064
1065    if (node.tag == "content") {
1066        result.content += std::string(node.text);
1067        return;
1068    }
1069
1070    if (node.tag == "tool") {
1071        auto name_id = arena.find_by_tag(node, "tool-name");
1072        auto args_id = arena.find_by_tag(node, "tool-args");
1073
1074        if (name_id != COMMON_PEG_INVALID_AST_ID && args_id != COMMON_PEG_INVALID_AST_ID) {
1075            const auto & name_node = arena.get(name_id);
1076            const auto & args_node = arena.get(args_id);
1077
1078            if (!name_node.is_partial) {
1079                common_chat_tool_call call;
1080                call.name = std::string(name_node.text);
1081                if (!args_node.children.empty()) {
1082                    call.arguments = gemma4_to_json(arena, args_node.children[0]);
1083                }
1084                result.tool_calls.push_back(call);
1085            }
1086        }
1087
1088        return;
1089    }
1090
1091    for (auto child_id : node.children) {
1092        visit(arena, child_id);
1093    }
1094}
1095
1096static void minimax_m3_collect(const common_peg_ast_arena &     arena,
1097                               const common_peg_ast_node &      node,
1098                               const std::string &              tag,
1099                               std::vector<common_peg_ast_id> & out) {
1100    for (auto child_id : node.children) {
1101        const auto & child = arena.get(child_id);
1102        if (child.tag == tag) {
1103            out.push_back(child_id);
1104        } else {
1105            minimax_m3_collect(arena, child, tag, out);
1106        }
1107    }
1108}
1109
1110static common_peg_ast_id minimax_m3_value_of(const common_peg_ast_arena & arena, const common_peg_ast_node & node) {
1111    for (auto child_id : node.children) {
1112        const auto & tag = arena.get(child_id).tag;
1113        if (tag == common_chat_peg_builder::TOOL_ARG_VALUE ||
1114            tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE ||
1115            tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT ||
1116            tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY) {
1117            return child_id;
1118        }
1119    }
1120    return COMMON_PEG_INVALID_AST_ID;
1121}
1122
1123static std::string minimax_m3_value_to_json(const common_peg_ast_arena & arena, common_peg_ast_id id, bool closed);
1124
1125static std::string minimax_m3_member_to_json(const common_peg_ast_arena & arena, const common_peg_ast_node & node) {
1126    auto name_id = arena.find_by_tag(node, common_chat_peg_builder::TOOL_ARG_NAME);
1127    if (name_id == COMMON_PEG_INVALID_AST_ID) {
1128        return "";
1129    }
1130
1131    return ordered_json(arena.get(name_id).text).dump() + ":" +
1132           minimax_m3_value_to_json(arena, minimax_m3_value_of(arena, node), !node.is_partial);
1133}
1134
1135static std::string minimax_m3_container_to_json(const common_peg_ast_arena & arena,
1136                                                const common_peg_ast_node & node,
1137                                                bool                        is_object,
1138                                                bool                        closed) {
1139    const std::string tag = is_object ? common_chat_peg_builder::TOOL_ARG
1140                                      : common_chat_peg_minimax_m3_mapper::TOOL_ARG_ITEM;
1141
1142    std::vector<common_peg_ast_id> entries;
1143    minimax_m3_collect(arena, node, tag, entries);
1144
1145    std::string result = is_object ? "{" : "[";
1146
1147    bool add_comma = false;
1148    for (auto entry_id : entries) {
1149        const auto & entry = arena.get(entry_id);
1150
1151        std::string text;
1152        if (is_object) {
1153            text = minimax_m3_member_to_json(arena, entry);
1154        } else {
1155            text = minimax_m3_value_to_json(arena, minimax_m3_value_of(arena, entry), !entry.is_partial);
1156        }
1157
1158        if (text.empty()) {
1159            continue;
1160        }
1161
1162        if (add_comma) {
1163            result += ",";
1164        }
1165        add_comma = true;
1166        result += text;
1167    }
1168
1169    if (closed) {
1170        result += is_object ? "}" : "]";
1171    }
1172    return result;
1173}
1174
1175static std::string minimax_m3_value_to_json(const common_peg_ast_arena & arena, common_peg_ast_id id, bool closed) {
1176    if (id == COMMON_PEG_INVALID_AST_ID) {
1177        return "";
1178    }
1179
1180    const auto & node = arena.get(id);
1181
1182    if (node.tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_OBJECT) {
1183        return minimax_m3_container_to_json(arena, node, /* is_object = */ true, closed);
1184    }
1185
1186    if (node.tag == common_chat_peg_minimax_m3_mapper::TOOL_ARG_ARRAY) {
1187        return minimax_m3_container_to_json(arena, node, /* is_object = */ false, closed);
1188    }
1189
1190    if (node.tag == common_chat_peg_builder::TOOL_ARG_STRING_VALUE) {
1191        return "\"" + escape_json_string_inner(std::string(node.text)) + (closed ? "\"" : "");
1192    }
1193
1194    // Numbers and booleans are written verbatim by the template
1195    return std::string(node.text);
1196}
1197
1198void common_chat_peg_minimax_m3_mapper::from_ast(const common_peg_ast_arena &    arena,
1199                                                 const common_peg_parse_result & result) {
1200    for (const auto & node : result.nodes) {
1201        visit(arena, node);
1202    }
1203}
1204
1205void common_chat_peg_minimax_m3_mapper::visit(const common_peg_ast_arena & arena, common_peg_ast_id id) {
1206    const auto & node = arena.get(id);
1207
1208    if (node.tag == common_chat_peg_builder::REASONING) {
1209        result.reasoning_content += std::string(node.text);
1210        return;
1211    }
1212
1213    if (node.tag == common_chat_peg_builder::CONTENT) {
1214        result.content += std::string(node.text);
1215        return;
1216    }
1217
1218    if (node.tag == common_chat_peg_builder::TOOL) {
1219        auto name_id = arena.find_by_tag(node, common_chat_peg_builder::TOOL_NAME);
1220        if (name_id != COMMON_PEG_INVALID_AST_ID) {
1221            common_chat_tool_call call;
1222            call.name      = std::string(arena.get(name_id).text);
1223            call.arguments = minimax_m3_container_to_json(arena, node, /* is_object = */ true, !node.is_partial);
1224            result.tool_calls.push_back(call);
1225        }
1226        return;
1227    }
1228
1229    for (auto child_id : node.children) {
1230        visit(arena, child_id);
1231    }
1232}