common_chat_params data;
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
+
+ if (inputs.add_generation_prompt && string_ends_with(data.prompt, "<turn|>\n")) {
+ // This may happen if the model generates content + tool_call, the
+ // template does not add the model's next turn and confuses the model
+ // from emitting its proper reasoning token sequence.
+ data.prompt += "<|turn>model\n";
+ }
+
data.format = COMMON_CHAT_FORMAT_PEG_GEMMA4;
data.supports_thinking = true;
data.thinking_start_tag = "<|channel>thought";
p.rule("thought", p.content(p.literal("<|channel>thought") + p.space() + p.until("<channel|>") + p.literal("<channel|>")));
}
- auto thought = (p.peek(p.literal("<|channel>")) + p.ref("thought")) | p.negate(p.literal("<|channel>"));
+ auto consume_empty_channels = p.gbnf(p.zero_or_more(p.literal("<|channel>") + p.negate(p.literal("thought"))), "");
+ auto thought = (p.peek(p.literal("<|channel>")) + consume_empty_channels + p.ref("thought")) | p.negate(p.literal("<|channel>"));
if (has_response_format) {
auto response_format = p.literal("```json") <<
/* max = */ inputs.parallel_tool_calls ? -1 : 1
));
- auto content = p.rule("content", p.content(p.until_one_of({"<|channel>", "<|tool_call>"})));
+ auto scan_to_toolcall = p.rule("scan-to-toolcall", p.until("<|tool_call>"));
+ auto content = p.rule("content", p.content(p.until_one_of({"<|channel>", "<channel|>", "<|tool_call>"})));
auto message = p.rule("message", thought + content);
- return start + p.zero_or_more(message) + tool_call;
+ return start + p.zero_or_more(message) + scan_to_toolcall + tool_call;
}
- auto content = p.rule("content", p.content(p.until("<|channel>")));
+ // Gemma 4 may emit an extra <|channel>thought\n<channel|> at the end of the content. It may
+ // also emit a single trailing <channel|> token. Consume all complete reasoning blocks and
+ // then stop at the first unmatched <channel|> token.
+ auto content = p.rule("content", p.content(p.until_one_of({"<|channel>", "<channel|>"})));
auto message = p.rule("message", thought + content);
return start + p.one_or_more(message);
});
}
return result;
}
+
+ common_peg_parse_result operator()(const common_peg_gbnf_parser & p) {
+ return arena.parse(p.child, ctx, start_pos);
+ }
};
common_peg_parse_result common_peg_arena::parse(common_peg_parse_context & ctx, size_t start) const {
std::is_same_v<T, common_peg_and_parser> ||
std::is_same_v<T, common_peg_not_parser> ||
std::is_same_v<T, common_peg_tag_parser> ||
- std::is_same_v<T, common_peg_atomic_parser>) {
+ std::is_same_v<T, common_peg_atomic_parser> ||
+ std::is_same_v<T, common_peg_gbnf_parser>) {
p.child = resolve_ref(p.child);
} else if constexpr (std::is_same_v<T, common_peg_rule_parser>) {
p.child = resolve_ref(p.child);
return "Not(" + dump_impl(p.child, visited) + ")";
} else if constexpr (std::is_same_v<T, common_peg_atomic_parser>) {
return "Atomic(" + dump_impl(p.child, visited) + ")";
+ } else if constexpr (std::is_same_v<T, common_peg_gbnf_parser>) {
+ return "Gbnf(" + p.grammar + ", " + dump_impl(p.child, visited) + ")";
} else if constexpr (std::is_same_v<T, common_peg_any_parser>) {
return "Any";
} else if constexpr (std::is_same_v<T, common_peg_space_parser>) {
std::is_same_v<T, common_peg_not_parser> ||
std::is_same_v<T, common_peg_tag_parser> ||
std::is_same_v<T, common_peg_atomic_parser> ||
+ std::is_same_v<T, common_peg_gbnf_parser> ||
std::is_same_v<T, common_peg_schema_parser>) {
visit(p.child);
} else if constexpr (std::is_same_v<T, common_peg_rule_parser>) {
} else if constexpr (std::is_same_v<T, common_peg_sequence_parser>) {
std::string s;
for (const auto & child : p.children) {
+ auto child_gbnf = to_gbnf(child);
+ if (child_gbnf.empty()) {
+ continue;
+ }
if (!s.empty()) {
s += " ";
}
- auto child_gbnf = to_gbnf(child);
const auto & child_parser = effective_parser(child);
if (std::holds_alternative<common_peg_choice_parser>(child_parser) ||
std::holds_alternative<common_peg_sequence_parser>(child_parser)) {
return to_gbnf(p.child);
} else if constexpr (std::is_same_v<T, common_peg_atomic_parser>) {
return to_gbnf(p.child);
+ } else if constexpr (std::is_same_v<T, common_peg_gbnf_parser>) {
+ return p.grammar;
} else {
static_assert(is_always_false_v<T>);
}
{"child", p.child},
{"tag", p.tag}
};
+ } else if constexpr (std::is_same_v<T, common_peg_gbnf_parser>) {
+ return json{{"type", "gbnf"}, {"child", p.child}, {"grammar", p.grammar}};
}
}, variant);
}
};
}
+ if (type == "gbnf") {
+ if (!j.contains("child") || !j.contains("grammar")) {
+ throw std::runtime_error("gbnf parser missing required fields");
+ }
+ return common_peg_gbnf_parser{
+ j["child"].get<common_peg_parser_id>(),
+ j["grammar"].get<std::string>(),
+ };
+ }
+
throw std::runtime_error("Unknown parser type: " + type);
}
std::string tag;
};
+struct common_peg_gbnf_parser {
+ common_peg_parser_id child;
+ std::string grammar;
+};
+
// Variant holding all parser types
using common_peg_parser_variant = std::variant<
common_peg_epsilon_parser,
common_peg_rule_parser,
common_peg_ref_parser,
common_peg_atomic_parser,
- common_peg_tag_parser
+ common_peg_tag_parser,
+ common_peg_gbnf_parser
>;
class common_peg_arena {
// Unlike rules, you can tag multiple nodes with the same tag.
common_peg_parser tag(const std::string & tag, const common_peg_parser & p) { return add(common_peg_tag_parser{p.id(), tag}); }
+ // Wraps a child parser but emits a custom GBNF grammar string instead of
+ // the child's grammar. Parsing delegates entirely to the child.
+ common_peg_parser gbnf(const common_peg_parser & p, const std::string & grammar) { return add(common_peg_gbnf_parser{p, grammar}); }
+
void set_root(const common_peg_parser & p);
common_peg_arena build();
)""", gbnf);
});
+ t.test("silent parser emits nothing in gbnf", [](testing &t) {
+ auto parser = build_peg_parser([](common_peg_parser_builder & p) {
+ return p.literal("hello") + p.gbnf(p.literal("world"), "");
+ });
+
+ auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
+ parser.build_grammar(builder);
+ });
+
+ assert_gbnf_equal(t, R"""(
+ root ::= "hello"
+ space ::= | " " | "\n"{1,2} [ \t]{0,20}
+ )""", gbnf);
+ });
+
+ t.test("silent choice inside sequence emits nothing", [](testing &t) {
+ auto parser = build_peg_parser([](common_peg_parser_builder & p) {
+ return p.literal("a") + p.gbnf(p.literal("b") | p.literal("c"), "") + p.literal("d");
+ });
+
+ auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
+ parser.build_grammar(builder);
+ });
+
+ assert_gbnf_equal(t, R"""(
+ root ::= "a" "d"
+ space ::= | " " | "\n"{1,2} [ \t]{0,20}
+ )""", gbnf);
+ });
+
+ t.test("silent wrapped in tag emits nothing", [](testing &t) {
+ auto parser = build_peg_parser([](common_peg_parser_builder & p) {
+ return p.literal("a") + p.tag("t", p.gbnf(p.literal("b"), ""));
+ });
+
+ auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
+ parser.build_grammar(builder);
+ });
+
+ assert_gbnf_equal(t, R"""(
+ root ::= "a"
+ space ::= | " " | "\n"{1,2} [ \t]{0,20}
+ )""", gbnf);
+ });
+
+ t.test("gbnf parser emits custom grammar", [](testing &t) {
+ auto parser = build_peg_parser([](common_peg_parser_builder & p) {
+ return p.literal("a") + p.gbnf(p.literal("b"), "[a-z]+");
+ });
+
+ auto gbnf = build_grammar([&](const common_grammar_builder & builder) {
+ parser.build_grammar(builder);
+ });
+
+ assert_gbnf_equal(t, R"""(
+ root ::= "a" [a-z]+
+ space ::= | " " | "\n"{1,2} [ \t]{0,20}
+ )""", gbnf);
+ });
+
t.test("nested transparent wrappers get parenthesized", [](testing &t) {
auto parser = build_peg_parser([](common_peg_parser_builder & p) {
return p.literal("x") + p.tag("outer", p.atomic(p.literal("a") | p.literal("b")));
.tools({ amount_tool })
.expect(message_with_tool_calls("amount", R"({"orig": 1.5e10})"))
.run();
+
+ // Edge cases
+ tst.test(
+ "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<channel|>")
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
+ .expect(message_assist)
+ .run();
+
+ tst.test(
+ "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|>")
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
+ .expect(message_assist)
+ .run();
+
+ tst.test(
+ "<|channel>thought\n<channel|>Hello, world!\nWhat's up?<|channel>thought\n<channel|><channel|>")
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
+ .expect(message_assist)
+ .run();
+
+ tst.test(
+ "<|channel><|channel>thought\n<channel|>Hello, world!\nWhat's up?")
+ .reasoning_format(COMMON_REASONING_FORMAT_AUTO)
+ .expect(message_assist)
+ .run();
}
{