add gigachat v3 parser in PEG format

This commit is contained in:
Mishusha 2025-12-27 18:09:03 +03:00
parent d4f723b56e
commit 37fa3365eb
4 changed files with 76 additions and 134 deletions

View File

@ -879,32 +879,6 @@ static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) {
}
}
static void common_chat_parse_gigachat_v3(common_chat_msg_parser & builder) {
if (!builder.syntax().parse_tool_calls) {
builder.add_content(builder.consume_rest());
return;
}
// Regex to capture function name from JSON after "function call<|role_sep|\n"
static const common_regex function_regex(
R"(<\|message_sep\|>\n\nfunction call<\|role_sep\|>\n\s*\{\s*\"name\"\s*:\s*\"([^\"]+)\"\s*,\s*\"arguments\"\s*:)"
);
// Closing token of a tool call
static const common_regex close_regex(
R"(\}\s*)"
);
parse_json_tool_calls(
builder,
/* block_open = */ std::nullopt,
/* function_regex_start_only = */ std::nullopt,
/* function_regex = */ function_regex,
close_regex,
/* block_close = */ std::nullopt
);
}
static void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) {
static const xml_tool_call_format form {
/* form.scope_start = */ "<minimax:tool_call>",
@ -1505,9 +1479,6 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
common_chat_parse_xiaomi_mimo(builder);
break;
case COMMON_CHAT_FORMAT_GIGACHAT_V3:
common_chat_parse_gigachat_v3(builder);
break;
default:
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
}

View File

@ -672,7 +672,6 @@ const char * common_chat_format_name(common_chat_format format) {
case COMMON_CHAT_FORMAT_PEG_SIMPLE: return "peg-simple";
case COMMON_CHAT_FORMAT_PEG_NATIVE: return "peg-native";
case COMMON_CHAT_FORMAT_PEG_CONSTRUCTED: return "peg-constructed";
case COMMON_CHAT_FORMAT_GIGACHAT_V3: return "GigaChat V3";
default:
throw std::runtime_error("Unknown chat format");
}
@ -1761,55 +1760,68 @@ static common_chat_params common_chat_params_init_gigachat_v3(
auto prompt = apply(tmpl, inputs);
data.prompt = prompt;
data.format = COMMON_CHAT_FORMAT_GIGACHAT_V3;
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
data.preserved_tokens = {
"<|message_sep|>\n\n",
"<|role_sep|>\n",
};
if (inputs.tools.is_array() && !inputs.tools.empty()) {
data.grammar_lazy = inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED && inputs.json_schema.is_null();
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
auto include_grammar = true;
auto tool_call_start_prefix = "<|message_sep|>\n\nfunction call<|role_sep|>\n";
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
std::vector<std::string> rules;
foreach_function(inputs.tools, [&](const json & tool) {
auto parser = build_chat_peg_native_parser([&](common_chat_peg_native_builder & p) {
if (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
// Build a choice of all available tools
auto tool_choice = p.choice();
for (const auto & tool : inputs.tools) {
const auto & function = tool.at("function");
std::string name = function.at("name");
auto parameters = function.at("parameters");
builder.resolve_refs(parameters);
const auto & schema = function.at("parameters");
// JSON schema for this tool
json schema = {
{"type", "object"},
{"properties", {
{"name", {{"type", "string"}, {"const", name}}},
{"arguments", parameters}
}},
{"required", json::array({"name", "arguments"})}
};
auto tool_name = p.json_member("name", "\"" + p.tool_name(p.literal(name)) + "\"");
auto tool_args = p.json_member("arguments", p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)));
// Add a rule for this tool
rules.push_back(
R"("<|message_sep|>\n\nfunction call<|role_sep|>\n")" +
builder.add_schema(name + "-call", schema)
);
});
builder.add_rule("root",
"(" + string_join(rules, " | ") + ")" +
(inputs.parallel_tool_calls ? "*" : "")
);
data.grammar_triggers.push_back({
COMMON_GRAMMAR_TRIGGER_TYPE_WORD,
"<|message_sep|>\n\nfunction call<|role_sep|>\n"
auto tool_open = p.tool_open(p.literal("{") << tool_name);
tool_choice |= p.rule("tool-" + name, tool_open << "," << tool_args << "}");
}
// Define the tool call structure
auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0;
auto max_calls = 1; // parallel toolcalls are not supported
auto tool_call = p.rule("tool-call", p.literal(tool_call_start_prefix) + tool_choice);
auto tool_calls = p.trigger_rule("tool-call-root", p.repeat(tool_call, /* min = */ min_calls, /* max = */ max_calls));
return p.content(p.until("<|message_sep|>\n\n")) << tool_calls;
}
// Content only parser
include_grammar = false;
return p.content(p.rest());
});
data.parser = parser.save();
if (include_grammar) {
data.grammar_lazy = has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");
auto schema = function.at("parameters");
builder.resolve_refs(schema);
});
parser.build_grammar(builder, data.grammar_lazy);
});
}
data.grammar_triggers = {
{COMMON_GRAMMAR_TRIGGER_TYPE_WORD, tool_call_start_prefix}
};
}
return data;
}

