Skip to content

Commit 5dd0f44

Browse files
aldehirwordingone
authored andcommitted
common : add gemma 4 specialized parser (ggml-org#21418)
* common : add gemma4 dedicated parser * cont : add '<|tool_response>' as eog * cont : emit JSON from Gemma4 tool call AST * cont : more fixes * cont : refactor convert function * cont : refine rules and mapping * cont : add more tests * cont : clean up * cont : remove autoparser gemma4 implementation * cont : more cleanup * cont : rename gemma4.jinja to match the others * cont : add custom template to support interleaved thinking * cont : preserve reasoning in model turns * cont : fix initializer error * cont : fix unused vars * cont : fix accidental static * cont : fix specialized_template signature * fix extra semicolon * remove debug line and extra space [no ci]
1 parent 2c01ade commit 5dd0f44

13 files changed

Lines changed: 767 additions & 332 deletions

common/chat-auto-parser-generator.cpp

Lines changed: 1 addition & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -7,109 +7,11 @@
77
#include "log.h"
88
#include "nlohmann/json.hpp"
99

10-
#include <algorithm>
1110
#include <stdexcept>
1211
#include <string>
1312

1413
using json = nlohmann::ordered_json;
1514

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-
11315
// Helper to iterate over tools/functions
11416
static void foreach_function(const json & tools, const std::function<void(const json &)> & fn) {
11517
for (const auto & tool : tools) {
@@ -141,9 +43,7 @@ common_chat_params peg_generator::generate_parser(const common_chat_template &
14143
// Create the result structure
14244
common_chat_params data;
14345
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;
14747
data.preserved_tokens = autoparser.preserved_tokens;
14848

14949
auto parser = autoparser.build_parser(inputs);
@@ -539,121 +439,4 @@ common_peg_parser analyze_tools::build_tool_parser_tag_tagged(parser_build_conte
539439
p.end();
540440
}
541441

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-
659442
} // namespace autoparser

common/chat-auto-parser.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,13 @@ struct analyze_tools : analyze_base {
352352
common_peg_parser build_tool_parser_json_native(parser_build_context & ctx) const;
353353
common_peg_parser build_tool_parser_tag_json(parser_build_context & ctx) const;
354354
common_peg_parser build_tool_parser_tag_tagged(parser_build_context & ctx) const;
355+
356+
// Shared helper: builds func_parser from open+call_id+args, handling atomic wrapping and close.
357+
// atomic_peek: if present, used as the peek expression in the third atomicity branch.
358+
common_peg_parser build_func_parser(common_chat_peg_builder & p, const std::string & name,
359+
const common_peg_parser & call_id_section, bool have_call_id,
360+
const common_peg_parser & args,
361+
std::optional<common_peg_parser> atomic_peek) const;
355362
};
356363

357364
// ============================================================================

common/chat-diff-analyzer.cpp

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -92,34 +92,6 @@ static std::vector<std::function<void(const common_chat_template & tmpl, autopar
9292
LOG_DBG(ANSI_ORANGE "[Patch: Functionary 3.1]\n" ANSI_RESET);
9393
}
9494
},
95-
// Gemma4 - custom dict format: <|tool_call>call:name{key:<|"|>val<|"|>}<tool_call|>
96-
[](const common_chat_template & tmpl, autoparser & analysis) -> void {
97-
if (tmpl.src.find("'<|tool_call>call:'") != std::string::npos) {
98-
analysis.tools.format.mode = tool_format::TAG_WITH_GEMMA4_DICT;
99-
analysis.tools.format.per_call_start = "<|tool_call>";
100-
analysis.tools.format.per_call_end = "<tool_call|>";
101-
analysis.tools.format.section_start = "";
102-
analysis.tools.format.section_end = "";
103-
analysis.tools.function.name_prefix = "call:";
104-
analysis.tools.function.name_suffix = "";
105-
analysis.tools.arguments.start = "{";
106-
analysis.tools.arguments.end = "}";
107-
analysis.tools.arguments.name_prefix = "";
108-
analysis.tools.arguments.name_suffix = ":";
109-
analysis.tools.arguments.separator = ",";
110-
analysis.reasoning.mode = reasoning_mode::TAG_BASED;
111-
analysis.reasoning.start = "<|channel>thought";
112-
analysis.reasoning.end = "<channel|>";
113-
analysis.preserved_tokens.clear();
114-
analysis.preserved_tokens.push_back("<|tool_call>");
115-
analysis.preserved_tokens.push_back("<tool_call|>");
116-
analysis.preserved_tokens.push_back("<|tool_response>");
117-
analysis.preserved_tokens.push_back("<tool_response|>");
118-
analysis.preserved_tokens.push_back("<|\"|>");
119-
analysis.preserved_tokens.push_back("<|turn>");
120-
LOG_DBG(ANSI_ORANGE "[Patch: Gemma4]\n" ANSI_RESET);
121-
}
122-
},
12395
// DeepSeek-R1-Distill-Qwen
12496
[](const common_chat_template & tmpl, autoparser & analysis) -> void {
12597
if (tmpl.src.find(

0 commit comments

Comments
 (0)