Merge c9315a3c73 into 58062860af
This commit is contained in:
commit
4094e9141d
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = "<|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 = "</|DSML|parameter>";
|
||||
form.tool_end = "</|DSML|invoke>";
|
||||
form.scope_end = "</|DSML|function_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>",
|
||||
|
|
@ -1479,6 +1497,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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_NATIVE: return "peg-native";
|
||||
case COMMON_CHAT_FORMAT_PEG_CONSTRUCTED: return "peg-constructed";
|
||||
case COMMON_CHAT_FORMAT_DEEPSEEK_V3_2: return "DeepSeek V3.2";
|
||||
default:
|
||||
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;
|
||||
}
|
||||
|
||||
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 = "<|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 = "</|DSML|parameter>\n";
|
||||
form.tool_end = "</|DSML|invoke>\n";
|
||||
form.scope_end = "</|DSML|function_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;
|
||||
|
|
@ -2741,6 +2790,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())) {
|
||||
|
|
|
|||
|
|
@ -124,6 +124,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,
|
||||
|
||||
// These are intended to be parsed by the PEG parser
|
||||
COMMON_CHAT_FORMAT_PEG_SIMPLE,
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
@ -3499,6 +3499,167 @@ 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/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(
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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><|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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(
|
||||
"<|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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><|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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<|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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<|DSML|function_calls><|DSML|invoke name=\"special_function\"><|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter></|DSML|invoke></|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"special_function\">\n<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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,
|
||||
"<|DSML|function_calls>\n<|DSML|invoke name=\"special_function_with_opt\">\n<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n<|DSML|parameter name=\"arg2\" string=\"false\">2</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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}"),
|
||||
"<|DSML|function_calls>\n"
|
||||
"<|DSML|invoke name=\"complex_function\">\n"
|
||||
"<|DSML|parameter name=\"name\" string=\"true\">\"John\" Doe</|DSML|parameter>\n"
|
||||
"<|DSML|parameter name=\"age\" string=\"false\">30</|DSML|parameter>\n"
|
||||
"<|DSML|parameter name=\"active\" string=\"false\">true</|DSML|parameter>\n"
|
||||
"<|DSML|parameter name=\"score\" string=\"false\">95.5</|DSML|parameter>\n"
|
||||
"</|DSML|invoke>\n"
|
||||
"</|DSML|function_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 \"<|DSML|function_calls>\" block like the following as part of your reply to the user:\n<|DSML|function_calls>\n<|DSML|invoke name=\"$FUNCTION_NAME\">\n<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML|parameter>\n...\n</|DSML|invoke>\n<|DSML|invoke name=\"$FUNCTION_NAME2\">\n...\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n...\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"complex_function\">\n<|DSML|parameter name=\"name\" string=\"true\">John Doe</|DSML|parameter>\n<|DSML|parameter name=\"age\" string=\"false\">30</|DSML|parameter>\n<|DSML|parameter name=\"active\" string=\"false\">true</|DSML|parameter>\n<|DSML|parameter name=\"score\" string=\"false\">95.5</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"web_search\">\n<|DSML|parameter name=\"query\" string=\"true\">\"From Zero\" Linkin Park album tracklist complete songs</|DSML|parameter>\n<|DSML|parameter name=\"limit\" string=\"false\">3</|DSML|parameter>\n<|DSML|parameter name=\"type\" string=\"true\">text</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"read_file\">\n<|DSML|parameter 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\"}]</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"special_function\">\n<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"special_function_with_opt\">\n<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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<|DSML|function_calls>\n<|DSML|invoke name=\"special_function_with_opt\">\n<|DSML|parameter name=\"arg1\" string=\"false\">1</|DSML|parameter>\n<|DSML|parameter name=\"arg2\" string=\"false\">2</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|function_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() {
|
||||
|
|
|
|||
|
|
@ -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, "<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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue