chat_template.jinja
| 1 | {#- Based on Unsloth's DeepSeek-V4.1 template, extracted from |
| 2 | unsloth/DeepSeek-V4.1-Flash-1017-GGUF at revision |
| 3 | 60ad3651641b5c97679c807e064ab1d7a3ca1455. |
| 4 | Adaptations: use DeepSeek's native reasoning-effort aliases and default; |
| 5 | accept numeric reasoning_effort strings used by serving APIs; |
| 6 | preserve content_blocks on tool messages as in DeepSeek's reference encoder. |
| 7 | Image blocks render placeholders; the runtime supplies image embeddings through mmproj. |
| 8 | -#} |
| 9 | {%- set IMG = "<|deepseek_image|>" -%} |
| 10 | {%- set DSML = "|DSML|" -%} |
| 11 | |
| 12 | {%- if thinking_mode is not defined or thinking_mode is none -%} |
| 13 | {%- if enable_thinking is defined and enable_thinking is not none -%} |
| 14 | {%- set thinking_mode = "thinking" if enable_thinking else "chat" -%} |
| 15 | {%- elif thinking is defined and thinking is not none and thinking is not string -%} |
| 16 | {%- set thinking_mode = "thinking" if thinking else "chat" -%} |
| 17 | {%- else -%} |
| 18 | {%- set thinking_mode = "thinking" -%} |
| 19 | {%- endif -%} |
| 20 | {%- endif -%} |
| 21 | {%- if thinking_mode not in ["chat", "thinking"] -%} |
| 22 | {{- raise_exception("Invalid thinking_mode `" ~ thinking_mode ~ "`, should be \"chat\" or \"thinking\"") -}} |
| 23 | {%- endif -%} |
| 24 | {%- if drop_thinking is not defined or drop_thinking is none -%}{%- set drop_thinking = true -%}{%- endif -%} |
| 25 | {%- if add_generation_prompt is not defined -%}{%- set add_generation_prompt = false -%}{%- endif -%} |
| 26 | {%- if tools is not defined -%}{%- set tools = none -%}{%- endif -%} |
| 27 | {%- if response_format is not defined -%}{%- set response_format = none -%}{%- endif -%} |
| 28 | |
| 29 | {%- if reasoning_effort is not defined or reasoning_effort is none -%}{%- set reasoning_effort = "high" -%}{%- endif -%} |
| 30 | {%- set effort_map = {"low": 50, "high": 75, "max": 100} -%} |
| 31 | {%- if reasoning_effort is string and reasoning_effort in effort_map -%} |
| 32 | {%- set budget = effort_map[reasoning_effort] -%} |
| 33 | {%- elif reasoning_effort is integer and reasoning_effort is not boolean and reasoning_effort >= 1 and reasoning_effort <= 100 -%} |
| 34 | {%- set budget = reasoning_effort -%} |
| 35 | {%- elif reasoning_effort is string and (reasoning_effort | int | string) == reasoning_effort and (reasoning_effort | int) >= 1 and (reasoning_effort | int) <= 100 -%} |
| 36 | {%- set budget = reasoning_effort | int -%} |
| 37 | {%- else -%} |
| 38 | {{- raise_exception("Invalid reasoning effort: " ~ reasoning_effort ~ ", should be int within [1,100] or one of \"low\", \"high\", \"max\"") -}} |
| 39 | {%- endif -%} |
| 40 | |
| 41 | {#- Splits/validates "ns::name" against an explicit namespace and returns the qualified name. -#} |
| 42 | {%- macro qualified_name(name, ns) -%} |
| 43 | {%- if ns is mapping -%}{%- set ns = ns.name -%}{%- endif -%} |
| 44 | {%- set parts = name.split("::", 1) -%} |
| 45 | {%- if parts | length == 2 -%} |
| 46 | {%- if ns is not none and ns != parts[0] -%}{{- raise_exception("Conflicting tool namespaces: " ~ ns ~ " != " ~ parts[0]) -}}{%- endif -%} |
| 47 | {%- set ns = parts[0] -%} |
| 48 | {%- set name = parts[1] -%} |
| 49 | {%- endif -%} |
| 50 | {%- if "::" in name -%}{{- raise_exception("Tool name must not contain '::': " ~ name) -}}{%- endif -%} |
| 51 | {%- if ns is not none and "::" in ns -%}{{- raise_exception("Tool namespace must not contain '::': " ~ ns) -}}{%- endif -%} |
| 52 | {{- name if ns is none else ns ~ "::" ~ name -}} |
| 53 | {%- endmacro -%} |
| 54 | |
| 55 | {%- macro render_tools(tool_list) -%} |
| 56 | {%- set ts = namespace(schemas=[]) -%} |
| 57 | {%- for tool in tool_list -%} |
| 58 | {%- set fn = tool.function if tool.function is defined else tool -%} |
| 59 | {%- set ns = tool.namespace if tool.namespace is defined and tool.namespace is not none else fn.get("namespace") -%} |
| 60 | {#- JSON assembled by hand (key order kept, name/description replaced in place): llama.cpp has no ** unpacking. -#} |
| 61 | {%- set qname = qualified_name(fn.name, ns) -%} |
| 62 | {%- set sch = namespace(parts=[]) -%} |
| 63 | {%- for k, v in fn.items() if k != "namespace" -%} |
| 64 | {%- if k == "name" -%}{%- set v = qname -%} |
| 65 | {%- elif k == "description" and ns is mapping and ns.description -%}{%- set v = ns.description ~ "\n" ~ (fn.description or "") -%} |
| 66 | {%- endif -%} |
| 67 | {%- set sch.parts = sch.parts + [(k | tojson) ~ ": " ~ (v | tojson)] -%} |
| 68 | {%- endfor -%} |
| 69 | {%- if "description" not in fn and ns is mapping and ns.description -%} |
| 70 | {%- set sch.parts = sch.parts + ['"description": ' ~ ((ns.description ~ "\n") | tojson)] -%} |
| 71 | {%- endif -%} |
| 72 | {%- set ts.schemas = ts.schemas + ["{" ~ sch.parts | join(", ") ~ "}"] -%} |
| 73 | {%- endfor -%} |
| 74 | {{- "## 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| calls>\" block like the following:\n\n<|DSML| calls>\n<|DSML| invoke name=\"$TOOL_NAME\">\n<|DSML| parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</|DSML| parameter>\n...\n</|DSML| invoke>\n<|DSML| invoke name=\"$TOOL_NAME2\">\n...\n</|DSML| invoke>\n</|DSML| calls>\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 <think>), you MUST output your complete reasoning inside <think>...</think> BEFORE any tool calls or final response.\n\nOtherwise, output directly after </think> with tool calls or final response.\n\n### Available Tool Schemas\n\n" -}} |
| 75 | {{- ts.schemas | join("\n") -}} |
| 76 | {{- "\n\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.\n" -}} |
| 77 | {%- endmacro -%} |
| 78 | |
| 79 | {#- 1. Merge tool results (and consecutive user turns) into user messages made of content blocks. -#} |
| 80 | {%- set mns = namespace(msgs=[]) -%} |
| 81 | {%- for msg in messages -%} |
| 82 | {%- set content = msg.get("content") -%} |
| 83 | {%- if msg.role == "tool" and msg.get("content_blocks") and content is not string -%} |
| 84 | {%- set content = msg.content_blocks -%} |
| 85 | {%- endif -%} |
| 86 | {%- set is_blocks = content is iterable and content is not string and content is not mapping -%} |
| 87 | {%- if content is string and IMG in content -%} |
| 88 | {{- raise_exception("Message content contains image special token " ~ IMG ~ ". Images should be provided as image content blocks.") -}} |
| 89 | {%- endif -%} |
| 90 | {%- set rc = msg.reasoning_content if msg.get("reasoning_content") is not none else msg.get("thinking") -%} |
| 91 | {%- if rc is string and IMG in rc -%} |
| 92 | {{- raise_exception("reasoning_content contains image special token " ~ IMG) -}} |
| 93 | {%- endif -%} |
| 94 | {%- set prev = mns.msgs[-1] if mns.msgs else none -%} |
| 95 | {%- if msg.role == "tool" -%} |
| 96 | {%- set tblock = {"type": "tool_result", "tool_use_id": msg.get("tool_call_id", ""), "content": content if content is not none else ""} -%} |
| 97 | {%- if prev is not none and prev.role == "user" and prev.content_blocks is defined -%} |
| 98 | {%- set mns.msgs = mns.msgs[:-1] + [dict(prev, content_blocks=prev.content_blocks + [tblock])] -%} |
| 99 | {%- else -%} |
| 100 | {%- set mns.msgs = mns.msgs + [{"role": "user", "content_blocks": [tblock]}] -%} |
| 101 | {%- endif -%} |
| 102 | {%- elif msg.role == "user" -%} |
| 103 | {%- set blocks = msg.content_blocks if msg.get("content_blocks") is not none else (content if is_blocks else [{"type": "text", "text": content or ""}]) -%} |
| 104 | {%- if prev is not none and prev.role == "user" and prev.content_blocks is defined and prev.get("task") is none -%} |
| 105 | {%- set mns.msgs = mns.msgs[:-1] + [dict(prev, content_blocks=prev.content_blocks + blocks)] -%} |
| 106 | {%- else -%} |
| 107 | {%- set mns.msgs = mns.msgs + [dict(msg, content_blocks=blocks)] -%} |
| 108 | {%- endif -%} |
| 109 | {%- else -%} |
| 110 | {%- set mns.msgs = mns.msgs + [msg] -%} |
| 111 | {%- endif -%} |
| 112 | {%- endfor -%} |
| 113 | |
| 114 | {#- Top-level tools / response_format attach to a leading system message, or to a new empty one. -#} |
| 115 | {%- if tools or response_format -%} |
| 116 | {%- if not (mns.msgs and mns.msgs[0].role == "system") -%} |
| 117 | {%- set mns.msgs = [{"role": "system", "content": ""}] + mns.msgs -%} |
| 118 | {%- endif -%} |
| 119 | {%- set first = mns.msgs[0] -%} |
| 120 | {%- if tools and not first.get("tools") -%}{%- set first = dict(first, tools=tools) -%}{%- endif -%} |
| 121 | {%- if response_format and not first.get("response_format") -%}{%- set first = dict(first, response_format=response_format) -%}{%- endif -%} |
| 122 | {%- set mns.msgs = [first] + mns.msgs[1:] -%} |
| 123 | {%- endif -%} |
| 124 | |
| 125 | {#- 2. Reorder tool results to match the tool_calls order of the preceding assistant message. -#} |
| 126 | {%- set sns = namespace(msgs=[], ids=[]) -%} |
| 127 | {%- for m in mns.msgs -%} |
| 128 | {%- if m.role == "assistant" and m.get("tool_calls") -%} |
| 129 | {%- set sns.ids = [] -%} |
| 130 | {%- for tc in m.tool_calls -%} |
| 131 | {%- set sns.ids = sns.ids + [tc.get("id") or (tc.function.get("id", "") if tc.function is defined else "")] -%} |
| 132 | {%- endfor -%} |
| 133 | {%- set sns.msgs = sns.msgs + [m] -%} |
| 134 | {%- elif m.role == "user" and m.content_blocks -%} |
| 135 | {%- set tb = namespace(blocks=[]) -%} |
| 136 | {%- for b in m.content_blocks if b.type == "tool_result" -%} |
| 137 | {%- set order = namespace(idx=0) -%} |
| 138 | {%- for tc_id in sns.ids -%} |
| 139 | {%- if tc_id and tc_id == b.get("tool_use_id", "") -%}{%- set order.idx = loop.index0 -%}{%- endif -%} |
| 140 | {%- endfor -%} |
| 141 | {%- set tb.blocks = tb.blocks + [dict(b, _order=order.idx)] -%} |
| 142 | {%- endfor -%} |
| 143 | {%- if tb.blocks | length > 1 and sns.ids -%} |
| 144 | {%- set sorted_blocks = tb.blocks | sort(attribute="_order") -%} |
| 145 | {%- set rb = namespace(blocks=[], i=0) -%} |
| 146 | {%- for b in m.content_blocks -%} |
| 147 | {%- if b.type == "tool_result" -%} |
| 148 | {%- set rb.blocks = rb.blocks + [sorted_blocks[rb.i]] -%} |
| 149 | {%- set rb.i = rb.i + 1 -%} |
| 150 | {%- else -%} |
| 151 | {%- set rb.blocks = rb.blocks + [b] -%} |
| 152 | {%- endif -%} |
| 153 | {%- endfor -%} |
| 154 | {%- set sns.msgs = sns.msgs + [dict(m, content_blocks=rb.blocks)] -%} |
| 155 | {%- else -%} |
| 156 | {%- set sns.msgs = sns.msgs + [m] -%} |
| 157 | {%- endif -%} |
| 158 | {%- else -%} |
| 159 | {%- set sns.msgs = sns.msgs + [m] -%} |
| 160 | {%- endif -%} |
| 161 | {%- endfor -%} |
| 162 | {%- set msgs = sns.msgs -%} |
| 163 | |
| 164 | {#- Last user turn: users and mid-conversation systems count, except user turns made only of tool results. |
| 165 | Tools anywhere disable drop_thinking. -#} |
| 166 | {%- set state = namespace(last_user=-1, has_tools=false) -%} |
| 167 | {%- for m in msgs -%} |
| 168 | {%- if m.role == "user" or (m.role == "system" and loop.index0 > 0) -%} |
| 169 | {%- set real = namespace(v=true) -%} |
| 170 | {%- if m.get("content_blocks") -%} |
| 171 | {%- set real.v = false -%} |
| 172 | {%- for b in m.content_blocks if b.type in ["text", "image", "image_url"] -%}{%- set real.v = true -%}{%- endfor -%} |
| 173 | {%- endif -%} |
| 174 | {%- if real.v -%}{%- set state.last_user = loop.index0 -%}{%- endif -%} |
| 175 | {%- endif -%} |
| 176 | {%- if m.get("tools") -%}{%- set state.has_tools = true -%}{%- endif -%} |
| 177 | {%- endfor -%} |
| 178 | {%- set effective_drop = drop_thinking and not state.has_tools -%} |
| 179 | {%- set is_thinking = thinking_mode == "thinking" -%} |
| 180 | {#- Like encoding.py, thinking mode with drop_thinking silently drops messages of other roles before the last user turn. -#} |
| 181 | {%- if is_thinking and effective_drop -%} |
| 182 | {%- set kept = namespace(msgs=[]) -%} |
| 183 | {%- for m in msgs -%} |
| 184 | {%- if m.role in ["user", "system", "tool", "latest_reminder", "direct_search_results", "assistant"] or loop.index0 >= state.last_user -%} |
| 185 | {%- set kept.msgs = kept.msgs + [m] -%} |
| 186 | {%- endif -%} |
| 187 | {%- endfor -%} |
| 188 | {%- if kept.msgs | length != msgs | length -%} |
| 189 | {%- set msgs = kept.msgs -%} |
| 190 | {%- set state.last_user = -1 -%} |
| 191 | {%- for m in msgs -%} |
| 192 | {%- if m.role == "user" or (m.role == "system" and loop.index0 > 0) -%} |
| 193 | {%- set real = namespace(v=true) -%} |
| 194 | {%- if m.get("content_blocks") -%} |
| 195 | {%- set real.v = false -%} |
| 196 | {%- for b in m.content_blocks if b.type in ["text", "image", "image_url"] -%}{%- set real.v = true -%}{%- endfor -%} |
| 197 | {%- endif -%} |
| 198 | {%- if real.v -%}{%- set state.last_user = loop.index0 -%}{%- endif -%} |
| 199 | {%- endif -%} |
| 200 | {%- endfor -%} |
| 201 | {%- endif -%} |
| 202 | {%- endif -%} |
| 203 | |
| 204 | {{- "<|begin▁of▁sentence|>" -}} |
| 205 | {#- 3. Render each message. -#} |
| 206 | {%- for m in msgs -%} |
| 207 | {%- set idx = loop.index0 -%} |
| 208 | {%- set content = m.get("content") -%} |
| 209 | {%- if content is iterable and content is not string and content is not mapping -%} |
| 210 | {%- set sp = namespace(parts=[]) -%} |
| 211 | {%- for b in content -%} |
| 212 | {%- if b.type == "text" -%}{%- set sp.parts = sp.parts + [b.text or ""] -%} |
| 213 | {%- elif b.type in ["image", "image_url"] -%}{%- set sp.parts = sp.parts + [IMG] -%} |
| 214 | {%- endif -%} |
| 215 | {%- endfor -%} |
| 216 | {%- set content = sp.parts | join("\n\n") -%} |
| 217 | {%- endif -%} |
| 218 | |
| 219 | {%- if idx == 0 -%} |
| 220 | {%- if is_thinking or m.role == "system" -%}{{- "<|System|>" -}}{%- endif -%} |
| 221 | {%- if is_thinking -%}{{- "Reasoning Effort: " ~ budget ~ " (range 1-100, the higher the value, the more thorough the reasoning)\n\n" -}}{%- endif -%} |
| 222 | {%- endif -%} |
| 223 | |
| 224 | {%- if m.role == "system" -%} |
| 225 | {%- if idx > 0 -%}{{- "<|System|>" -}}{%- endif -%} |
| 226 | {{- content or "" -}} |
| 227 | {%- if m.get("tools") -%}{{- "\n\n" ~ render_tools(m.tools) -}}{%- endif -%} |
| 228 | {%- if m.get("response_format") -%} |
| 229 | {{- "\n\n## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n" ~ (m.response_format | tojson) -}} |
| 230 | {%- endif -%} |
| 231 | |
| 232 | {%- elif m.role == "user" -%} |
| 233 | {{- "<|User|>" -}} |
| 234 | {%- set up = namespace(parts=[]) -%} |
| 235 | {%- for b in m.content_blocks -%} |
| 236 | {%- if b.type == "text" -%} |
| 237 | {%- if IMG in (b.text or "") -%}{{- raise_exception("Text block contains image placeholder " ~ IMG ~ ". Images should be separate content blocks.") -}}{%- endif -%} |
| 238 | {%- set up.parts = up.parts + [b.text or ""] -%} |
| 239 | {%- elif b.type in ["image", "image_url"] -%} |
| 240 | {%- set up.parts = up.parts + [IMG] -%} |
| 241 | {%- elif b.type == "tool_result" -%} |
| 242 | {%- set tc = b.get("content", "") -%} |
| 243 | {%- if tc is iterable and tc is not string and tc is not mapping -%} |
| 244 | {%- set sub = namespace(parts=[]) -%} |
| 245 | {%- for s in tc -%} |
| 246 | {%- if s.type == "text" -%} |
| 247 | {%- if IMG in (s.text or "") -%}{{- raise_exception("Text block contains image placeholder " ~ IMG ~ ". Images should be separate content blocks.") -}}{%- endif -%} |
| 248 | {%- set sub.parts = sub.parts + [s.get("text", "")] -%} |
| 249 | {%- elif s.type in ["image", "image_url"] -%}{%- set sub.parts = sub.parts + [IMG] -%} |
| 250 | {%- else -%}{%- set sub.parts = sub.parts + ["[Unsupported " ~ s.type ~ "]"] -%} |
| 251 | {%- endif -%} |
| 252 | {%- endfor -%} |
| 253 | {%- set tc = sub.parts | join("\n\n") -%} |
| 254 | {%- endif -%} |
| 255 | {%- set up.parts = up.parts + ["<tool_result>" ~ tc ~ "</tool_result>"] -%} |
| 256 | {%- else -%} |
| 257 | {%- set up.parts = up.parts + ["[Unsupported " ~ b.type ~ "]"] -%} |
| 258 | {%- endif -%} |
| 259 | {%- endfor -%} |
| 260 | {{- up.parts | join("\n\n") -}} |
| 261 | |
| 262 | {%- elif m.role == "latest_reminder" -%} |
| 263 | {{- "<|latest_reminder|>" ~ (content or "") -}} |
| 264 | |
| 265 | {%- elif m.role == "assistant" -%} |
| 266 | {%- set prev_has_task = idx > 0 and msgs[idx - 1].get("task") is not none -%} |
| 267 | {%- if is_thinking and not prev_has_task and (not effective_drop or idx > state.last_user) -%} |
| 268 | {{- ((m.reasoning_content if m.get("reasoning_content") is not none else m.get("thinking")) or "") ~ "</think>" -}} |
| 269 | {%- endif -%} |
| 270 | {{- content or "" -}} |
| 271 | {%- if m.get("tool_calls") -%} |
| 272 | {%- set tcs = namespace(lines=[]) -%} |
| 273 | {%- for tc in m.tool_calls -%} |
| 274 | {%- set fn = tc.function if tc.function is defined else tc -%} |
| 275 | {%- set params = namespace(lines=[]) -%} |
| 276 | {%- set args = fn.arguments if fn.arguments is mapping else {"arguments": fn.arguments} -%} |
| 277 | {%- for k, v in args.items() -%} |
| 278 | {%- set params.lines = params.lines + ["<" ~ DSML ~ " parameter name=\"" ~ k ~ "\" string=\"" ~ ("true" if v is string else "false") ~ "\">" ~ (v if v is string else v | tojson) ~ "</" ~ DSML ~ " parameter>"] -%} |
| 279 | {%- endfor -%} |
| 280 | {%- set name = qualified_name(fn.name, tc.get("namespace") or fn.get("namespace")) -%} |
| 281 | {%- set tcs.lines = tcs.lines + ["<" ~ DSML ~ " invoke name=\"" ~ name ~ "\">\n" ~ params.lines | join("\n") ~ "\n</" ~ DSML ~ " invoke>"] -%} |
| 282 | {%- endfor -%} |
| 283 | {{- "\n\n<" ~ DSML ~ " calls>\n" ~ tcs.lines | join("\n") ~ "\n</" ~ DSML ~ " calls>" -}} |
| 284 | {%- endif -%} |
| 285 | {%- if not m.get("wo_eos", false) -%}{{- "<|end▁of▁sentence|>" -}}{%- endif -%} |
| 286 | |
| 287 | {%- else -%} |
| 288 | {{- raise_exception("Unknown role: " ~ m.role) -}} |
| 289 | {%- endif -%} |
| 290 | |
| 291 | {#- Transition tokens, only when the next message is an assistant/latest_reminder turn or this is the last one. -#} |
| 292 | {%- if loop.last or msgs[idx + 1].role in ["assistant", "latest_reminder"] -%} |
| 293 | {%- set task = m.get("task") -%} |
| 294 | {%- if task is not none -%} |
| 295 | {%- set task_tokens = {"action": "<|action|>", "query": "<|query|>", "authority": "<|authority|>", "domain": "<|domain|>", "title": "<|title|>", "read_url": "<|read_url|>"} -%} |
| 296 | {%- if task not in task_tokens -%}{{- raise_exception("Invalid task: '" ~ task ~ "'") -}}{%- endif -%} |
| 297 | {%- if task == "action" -%}{{- "<|Assistant|>" ~ ("<think>" if is_thinking else "</think>") -}}{%- endif -%} |
| 298 | {{- task_tokens[task] -}} |
| 299 | {%- elif (m.role == "user" or (m.role == "system" and idx > 0)) and (add_generation_prompt or not loop.last) -%} |
| 300 | {{- "<|Assistant|>" ~ ("<think>" if is_thinking and (not effective_drop or idx >= state.last_user) else "</think>") -}} |
| 301 | {%- endif -%} |
| 302 | {%- endif -%} |
| 303 | {%- endfor -%} |
| 304 | |