@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c408fdd663
Fork 0

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