chat_template.jinja
18.1 KB · 304 lines · plaintext Raw
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