Preserve the correct ordering of content and tool calls when rendering OpenAI Chat Completions

#12
Files changed (3) hide show
  1. chat_template.jinja +74 -50
  2. config.json +1 -1
  3. model.safetensors +1 -1
chat_template.jinja CHANGED
@@ -172,16 +172,25 @@
172
  {{- '<tool_response|>' -}}
173
  {%- endmacro -%}
174
 
175
- {%- set ns = namespace(prev_message_type=None) -%}
 
 
 
 
 
 
 
 
 
 
176
  {%- set loop_messages = messages -%}
177
  {{- bos_token -}}
178
  {#- Handle System/Tool Definitions Block -#}
179
  {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
180
- {{- '<|turn>system\n' -}}
181
  {#- Inject Thinking token at the very top of the FIRST system turn -#}
182
  {%- if enable_thinking is defined and enable_thinking -%}
183
  {{- '<|think|>\n' -}}
184
- {%- set ns.prev_message_type = 'think' -%}
185
  {%- endif -%}
186
  {%- if messages[0]['role'] in ['system', 'developer'] -%}
187
  {%- if messages[0]['content'] is string -%}
@@ -199,9 +208,8 @@
199
  {{- format_function_declaration(tool) | trim -}}
200
  {{- '<tool|>' -}}
201
  {%- endfor %}
202
- {%- set ns.prev_message_type = 'tool' -%}
203
  {%- endif -%}
204
- {{- '<turn|>\n' -}}
205
  {%- endif %}
206
 
207
  {#- Pre-scan: find last user message index for reasoning guard -#}
@@ -215,31 +223,24 @@
215
  {#- Loop through messages -#}
216
  {%- for message in loop_messages -%}
217
  {%- if message['role'] != 'tool' -%}
218
- {%- set ns.prev_message_type = None -%}
219
  {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
220
- {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
221
- {%- set prev_nt = namespace(role=None, found=false) -%}
222
- {%- if loop.index0 > 0 -%}
223
- {%- for j in range(loop.index0 - 1, -1, -1) -%}
224
- {%- if not prev_nt.found -%}
225
- {%- if loop_messages[j]['role'] != 'tool' -%}
226
- {%- set prev_nt.role = loop_messages[j]['role'] -%}
227
- {%- set prev_nt.found = true -%}
228
- {%- endif -%}
229
- {%- endif -%}
230
- {%- endfor -%}
231
- {%- endif -%}
232
- {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
233
- {%- if not continue_same_model_turn -%}
234
- {{- '<|turn>' + role + '\n' }}
235
  {%- endif -%}
236
 
237
- {#- Render reasoning/reasoning_content as thinking channel -#}
238
- {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
239
- {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
240
- {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
241
- {%- endif -%}
 
242
 
 
243
  {%- if message['tool_calls'] -%}
244
  {%- for tool_call in message['tool_calls'] -%}
245
  {%- set function = tool_call['function'] -%}
@@ -256,18 +257,20 @@
256
  {%- endif -%}
257
  {{- '}<tool_call|>' -}}
258
  {%- endfor -%}
259
- {%- set ns.prev_message_type = 'tool_call' -%}
260
  {%- endif -%}
 
261
 
262
- {%- set ns_tr_out = namespace(flag=false) -%}
263
  {%- if message.get('tool_responses') -%}
264
  {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
265
  {%- for tool_response in message['tool_responses'] -%}
266
- {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
267
- {%- set ns_tr_out.flag = true -%}
268
- {%- set ns.prev_message_type = 'tool_response' -%}
269
  {%- endfor -%}
270
- {%- elif message.get('tool_calls') -%}
 
 
 
 
271
  {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
272
  {%- set ns_tool_scan = namespace(stopped=false) -%}
273
  {%- for k in range(loop.index0 + 1, loop_messages | length) -%}
@@ -277,7 +280,7 @@
277
  {%- else -%}
278
  {%- set follow = loop_messages[k] -%}
279
  {#- Resolve tool_call_id to function name -#}
280
- {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
281
  {%- for tc in message['tool_calls'] -%}
282
  {%- if tc.get('id') == follow.get('tool_call_id') -%}
283
  {%- set ns_tname.name = tc['function']['name'] -%}
@@ -307,13 +310,12 @@
307
  {%- else -%}
308
  {{- format_tool_response_block(ns_tname.name, tool_body) -}}
309
  {%- endif -%}
310
- {%- set ns_tr_out.flag = true -%}
311
- {%- set ns.prev_message_type = 'tool_response' -%}
312
  {%- endif -%}
313
  {%- endfor -%}
314
  {%- endif -%}
 
315
 
316
- {%- set captured_content -%}
317
  {%- if message['content'] is string -%}
318
  {%- if role == 'model' -%}
319
  {{- strip_thinking(message['content']) -}}
@@ -330,34 +332,56 @@
330
  {%- endif -%}
331
  {%- elif item['type'] == 'image' -%}
332
  {{- '<|image|>' -}}
333
- {%- set ns.prev_message_type = 'image' -%}
334
  {%- elif item['type'] == 'audio' -%}
335
  {{- '<|audio|>' -}}
336
- {%- set ns.prev_message_type = 'audio' -%}
337
  {%- elif item['type'] == 'video' -%}
338
  {{- '<|video|>' -}}
339
- {%- set ns.prev_message_type = 'video' -%}
340
  {%- endif -%}
341
  {%- endfor -%}
342
  {%- endif -%}
343
- {%- endset -%}
344
 
 
 
345
  {{- captured_content -}}
346
- {%- set has_content = captured_content | trim | length > 0 -%}
347
-
348
- {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
349
- {{- '<|tool_response>' -}}
350
- {%- elif not (ns_tr_out.flag and not has_content) -%}
351
- {{- '<turn|>\n' -}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  {%- endif -%}
353
  {%- endif -%}
354
  {%- endfor -%}
355
 
356
  {%- if add_generation_prompt -%}
357
- {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
358
- {{- '<|turn>model\n' -}}
359
- {%- if not enable_thinking | default(false) -%}
360
- {{- '<|channel>thought\n<channel|>' -}}
361
  {%- endif -%}
 
 
 
 
362
  {%- endif -%}
363
  {%- endif -%}
 
172
  {{- '<tool_response|>' -}}
173
  {%- endmacro -%}
174
 
175
+ {%- macro open_turn(role, ns) -%}
176
+ {%- set ns.open_turn_role = role -%}
177
+ {{- '<|turn>' + role + '\n' -}}
178
+ {%- endmacro -%}
179
+
180
+ {%- macro end_turn(ns) -%}
181
+ {%- set ns.open_turn_role = none -%}
182
+ {{- '<turn|>\n' -}}
183
+ {%- endmacro -%}
184
+
185
+ {%- set ns = namespace(open_turn_role=none) -%}
186
  {%- set loop_messages = messages -%}
187
  {{- bos_token -}}
188
  {#- Handle System/Tool Definitions Block -#}
189
  {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
190
+ {{- open_turn('system', ns) -}}
191
  {#- Inject Thinking token at the very top of the FIRST system turn -#}
192
  {%- if enable_thinking is defined and enable_thinking -%}
193
  {{- '<|think|>\n' -}}
 
194
  {%- endif -%}
195
  {%- if messages[0]['role'] in ['system', 'developer'] -%}
196
  {%- if messages[0]['content'] is string -%}
 
208
  {{- format_function_declaration(tool) | trim -}}
209
  {{- '<tool|>' -}}
210
  {%- endfor %}
 
211
  {%- endif -%}
212
+ {{- end_turn(ns) -}}
213
  {%- endif %}
214
 
215
  {#- Pre-scan: find last user message index for reasoning guard -#}
 
223
  {#- Loop through messages -#}
224
  {%- for message in loop_messages -%}
225
  {%- if message['role'] != 'tool' -%}
 
226
  {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
227
+ {%- if ns.open_turn_role -%}
228
+ {%- if ns.open_turn_role != role -%}
229
+ {{- end_turn(ns) -}}
230
+ {{- open_turn(role, ns) -}}
231
+ {%- endif -%}
232
+ {%- else -%}
233
+ {{- open_turn(role, ns) -}}
 
 
 
 
 
 
 
 
234
  {%- endif -%}
235
 
236
+ {%- set reasoning_block -%}
237
+ {%- set thinking_text = message['reasoning'] | default(message['reasoning_content']) | default(none) -%}
238
+ {%- if thinking_text is not none and loop.index0 > ns_turn.last_user_idx -%}
239
+ {{- '<|channel>thought\n' + thinking_text + '<channel|>' -}}
240
+ {%- endif -%}
241
+ {%- endset -%}
242
 
243
+ {%- set tool_calls_block -%}
244
  {%- if message['tool_calls'] -%}
245
  {%- for tool_call in message['tool_calls'] -%}
246
  {%- set function = tool_call['function'] -%}
 
257
  {%- endif -%}
258
  {{- '}<tool_call|>' -}}
259
  {%- endfor -%}
 
260
  {%- endif -%}
261
+ {%- endset -%}
262
 
263
+ {%- set legacy_tool_responses_block -%}
264
  {%- if message.get('tool_responses') -%}
265
  {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
266
  {%- for tool_response in message['tool_responses'] -%}
267
+ {{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}
 
 
268
  {%- endfor -%}
269
+ {%- endif -%}
270
+ {%- endset -%}
271
+
272
+ {%- set openai_tool_responses_block -%}
273
+ {%- if message.get('tool_calls') and not message.get('tool_responses') -%}
274
  {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
275
  {%- set ns_tool_scan = namespace(stopped=false) -%}
276
  {%- for k in range(loop.index0 + 1, loop_messages | length) -%}
 
280
  {%- else -%}
281
  {%- set follow = loop_messages[k] -%}
282
  {#- Resolve tool_call_id to function name -#}
283
+ {%- set ns_tname = namespace(name=follow.get('name') | default('unknown', true)) -%}
284
  {%- for tc in message['tool_calls'] -%}
285
  {%- if tc.get('id') == follow.get('tool_call_id') -%}
286
  {%- set ns_tname.name = tc['function']['name'] -%}
 
310
  {%- else -%}
311
  {{- format_tool_response_block(ns_tname.name, tool_body) -}}
312
  {%- endif -%}
 
 
313
  {%- endif -%}
314
  {%- endfor -%}
315
  {%- endif -%}
316
+ {%- endset -%}
317
 
318
+ {%- set captured_content -%}
319
  {%- if message['content'] is string -%}
320
  {%- if role == 'model' -%}
321
  {{- strip_thinking(message['content']) -}}
 
332
  {%- endif -%}
333
  {%- elif item['type'] == 'image' -%}
334
  {{- '<|image|>' -}}
 
335
  {%- elif item['type'] == 'audio' -%}
336
  {{- '<|audio|>' -}}
 
337
  {%- elif item['type'] == 'video' -%}
338
  {{- '<|video|>' -}}
 
339
  {%- endif -%}
340
  {%- endfor -%}
341
  {%- endif -%}
342
+ {%- endset -%}
343
 
344
+ {%- if role != 'model' -%}
345
+ {#- Non-model turn -#}
346
  {{- captured_content -}}
347
+ {{- end_turn(ns) -}}
348
+ {%- elif message.get('tool_responses') -%}
349
+ {#- Model turn with legacy tool -#}
350
+ {{- reasoning_block -}}
351
+ {{- tool_calls_block -}}
352
+ {{- legacy_tool_responses_block -}}
353
+ {%- if captured_content -%}
354
+ {{- captured_content -}}
355
+ {{- end_turn(ns) -}}
356
+ {%- endif -%}
357
+ {%- elif message.get('tool_calls') -%}
358
+ {#- Model turn with OpenAI tool -#}
359
+ {{- reasoning_block -}}
360
+ {{- captured_content -}}
361
+ {{- tool_calls_block -}}
362
+ {%- if openai_tool_responses_block -%}
363
+ {{- openai_tool_responses_block -}}
364
+ {%- elif loop.last -%}
365
+ {{- '<|tool_response>' -}}
366
+ {%- endif -%}
367
+ {%- else -%}
368
+ {#- Model turn without tool -#}
369
+ {{- reasoning_block -}}
370
+ {{- captured_content -}}
371
+ {{- end_turn(ns) -}}
372
  {%- endif -%}
373
  {%- endif -%}
374
  {%- endfor -%}
375
 
376
  {%- if add_generation_prompt -%}
377
+ {#- Close previous non-model turn and start a new model turn -#}
378
+ {%- if ns.open_turn_role != 'model' -%}
379
+ {%- if ns.open_turn_role -%}
380
+ {{- end_turn(ns) -}}
381
  {%- endif -%}
382
+ {{- open_turn('model', ns) -}}
383
+ {%- endif -%}
384
+ {%- if not enable_thinking | default(false) -%}
385
+ {{- '<|channel>thought\n<channel|>' -}}
386
  {%- endif -%}
387
  {%- endif -%}
config.json CHANGED
@@ -106,7 +106,7 @@
106
  "sliding_attention",
107
  "full_attention"
108
  ],
109
- "max_position_embeddings": 262144,
110
  "model_type": "gemma4_unified_text",
111
  "moe_intermediate_size": null,
112
  "num_attention_heads": 16,
 
106
  "sliding_attention",
107
  "full_attention"
108
  ],
109
+ "max_position_embeddings": 131072,
110
  "model_type": "gemma4_unified_text",
111
  "moe_intermediate_size": null,
112
  "num_attention_heads": 16,
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5a84cb313260ac447237b890387116dfa8682e49a6b44bc585ae8353abbff18d
3
  size 23919549408
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:366b79fc7e2ea81106d45e2b3ca10e144925f93dd9d456396692825ddb7bb788
3
  size 23919549408