From 79e5b248a6f4fef28461fdc452b140e18a523caf Mon Sep 17 00:00:00 2001 From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:48:41 +1100 Subject: [PATCH] common: chat parser for Deepseek V3.2 --- common/chat-parser-xml-toolcall.cpp | 44 ++++++++++++--- common/chat-parser-xml-toolcall.h | 1 + common/chat-parser.cpp | 21 +++++++ common/chat.cpp | 86 +++++++++++++++++++++++++++++ common/chat.h | 1 + 5 files changed, 144 insertions(+), 9 deletions(-) diff --git a/common/chat-parser-xml-toolcall.cpp b/common/chat-parser-xml-toolcall.cpp index a80900ff8d..e80e4f53fa 100644 --- a/common/chat-parser-xml-toolcall.cpp +++ b/common/chat-parser-xml-toolcall.cpp @@ -169,16 +169,10 @@ void build_grammar_xml_tool_call(common_chat_params & data, const json & tools, GGML_ASSERT(!form.tool_start.empty()); GGML_ASSERT(!form.tool_sep.empty()); GGML_ASSERT(!form.key_start.empty()); + GGML_ASSERT(!form.key_val_sep.empty()); GGML_ASSERT(!form.val_end.empty()); GGML_ASSERT(!form.tool_end.empty()); - std::string key_val_sep = form.key_val_sep; - if (form.key_val_sep2) { - key_val_sep += "\n"; - key_val_sep += *form.key_val_sep2; - } - GGML_ASSERT(!key_val_sep.empty()); - if (tools.is_array() && !tools.empty()) { data.grammar = build_grammar([&](const common_grammar_builder &builder) { auto string_arg_val = form.last_val_end ? @@ -224,14 +218,29 @@ void build_grammar_xml_tool_call(common_chat_params & data, const json & tools, for (const auto & [key, value] : parameters.at("properties").items()) { std::string quoted_key = key; bool required = std::binary_search(requiredParameters.begin(), requiredParameters.end(), key); - if (form.key_start.back() == '"' && key_val_sep[0] == '"') { + if (form.key_start.back() == '"' && form.key_val_sep[0] == '"') { quoted_key = gbnf_format_literal(key); quoted_key = quoted_key.substr(1, quoted_key.size() - 2); } + std::string kvsep = gbnf_format_literal(form.key_val_sep); + if (!form.allowed_literal_between_kvsep.empty()) { + kvsep += " ("; + for (auto s: form.allowed_literal_between_kvsep) { + kvsep += " "; + kvsep += gbnf_format_literal(s); + kvsep += " |"; + } + kvsep.resize(kvsep.size() - 2); + kvsep += " )"; + } + if (form.key_val_sep2) { + kvsep += " "; + kvsep += gbnf_format_literal(*form.key_val_sep2); + } arg_rules.push_back(parameter_rule {builder.add_rule("func-" + name + "-kv-" + key, gbnf_format_literal(form.key_start) + " " + gbnf_format_literal(quoted_key) + " " + - gbnf_format_literal(key_val_sep) + " " + + kvsep + " " + ((value.contains("type") && value["type"].is_string() && value["type"] == "string" && (!form.raw_argval || *form.raw_argval)) ? (form.raw_argval ? string_arg_val : @@ -476,6 +485,23 @@ inline bool parse_xml_tool_calls(common_chat_msg_parser & builder, const struct auto &key = key_res->prelude; recovery = false; + if (!form.allowed_literal_between_kvsep.empty()) { + for (bool consumed = true; consumed;) { + consumed = false; + auto pos = builder.pos(); + for (auto s: form.allowed_literal_between_kvsep) { + if (auto tc = builder.try_find_literal(s)) { + if (all_space(tc->prelude)) { + consumed = true; + pos = builder.pos(); + } else { + builder.move_to(pos); + } + } + } + } + } + // Parse arg_value if (form.key_val_sep2) { if (auto tc = builder.try_find_literal(*form.key_val_sep2)) { diff --git a/common/chat-parser-xml-toolcall.h b/common/chat-parser-xml-toolcall.h index b309fb6670..7132e863d8 100644 --- a/common/chat-parser-xml-toolcall.h +++ b/common/chat-parser-xml-toolcall.h @@ -32,6 +32,7 @@ struct xml_tool_call_format { std::optional last_tool_end = std::nullopt; bool trim_raw_argval = false; bool allow_toolcall_in_think = false; + std::vector allowed_literal_between_kvsep = {}; }; // make a GBNF that accept any strings except those containing any of the forbidden strings. diff --git a/common/chat-parser.cpp b/common/chat-parser.cpp index 21ce25f321..b4e3a9c1f3 100644 --- a/common/chat-parser.cpp +++ b/common/chat-parser.cpp @@ -877,6 +877,24 @@ static void common_chat_parse_deepseek_v3_1(common_chat_msg_parser & builder) { } } +static void common_chat_parse_deepseek_v3_2(common_chat_msg_parser & builder) { + static const xml_tool_call_format form = ([]() { + xml_tool_call_format form {}; + form.scope_start = "<|DSML|function_calls>"; + form.tool_start = "<|DSML|invoke name=\""; + form.tool_sep = "\">"; + form.key_start = "<|DSML|parameter name=\""; + form.key_val_sep = "\" string=\""; + form.allowed_literal_between_kvsep = {"true", "false"}; + form.key_val_sep2 = "\">"; + form.val_end = ""; + form.tool_end = ""; + form.scope_end = ""; + return form; + })(); + builder.consume_reasoning_with_xml_tool_calls(form, "", ""); +} + static void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) { static const xml_tool_call_format form { /* form.scope_start = */ "", @@ -1477,6 +1495,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_DEEPSEEK_V3_2: + common_chat_parse_deepseek_v3_2(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 b4a0f985e2..08c753f759 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -649,6 +649,7 @@ const char * common_chat_format_name(common_chat_format format) { case COMMON_CHAT_FORMAT_QWEN3_CODER_XML: return "Qwen3 Coder"; case COMMON_CHAT_FORMAT_APRIEL_1_5: return "Apriel 1.5"; case COMMON_CHAT_FORMAT_XIAOMI_MIMO: return "Xiaomi MiMo"; + case COMMON_CHAT_FORMAT_DEEPSEEK_V3_2: return "DeepSeek V3.2"; default: throw std::runtime_error("Unknown chat format"); } @@ -1481,6 +1482,76 @@ static common_chat_params common_chat_params_init_deepseek_v3_1(const common_cha return data; } +static common_chat_params common_chat_params_init_deepseek_v3_2(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; + data.format = COMMON_CHAT_FORMAT_DEEPSEEK_V3_2; + + /*minja::chat_template_inputs tmpl_inputs; + tmpl_inputs.messages = params.messages; + tmpl_inputs.tools = params.tools.empty() ? json() : params.tools; + tmpl_inputs.add_generation_prompt = params.add_generation_prompt; + tmpl_inputs.extra_context = params.extra_context; + tmpl_inputs.extra_context["enable_thinking"] = params.enable_thinking; + + minja::chat_template_options tmpl_opts; + tmpl_opts.apply_polyfills = true; + tmpl_opts.polyfill_object_arguments = true; + tmpl_opts.polyfill_tools = false; + tmpl_opts.polyfill_tool_calls = false; + tmpl_opts.polyfill_tool_responses = false; + tmpl_opts.polyfill_system_role = false; + auto prompt = tmpl.apply(tmpl_inputs, tmpl_opts); + if (params.add_bos && string_starts_with(prompt, tmpl.bos_token())) { + prompt = prompt.substr(tmpl.bos_token().size()); + } + if (params.add_eos && string_ends_with(prompt, tmpl.eos_token())) { + prompt = prompt.substr(0, prompt.size() - tmpl.eos_token().size()); + }*/ + + data.prompt = apply(tmpl, params); + + if (string_ends_with(data.prompt, "")) { + if (!params.enable_thinking) { + // Close the thinking tag immediately if thinking is disabled + data.prompt += ""; + } else { + // Mark thinking as forced open (template started with ) + data.thinking_forced_open = true; + } + } + + data.preserved_tokens = { + "", + "", + "function_calls>", + "invoke>", + "<|end▁of▁sentence|>", + }; + + data.additional_stops.insert(data.additional_stops.end(), { + "<|end▁of▁sentence|>" + }); + // build grammar for tool call + static const xml_tool_call_format form = ([]() { + xml_tool_call_format form {}; + form.scope_start = "<|DSML|function_calls>\n"; + form.tool_start = "<|DSML|invoke name=\""; + form.tool_sep = "\">\n"; + form.key_start = "<|DSML|parameter name=\""; + form.key_val_sep = "\" string=\""; + form.allowed_literal_between_kvsep = {"true", "false"}; + form.key_val_sep2 = "\">"; + form.val_end = "\n"; + form.tool_end = "\n"; + form.scope_end = ""; + return form; + })(); + build_grammar_xml_tool_call(data, params.tools, form); + + return data; +} + 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; @@ -2465,6 +2536,21 @@ static common_chat_params common_chat_templates_apply_jinja( return common_chat_params_init_apriel_1_5(tmpl, params); } + // DeepSeek V3.2 format detection + if (src.find("") != std::string::npos && + src.find("") != std::string::npos && + src.find("<|begin▁of▁sentence|>") != std::string::npos && + src.find("<|end▁of▁sentence|>") != std::string::npos && + src.find("|DSML|") != std::string::npos && + src.find("function_calls>") != std::string::npos && + src.find("") != std::string::npos && + src.find("") != std::string::npos && + src.find("invoke name=") != std::string::npos && + src.find("parameter name=") != std::string::npos && + src.find("string=\"true|false\">") != std::string::npos) { + return common_chat_params_init_deepseek_v3_2(tmpl, params); + } + // Use generic handler when mixing tools + JSON schema. // TODO: support that mix in handlers below. if ((params.tools.is_array() && params.json_schema.is_object())) { diff --git a/common/chat.h b/common/chat.h index 754c411e23..36f81cdca2 100644 --- a/common/chat.h +++ b/common/chat.h @@ -123,6 +123,7 @@ enum common_chat_format { COMMON_CHAT_FORMAT_QWEN3_CODER_XML, COMMON_CHAT_FORMAT_APRIEL_1_5, COMMON_CHAT_FORMAT_XIAOMI_MIMO, + COMMON_CHAT_FORMAT_DEEPSEEK_V3_2, COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats };