This commit is contained in:
hksdpc255 2025-12-17 05:51:06 +02:00 committed by GitHub
commit 4094e9141d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 535 additions and 10 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_start.empty());
GGML_ASSERT(!form.tool_sep.empty()); GGML_ASSERT(!form.tool_sep.empty());
GGML_ASSERT(!form.key_start.empty()); GGML_ASSERT(!form.key_start.empty());
GGML_ASSERT(!form.key_val_sep.empty());
GGML_ASSERT(!form.val_end.empty()); GGML_ASSERT(!form.val_end.empty());
GGML_ASSERT(!form.tool_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()) { if (tools.is_array() && !tools.empty()) {
data.grammar = build_grammar([&](const common_grammar_builder &builder) { data.grammar = build_grammar([&](const common_grammar_builder &builder) {
auto string_arg_val = form.last_val_end ? 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()) { for (const auto & [key, value] : parameters.at("properties").items()) {
std::string quoted_key = key; std::string quoted_key = key;
bool required = std::binary_search(requiredParameters.begin(), requiredParameters.end(), 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 = gbnf_format_literal(key);
quoted_key = quoted_key.substr(1, quoted_key.size() - 2); 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, arg_rules.push_back(parameter_rule {builder.add_rule("func-" + name + "-kv-" + key,
gbnf_format_literal(form.key_start) + " " + gbnf_format_literal(form.key_start) + " " +
gbnf_format_literal(quoted_key) + " " + 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)) ? ((value.contains("type") && value["type"].is_string() && value["type"] == "string" && (!form.raw_argval || *form.raw_argval)) ?
(form.raw_argval ? (form.raw_argval ?
string_arg_val : 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; auto &key = key_res->prelude;
recovery = false; 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 // Parse arg_value
if (form.key_val_sep2) { if (form.key_val_sep2) {
if (auto tc = builder.try_find_literal(*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; std::optional<std::string> last_tool_end = std::nullopt;
bool trim_raw_argval = false; bool trim_raw_argval = false;
bool allow_toolcall_in_think = 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. // make a GBNF that accept any strings except those containing any of the forbidden strings.

View File

@ -879,6 +879,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 void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) {
static const xml_tool_call_format form { static const xml_tool_call_format form {
/* form.scope_start = */ "<minimax:tool_call>", /* form.scope_start = */ "<minimax:tool_call>",
@ -1479,6 +1497,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
case COMMON_CHAT_FORMAT_XIAOMI_MIMO: case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
common_chat_parse_xiaomi_mimo(builder); common_chat_parse_xiaomi_mimo(builder);
break; break;
case COMMON_CHAT_FORMAT_DEEPSEEK_V3_2:
common_chat_parse_deepseek_v3_2(builder);
break;
default: default:
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format)); throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
} }

View File

@ -672,6 +672,7 @@ const char * common_chat_format_name(common_chat_format format) {
case COMMON_CHAT_FORMAT_PEG_SIMPLE: return "peg-simple"; case COMMON_CHAT_FORMAT_PEG_SIMPLE: return "peg-simple";
case COMMON_CHAT_FORMAT_PEG_NATIVE: return "peg-native"; case COMMON_CHAT_FORMAT_PEG_NATIVE: return "peg-native";
case COMMON_CHAT_FORMAT_PEG_CONSTRUCTED: return "peg-constructed"; case COMMON_CHAT_FORMAT_PEG_CONSTRUCTED: return "peg-constructed";
case COMMON_CHAT_FORMAT_DEEPSEEK_V3_2: return "DeepSeek V3.2";
default: default:
throw std::runtime_error("Unknown chat format"); throw std::runtime_error("Unknown chat format");
} }
@ -1752,6 +1753,54 @@ static common_chat_params common_chat_params_init_deepseek_v3_1(const common_cha
return data; 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;
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) { static common_chat_params common_chat_params_init_minimax_m2(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data; common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED; data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
@ -2741,6 +2790,21 @@ static common_chat_params common_chat_templates_apply_jinja(
return common_chat_params_init_apriel_1_5(tmpl, params); 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. // Use generic handler when mixing tools + JSON schema.
// TODO: support that mix in handlers below. // TODO: support that mix in handlers below.
if ((params.tools.is_array() && params.json_schema.is_object())) { if ((params.tools.is_array() && params.json_schema.is_object())) {

View File

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

View File

@ -0,0 +1,251 @@
{#- DeepSeek V3.2 Chat Template -#}
{#-
Standard minja interface:
- messages: list of message dicts (required)
- tools: list of tool definitions (optional, can be extracted from messages)
- add_generation_prompt: bool (optional, default: true)
Custom DeepSeek variables:
- enable_thinking: bool (optional, default: true)
When true, enables <think> blocks for reasoning_content
When false, disables thinking mode entirely
-#}
{#- ========================================================================== -#}
{#- Setup: Special tokens and defaults -#}
{#- ========================================================================== -#}
{%- set add_generation_prompt = add_generation_prompt | default(true) -%}
{%- set enable_thinking = enable_thinking | default(true) -%}
{%- set bos_token = "<begin▁of▁sentence>" -%}
{%- set eos_token = "<end▁of▁sentence>" -%}
{%- set thinking_start_token = "<think>" -%}
{%- set thinking_end_token = "</think>" -%}
{%- set dsml_token = "DSML" -%}
{#- ========================================================================== -#}
{#- Backward compatibility: extract tools and response_format from messages -#}
{#- ========================================================================== -#}
{%- if not tools -%}
{%- set tools = namespace(value=[]) -%}
{%- for message in messages -%}
{%- if message.get('tools') -%}
{%- for tool in message.tools -%}
{%- set _ = tools.value.append(tool.function if tool.get('function') else tool) -%}
{%- endfor -%}
{%- endif -%}
{%- endfor -%}
{%- set tools = tools.value -%}
{%- endif -%}
{%- set response_format = namespace(value=none) -%}
{%- for message in messages -%}
{%- if message.get('response_format') -%}
{%- set response_format.value = message.response_format -%}
{%- endif -%}
{%- endfor -%}
{#- ========================================================================== -#}
{#- Macros -#}
{#- ========================================================================== -#}
{%- macro render_tools(tools) -%}
## Tools
You have access to a set of tools you can use to answer the user's question.
You can invoke functions by writing a "<{{ dsml_token }}function_calls>" block like the following as part of your reply to the user:
<{{ dsml_token }}function_calls>
<{{ dsml_token }}invoke name="$FUNCTION_NAME">
<{{ dsml_token }}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{{ dsml_token }}parameter>
...
</{{ dsml_token }}invoke>
<{{ dsml_token }}invoke name="$FUNCTION_NAME2">
...
</{{ dsml_token }}invoke>
</{{ dsml_token }}function_calls>
String and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The "string" attribute should be set to "true" for string type parameters and "false" for other types (numbers, booleans, arrays, objects).
If the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:
<{{ dsml_token }}function_calls>
...
</{{ dsml_token }}function_calls>
<function_results>
...
</function_results>
{{ thinking_start_token }}...thinking about results{{ thinking_end_token }}
Here are the functions available in JSONSchema format:
<functions>
{%- for tool in tools -%}
{{ "\n" }}{{ tool | tojson(ensure_ascii=False) }}
{%- endfor -%}
{{ "\n" }}</functions>
{%- endmacro -%}
{%- macro encode_arguments_to_dsml(tool_call) -%}
{%- set arguments = tool_call.function.arguments -%}
{%- if arguments is mapping -%}
{#- Object arguments: iterate and render as DSML parameters -#}
{%- for key, value in arguments.items() -%}
{%- if not loop.first -%}{{ "\n" }}{%- endif -%}
<{{ dsml_token }}parameter name="{{ key }}" string="{{ 'true' if value is string else 'false' }}">{{ value if value is string else (value | tojson(ensure_ascii=False)) }}</{{ dsml_token }}parameter>
{%- endfor -%}
{%- endif -%}
{#- String arguments: skip rendering - this tells minja's detection we require object arguments -#}
{%- endmacro -%}
{%- macro render_response_format(response_format) -%}
## Response Format:
You MUST strictly adhere to the following schema to reply:
{{ response_format | tojson(ensure_ascii=False) }}
{%- endmacro -%}
{#- ========================================================================== -#}
{#- Preprocessing: Find last user message -#}
{#- ========================================================================== -#}
{%- set last_user_index = namespace(value=-1) -%}
{%- for msg in messages -%}
{%- if msg.role in ["user", "developer"] -%}
{%- set last_user_index.value = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{#- ========================================================================== -#}
{#- Rendering: Output the formatted chat -#}
{#- ========================================================================== -#}
{#- BOS token -#}
{{ bos_token }}
{#- System message (if present) with tools and response format -#}
{%- set has_system = messages|length > 0 and messages[0].role == "system" -%}
{%- set first_is_developer = messages|length > 0 and messages[0].role == "developer" -%}
{%- if has_system -%}
{{ messages[0].get('content', '') }}
{%- if tools -%}
{{ "\n\n" }}{{ render_tools(tools) }}{{ "\n" }}
{%- endif -%}
{%- if response_format.value -%}
{{ "\n\n" }}{{ render_response_format(response_format.value) }}
{%- endif -%}
{%- elif not first_is_developer -%}
{#- If no system message and first message is NOT developer, render tools/response_format at the top -#}
{#- (Developer messages render tools themselves, so we skip this to avoid duplication) -#}
{%- if tools or response_format.value -%}
{%- if tools -%}
{{ render_tools(tools) }}{{ "\n" }}
{%- endif -%}
{%- if response_format.value -%}
{{ "\n\n" }}{{ render_response_format(response_format.value) }}
{%- endif -%}
{%- endif -%}
{%- endif -%}
{#- Main message loop -#}
{%- set start_idx = 1 if has_system else 0 -%}
{%- for message in messages[start_idx:] -%}
{%- set msg_index = loop.index0 + start_idx -%}
{#- ====================================================================== -#}
{#- Developer message -#}
{#- ====================================================================== -#}
{%- if message.role == "developer" -%}
<User>
{%- if tools -%}
{{ "\n\n" }}{{ render_tools(tools) }}{{ "\n\n" }}
{%- endif -%}
{%- if response_format.value -%}
{{ "\n\n" }}{{ render_response_format(response_format.value) }}
{%- endif -%}
{{ "\n" }}# The user's message is: {{ message.content }}<Assistant>
{%- if msg_index == last_user_index.value and enable_thinking -%}
{{ thinking_start_token }}
{%- else -%}
{{ thinking_end_token }}
{%- endif -%}
{#- ====================================================================== -#}
{#- User message -#}
{#- ====================================================================== -#}
{%- elif message.role == "user" -%}
<User>{{ message.content }}<Assistant>
{%- if msg_index == last_user_index.value and enable_thinking -%}
{{ thinking_start_token }}
{%- else -%}
{{ thinking_end_token }}
{%- endif -%}
{#- ====================================================================== -#}
{#- Tool message -#}
{#- ====================================================================== -#}
{%- elif message.role == "tool" -%}
{#- Find the previous assistant message -#}
{%- set prev_assistant_idx = namespace(value=-1) -%}
{%- for i in range(msg_index - 1, -1, -1) -%}
{%- if messages[i].role != "tool" and prev_assistant_idx.value == -1 -%}
{%- set prev_assistant_idx.value = i -%}
{%- endif -%}
{%- endfor -%}
{%- set tool_call_order = msg_index - prev_assistant_idx.value -%}
{%- set assistant_msg = messages[prev_assistant_idx.value] -%}
{#- Open function_results block on first tool result -#}
{%- if tool_call_order == 1 -%}
{{ "\n\n" }}<function_results>
{%- endif -%}
{#- Add this tool result -#}
{{ "\n" }}<result>{{ message.content }}</result>
{#- Close function_results block on last tool result -#}
{%- if tool_call_order == (assistant_msg.get('tool_calls', []) | length) -%}
{{ "\n" }}</function_results>
{%- if msg_index >= last_user_index.value and enable_thinking -%}
{{ "\n\n" }}{{ thinking_start_token }}
{%- else -%}
{{ "\n\n" }}{{ thinking_end_token }}
{%- endif -%}
{%- endif -%}
{#- ====================================================================== -#}
{#- Assistant message -#}
{#- ====================================================================== -#}
{%- elif message.role == "assistant" -%}
{#- Render reasoning content if in thinking mode -#}
{%- if enable_thinking and msg_index > last_user_index.value -%}
{{ message.get('reasoning_content', '') }}{{ thinking_end_token }}
{%- endif -%}
{#- Render assistant content -#}
{{ message.get('content', '') }}
{#- Render tool calls if present -#}
{%- if message.get('tool_calls') -%}
{{ "\n\n" }}<{{ dsml_token }}function_calls>
{%- for tool_call in message.tool_calls -%}
{{ "\n" }}<{{ dsml_token }}invoke name="{{ tool_call.function.name }}">{{ "\n" }}{{- encode_arguments_to_dsml(tool_call) -}}{{ "\n" }}</{{ dsml_token }}invoke>
{%- endfor -%}
{{ "\n" }}</{{ dsml_token }}function_calls>
{%- endif -%}
{#- EOS token -#}
{{ eos_token }}
{%- endif -%}
{%- endfor -%}
{#- Generation prompt (if requested) -#}
{%- if add_generation_prompt -%}
<User>{{ "\n" }}<Assistant>
{%- if enable_thinking -%}
{{ thinking_start_token }}
{%- else -%}
{{ thinking_end_token }}
{%- endif -%}
{%- endif -%}

View File

@ -3499,6 +3499,167 @@ Hey there!<|im_end|>
auto grammar = build_grammar(params.grammar); auto grammar = build_grammar(params.grammar);
GGML_ASSERT(grammar && "Failed to build Qwen3-Coder grammar with union types"); GGML_ASSERT(grammar && "Failed to build Qwen3-Coder grammar with union types");
} }
{
auto tmpls = read_templates("models/templates/DeepSeek-V3.2.jinja");
std::vector<std::string> end_tokens{ "<end▁of▁sentence>" };
assert_equals(COMMON_CHAT_FORMAT_DEEPSEEK_V3_2, common_chat_templates_apply(tmpls.get(), inputs_no_tools).format);
assert_equals(COMMON_CHAT_FORMAT_DEEPSEEK_V3_2, 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_DEEPSEEK_V3_2}));
// Test parsing content with thinking
assert_msg_equals(message_assist_thoughts,
common_chat_parse(
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?",
/* is_partial= */ false,
{
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK,
}));
// Test parsing tool calls
assert_msg_equals(message_assist_call,
common_chat_parse(
"<DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>",
/* is_partial= */ false,
{COMMON_CHAT_FORMAT_DEEPSEEK_V3_2}));
// Test parsing tool calls with thinking
assert_msg_equals(message_assist_call_thoughts,
common_chat_parse(
"<think>I'm\nthinking</think><DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>",
/* is_partial= */ false,
{
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}));
// Test tool calls with extra content
assert_msg_equals(message_assist_call_content,
common_chat_parse(
"<DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>Hello, world!\nWhat's up?",
/* is_partial= */ false,
{COMMON_CHAT_FORMAT_DEEPSEEK_V3_2}
));
// Test tool calls with extra content AND thinking
assert_msg_equals(message_assist_call_thoughts_content,
common_chat_parse(
"<think>I'm\nthinking</think><DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>Hello, world!\nWhat's up?",
/* is_partial= */ false,
{
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}));
// Test streaming
test_parser_with_streaming(message_assist_call_thoughts_content,
"<think>I'm\nthinking\n</think>Hello, world!\nWhat's up?\n<DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); });
test_parser_with_streaming(message_assist_call_thoughts_unparsed,
"<think>I'm\nthinking</think>\n\n<DSMLfunction_calls><DSMLinvoke name=\"special_function\"><DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter></DSMLinvoke></DSMLfunction_calls>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE
}); });
test_parser_with_streaming(message_assist_call_thoughts_content,
"<think>I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"special_function\">\n<DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls>\n",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); });
test_parser_with_streaming(message_assist_call_withopt,
"<DSMLfunction_calls>\n<DSMLinvoke name=\"special_function_with_opt\">\n<DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter>\n<DSMLparameter name=\"arg2\" string=\"false\">2</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_DEEPSEEK_V3_2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE
}); });
test_parser_with_streaming(
simple_assist_msg("", "", "complex_function", "{\"name\":\"\\\"John\\\" Doe\",\"age\":30,\"active\":true,\"score\":95.5}"),
"<DSMLfunction_calls>\n"
"<DSMLinvoke name=\"complex_function\">\n"
"<DSMLparameter name=\"name\" string=\"true\">\"John\" Doe</DSMLparameter>\n"
"<DSMLparameter name=\"age\" string=\"false\">30</DSMLparameter>\n"
"<DSMLparameter name=\"active\" string=\"false\">true</DSMLparameter>\n"
"<DSMLparameter name=\"score\" string=\"false\">95.5</DSMLparameter>\n"
"</DSMLinvoke>\n"
"</DSMLfunction_calls>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_DEEPSEEK_V3_2}); });
// Test template rendering
common_chat_templates_inputs conversation_with_tools = inputs_tools;
conversation_with_tools.messages.push_back(simple_assist_msg("Let's do it", "Think first", "complex_function", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 1",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "complex_function",
/* .tool_call_id = */ "",
});
conversation_with_tools.messages.push_back(simple_assist_msg("Continue", "Think next", "web_search", "{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 2",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "web_search",
/* .tool_call_id = */ "",
});
conversation_with_tools.messages.push_back(simple_assist_msg("CC", "Think last", "read_file", "{\"args\": [{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 3",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "read_file",
/* .tool_call_id = */ "",
});
assert_equals(common_chat_templates_apply(tmpls.get(), conversation_with_tools).prompt, std::string("<begin▁of▁sentence>## Tools\n\nYou have access to a set of tools you can use to answer the user's question.\nYou can invoke functions by writing a \"<DSMLfunction_calls>\" block like the following as part of your reply to the user:\n<DSMLfunction_calls>\n<DSMLinvoke name=\"$FUNCTION_NAME\">\n<DSMLparameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</DSMLparameter>\n...\n</DSMLinvoke>\n<DSMLinvoke name=\"$FUNCTION_NAME2\">\n...\n</DSMLinvoke>\n</DSMLfunction_calls>\n\nString and scalar parameters should be specified as is without any escaping or quotes, while lists and objects should use JSON format. The \"string\" attribute should be set to \"true\" for string type parameters and \"false\" for other types (numbers, booleans, arrays, objects).\n\nIf the thinking_mode is enabled, then after function results you should strongly consider outputting a thinking block. Here is an example:\n\n<DSMLfunction_calls>\n...\n</DSMLfunction_calls>\n\n<function_results>\n...\n</function_results>\n\n<think>...thinking about results</think>\n\nHere are the functions available in JSONSchema format:\n<functions>\n{\"type\": \"function\", \"function\": {\"name\": \"special_function\", \"description\": \"I'm special\", \"parameters\": {\"type\": \"object\", \"properties\": {\"arg1\": {\"type\": \"integer\", \"description\": \"The arg.\"}}, \"required\": [\"arg1\"]}}}\n</functions>\n<User>Hey there!<Assistant><think>Think first</think>Let's do it\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"complex_function\">\n<DSMLparameter name=\"name\" string=\"true\">John Doe</DSMLparameter>\n<DSMLparameter name=\"age\" string=\"false\">30</DSMLparameter>\n<DSMLparameter name=\"active\" string=\"false\">true</DSMLparameter>\n<DSMLparameter name=\"score\" string=\"false\">95.5</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls><end▁of▁sentence>\n\n<function_results>\n<result>Tool response 1</result>\n</function_results>\n\n<think>Think next</think>Continue\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"web_search\">\n<DSMLparameter name=\"query\" string=\"true\">\"From Zero\" Linkin Park album tracklist complete songs</DSMLparameter>\n<DSMLparameter name=\"limit\" string=\"false\">3</DSMLparameter>\n<DSMLparameter name=\"type\" string=\"true\">text</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls><end▁of▁sentence>\n\n<function_results>\n<result>Tool response 2</result>\n</function_results>\n\n<think>Think last</think>CC\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"read_file\">\n<DSMLparameter name=\"args\" string=\"false\">[{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls><end▁of▁sentence>\n\n<function_results>\n<result>Tool response 3</result>\n</function_results>\n\n<think><User>\n<Assistant><think>"));
// Test template generation for regular content
test_templates(tmpls.get(), end_tokens, message_assist, tools,
"</think>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,
"</think>\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"special_function\">\n<DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls>",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,
/* ignore_whitespace_differences= */ true
);
// Test template generation for tools with optional parameters
test_templates(tmpls.get(), end_tokens, message_assist_call_noopt, tools,
"</think>\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"special_function_with_opt\">\n<DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls>",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,
/* ignore_whitespace_differences= */ true
);
test_templates(tmpls.get(), end_tokens, message_assist_call_withopt, tools,
"</think>\n\n<DSMLfunction_calls>\n<DSMLinvoke name=\"special_function_with_opt\">\n<DSMLparameter name=\"arg1\" string=\"false\">1</DSMLparameter>\n<DSMLparameter name=\"arg2\" string=\"false\">2</DSMLparameter>\n</DSMLinvoke>\n</DSMLfunction_calls>",
/* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,
/* ignore_whitespace_differences= */ true
);
}
} }
static void test_template_output_peg_parsers() { static void test_template_output_peg_parsers() {

View File

@ -197,7 +197,7 @@ class chat_template {
|| contains(out_str, "\"argument_needle\":") || contains(out_str, "\"argument_needle\":")
|| contains(out_str, "'argument_needle':") || contains(out_str, "'argument_needle':")
|| contains(out_str, ">argument_needle<") || contains(out_str, ">argument_needle<")
|| contains(out_str, "<parameter name=\"argument_needle\">"); || contains(out_str, "=\"argument_needle\"");
}; };
// Note: the arguments are rendered in both cases, but may be double-escaped, which we don't want. // Note: the arguments are rendered in both cases, but may be double-escaped, which we don't want.