From 37fa3365ebeed01d52e4c33386c28edc90b4517d Mon Sep 17 00:00:00 2001 From: Mishusha Date: Sat, 27 Dec 2025 18:09:03 +0300 Subject: [PATCH] add gigachat v3 parser in PEG format --- common/chat-parser.cpp | 29 ------------- common/chat.cpp | 84 ++++++++++++++++++++---------------- common/chat.h | 1 - tests/test-chat.cpp | 96 ++++++++++++------------------------------ 4 files changed, 76 insertions(+), 134 deletions(-) diff --git a/common/chat-parser.cpp b/common/chat-parser.cpp index bdd53cb879..d740dac065 100644 --- a/common/chat-parser.cpp +++ b/common/chat-parser.cpp @@ -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 = */ "", @@ -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)); } diff --git a/common/chat.cpp b/common/chat.cpp index 04827061fb..7b7476a4e8 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -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 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; } diff --git a/common/chat.h b/common/chat.h index b89ea91b07..6085510a40 100644 --- a/common/chat.h +++ b/common/chat.h @@ -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, diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 61c18583d5..ccb641de39 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -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 end_tokens{ "", "<|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() {