common: chat parser for Deepseek V3.2

This commit is contained in:
hksdpc255 2025-12-03 12:48:41 +11:00 committed by GitHub
parent 0e6502eaf9
commit 79e5b248a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 9 deletions

View File

@ -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)) {

View File

@ -32,6 +32,7 @@ struct xml_tool_call_format {
std::optional<std::string> last_tool_end = std::nullopt;
bool trim_raw_argval = false;
bool allow_toolcall_in_think = false;
std::vector<std::string> allowed_literal_between_kvsep = {};
};
// make a GBNF that accept any strings except those containing any of the forbidden strings.

View File

@ -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 = "<DSMLfunction_calls>";
form.tool_start = "<DSMLinvoke name=\"";
form.tool_sep = "\">";
form.key_start = "<DSMLparameter name=\"";
form.key_val_sep = "\" string=\"";
form.allowed_literal_between_kvsep = {"true", "false"};
form.key_val_sep2 = "\">";
form.val_end = "</DSMLparameter>";
form.tool_end = "</DSMLinvoke>";
form.scope_end = "</DSMLfunction_calls>";
return form;
})();
builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");
}
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>",
@ -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));
}

View File

@ -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, "<think>")) {
if (!params.enable_thinking) {
// Close the thinking tag immediately if thinking is disabled
data.prompt += "</think>";
} else {
// Mark thinking as forced open (template started with <think>)
data.thinking_forced_open = true;
}
}
data.preserved_tokens = {
"<think>",
"</think>",
"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 = "<DSMLfunction_calls>\n";
form.tool_start = "<DSMLinvoke name=\"";
form.tool_sep = "\">\n";
form.key_start = "<DSMLparameter name=\"";
form.key_val_sep = "\" string=\"";
form.allowed_literal_between_kvsep = {"true", "false"};
form.key_val_sep2 = "\">";
form.val_end = "</DSMLparameter>\n";
form.tool_end = "</DSMLinvoke>\n";
form.scope_end = "</DSMLfunction_calls>";
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("<think>") != std::string::npos &&
src.find("</think>") != 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("<function_results>") != std::string::npos &&
src.find("</function_results>") != 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())) {

View File

@ -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
};