@@ -1656,6 +1656,173 @@ static common_chat_params common_chat_params_init_gigachat_v3(
16561656 return data;
16571657}
16581658
1659+ static common_chat_params common_chat_params_init_deepseek_v3_2 (const common_chat_template & tmpl,
1660+ const autoparser::generation_params & inputs) {
1661+ common_chat_params data;
1662+
1663+ data.prompt = common_chat_template_direct_apply_impl (tmpl, inputs);
1664+ data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
1665+ data.supports_thinking = true ;
1666+ data.thinking_start_tag = " <think>" ;
1667+ data.thinking_end_tag = " </think>" ;
1668+ data.preserved_tokens = {
1669+ " |DSML|" ,
1670+ " <think>" ,
1671+ " </think>" ,
1672+ };
1673+
1674+ auto has_tools = inputs.tools .is_array () && !inputs.tools .empty ();
1675+ auto has_response_format = !inputs.json_schema .is_null () && inputs.json_schema .is_object ();
1676+ auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
1677+ auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
1678+
1679+ const std::string DSML = " |DSML|" ;
1680+ const std::string THINK_START = " <think>" ;
1681+ const std::string THINK_END = " </think>" ;
1682+ const std::string FC_START = " <" + DSML + " function_calls>" ;
1683+ const std::string FC_END = " </" + DSML + " function_calls>" ;
1684+ const std::string INVOKE_START = " <" + DSML + " invoke" ;
1685+ const std::string INVOKE_END = " </" + DSML + " invoke>" ;
1686+ const std::string PARAM_START = " <" + DSML + " parameter" ;
1687+ const std::string PARAM_END = " </" + DSML + " parameter>" ;
1688+
1689+ auto parser = build_chat_peg_parser ([&](common_chat_peg_builder & p) {
1690+ auto generation_prompt = p.prefix (inputs.generation_prompt , THINK_START);
1691+ auto end = p.end ();
1692+
1693+ auto reasoning = p.eps ();
1694+ if (extract_reasoning && inputs.enable_thinking ) {
1695+ reasoning = p.optional (THINK_START + p.reasoning (p.until (THINK_END)) + THINK_END);
1696+ } else if (extract_reasoning) {
1697+ // Thinking disabled but reasoning extraction requested: the generation prompt
1698+ // contains an empty <think></think> pair that must still be consumed.
1699+ reasoning = p.optional (p.literal (THINK_START) + p.until (THINK_END) + p.literal (THINK_END));
1700+ }
1701+
1702+ if (has_response_format) {
1703+ auto response_format = p.rule (" response-format" ,
1704+ p.literal (" ```json" ) + p.space () +
1705+ p.content (p.schema (p.json (), " response-format-schema" , inputs.json_schema )) +
1706+ p.space () + p.literal (" ```" ));
1707+ return generation_prompt + reasoning + response_format + end;
1708+ }
1709+
1710+ if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
1711+ return generation_prompt + reasoning + p.content (p.rest ()) + end;
1712+ }
1713+
1714+ auto tool_choice = p.choice ();
1715+ foreach_function (inputs.tools , [&](const json & tool) {
1716+ const auto & function = tool.at (" function" );
1717+ std::string name = function.at (" name" );
1718+ auto params = function.contains (" parameters" ) ? function.at (" parameters" ) : json::object ();
1719+ const auto & props = params.contains (" properties" ) ? params.at (" properties" ) : json::object ();
1720+
1721+ std::set<std::string> required;
1722+ if (params.contains (" required" )) {
1723+ params.at (" required" ).get_to (required);
1724+ }
1725+
1726+ auto schema_info = common_schema_info ();
1727+ schema_info.resolve_refs (params);
1728+
1729+ std::vector<common_peg_parser> required_parsers;
1730+ std::vector<common_peg_parser> optional_parsers;
1731+ for (const auto & [param_name, param_schema] : props.items ()) {
1732+ bool is_required = required.find (param_name) != required.end ();
1733+ bool is_string = schema_info.resolves_to_string (param_schema);
1734+
1735+ auto arg = p.tool_arg (
1736+ p.tool_arg_open (
1737+ p.literal (PARAM_START + " name=\" " ) +
1738+ p.tool_arg_name (p.literal (param_name)) +
1739+ p.literal (" \" string=\" " + std::string (is_string ? " true" : " false" ) + " \" >" )) +
1740+ (is_string
1741+ ? p.tool_arg_string_value (p.until (PARAM_END))
1742+ : p.tool_arg_json_value (p.schema (p.json (),
1743+ " tool-" + name + " -arg-" + param_name + " -schema" ,
1744+ param_schema, false ))) +
1745+ p.tool_arg_close (p.literal (PARAM_END)));
1746+
1747+ auto named_arg = p.rule (" tool-" + name + " -arg-" + param_name, arg);
1748+ if (is_required) {
1749+ required_parsers.push_back (named_arg);
1750+ } else {
1751+ optional_parsers.push_back (named_arg);
1752+ }
1753+ }
1754+
1755+ common_peg_parser args_seq = p.eps ();
1756+ for (size_t i = 0 ; i < required_parsers.size (); i++) {
1757+ if (i > 0 ) {
1758+ args_seq = args_seq + p.space ();
1759+ }
1760+ args_seq = args_seq + required_parsers[i];
1761+ }
1762+
1763+ if (!optional_parsers.empty ()) {
1764+ common_peg_parser any_opt = p.choice ();
1765+ for (const auto & opt : optional_parsers) {
1766+ any_opt |= opt;
1767+ }
1768+ args_seq = args_seq + p.repeat (p.space () + any_opt, 0 , -1 );
1769+ }
1770+
1771+ common_peg_parser invoke_body = args_seq;
1772+ auto func_parser = p.tool (
1773+ p.tool_open (p.literal (INVOKE_START + " name=\" " ) +
1774+ p.tool_name (p.literal (name)) + p.literal (" \" >\n " )) +
1775+ invoke_body + p.space () +
1776+ p.tool_close (p.literal (INVOKE_END)));
1777+
1778+ tool_choice |= p.rule (" tool-" + name, func_parser);
1779+ });
1780+
1781+ auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
1782+
1783+ common_peg_parser tool_calls = p.eps ();
1784+ if (inputs.parallel_tool_calls ) {
1785+ tool_calls = p.trigger_rule (" tool-call" ,
1786+ p.literal (FC_START) + p.space () + tool_choice +
1787+ p.zero_or_more (p.space () + tool_choice) + p.space () + p.literal (FC_END));
1788+ } else {
1789+ tool_calls = p.trigger_rule (" tool-call" ,
1790+ p.literal (FC_START) + p.space () + tool_choice + p.space () + p.literal (FC_END));
1791+ }
1792+
1793+ if (!require_tools) {
1794+ tool_calls = p.optional (tool_calls);
1795+ }
1796+
1797+ auto content_before_tools = p.content (p.until (FC_START));
1798+ return generation_prompt + reasoning + content_before_tools + tool_calls + end;
1799+ });
1800+
1801+ data.parser = parser.save ();
1802+
1803+ if (include_grammar) {
1804+ data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
1805+ data.grammar = build_grammar ([&](const common_grammar_builder & builder) {
1806+ foreach_function (inputs.tools , [&](const json & tool) {
1807+ const auto & function = tool.at (" function" );
1808+ auto schema = function.contains (" parameters" ) ? function.at (" parameters" ) : json::object ();
1809+ builder.resolve_refs (schema);
1810+ });
1811+ if (has_response_format) {
1812+ auto schema = inputs.json_schema ;
1813+ builder.resolve_refs (schema);
1814+ }
1815+ parser.build_grammar (builder, data.grammar_lazy );
1816+ });
1817+
1818+ data.grammar_triggers = {
1819+ { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
1820+ };
1821+ }
1822+
1823+ return data;
1824+ }
1825+
16591826namespace workaround {
16601827
16611828static void map_developer_role_to_system (json & messages) {
@@ -1927,6 +2094,15 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
19272094 return common_chat_params_init_gigachat_v3 (tmpl, params);
19282095 }
19292096
2097+ // DeepSeek V3.2 format detection: template defines dsml_token and uses it for tool calls.
2098+ // The template source contains the token as a variable assignment, not as a literal in markup.
2099+ if (src.find (" dsml_token" ) != std::string::npos &&
2100+ src.find (" function_calls" ) != std::string::npos &&
2101+ src.find (" DSML" ) != std::string::npos) {
2102+ LOG_DBG (" Using specialized template: DeepSeek V3.2\n " );
2103+ return common_chat_params_init_deepseek_v3_2 (tmpl, params);
2104+ }
2105+
19302106 // Gemma4 format detection
19312107 if (src.find (" '<|tool_call>call:'" ) != std::string::npos) {
19322108 if (src.find (" {#- OpenAI Chat Completions:" ) == std::string::npos) {
0 commit comments