chat : add qwen3 specialized parser (#26252)

* Add tagged thinking tool parser

* chat : refactor and add permute helper

* cont : add support for <tool_call> omission

* cont : update tool delimiters

* cont : add comment for qwen3-coder

* cont : fix trigger pattern for <function

---------

Co-authored-by: Bart de Boer <bart.deboer@gmail.com>
This commit is contained in:
Aldehir Rojas 2026-08-02 04:13:20 -05:00 committed by GitHub
parent 272700b360
commit f5919bf458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 433 additions and 86 deletions

View file

@ -8,6 +8,7 @@
#include <iostream>
#include <numeric>
#include <regex>
#include <string>
#include "nlohmann/json.hpp"
@ -21,6 +22,7 @@ static void test_example_qwen3_non_coder(testing & t);
static void test_command7_parser_compare(testing & t);
static void test_prefix_tool_names(testing & t);
static void test_tagged_peg_parser(testing & t);
static void test_permute(testing & t);
int main(int argc, char * argv[]) {
testing t(std::cout);
@ -39,6 +41,7 @@ int main(int argc, char * argv[]) {
t.test("comparison", test_command7_parser_compare);
t.test("prefix tool names", test_prefix_tool_names);
t.test("tagged peg parser", test_tagged_peg_parser);
t.test("permute", test_permute);
return t.summary();
}
@ -981,3 +984,103 @@ static void test_tagged_peg_parser(testing & t) {
t.assert_equal("fun_post should be '>'", ">", result.tags["fun_post"]);
});
}
static void test_permute(testing & t) {
auto accepts = [](const common_peg_arena & parser, const std::string & input) {
common_peg_parse_context ctx(input);
return parser.parse(ctx).success();
};
auto gbnf_of = [](const common_peg_arena & parser) {
return build_grammar([&](const common_grammar_builder & builder) { parser.build_grammar(builder); });
};
auto assert_gbnf_equal = [](testing & t, const std::string & expected, const std::string & actual) {
static const std::regex leading_ws_re = std::regex(R"((^|\n)\s+)");
t.assert_equal("gbnf are equal", std::regex_replace(expected, leading_ws_re, "$1"), actual);
};
auto count_rules = [](const std::string & gbnf, const std::string & prefix) {
size_t count = 0;
for (const auto & line : string_split<std::string>(gbnf, '\n')) {
if (line.rfind(prefix, 0) == 0) {
count++;
}
}
return count;
};
t.test("accepts every ordering", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
return p.permute("abc", { p.literal("a"), p.literal("b"), p.literal("c") }) + p.end();
});
for (const std::string input : { "abc", "acb", "bac", "bca", "cab", "cba" }) {
t.assert_true("accepts " + input, accepts(parser, input));
}
});
t.test("single element", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
return p.permute("a", { p.literal("a") }) + p.end();
});
t.assert_true("accepts a", accepts(parser, "a"));
t.assert_true("rejects aa", !accepts(parser, "aa"));
});
t.test("grammar left-factorizes shared tails", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
return p.permute("abc", { p.literal("a"), p.literal("b"), p.literal("c") }) + p.end();
});
// Every rule is one remaining subset, keyed by bitmask: abc-3 is {a,b}, abc-7 is {a,b,c}.
// Each subset is emitted once and shared by every branch that leads into it.
assert_gbnf_equal(t, R"""(
abc-1 ::= "a"
abc-2 ::= "b"
abc-3 ::= "a" abc-2 | "b" abc-1
abc-4 ::= "c"
abc-5 ::= "a" abc-4 | "c" abc-1
abc-6 ::= "b" abc-4 | "c" abc-2
abc-7 ::= "a" abc-6 | "b" abc-5 | "c" abc-3
root ::= abc-7
space ::= | " " | "\n"{1,2} [ \t]{0,20}
)""", gbnf_of(parser));
});
t.test("grammar emits one rule per remaining subset", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
return p.permute("abcd", { p.literal("a"), p.literal("b"), p.literal("c"), p.literal("d") }) + p.end();
});
// 2^4 - 1 non-empty subsets, one rule each - not the 4! = 24 orderings.
t.assert_equal("permute rule count", 15u, count_rules(gbnf_of(parser), "abcd-"));
});
t.test("grammar emits no rules for a single element", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
return p.permute("a", { p.literal("a") }) + p.end();
});
assert_gbnf_equal(t, R"""(
root ::= "a"
space ::= | " " | "\n"{1,2} [ \t]{0,20}
)""", gbnf_of(parser));
});
t.test("grammar falls back to the given order when too large", [&](testing & t) {
auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {
std::vector<common_peg_parser> parsers;
for (size_t i = 0; i <= COMMON_CHAT_MAX_PERMUTE; i++) {
parsers.push_back(p.literal(std::string(1, (char) ('a' + i))));
}
return p.permute("big", parsers) + p.end();
});
assert_gbnf_equal(t, R"""(
root ::= "a" "b" "c" "d" "e" "f" "g"
space ::= | " " | "\n"{1,2} [ \t]{0,20}
)""", gbnf_of(parser));
});
}