|
11 | 11 | description:<|"|>{{ value['description'] }}<|"|> |
12 | 12 | {%- set add_comma = true -%} |
13 | 13 | {%- endif -%} |
14 | | - {%- if value['nullable'] %} |
15 | | - {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
16 | | - nullable:true |
17 | | - {%- endif -%} |
18 | 14 | {%- if value['type'] | upper == 'STRING' -%} |
19 | 15 | {%- if value['enum'] -%} |
20 | 16 | {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
21 | 17 | enum:{{ format_argument(value['enum']) }} |
22 | 18 | {%- endif -%} |
23 | | - {%- elif value['type'] | upper == 'OBJECT' -%} |
24 | | - ,properties:{ |
25 | | - {%- if value['properties'] is defined and value['properties'] is mapping -%} |
26 | | - {{- format_parameters(value['properties'], value['required'] | default([])) -}} |
27 | | - {%- elif value is mapping -%} |
28 | | - {{- format_parameters(value, value['required'] | default([])) -}} |
29 | | - {%- endif -%} |
30 | | - } |
31 | | - {%- if value['required'] -%} |
32 | | - ,required:[ |
33 | | - {%- for item in value['required'] | default([]) -%} |
34 | | - <|"|>{{- item -}}<|"|> |
35 | | - {%- if not loop.last %},{% endif -%} |
36 | | - {%- endfor -%} |
37 | | - ] |
38 | | - {%- endif -%} |
39 | 19 | {%- elif value['type'] | upper == 'ARRAY' -%} |
40 | 20 | {%- if value['items'] is mapping and value['items'] -%} |
41 | | - ,items:{ |
| 21 | + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
| 22 | + items:{ |
42 | 23 | {%- set ns_items = namespace(found_first=false) -%} |
43 | 24 | {%- for item_key, item_value in value['items'] | dictsort -%} |
44 | 25 | {%- if item_value is not none -%} |
|
71 | 52 | } |
72 | 53 | {%- endif -%} |
73 | 54 | {%- endif -%} |
| 55 | + {%- if value['nullable'] %} |
| 56 | + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
| 57 | + nullable:true |
| 58 | + {%- endif -%} |
| 59 | + {%- if value['type'] | upper == 'OBJECT' -%} |
| 60 | + {%- if value['properties'] is defined and value['properties'] is mapping -%} |
| 61 | + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
| 62 | + properties:{ |
| 63 | + {{- format_parameters(value['properties'], value['required'] | default([])) -}} |
| 64 | + } |
| 65 | + {%- elif value is mapping -%} |
| 66 | + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
| 67 | + properties:{ |
| 68 | + {{- format_parameters(value, value['required'] | default([])) -}} |
| 69 | + } |
| 70 | + {%- endif -%} |
| 71 | + {%- if value['required'] -%} |
| 72 | + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
| 73 | + required:[ |
| 74 | + {%- for item in value['required'] | default([]) -%} |
| 75 | + <|"|>{{- item -}}<|"|> |
| 76 | + {%- if not loop.last %},{% endif -%} |
| 77 | + {%- endfor -%} |
| 78 | + ] |
| 79 | + {%- endif -%} |
| 80 | + {%- endif -%} |
74 | 81 | {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} |
75 | 82 | type:<|"|>{{ value['type'] | upper }}<|"|>} |
76 | 83 | {%- endif -%} |
|
150 | 157 | {{- ns.result | trim -}} |
151 | 158 | {%- endmacro -%} |
152 | 159 |
|
| 160 | +{%- macro format_tool_response_block(tool_name, response) -%} |
| 161 | + {{- '<|tool_response>' -}} |
| 162 | + {%- if response is mapping -%} |
| 163 | + {{- 'response:' + tool_name + '{' -}} |
| 164 | + {%- for key, value in response | dictsort -%} |
| 165 | + {{- key -}}:{{- format_argument(value, escape_keys=False) -}} |
| 166 | + {%- if not loop.last %},{% endif -%} |
| 167 | + {%- endfor -%} |
| 168 | + {{- '}' -}} |
| 169 | + {%- else -%} |
| 170 | + {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}} |
| 171 | + {%- endif -%} |
| 172 | + {{- '<tool_response|>' -}} |
| 173 | +{%- endmacro -%} |
| 174 | + |
153 | 175 | {%- set ns = namespace(prev_message_type=None) -%} |
154 | 176 | {%- set loop_messages = messages -%} |
155 | | -{{ bos_token }} |
| 177 | +{{- bos_token -}} |
156 | 178 | {#- Handle System/Tool Definitions Block -#} |
157 | 179 | {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} |
158 | 180 | {{- '<|turn>system\n' -}} |
159 | 181 |
|
160 | 182 | {#- Inject Thinking token at the very top of the FIRST system turn -#} |
161 | 183 | {%- if enable_thinking is defined and enable_thinking -%} |
162 | | - {{- '<|think|>' -}} |
| 184 | + {{- '<|think|>\n' -}} |
163 | 185 | {%- set ns.prev_message_type = 'think' -%} |
164 | 186 | {%- endif -%} |
165 | 187 |
|
|
180 | 202 | {{- '<turn|>\n' -}} |
181 | 203 | {%- endif %} |
182 | 204 |
|
| 205 | +{#- Pre-scan: find last user message index for reasoning guard -#} |
| 206 | +{%- set ns_turn = namespace(last_user_idx=-1) -%} |
| 207 | +{%- for i in range(loop_messages | length) -%} |
| 208 | + {%- if loop_messages[i]['role'] == 'user' -%} |
| 209 | + {%- set ns_turn.last_user_idx = i -%} |
| 210 | + {%- endif -%} |
| 211 | +{%- endfor -%} |
| 212 | + |
183 | 213 | {#- Loop through messages -#} |
184 | 214 | {%- for message in loop_messages -%} |
| 215 | + {%- if message['role'] != 'tool' -%} |
185 | 216 | {%- set ns.prev_message_type = None -%} |
186 | 217 | {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} |
| 218 | + {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#} |
| 219 | + {%- set prev_nt = namespace(role=None, found=false) -%} |
| 220 | + {%- if loop.index0 > 0 -%} |
| 221 | + {%- for j in range(loop.index0 - 1, -1, -1) -%} |
| 222 | + {%- if not prev_nt.found -%} |
| 223 | + {%- if loop_messages[j]['role'] != 'tool' -%} |
| 224 | + {%- set prev_nt.role = loop_messages[j]['role'] -%} |
| 225 | + {%- set prev_nt.found = true -%} |
| 226 | + {%- endif -%} |
| 227 | + {%- endif -%} |
| 228 | + {%- endfor -%} |
| 229 | + {%- endif -%} |
| 230 | + {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%} |
| 231 | + {%- if not continue_same_model_turn -%} |
187 | 232 | {{- '<|turn>' + role + '\n' }} |
| 233 | + {%- endif -%} |
| 234 | + |
| 235 | + {#- Render reasoning/reasoning_content as thinking channel -#} |
| 236 | + {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%} |
| 237 | + {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%} |
| 238 | + {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}} |
| 239 | + {%- endif -%} |
188 | 240 |
|
189 | 241 | {%- if message['tool_calls'] -%} |
190 | 242 | {%- for tool_call in message['tool_calls'] -%} |
|
205 | 257 | {%- set ns.prev_message_type = 'tool_call' -%} |
206 | 258 | {%- endif -%} |
207 | 259 |
|
208 | | - {%- if message['tool_responses'] -%} |
209 | | - {#- Tool Response handling -#} |
| 260 | + {%- set ns_tr_out = namespace(flag=false) -%} |
| 261 | + {%- if message.get('tool_responses') -%} |
| 262 | + {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#} |
210 | 263 | {%- for tool_response in message['tool_responses'] -%} |
211 | | - {{- '<|tool_response>' -}} |
212 | | - {%- if tool_response['response'] is mapping -%} |
213 | | - {{- 'response:' + tool_response['name'] | default('unknown') + '{' -}} |
214 | | - {%- for key, value in tool_response['response'] | dictsort -%} |
215 | | - {{- key -}}:{{- format_argument(value, escape_keys=False) -}} |
216 | | - {%- if not loop.last %},{% endif -%} |
217 | | - {%- endfor -%} |
218 | | - {{- '}' -}} |
| 264 | + {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}} |
| 265 | + {%- set ns_tr_out.flag = true -%} |
| 266 | + {%- set ns.prev_message_type = 'tool_response' -%} |
| 267 | + {%- endfor -%} |
| 268 | + {%- elif message.get('tool_calls') -%} |
| 269 | + {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#} |
| 270 | + {%- set ns_tool_scan = namespace(stopped=false) -%} |
| 271 | + {%- for k in range(loop.index0 + 1, loop_messages | length) -%} |
| 272 | + {%- if ns_tool_scan.stopped -%} |
| 273 | + {%- elif loop_messages[k]['role'] != 'tool' -%} |
| 274 | + {%- set ns_tool_scan.stopped = true -%} |
219 | 275 | {%- else -%} |
220 | | - {{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}} |
| 276 | + {%- set follow = loop_messages[k] -%} |
| 277 | + {#- Resolve tool_call_id to function name -#} |
| 278 | + {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%} |
| 279 | + {%- for tc in message['tool_calls'] -%} |
| 280 | + {%- if tc.get('id') == follow.get('tool_call_id') -%} |
| 281 | + {%- set ns_tname.name = tc['function']['name'] -%} |
| 282 | + {%- endif -%} |
| 283 | + {%- endfor -%} |
| 284 | + {#- Handle content as string or content-parts array -#} |
| 285 | + {%- set tool_body = follow.get('content') -%} |
| 286 | + {%- if tool_body is string -%} |
| 287 | + {{- format_tool_response_block(ns_tname.name, tool_body) -}} |
| 288 | + {%- elif tool_body is sequence and tool_body is not string -%} |
| 289 | + {%- set ns_txt = namespace(s='') -%} |
| 290 | + {%- for part in tool_body -%} |
| 291 | + {%- if part.get('type') == 'text' -%} |
| 292 | + {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%} |
| 293 | + {%- endif -%} |
| 294 | + {%- endfor -%} |
| 295 | + {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}} |
| 296 | + {%- else -%} |
| 297 | + {{- format_tool_response_block(ns_tname.name, tool_body) -}} |
| 298 | + {%- endif -%} |
| 299 | + {%- set ns_tr_out.flag = true -%} |
| 300 | + {%- set ns.prev_message_type = 'tool_response' -%} |
221 | 301 | {%- endif -%} |
222 | | - {{- '<tool_response|>' -}} |
223 | 302 | {%- endfor -%} |
224 | | - {%- set ns.prev_message_type = 'tool_response' -%} |
225 | 303 | {%- endif -%} |
226 | 304 |
|
227 | 305 | {%- if message['content'] is string -%} |
|
239 | 317 | {{- item['text'] | trim -}} |
240 | 318 | {%- endif -%} |
241 | 319 | {%- elif item['type'] == 'image' -%} |
242 | | - {{- '\n\n<|image|>\n\n' -}} |
| 320 | + {{- '<|image|>' -}} |
243 | 321 | {%- set ns.prev_message_type = 'image' -%} |
244 | 322 | {%- elif item['type'] == 'audio' -%} |
245 | 323 | {{- '<|audio|>' -}} |
246 | 324 | {%- set ns.prev_message_type = 'audio' -%} |
247 | 325 | {%- elif item['type'] == 'video' -%} |
248 | | - {{- '\n\n<|video|>\n\n' -}} |
| 326 | + {{- '<|video|>' -}} |
249 | 327 | {%- set ns.prev_message_type = 'video' -%} |
250 | 328 | {%- endif -%} |
251 | 329 | {%- endfor -%} |
252 | 330 | {%- endif -%} |
253 | 331 |
|
254 | | - {%- if not (message['tool_responses'] and not message['content']) -%} |
| 332 | + {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%} |
| 333 | + {{- '<|tool_response>' -}} |
| 334 | + {%- elif not (ns_tr_out.flag and not message.get('content')) -%} |
255 | 335 | {{- '<turn|>\n' -}} |
256 | 336 | {%- endif -%} |
| 337 | + {%- endif -%} |
257 | 338 | {%- endfor -%} |
258 | 339 |
|
259 | 340 | {%- if add_generation_prompt -%} |
260 | | - {%- if ns.prev_message_type != 'tool_response' -%} |
| 341 | + {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%} |
261 | 342 | {{- '<|turn>model\n' -}} |
262 | | - {%- endif -%} |
263 | | - {%- if not enable_thinking | default(false) -%} |
264 | | - {{- '<|channel>thought\n<channel|>' -}} |
| 343 | + {%- if not enable_thinking | default(false) -%} |
| 344 | + {{- '<|channel>thought\n<channel|>' -}} |
| 345 | + {%- endif -%} |
265 | 346 | {%- endif -%} |
266 | 347 | {%- endif -%} |
0 commit comments