View File

@ -124,7 +124,6 @@ enum common_chat_format {
COMMON_CHAT_FORMAT_QWEN3_CODER_XML,
COMMON_CHAT_FORMAT_APRIEL_1_5,
COMMON_CHAT_FORMAT_XIAOMI_MIMO,
COMMON_CHAT_FORMAT_GIGACHAT_V3,
// These are intended to be parsed by the PEG parser
COMMON_CHAT_FORMAT_PEG_SIMPLE,

View File

@ -3499,74 +3499,6 @@ Hey there!<|im_end|>
auto grammar = build_grammar(params.grammar);
GGML_ASSERT(grammar && "Failed to build Qwen3-Coder grammar with union types");
}
{
auto tmpls = read_templates("models/templates/GigaChat3-10B-A1.8B.jinja");
std::vector<std::string> end_tokens{ "</s>", "<|message_sep|>\n\n" };
assert_equals(COMMON_CHAT_FORMAT_GIGACHAT_V3, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format);
assert_equals(COMMON_CHAT_FORMAT_GIGACHAT_V3, common_chat_templates_apply(tmpls.get(), inputs_tools).format);
// Test parsing regular content
assert_msg_equals(message_assist,
common_chat_parse(
"Hello, world!\nWhat's up?",
/* is_partial= */ false,
{COMMON_CHAT_FORMAT_GIGACHAT_V3}));
// Test parsing tool calls
assert_msg_equals(message_assist_call,
common_chat_parse(
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
/* is_partial= */ false,
{COMMON_CHAT_FORMAT_GIGACHAT_V3}));
// Test tool calls with extra content
assert_msg_equals(message_assist_call_content,
common_chat_parse(
"Hello, world!\nWhat's up?<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
/* is_partial= */ false,
{COMMON_CHAT_FORMAT_GIGACHAT_V3}
));
// Test streaming
test_parser_with_streaming(message_assist_call_withopt,
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1, \"arg2\": 2}}",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_GIGACHAT_V3,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE
}); });
// Test template generation for regular content
test_templates(tmpls.get(), end_tokens, message_assist, tools,
"Hello, world!\nWhat's up?",
/* expect_grammar_triggered= */ false);
// Test template generation for tool calls
test_templates(tmpls.get(), end_tokens, message_assist_call, tools,
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_NONE,
/* ignore_whitespace_differences= */ true
);
// Test template generation for tools with optional parameters
test_templates(tmpls.get(), end_tokens, message_assist_call_noopt, tools,
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1}}",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_NONE,
/* ignore_whitespace_differences= */ true
);
test_templates(tmpls.get(), end_tokens, message_assist_call_withopt, tools,
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function_with_opt\", \"arguments\": {\"arg1\": 1, \"arg2\": 2}}",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_NONE,
/* ignore_whitespace_differences= */ true
);
}
}
static void test_template_output_peg_parsers() {
@ -3812,6 +3744,34 @@ static void test_template_output_peg_parsers() {
t.expect.content = R"({"amount": 123.45, "date": "2025-12-03"})";
});
}
{
// GigaChat V3
auto tmpls = read_templates("models/templates/GigaChat3-10B-A1.8B.jinja");
// Test basic message
test_peg_parser(tmpls.get(), [&](auto & t) {
t.input = "Hello, world!\nWhat's up?";
t.expect = message_assist;
});
// Test tool call
test_peg_parser(tmpls.get(), [&](auto & t) {
t.input = "<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}";
t.params.tools = {special_function_tool};
t.expect = message_assist_call;
});
// Test tool call with content before
test_peg_parser(tmpls.get(), [&](auto & t) {
t.input = "Hello, world!\nWhat's up?"
"<|message_sep|>\n\nfunction call<|role_sep|>\n{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}";
t.params.tools = {special_function_tool};
t.expect = message_assist_call_content;
});
}
}
static void test_msg_diffs_compute() {