diff --git a/common/chat.cpp b/common/chat.cpp index ae7bc77b8..600d2f7a9 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include @@ -1794,11 +1796,83 @@ static common_chat_params common_chat_params_init_gigachat_v3( return data; } +// The DeepSeek V4 reference implementation renders consecutive tool results into a single +// user block, ordered by the tool call order of the preceding assistant message (matched +// by tool call id) rather than by the order they appear in the conversation. +static json deepseek_v4_sort_tool_results(const json & messages) { + json adjusted = messages; + std::map call_order; + + for (size_t i = 0; i < adjusted.size();) { + const auto & msg = adjusted[i]; + const auto role = msg.value("role", ""); + + if (role == "assistant" && msg.contains("tool_calls") && + msg.at("tool_calls").is_array() && !msg.at("tool_calls").empty()) { + call_order.clear(); + const auto & tool_calls = msg.at("tool_calls"); + for (size_t idx = 0; idx < tool_calls.size(); idx++) { + auto id = tool_calls[idx].value("id", ""); + if (!id.empty()) { + call_order[id] = idx; + } + } + i++; + continue; + } + + if (role != "user" && role != "tool") { + i++; + continue; + } + + std::vector tool_positions; + size_t run_end = i; + for (; run_end < adjusted.size(); run_end++) { + const auto r = adjusted[run_end].value("role", ""); + if (r == "tool") { + tool_positions.push_back(run_end); + } else if (r != "user") { + break; + } + } + + if (tool_positions.size() > 1 && !call_order.empty()) { + std::vector results; + results.reserve(tool_positions.size()); + for (auto pos : tool_positions) { + results.push_back(adjusted[pos]); + } + std::stable_sort(results.begin(), results.end(), [&](const json & a, const json & b) { + const auto order = [&](const json & m) { + auto it = call_order.find(m.value("tool_call_id", "")); + return it == call_order.end() ? (size_t) 0 : it->second; + }; + return order(a) < order(b); + }); + for (size_t k = 0; k < tool_positions.size(); k++) { + adjusted[tool_positions[k]] = std::move(results[k]); + } + } + + i = run_end; + } + + return adjusted; +} + static common_chat_params common_chat_params_init_deepseek_v3_2(const common_chat_template & tmpl, const autoparser::generation_params & inputs) { common_chat_params data; - data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs); + const bool is_v4 = tmpl.source().find("function_calls") == std::string::npos; + + std::optional adjusted_messages; + if (is_v4) { + adjusted_messages = deepseek_v4_sort_tool_results(inputs.messages); + } + + data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, adjusted_messages); data.format = COMMON_CHAT_FORMAT_PEG_NATIVE; data.supports_thinking = true; data.thinking_start_tag = ""; @@ -1817,8 +1891,9 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha const std::string DSML = "|DSML|"; const std::string THINK_START = ""; const std::string THINK_END = ""; - const std::string FC_START = "<" + DSML + "function_calls>"; - const std::string FC_END = ""; + const std::string TC_BLOCK = is_v4 ? "tool_calls" : "function_calls"; + const std::string FC_START = "<" + DSML + TC_BLOCK + ">"; + const std::string FC_END = ""; const std::string INVOKE_START = "<" + DSML + "invoke"; const std::string INVOKE_END = ""; const std::string PARAM_START = "<" + DSML + "parameter"; @@ -1832,9 +1907,12 @@ static common_chat_params common_chat_params_init_deepseek_v3_2(const common_cha if (extract_reasoning && inputs.enable_thinking) { reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END); } else if (extract_reasoning) { - // Thinking disabled but reasoning extraction requested: the generation prompt - // contains an empty pair that must still be consumed. - reasoning = p.optional(p.literal(THINK_START) + p.until(THINK_END) + p.literal(THINK_END)); + // Thinking disabled but reasoning extraction requested: consume the thinking + // delimiters the generation prompt placed. V3.2 emits an empty + // pair, while V4 emits a bare . + reasoning = is_v4 + ? p.optional(p.literal(THINK_END)) + : p.optional(p.literal(THINK_START) + p.until(THINK_END) + p.literal(THINK_END)); } if (has_response_format) { @@ -2575,12 +2653,14 @@ std::optional common_chat_try_specialized_template( return common_chat_params_init_minimax_m3(tmpl, params); } - // DeepSeek V3.2 format detection: template defines dsml_token and uses it for tool calls. + // DeepSeek V3.2/V4 format detection: template defines dsml_token and uses it for tool calls. // The template source contains the token as a variable assignment, not as a literal in markup. + // V3.2 names the tool call block "function_calls", V4 names it "tool_calls". if (src.find("dsml_token") != std::string::npos && - src.find("function_calls") != std::string::npos && - src.find("DSML") != std::string::npos) { - LOG_DBG("Using specialized template: DeepSeek V3.2\n"); + src.find("DSML") != std::string::npos && + (src.find("function_calls") != std::string::npos || + src.find("tool_calls") != std::string::npos)) { + LOG_DBG("Using specialized template: DeepSeek V3.2/V4\n"); return common_chat_params_init_deepseek_v3_2(tmpl, params); } diff --git a/models/templates/deepseek-ai-DeepSeek-V4.jinja b/models/templates/deepseek-ai-DeepSeek-V4.jinja index d4b0165de..2f0d449b4 100644 --- a/models/templates/deepseek-ai-DeepSeek-V4.jinja +++ b/models/templates/deepseek-ai-DeepSeek-V4.jinja @@ -8,12 +8,17 @@ {%- set thinking = false -%} {%- endif -%} {%- endif -%} +{%- if not drop_thinking is defined -%} + {%- set drop_thinking = true -%} +{%- endif -%} {%- set dsml_token = '|DSML|' -%} {%- set thinking_start_token = '' -%} {%- set thinking_end_token = '' -%} +{%- set reasoning_effort_high = 'Reasoning Effort: Absolute maximum with no shortcuts permitted.\nYou MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\nExplicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n' -%} +{%- set reasoning_effort_max = 'Reasoning Effort: Beyond maximum — exhaustive, relentless, and uncompromising.\nYou MUST reason with the utmost depth and rigor, leaving absolutely nothing to chance: exhaustively decompose the problem into its most fundamental components, trace every causal chain to its root, and resolve the underlying cause rather than any surface symptom.\nDo not stop reasoning until you have independently verified the solution from multiple angles and are certain that no assumption remains unchecked and no error remains undiscovered.\n\n' -%} {%- set tools_header = '## Tools\n\nYou have access to a set of tools to help answer the user\'s question. You can invoke tools by writing a "<' + dsml_token + 'tool_calls>" block like the following:\n\n<' + dsml_token + 'tool_calls>\n<' + dsml_token + 'invoke name="$TOOL_NAME">\n<' + dsml_token + 'parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE\n...\n\n<' + dsml_token + 'invoke name="$TOOL_NAME2">\n...\n\n\n\nString parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.\n\nIf thinking_mode is enabled (triggered by ' + thinking_start_token + '), you MUST output your complete reasoning inside ' + thinking_start_token + '...' + thinking_end_token + ' BEFORE any tool calls or final response.\n\nOtherwise, output directly after ' + thinking_end_token + ' with tool calls or final response.\n\n### Available Tool Schemas\n\n' -%} {%- set tools_footer = '\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n' -%} -{%- set ns = namespace(system_prompt = '', is_first_sp = true) -%} +{%- set ns = namespace(system_prompt='', is_first_sp=true, has_tool_calls=false) -%} {%- for message in messages -%} {%- if message['role'] == 'system' -%} {%- if ns.is_first_sp -%} @@ -25,7 +30,7 @@ {%- endif -%} {%- endfor -%} {%- if tools is defined and tools -%} - {%- set ts = namespace(schemas = '') -%} + {%- set ts = namespace(schemas='') -%} {%- for tool in tools -%} {%- if tool['type'] == 'function' -%} {%- set ts.schemas = ts.schemas + (tool['function'] | tojson) + '\n' -%} @@ -38,14 +43,29 @@ {%- endif -%} {%- endif -%} {{- bos_token -}} +{%- if thinking and reasoning_effort is defined -%} + {%- if reasoning_effort == 'high' -%} + {{- reasoning_effort_high -}} + {%- elif reasoning_effort == 'max' -%} + {{- reasoning_effort_max -}} + {%- endif -%} +{%- endif -%} {{- ns.system_prompt -}} -{%- set last_user_idx = namespace(value = -1) -%} +{%- set last_user_idx = namespace(value=-1) -%} {%- for message in messages -%} {%- if message['role'] == 'user' or message['role'] == 'developer' or message['role'] == 'tool' -%} {%- set last_user_idx.value = loop.index0 -%} {%- endif -%} {%- endfor -%} -{%- set state = namespace(in_user = false) -%} +{%- set state = namespace(in_user=false) -%} +{%- if tools is defined and tools -%} + {%- set ns.has_tool_calls = true -%} +{%- endif -%} +{%- for message in messages -%} + {%- if message['role'] == 'tool' -%} + {%- set ns.has_tool_calls = true -%} + {%- endif -%} +{%- endfor -%} {%- for message in messages -%} {%- if message['role'] == 'user' or message['role'] == 'developer' -%} {%- if state.in_user -%} @@ -67,7 +87,8 @@ {%- set state.in_user = false -%} {{- '<|Assistant|>' -}} {%- set is_after_last_user = loop.index0 > last_user_idx.value -%} - {%- if is_after_last_user and thinking -%} + {%- set retain_reasoning = (not drop_thinking) or (is_after_last_user or ns.has_tool_calls) -%} + {%- if retain_reasoning and thinking -%} {{- thinking_start_token -}} {%- if message['reasoning_content'] is defined and message['reasoning_content'] -%} {{- message['reasoning_content'] -}}