|
7 | 7 | #include "log.h" |
8 | 8 | #include "nlohmann/json.hpp" |
9 | 9 |
|
10 | | -#include <algorithm> |
11 | 10 | #include <stdexcept> |
12 | 11 | #include <string> |
13 | 12 |
|
14 | 13 | using json = nlohmann::ordered_json; |
15 | 14 |
|
16 | | -namespace { |
17 | | - |
18 | | -// Gemma4-specific PEG builder extending the standard chat builder. |
19 | | -// Adds value type parsers that use <|\"|> as string delimiters |
20 | | -// instead of JSON's double quotes, and disables json-to-schema |
21 | | -// conversion for these types. |
22 | | -class common_peg_gemma4_builder { |
23 | | - common_chat_peg_builder & p_; |
24 | | - static constexpr const char * QUOTE = "<|\"|>"; |
25 | | - |
26 | | -public: |
27 | | - explicit common_peg_gemma4_builder(common_chat_peg_builder & p) : p_(p) {} |
28 | | - |
29 | | - common_peg_parser gemma4_string() { |
30 | | - return p_.rule("gemma4-string", [&]() { |
31 | | - return p_.literal(QUOTE) + p_.until(QUOTE) + p_.literal(QUOTE); |
32 | | - }); |
33 | | - } |
34 | | - |
35 | | - common_peg_parser gemma4_number() { |
36 | | - return p_.rule("gemma4-number", [&]() { |
37 | | - auto digit1_9 = p_.chars("[1-9]", 1, 1); |
38 | | - auto digits = p_.chars("[0-9]"); |
39 | | - auto int_part = p_.choice({p_.literal("0"), p_.sequence({digit1_9, p_.chars("[0-9]", 0, -1)})}); |
40 | | - auto frac = p_.sequence({p_.literal("."), digits}); |
41 | | - auto exp = p_.sequence({p_.choice({p_.literal("e"), p_.literal("E")}), |
42 | | - p_.optional(p_.chars("[+-]", 1, 1)), digits}); |
43 | | - auto not_number_continuation = p_.negate(p_.chars("[0-9.eE+-]", 1, 1)); |
44 | | - return p_.sequence({p_.optional(p_.literal("-")), int_part, p_.optional(frac), |
45 | | - p_.optional(exp), not_number_continuation}); |
46 | | - }); |
47 | | - } |
48 | | - |
49 | | - common_peg_parser gemma4_bool() { |
50 | | - return p_.rule("gemma4-bool", [&]() { |
51 | | - return p_.choice({p_.literal("true"), p_.literal("false")}); |
52 | | - }); |
53 | | - } |
54 | | - |
55 | | - common_peg_parser gemma4_null() { |
56 | | - return p_.rule("gemma4-null", [&]() { |
57 | | - return p_.literal("null"); |
58 | | - }); |
59 | | - } |
60 | | - |
61 | | - common_peg_parser gemma4_dict() { |
62 | | - return p_.rule("gemma4-dict", [&]() { |
63 | | - auto ws = p_.space(); |
64 | | - auto key = p_.until(":"); |
65 | | - auto member = p_.sequence({key, p_.literal(":"), ws, gemma4_value()}); |
66 | | - auto members = p_.sequence({member, p_.zero_or_more(p_.sequence({p_.literal(","), ws, member}))}); |
67 | | - return p_.sequence({ |
68 | | - p_.literal("{"), ws, |
69 | | - p_.choice({p_.literal("}"), p_.sequence({members, ws, p_.literal("}")})}) |
70 | | - }); |
71 | | - }); |
72 | | - } |
73 | | - |
74 | | - common_peg_parser gemma4_array() { |
75 | | - return p_.rule("gemma4-array", [&]() { |
76 | | - auto ws = p_.space(); |
77 | | - auto elements = p_.sequence({gemma4_value(), p_.zero_or_more(p_.sequence({p_.literal(","), ws, gemma4_value()}))}); |
78 | | - return p_.sequence({ |
79 | | - p_.literal("["), ws, |
80 | | - p_.choice({p_.literal("]"), p_.sequence({elements, ws, p_.literal("]")})}) |
81 | | - }); |
82 | | - }); |
83 | | - } |
84 | | - |
85 | | - common_peg_parser gemma4_value() { |
86 | | - return p_.rule("gemma4-value", [&]() { |
87 | | - return p_.choice({gemma4_string(), gemma4_dict(), gemma4_array(), |
88 | | - gemma4_number(), gemma4_bool(), gemma4_null()}); |
89 | | - }); |
90 | | - } |
91 | | - |
92 | | - // Select the appropriate value parser based on JSON schema type. |
93 | | - // Does NOT use schema() - the gemma4 types are pure PEG without |
94 | | - // JSON schema metadata, so GBNF is generated directly from the |
95 | | - // PEG structure. |
96 | | - common_peg_parser gemma4_value_for_type(const json & schema) { |
97 | | - if (!schema.contains("type") || !schema.at("type").is_string()) { |
98 | | - return gemma4_value(); |
99 | | - } |
100 | | - std::string type = schema.at("type").get<std::string>(); |
101 | | - if (type == "string") { return gemma4_string(); } |
102 | | - if (type == "number") { return gemma4_number(); } |
103 | | - if (type == "integer") { return gemma4_number(); } |
104 | | - if (type == "boolean") { return gemma4_bool(); } |
105 | | - if (type == "object") { return gemma4_dict(); } |
106 | | - if (type == "array") { return gemma4_array(); } |
107 | | - return gemma4_value(); |
108 | | - } |
109 | | -}; |
110 | | - |
111 | | -} // anonymous namespace |
112 | | - |
113 | 15 | // Helper to iterate over tools/functions |
114 | 16 | static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) { |
115 | 17 | for (const auto & tool : tools) { |
@@ -141,9 +43,7 @@ common_chat_params peg_generator::generate_parser(const common_chat_template & |
141 | 43 | // Create the result structure |
142 | 44 | common_chat_params data; |
143 | 45 | data.prompt = common_chat_template_direct_apply(tmpl, inputs); |
144 | | - data.format = (autoparser.tools.format.mode == tool_format::TAG_WITH_GEMMA4_DICT) |
145 | | - ? COMMON_CHAT_FORMAT_PEG_GEMMA4 |
146 | | - : COMMON_CHAT_FORMAT_PEG_NATIVE; |
| 46 | + data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; |
147 | 47 | data.preserved_tokens = autoparser.preserved_tokens; |
148 | 48 |
|
149 | 49 | auto parser = autoparser.build_parser(inputs); |
@@ -539,121 +439,4 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte |
539 | 439 | p.end(); |
540 | 440 | } |
541 | 441 |
|
542 | | -common_peg_parser analyze_tools::build_tool_parser_tag_gemma4_dict(parser_build_context & ctx) const { |
543 | | - auto & p = ctx.p; |
544 | | - const auto & inputs = ctx.inputs; |
545 | | - bool force_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED; |
546 | | - |
547 | | - common_peg_gemma4_builder g4(p); |
548 | | - static const std::string QUOTE = "<|\"|>"; |
549 | | - |
550 | | - common_peg_parser tool_choice = p.choice(); |
551 | | - |
552 | | - foreach_function(inputs.tools, [&](const json & tool) { |
553 | | - const auto & func = tool.at("function"); |
554 | | - std::string name = func.at("name"); |
555 | | - const auto & params = func.at("parameters"); |
556 | | - |
557 | | - if (!params.contains("properties") || !params.at("properties").is_object()) { |
558 | | - auto func_parser = p.atomic( |
559 | | - p.tool_open(p.literal(function.name_prefix) + p.tool_name(p.literal(name)) + p.literal("{")) + |
560 | | - p.tool_args(p.eps()) + |
561 | | - p.tool_close(p.literal("}"))); |
562 | | - tool_choice |= p.rule("tool-" + name, func_parser); |
563 | | - return; |
564 | | - } |
565 | | - |
566 | | - const auto & properties = params.at("properties"); |
567 | | - std::set<std::string> required; |
568 | | - if (params.contains("required") && params.at("required").is_array()) { |
569 | | - params.at("required").get_to(required); |
570 | | - } |
571 | | - |
572 | | - // Build per-argument parsers, sorted alphabetically (matching template's dictsort) |
573 | | - struct arg_entry { |
574 | | - std::string param_name; |
575 | | - common_peg_parser parser; |
576 | | - }; |
577 | | - std::vector<arg_entry> arg_entries; |
578 | | - |
579 | | - for (const auto & [param_name, param_schema] : properties.items()) { |
580 | | - std::string type = "object"; |
581 | | - auto type_v = param_schema.contains("type") ? param_schema.at("type") : json::object(); |
582 | | - if (type_v.is_string()) type_v.get_to(type); |
583 | | - |
584 | | - common_peg_parser value_parser = p.eps(); |
585 | | - if (type == "string") { |
586 | | - // String values are delimited by <|"|>...<|"|> |
587 | | - value_parser = |
588 | | - p.literal(QUOTE) + |
589 | | - p.tool_arg_string_value(p.schema(p.until(QUOTE), |
590 | | - "tool-" + name + "-arg-" + param_name + "-schema", param_schema, true)) + |
591 | | - p.literal(QUOTE); |
592 | | - } else if (type == "number" || type == "integer") { |
593 | | - value_parser = p.tool_arg_value(g4.gemma4_number()); |
594 | | - } else if (type == "boolean") { |
595 | | - value_parser = p.tool_arg_value(g4.gemma4_bool()); |
596 | | - } else if (type == "null") { |
597 | | - value_parser = p.tool_arg_value(g4.gemma4_null()); |
598 | | - } else if (type == "object") { |
599 | | - value_parser = p.tool_arg_value(g4.gemma4_dict()); |
600 | | - } else if (type == "array") { |
601 | | - value_parser = p.tool_arg_value(g4.gemma4_array()); |
602 | | - } else { |
603 | | - value_parser = p.tool_arg_value(g4.gemma4_value()); |
604 | | - } |
605 | | - |
606 | | - auto arg = p.tool_arg( |
607 | | - p.tool_arg_open(p.tool_arg_name(p.literal(param_name)) + p.literal(":")) + |
608 | | - value_parser + |
609 | | - p.tool_arg_close(p.eps())); |
610 | | - |
611 | | - arg_entries.push_back({param_name, p.rule("tool-" + name + "-arg-" + param_name, arg)}); |
612 | | - } |
613 | | - |
614 | | - // Sort alphabetically to match Jinja's dictsort |
615 | | - std::sort(arg_entries.begin(), arg_entries.end(), [](const auto & a, const auto & b) { |
616 | | - return a.param_name < b.param_name; |
617 | | - }); |
618 | | - |
619 | | - // Build arg sequence: any arg, then zero-or-more comma-separated additional args |
620 | | - common_peg_parser args_seq = p.eps(); |
621 | | - if (!arg_entries.empty()) { |
622 | | - common_peg_parser any_arg = p.choice(); |
623 | | - for (auto & entry : arg_entries) { |
624 | | - any_arg |= entry.parser; |
625 | | - } |
626 | | - args_seq = p.optional( |
627 | | - any_arg + p.repeat(p.literal(",") + any_arg, 0, (int) arg_entries.size() - 1)); |
628 | | - } |
629 | | - |
630 | | - // Full parser: call:name{args} |
631 | | - auto func_parser = p.atomic( |
632 | | - p.tool_open(p.literal(function.name_prefix) + p.tool_name(p.literal(name)) + p.literal("{")) + |
633 | | - p.tool_args(args_seq) + |
634 | | - p.tool_close(p.literal("}"))); |
635 | | - |
636 | | - tool_choice |= p.rule("tool-" + name, func_parser); |
637 | | - }); |
638 | | - |
639 | | - // Wrap each call in <|tool_call>...</tool_call|> |
640 | | - auto wrapped_call = p.literal(format.per_call_start) + tool_choice + p.literal(format.per_call_end); |
641 | | - |
642 | | - common_peg_parser tool_calls = p.eps(); |
643 | | - if (inputs.parallel_tool_calls) { |
644 | | - tool_calls = p.trigger_rule("tool-call", wrapped_call + p.zero_or_more(p.space() + wrapped_call)); |
645 | | - } else { |
646 | | - tool_calls = p.trigger_rule("tool-call", wrapped_call); |
647 | | - } |
648 | | - |
649 | | - if (!force_tools) { |
650 | | - tool_calls = p.optional(tool_calls); |
651 | | - } |
652 | | - |
653 | | - auto content_before_tools = p.until_one_of({ format.per_call_start, ctx.reasoning->start }); |
654 | | - return ctx.reasoning_parser + |
655 | | - (force_tools ? p.eps() : p.optional(p.content(content_before_tools) + p.optional(ctx.reasoning_parser))) + |
656 | | - tool_calls + p.end(); |
657 | | -} |
658 | | - |
659 | 442 | } // namespace autoparser |
0 commit comments