A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 4, 08:45 UTC
[Fix] Sync gemma4 chat template from hf (#39570)
Signed-off-by: Frederic Odermatt <frederic.odermatt@44ai.ch>
FredericOdermatt committed
May 2, 2026 at 05:06 UTC
c408fdd663afb34ab82a10b26f553bec9e8052d9
1 file changed
+67
-44
examples/tool_chat_template_gemma4.jinja
+67
-44
@@ -1,9 +1,9 @@
1
-{%- macro format_parameters(properties, required) -%}
1
+{%- macro format_parameters(properties, required, filter_keys=false) -%}
2
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
3
{%- set ns = namespace(found_first=false) -%}
4
{%- for key, value in properties | dictsort -%}
5
{%- set add_comma = false -%}
6
- {%- if key not in standard_keys -%}
6
+ {%- if not filter_keys or key not in standard_keys -%}
7
{%- if ns.found_first %},{% endif -%}
8
{%- set ns.found_first = true -%}
9
{{ key }}:{
@@ -11,34 +11,15 @@
11
description:<|"|>{{ value['description'] }}<|"|>
12
{%- set add_comma = true -%}
13
{%- endif -%}
14
- {%- if value['nullable'] %}
15
- {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
16
- nullable:true
17
- {%- endif -%}
14
{%- if value['type'] | upper == 'STRING' -%}
15
{%- if value['enum'] -%}
16
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
17
enum:{{ format_argument(value['enum']) }}
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 -%}
19
{%- elif value['type'] | upper == 'ARRAY' -%}
20
{%- if value['items'] is mapping and value['items'] -%}
41
- ,items:{
21
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
22
+ items:{
23
{%- set ns_items = namespace(found_first=false) -%}
24
{%- for item_key, item_value in value['items'] | dictsort -%}
25
{%- if item_value is not none -%}
@@ -71,6 +52,32 @@
52
}
53
{%- endif -%}
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([]), filter_keys=true) -}}
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 -%}
81
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
82
type:<|"|>{{ value['type'] | upper }}<|"|>}
83
{%- endif -%}
@@ -167,20 +174,25 @@
174
175
{%- set ns = namespace(prev_message_type=None) -%}
176
{%- set loop_messages = messages -%}
170
-{{ bos_token }}
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' -}}
173
-
181
+ {#- Inject Thinking token at the very top of the FIRST system turn -#}
182
{%- if enable_thinking is defined and enable_thinking -%}
175
- {{- '<|think|>' -}}
183
+ {{- '<|think|>\n' -}}
184
{%- set ns.prev_message_type = 'think' -%}
185
{%- endif -%}
178
-
186
{%- if messages[0]['role'] in ['system', 'developer'] -%}
180
- {{- messages[0]['content'] | trim -}}
187
+ {%- if messages[0]['content'] is string -%}
188
+ {{- messages[0]['content'] | trim -}}
189
+ {%- elif messages[0]['content'] is sequence -%}
190
+ {%- for item in messages[0]['content'] -%}
191
+ {{- item['text'] | trim + ' '-}}
192
+ {%- endfor -%}
193
+ {%- endif -%}
194
{%- set loop_messages = messages[1:] -%}
195
{%- endif -%}
183
-
196
{%- if tools -%}
197
{%- for tool in tools %}
198
{{- '<|tool>' -}}
@@ -189,10 +201,10 @@
201
{%- endfor %}
202
{%- set ns.prev_message_type = 'tool' -%}
203
{%- endif -%}
192
-
204
{{- '<turn|>\n' -}}
205
{%- endif %}
206
207
+{#- Pre-scan: find last user message index for reasoning guard -#}
208
{%- set ns_turn = namespace(last_user_idx=-1) -%}
209
{%- for i in range(loop_messages | length) -%}
210
{%- if loop_messages[i]['role'] == 'user' -%}
@@ -200,12 +212,12 @@
212
{%- endif -%}
213
{%- endfor -%}
214
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'] -%}
207
- {#- OpenAI may emit multiple assistant messages in one tool loop (user → asst → tool → asst → tool).
208
- Only the first of those should open <|turn>model; later ones continue the same model turn. -#}
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) -%}
@@ -222,8 +234,10 @@
234
{{- '<|turn>' + role + '\n' }}
235
{%- endif -%}
236
225
- {%- if message.get('reasoning') and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
226
- {{- '<|channel>thought\n' + message['reasoning'] + '\n<channel|>'}}
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'] -%}
@@ -247,14 +261,14 @@
261
262
{%- set ns_tr_out = namespace(flag=false) -%}
263
{%- if message.get('tool_responses') -%}
250
- {#- Legacy: tool_responses embedded on the assistant message -#}
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') -%}
257
- {#- OpenAI Chat Completions: consecutive following messages with role "tool" (no break/continue; range scan) -#}
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) -%}
274
{%- if ns_tool_scan.stopped -%}
@@ -262,12 +276,14 @@
276
{%- set ns_tool_scan.stopped = true -%}
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'] -%}
284
{%- endif -%}
285
{%- endfor -%}
286
+ {#- Handle content as string or content-parts array -#}
287
{%- set tool_body = follow.get('content') -%}
288
{%- if tool_body is string -%}
289
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
@@ -288,6 +304,7 @@
304
{%- endfor -%}
305
{%- endif -%}
306
307
+ {%- set captured_content -%}
308
{%- if message['content'] is string -%}
309
{%- if role == 'model' -%}
310
{{- strip_thinking(message['content']) -}}
@@ -303,29 +320,35 @@
320
{{- item['text'] | trim -}}
321
{%- endif -%}
322
{%- elif item['type'] == 'image' -%}
306
- {{- '\n\n<|image|>\n\n' -}}
323
+ {{- '<|image|>' -}}
324
{%- set ns.prev_message_type = 'image' -%}
325
{%- elif item['type'] == 'audio' -%}
326
{{- '<|audio|>' -}}
327
{%- set ns.prev_message_type = 'audio' -%}
328
{%- elif item['type'] == 'video' -%}
312
- {{- '\n\n<|video|>\n\n' -}}
329
+ {{- '<|video|>' -}}
330
{%- set ns.prev_message_type = 'video' -%}
331
{%- endif -%}
332
{%- endfor -%}
333
{%- endif -%}
334
+ {%- endset -%}
335
+
336
+ {{- captured_content -}}
337
+ {%- set has_content = captured_content | trim | length > 0 -%}
338
318
- {%- if not (ns_tr_out.flag and not message.get('content')) -%}
339
+ {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
340
+ {{- '<|tool_response>' -}}
341
+ {%- elif not (ns_tr_out.flag and not has_content) -%}
342
{{- '<turn|>\n' -}}
343
{%- endif -%}
344
{%- endif -%}
345
{%- endfor -%}
346
347
{%- if add_generation_prompt -%}
325
- {%- if ns.prev_message_type != 'tool_response' -%}
348
+ {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
349
{{- '<|turn>model\n' -}}
350
+ {%- if not enable_thinking | default(false) -%}
351
+ {{- '<|channel>thought\n<channel|>' -}}
352
+ {%- endif -%}
353
{%- endif -%}
328
- {%- if not enable_thinking | default(false) -%}
329
- {{- '<|channel>thought\n<channel|>' -}}
330
- {%- endif -%}
331
-{%- endif -%}
354
+{%- endif -%}
\ No newline at end of file