diff --git a/common/chat-parser.cpp b/common/chat-parser.cpp index d740dac065..bdd53cb879 100644 --- a/common/chat-parser.cpp +++ b/common/chat-parser.cpp @@ -879,6 +879,32 @@ 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 = */ "", @@ -1479,6 +1505,9 @@ 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 2ed543df43..e968db8718 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -1677,115 +1677,6 @@ static common_chat_params common_chat_params_init_gigachat_v3( return data; } -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_deepseek_r1(common_chat_msg_parser & builder) { - builder.try_parse_reasoning("", ""); - if (!builder.syntax().parse_tool_calls) { - builder.add_content(builder.consume_rest()); - return; - } - - static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)"); - static const common_regex tool_calls_end("<|tool▁calls▁end|>"); - static const common_regex function_regex("(?:<|tool▁call▁begin|>)?function<|tool▁sep|>([^\n]+)\n```json\n"); - static const common_regex close_regex("```[\\s\\r\\n]*<|tool▁call▁end|>"); - - parse_json_tool_calls( - builder, - /* block_open= */ tool_calls_begin, - /* function_regex_start_only= */ std::nullopt, - function_regex, - close_regex, - tool_calls_end); -} - -static void common_chat_parse_deepseek_v3_1_content(common_chat_msg_parser & builder) { - static const common_regex function_regex("(?:<|tool▁call▁begin|>)?([^\\n<]+)(?:<|tool▁sep|>)"); - - static const common_regex close_regex("(?:[\\s]*)?<|tool▁call▁end|>"); - static const common_regex tool_calls_begin("(?:<|tool▁calls▁begin|>|<|tool_calls_begin|>|<|tool calls begin|>|<|tool\\\\_calls\\\\_begin|>|<|tool▁calls|>)"); - static const common_regex tool_calls_end("<|tool▁calls▁end|>"); - - if (!builder.syntax().parse_tool_calls) { - LOG_DBG("%s: not parse_tool_calls\n", __func__); - builder.add_content(builder.consume_rest()); - return; - } - - LOG_DBG("%s: parse_tool_calls\n", __func__); - - parse_json_tool_calls( - builder, - /* block_open= */ tool_calls_begin, - /* function_regex_start_only= */ std::nullopt, - function_regex, - close_regex, - tool_calls_end); -} - -static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) { - // DeepSeek V3.1 outputs reasoning content between "" and "" tags, followed by regular content - // First try to parse using the standard reasoning parsing method - LOG_DBG("%s: thinking_forced_open: %s\n", __func__, std::to_string(builder.syntax().thinking_forced_open).c_str()); - - auto start_pos = builder.pos(); - auto found_end_think = builder.try_find_literal(""); - builder.move_to(start_pos); - - if (builder.syntax().thinking_forced_open && !builder.is_partial() && !found_end_think) { - LOG_DBG("%s: no end_think, not partial, adding content\n", __func__); - common_chat_parse_deepseek_v3_1_content(builder); - } else if (builder.try_parse_reasoning("", "")) { - // If reasoning was parsed successfully, the remaining content is regular content - LOG_DBG("%s: parsed reasoning, adding content\n", __func__); - // <|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>NAME\n```json\nJSON\n```<|tool▁call▁end|><|tool▁calls▁end|> - common_chat_parse_deepseek_v3_1_content(builder); - } else { - if (builder.syntax().reasoning_format == COMMON_REASONING_FORMAT_NONE) { - LOG_DBG("%s: reasoning_format none, adding content\n", __func__); - common_chat_parse_deepseek_v3_1_content(builder); - return; - } - // If no reasoning tags found, check if we should treat everything as reasoning - if (builder.syntax().thinking_forced_open) { - // If thinking is forced open but no tags found, treat everything as reasoning - LOG_DBG("%s: thinking_forced_open, adding reasoning content\n", __func__); - builder.add_reasoning_content(builder.consume_rest()); - } else { - LOG_DBG("%s: no thinking_forced_open, adding content\n", __func__); - // <|tool▁call▁begin|>NAME<|tool▁sep|>JSON<|tool▁call▁end|> - common_chat_parse_deepseek_v3_1_content(builder); - } - } -} - - static common_chat_params common_chat_params_init_minimax_m2(const common_chat_template & tmpl, const struct templates_params & params) { common_chat_params data; data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED; diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp index 9408b54541..f74f507be0 100644 --- a/tests/test-chat.cpp +++ b/tests/test-chat.cpp @@ -3499,9 +3499,76 @@ 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 + ); + } } -<<<<<<< HEAD static void test_template_output_peg_parsers() { printf("[%s]\n", __func__); @@ -3589,76 +3656,6 @@ static void test_template_output_peg_parsers() { t.expect.content =R"({"amount": 123.45, "date": "2025-12-03"})"; }); } -======= - { - 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 - ); - } - ->>>>>>> 6f9ffbd9d (implement gigachat v3 toolcall parser) } static void test_msg_diffs_compute() {