Skip to content

Commit 4c76996

Browse files
authored
common : better align to the updated official gemma4 template (ggml-org#21704)
1 parent 52b9a0a commit 4c76996

3 files changed

Lines changed: 134 additions & 48 deletions

File tree

common/chat.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1916,7 +1916,12 @@ std::optional<common_chat_params> common_chat_try_specialized_template(
19161916

19171917
// Gemma4 format detection
19181918
if (src.find("'<|tool_call>call:'") != std::string::npos) {
1919-
workaround::convert_tool_responses_gemma4(params.messages);
1919+
if (src.find("{#- OpenAI Chat Completions:") == std::string::npos) {
1920+
// apply workarounds if using the older gemma4 templates
1921+
LOG_WRN("%s: detected an outdated gemma4 chat template, applying compatibility workarounds. "
1922+
"Consider updating to the official template.\n", __func__);
1923+
workaround::convert_tool_responses_gemma4(params.messages);
1924+
}
19201925
return common_chat_params_init_gemma4(tmpl, params);
19211926
}
19221927

models/templates/google-gemma-4-31B-it-interleaved.jinja

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,14 @@
152152

153153
{%- set ns = namespace(prev_message_type=None, last_user_message=-1) -%}
154154
{%- set loop_messages = messages -%}
155-
{{ bos_token }}
155+
{{- bos_token -}}
156156
{#- Handle System/Tool Definitions Block -#}
157157
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
158158
{{- '<|turn>system\n' -}}
159159

160160
{#- Inject Thinking token at the very top of the FIRST system turn -#}
161161
{%- if enable_thinking is defined and enable_thinking -%}
162-
{{- '<|think|>' -}}
162+
{{- '<|think|>\n' -}}
163163
{%- set ns.prev_message_type = 'think' -%}
164164
{%- endif -%}
165165

@@ -255,13 +255,13 @@
255255
{{- item['text'] | trim -}}
256256
{%- endif -%}
257257
{%- elif item['type'] == 'image' -%}
258-
{{- '\n\n<|image|>\n\n' -}}
258+
{{- '<|image|>' -}}
259259
{%- set ns.prev_message_type = 'image' -%}
260260
{%- elif item['type'] == 'audio' -%}
261261
{{- '<|audio|>' -}}
262262
{%- set ns.prev_message_type = 'audio' -%}
263263
{%- elif item['type'] == 'video' -%}
264-
{{- '\n\n<|video|>\n\n' -}}
264+
{{- '<|video|>' -}}
265265
{%- set ns.prev_message_type = 'video' -%}
266266
{%- endif -%}
267267
{%- endfor -%}

models/templates/google-gemma-4-31B-it.jinja

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,15 @@
1111
description:<|"|>{{ value['description'] }}<|"|>
1212
{%- set add_comma = true -%}
1313
{%- endif -%}
14-
{%- if value['nullable'] %}
15-
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
16-
nullable:true
17-
{%- endif -%}
1814
{%- if value['type'] | upper == 'STRING' -%}
1915
{%- if value['enum'] -%}
2016
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
2117
enum:{{ format_argument(value['enum']) }}
2218
{%- 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 -%}
3919
{%- elif value['type'] | upper == 'ARRAY' -%}
4020
{%- if value['items'] is mapping and value['items'] -%}
41-
,items:{
21+
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
22+
items:{
4223
{%- set ns_items = namespace(found_first=false) -%}
4324
{%- for item_key, item_value in value['items'] | dictsort -%}
4425
{%- if item_value is not none -%}
@@ -71,6 +52,32 @@
7152
}
7253
{%- endif -%}
7354
{%- 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 -%}
7481
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
7582
type:<|"|>{{ value['type'] | upper }}<|"|>}
7683
{%- endif -%}
@@ -150,16 +157,31 @@
150157
{{- ns.result | trim -}}
151158
{%- endmacro -%}
152159

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+
153175
{%- set ns = namespace(prev_message_type=None) -%}
154176
{%- set loop_messages = messages -%}
155-
{{ bos_token }}
177+
{{- bos_token -}}
156178
{#- Handle System/Tool Definitions Block -#}
157179
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
158180
{{- '<|turn>system\n' -}}
159181

160182
{#- Inject Thinking token at the very top of the FIRST system turn -#}
161183
{%- if enable_thinking is defined and enable_thinking -%}
162-
{{- '<|think|>' -}}
184+
{{- '<|think|>\n' -}}
163185
{%- set ns.prev_message_type = 'think' -%}
164186
{%- endif -%}
165187

@@ -180,11 +202,41 @@
180202
{{- '<turn|>\n' -}}
181203
{%- endif %}
182204

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+
183213
{#- Loop through messages -#}
184214
{%- for message in loop_messages -%}
215+
{%- if message['role'] != 'tool' -%}
185216
{%- set ns.prev_message_type = None -%}
186217
{%- 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 -%}
187232
{{- '<|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 -%}
188240

189241
{%- if message['tool_calls'] -%}
190242
{%- for tool_call in message['tool_calls'] -%}
@@ -205,23 +257,49 @@
205257
{%- set ns.prev_message_type = 'tool_call' -%}
206258
{%- endif -%}
207259

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) -#}
210263
{%- 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 -%}
219275
{%- 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' -%}
221301
{%- endif -%}
222-
{{- '<tool_response|>' -}}
223302
{%- endfor -%}
224-
{%- set ns.prev_message_type = 'tool_response' -%}
225303
{%- endif -%}
226304

227305
{%- if message['content'] is string -%}
@@ -239,28 +317,31 @@
239317
{{- item['text'] | trim -}}
240318
{%- endif -%}
241319
{%- elif item['type'] == 'image' -%}
242-
{{- '\n\n<|image|>\n\n' -}}
320+
{{- '<|image|>' -}}
243321
{%- set ns.prev_message_type = 'image' -%}
244322
{%- elif item['type'] == 'audio' -%}
245323
{{- '<|audio|>' -}}
246324
{%- set ns.prev_message_type = 'audio' -%}
247325
{%- elif item['type'] == 'video' -%}
248-
{{- '\n\n<|video|>\n\n' -}}
326+
{{- '<|video|>' -}}
249327
{%- set ns.prev_message_type = 'video' -%}
250328
{%- endif -%}
251329
{%- endfor -%}
252330
{%- endif -%}
253331

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')) -%}
255335
{{- '<turn|>\n' -}}
256336
{%- endif -%}
337+
{%- endif -%}
257338
{%- endfor -%}
258339

259340
{%- 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' -%}
261342
{{- '<|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 -%}
265346
{%- endif -%}
266347
{%- endif -%}

0 commit comments

Comments
 (0)