WaveCut commited on
Commit
51b3bbe
·
verified ·
1 Parent(s): edfc49b

Refresh Gemma 4 runtime metadata

Browse files

Keep the verified W4A16 G64 RTN tensors; source Google LFS hashes are unchanged. Refresh the chat template, tokenizer metadata, processor metadata, and public model card.

Files changed (4) hide show
  1. README.md +50 -81
  2. chat_template.jinja +100 -43
  3. processor_config.json +75 -0
  4. tokenizer_config.json +20 -42
README.md CHANGED
@@ -1,115 +1,84 @@
1
  ---
2
  license: apache-2.0
3
- base_model: coder3101/gemma-4-26B-A4B-it-heretic
 
 
4
  library_name: vllm
 
5
  tags:
6
  - gemma4
 
7
  - vllm
8
  - llm-compressor
9
  - compressed-tensors
10
  - w4a16
11
- - 4-bit
 
 
12
  - moe
13
- - rtx-3090
14
- pipeline_tag: text-generation
15
- private_benchmark: true
16
  ---
17
 
18
  # Gemma 4 26B A4B Heretic W4A16 G64 RTN for vLLM
19
 
20
- Private internal quantization of
21
- [`coder3101/gemma-4-26B-A4B-it-heretic`](https://huggingface.co/coder3101/gemma-4-26B-A4B-it-heretic)
22
- for vLLM on a single RTX 3090 24GB.
23
 
24
- This artifact was produced for AI Farm deployment experiments. It is intended
25
- for text-generation benchmarking and deployment validation. The vision path was
26
- not benchmarked.
27
 
28
- ## Provenance
 
 
 
 
 
 
 
 
 
29
 
30
- - Base model: `coder3101/gemma-4-26B-A4B-it-heretic`
31
- - Base commit: `e6d09d5c434a5e91326ce9d779786a91e7aaae35`
32
- - Source architecture: `Gemma4ForConditionalGeneration`
33
- - Quantization stack: `llm-compressor` main with Gemma4 MoE expert unpacking,
34
- `compressed-tensors`, Transformers 5.8.0
35
- - Quantization method: W4A16 RTN, int4 weights, per-group, group size 64,
36
- symmetric, dynamic false
37
- - Output format: single `model.safetensors` compressed-tensors checkpoint
38
- - Artifact size on disk: about 16 GiB
39
 
40
- ## Why group size 64
41
 
42
- The Gemma4 26B A4B MoE checkpoint has projection dimensions that are not
43
- compatible with group size 128 everywhere (`mlp.down_proj` columns 2112 and MoE
44
- expert down-proj columns 704). Group size 64 avoids that divisibility issue and
45
- was accepted by vLLM on RTX 3090 when running with fp16 activations.
 
 
46
 
47
- ## Tested vLLM Launch
48
 
49
- Tested environment:
50
-
51
- - GPU: NVIDIA RTX 3090 24GB, compute capability 8.6
52
- - vLLM: 0.20.2
53
- - Torch: 2.11.0
54
- - CUDA runtime as seen by Torch: 13.0
55
 
56
  ```bash
57
  vllm serve WaveCut/gemma-4-26B-A4B-it-heretic-W4A16-G64-RTN-vllm \
58
- --served-model-name default \
59
- --host 0.0.0.0 \
60
- --port 8000 \
61
  --trust-remote-code \
62
- --dtype float16 \
63
  --max-model-len 40192 \
64
  --max-num-seqs 2 \
65
  --max-num-batched-tokens 8192 \
66
  --gpu-memory-utilization 0.95 \
67
- --kv-cache-dtype auto
 
 
 
68
  ```
69
 
70
- Notes:
71
-
72
- - `--dtype float16` is required on RTX 3090 for this WNA16 path; bf16 activation
73
- selection failed kernel selection.
74
- - `--kv-cache-dtype fp8` failed on sm_86/Triton with unsupported `fp8e4nv`.
75
- - `--kv-cache-dtype fp8_e5m2` was rejected by vLLM for this compressed-tensors
76
- checkpoint.
77
- - `--kv-cache-dtype auto` started successfully at `max_model_len=40192` and
78
- `max_num_seqs=2`.
79
- - vLLM reported GPU KV cache size `99,951 tokens` and maximum concurrency
80
- `2.49x` for `40,192` tokens/request.
81
- - Observed VRAM after startup was about `23.5 GiB`, so this is a tight fit on
82
- RTX 3090.
83
-
84
- ## Benchmark Snapshot
85
-
86
- OpenAI-compatible `/v1/completions`, two concurrent requests, `max_tokens=128`,
87
- temperature 0, unique prompts per request to avoid artificial prefix-cache
88
- reuse.
89
-
90
- | Scenario | Prompt tokens/request | Concurrency | Streaming TTFT | Streaming total | Decode after TTFT |
91
- | --- | ---: | ---: | ---: | ---: | ---: |
92
- | Empty/warm | 64 | 2 | 71-73 ms | 0.92 s | ~150.8 tok/s per request |
93
- | Full context | 40,000 | 2 | 11.54 s and 22.46 s | ~23.8 s | scheduler-sensitive; non-stream wall ~12.8 s |
94
-
95
- The full-context run confirms that vLLM can admit two nearly-40k requests on a
96
- single RTX 3090 with this artifact, but the memory margin is small and chunked
97
- prefill scheduling can make one request wait behind the other.
98
-
99
- ## Files
100
-
101
- - `model.safetensors`: compressed W4A16 weights
102
- - `config.json`: Gemma4 config with compressed-tensors quantization metadata
103
- - `recipe.yaml`: llm-compressor recipe
104
- - tokenizer and chat template files
105
- - `preprocessor_config.json`: copied from the base model; required because vLLM
106
- treats Gemma4 as a multimodal architecture during profiling even for text-only
107
- serving
108
 
109
  ## Limitations
110
 
111
- - RTN was used for this first working vLLM artifact; AWQ/GPTQ/other calibration
112
- variants should be compared before choosing a final production quant.
113
- - Text-only serving was benchmarked. Image input was not validated.
114
- - The repository is private/internal and inherits the base model license and
115
- usage constraints.
 
 
1
  ---
2
  license: apache-2.0
3
+ license_link: https://ai.google.dev/gemma/docs/gemma_4_license
4
+ base_model: WaveCut/gemma-4-26B-A4B-it-heretic
5
+ base_model_relation: quantized
6
  library_name: vllm
7
+ pipeline_tag: image-text-to-text
8
  tags:
9
  - gemma4
10
+ - heretic
11
  - vllm
12
  - llm-compressor
13
  - compressed-tensors
14
  - w4a16
15
+ - int4
16
+ - rtn
17
+ - g64
18
  - moe
19
+ - multimodal
 
 
20
  ---
21
 
22
  # Gemma 4 26B A4B Heretic W4A16 G64 RTN for vLLM
23
 
24
+ `compressed-tensors` W4A16 quantization of
25
+ [`WaveCut/gemma-4-26B-A4B-it-heretic`](https://huggingface.co/WaveCut/gemma-4-26B-A4B-it-heretic)
26
+ for vLLM on a single 24 GB RTX 3090.
27
 
28
+ ## Quantization
 
 
29
 
30
+ | Setting | Value |
31
+ | --- | --- |
32
+ | Method | RTN / `memoryless_minmax` |
33
+ | Weights | signed INT4, symmetric |
34
+ | Activations | BF16 at runtime |
35
+ | Group size | 64 |
36
+ | Format | `compressed-tensors`, `pack-quantized` |
37
+ | Source architecture | `Gemma4ForConditionalGeneration` |
38
+ | Source Heretic commit | `e6d09d5c434a5e91326ce9d779786a91e7aaae35` |
39
+ | Original quantization stack | `llm-compressor` main, Transformers 5.8.0 |
40
 
41
+ Vision modules, routers, embeddings, and `lm_head` remain unquantized. Group
42
+ size 64 is required because Gemma 4 projection widths such as 2112 and 704 are
43
+ not divisible by 128. The exact recipe is stored in `recipe.yaml`.
 
 
 
 
 
 
44
 
45
+ ## Source refresh
46
 
47
+ On 2026-07-16, Google updated the Gemma 4 chat template but did not update the
48
+ model weights. The initial and current Google revisions have identical LFS
49
+ SHA256 values for both BF16 weight shards. This repository therefore keeps the
50
+ existing verified W4A16 tensors and refreshes the chat template, tokenizer
51
+ metadata, and processor metadata from Google revision
52
+ `01e5b3ee840d3a9e0b0b493c593e85398a30ef75`.
53
 
54
+ ## vLLM
55
 
56
+ The production profile uses:
 
 
 
 
 
57
 
58
  ```bash
59
  vllm serve WaveCut/gemma-4-26B-A4B-it-heretic-W4A16-G64-RTN-vllm \
60
+ --served-model-name "Gemma 4 26B Heretic" \
 
 
61
  --trust-remote-code \
62
+ --dtype bfloat16 \
63
  --max-model-len 40192 \
64
  --max-num-seqs 2 \
65
  --max-num-batched-tokens 8192 \
66
  --gpu-memory-utilization 0.95 \
67
+ --generation-config vllm \
68
+ --no-enable-prefix-caching \
69
+ --enable-auto-tool-choice \
70
+ --tool-call-parser gemma4
71
  ```
72
 
73
+ The historical RTX 3090 benchmark and harness are included under `bench/` and
74
+ `scripts/`. A fresh vLLM 0.23.0 versus 0.25.1 text, vision, and long-context
75
+ comparison will be added after the identical A/B run completes on AI Farm.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  ## Limitations
78
 
79
+ - RTN is a weight-only quantization; compare output quality against the BF16
80
+ Heretic checkpoint for quality-sensitive workloads.
81
+ - The 40,192-token, two-request profile is a tight fit on 24 GB and leaves
82
+ little free VRAM.
83
+ - This is a third-party post-processed and quantized checkpoint. It is not
84
+ affiliated with or endorsed by Google.
chat_template.jinja CHANGED
@@ -1,9 +1,15 @@
1
- {%- macro format_parameters(properties, required) -%}
 
 
 
 
 
 
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 -%}
7
  {%- if ns.found_first %},{% endif -%}
8
  {%- set ns.found_first = true -%}
9
  {{ key }}:{
@@ -65,7 +71,7 @@
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'] -%}
@@ -116,7 +122,9 @@
116
  }
117
  {%- endmacro -%}
118
  {%- macro format_argument(argument, escape_keys=True) -%}
119
- {%- if argument is string -%}
 
 
120
  {{- '<|"|>' + argument + '<|"|>' -}}
121
  {%- elif argument is boolean -%}
122
  {{- 'true' if argument else 'false' -}}
@@ -156,6 +164,7 @@
156
  {%- endfor -%}
157
  {{- ns.result | trim -}}
158
  {%- endmacro -%}
 
159
  {%- macro format_tool_response_block(tool_name, response) -%}
160
  {{- '<|tool_response>' -}}
161
  {%- if response is mapping -%}
@@ -170,19 +179,29 @@
170
  {%- endif -%}
171
  {{- '<tool_response|>' -}}
172
  {%- endmacro -%}
173
- {%- set ns = namespace(prev_message_type=None) -%}
 
 
174
  {%- set loop_messages = messages -%}
 
 
175
  {{- bos_token -}}
176
  {#- Handle System/Tool Definitions Block -#}
177
- {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
178
  {{- '<|turn>system\n' -}}
179
  {#- Inject Thinking token at the very top of the FIRST system turn -#}
180
- {%- if enable_thinking is defined and enable_thinking -%}
181
  {{- '<|think|>\n' -}}
182
  {%- set ns.prev_message_type = 'think' -%}
183
  {%- endif -%}
184
- {%- if messages[0]['role'] in ['system', 'developer'] -%}
185
- {{- messages[0]['content'] | trim -}}
 
 
 
 
 
 
186
  {%- set loop_messages = messages[1:] -%}
187
  {%- endif -%}
188
  {%- if tools -%}
@@ -195,6 +214,7 @@
195
  {%- endif -%}
196
  {{- '<turn|>\n' -}}
197
  {%- endif %}
 
198
  {#- Pre-scan: find last user message index for reasoning guard -#}
199
  {%- set ns_turn = namespace(last_user_idx=-1) -%}
200
  {%- for i in range(loop_messages | length) -%}
@@ -202,34 +222,28 @@
202
  {%- set ns_turn.last_user_idx = i -%}
203
  {%- endif -%}
204
  {%- endfor -%}
 
205
  {#- Loop through messages -#}
206
  {%- for message in loop_messages -%}
207
  {%- if message['role'] != 'tool' -%}
208
  {%- set ns.prev_message_type = None -%}
209
  {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
210
- {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
211
- {%- set prev_nt = namespace(role=None, found=false) -%}
212
- {%- if loop.index0 > 0 -%}
213
- {%- for j in range(loop.index0 - 1, -1, -1) -%}
214
- {%- if not prev_nt.found -%}
215
- {%- if loop_messages[j]['role'] != 'tool' -%}
216
- {%- set prev_nt.role = loop_messages[j]['role'] -%}
217
- {%- set prev_nt.found = true -%}
218
- {%- endif -%}
219
- {%- endif -%}
220
- {%- endfor -%}
221
- {%- endif -%}
222
- {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
223
  {%- if not continue_same_model_turn -%}
224
  {{- '<|turn>' + role + '\n' }}
 
225
  {%- endif -%}
 
226
  {#- Render reasoning/reasoning_content as thinking channel -#}
227
  {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
228
- {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
 
229
  {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
230
  {%- endif -%}
231
- {%- if message['tool_calls'] -%}
232
- {%- for tool_call in message['tool_calls'] -%}
 
233
  {%- set function = tool_call['function'] -%}
234
  {{- '<|tool_call>call:' + function['name'] + '{' -}}
235
  {%- if function['arguments'] is mapping -%}
@@ -239,18 +253,24 @@
239
  {%- set ns_args.found_first = true -%}
240
  {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
241
  {%- endfor -%}
242
- {%- elif function['arguments'] is string -%}
243
- {{- function['arguments'] -}}
 
 
 
 
 
244
  {%- endif -%}
245
  {{- '}<tool_call|>' -}}
246
  {%- endfor -%}
247
  {%- set ns.prev_message_type = 'tool_call' -%}
248
  {%- endif -%}
 
249
  {%- set ns_tr_out = namespace(flag=false) -%}
250
  {%- if message.get('tool_responses') -%}
251
  {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
252
- {%- for tool_response in message['tool_responses'] -%}
253
- {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
254
  {%- set ns_tr_out.flag = true -%}
255
  {%- set ns.prev_message_type = 'tool_response' -%}
256
  {%- endfor -%}
@@ -264,8 +284,8 @@
264
  {%- else -%}
265
  {%- set follow = loop_messages[k] -%}
266
  {#- Resolve tool_call_id to function name -#}
267
- {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
268
- {%- for tc in message['tool_calls'] -%}
269
  {%- if tc.get('id') == follow.get('tool_call_id') -%}
270
  {%- set ns_tname.name = tc['function']['name'] -%}
271
  {%- endif -%}
@@ -282,6 +302,15 @@
282
  {%- endif -%}
283
  {%- endfor -%}
284
  {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
 
 
 
 
 
 
 
 
 
285
  {%- else -%}
286
  {{- format_tool_response_block(ns_tname.name, tool_body) -}}
287
  {%- endif -%}
@@ -290,44 +319,72 @@
290
  {%- endif -%}
291
  {%- endfor -%}
292
  {%- endif -%}
293
- {%- if message['content'] is string -%}
 
 
294
  {%- if role == 'model' -%}
295
  {{- strip_thinking(message['content']) -}}
296
  {%- else -%}
297
  {{- message['content'] | trim -}}
298
  {%- endif -%}
299
- {%- elif message['content'] is sequence -%}
300
  {%- for item in message['content'] -%}
301
- {%- if item['type'] == 'text' -%}
302
  {%- if role == 'model' -%}
303
  {{- strip_thinking(item['text']) -}}
304
  {%- else -%}
305
  {{- item['text'] | trim -}}
306
  {%- endif -%}
307
- {%- elif item['type'] == 'image' -%}
308
  {{- '<|image|>' -}}
309
- {%- set ns.prev_message_type = 'image' -%}
310
- {%- elif item['type'] == 'audio' -%}
311
  {{- '<|audio|>' -}}
312
- {%- set ns.prev_message_type = 'audio' -%}
313
- {%- elif item['type'] == 'video' -%}
314
  {{- '<|video|>' -}}
315
- {%- set ns.prev_message_type = 'video' -%}
316
  {%- endif -%}
317
  {%- endfor -%}
318
  {%- endif -%}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
320
  {{- '<|tool_response>' -}}
321
- {%- elif not (ns_tr_out.flag and not message.get('content')) -%}
 
322
  {{- '<turn|>\n' -}}
323
  {%- endif -%}
 
 
 
324
  {%- endif -%}
325
  {%- endfor -%}
 
326
  {%- if add_generation_prompt -%}
327
  {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
328
  {{- '<|turn>model\n' -}}
329
- {%- if not enable_thinking | default(false) -%}
330
  {{- '<|channel>thought\n<channel|>' -}}
331
  {%- endif -%}
 
 
332
  {%- endif -%}
333
- {%- endif -%}
 
1
+ {#
2
+ Template: Google Gemma 4 Canonical Chat Template
3
+ Author: Google Gemma Engineering Team
4
+ Published: 2026-07-09
5
+ Context: Fixed tool-calling loops, turn closures, and thinking content-ordering.
6
+ #}
7
+ {%- macro format_parameters(properties, required, filter_keys=false) -%}
8
  {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
9
  {%- set ns = namespace(found_first=false) -%}
10
  {%- for key, value in properties | dictsort -%}
11
  {%- set add_comma = false -%}
12
+ {%- if not filter_keys or key not in standard_keys -%}
13
  {%- if ns.found_first %},{% endif -%}
14
  {%- set ns.found_first = true -%}
15
  {{ key }}:{
 
71
  {%- elif value is mapping -%}
72
  {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
73
  properties:{
74
+ {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
75
  }
76
  {%- endif -%}
77
  {%- if value['required'] -%}
 
122
  }
123
  {%- endmacro -%}
124
  {%- macro format_argument(argument, escape_keys=True) -%}
125
+ {%- if argument is none -%}
126
+ {{- 'null' -}}
127
+ {%- elif argument is string -%}
128
  {{- '<|"|>' + argument + '<|"|>' -}}
129
  {%- elif argument is boolean -%}
130
  {{- 'true' if argument else 'false' -}}
 
164
  {%- endfor -%}
165
  {{- ns.result | trim -}}
166
  {%- endmacro -%}
167
+
168
  {%- macro format_tool_response_block(tool_name, response) -%}
169
  {{- '<|tool_response>' -}}
170
  {%- if response is mapping -%}
 
179
  {%- endif -%}
180
  {{- '<tool_response|>' -}}
181
  {%- endmacro -%}
182
+
183
+ {#- ===== SETUP ===== -#}
184
+ {%- set ns = namespace(prev_message_type=None, prev_non_tool_role=None) -%}
185
  {%- set loop_messages = messages -%}
186
+ {%- set enable_thinking = enable_thinking | default(false) -%}
187
+ {%- set preserve_thinking = preserve_thinking | default(false) -%}
188
  {{- bos_token -}}
189
  {#- Handle System/Tool Definitions Block -#}
190
+ {%- if enable_thinking or tools or (messages and messages[0]['role'] in ['system', 'developer']) -%}
191
  {{- '<|turn>system\n' -}}
192
  {#- Inject Thinking token at the very top of the FIRST system turn -#}
193
+ {%- if enable_thinking -%}
194
  {{- '<|think|>\n' -}}
195
  {%- set ns.prev_message_type = 'think' -%}
196
  {%- endif -%}
197
+ {%- if messages and messages[0]['role'] in ['system', 'developer'] -%}
198
+ {%- if messages[0]['content'] is string -%}
199
+ {{- messages[0]['content'] | trim -}}
200
+ {%- elif messages[0]['content'] is sequence -%}
201
+ {%- for item in messages[0]['content'] -%}
202
+ {{- item['text'] | trim + ' '-}}
203
+ {%- endfor -%}
204
+ {%- endif -%}
205
  {%- set loop_messages = messages[1:] -%}
206
  {%- endif -%}
207
  {%- if tools -%}
 
214
  {%- endif -%}
215
  {{- '<turn|>\n' -}}
216
  {%- endif %}
217
+
218
  {#- Pre-scan: find last user message index for reasoning guard -#}
219
  {%- set ns_turn = namespace(last_user_idx=-1) -%}
220
  {%- for i in range(loop_messages | length) -%}
 
222
  {%- set ns_turn.last_user_idx = i -%}
223
  {%- endif -%}
224
  {%- endfor -%}
225
+
226
  {#- Loop through messages -#}
227
  {%- for message in loop_messages -%}
228
  {%- if message['role'] != 'tool' -%}
229
  {%- set ns.prev_message_type = None -%}
230
  {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
231
+ {#- Detect continuation using tracked state O(1) instead of O(n) backward scan -#}
232
+ {%- set continue_same_model_turn = (role == 'model' and ns.prev_non_tool_role == 'assistant') -%}
 
 
 
 
 
 
 
 
 
 
 
233
  {%- if not continue_same_model_turn -%}
234
  {{- '<|turn>' + role + '\n' }}
235
+
236
  {%- endif -%}
237
+
238
  {#- Render reasoning/reasoning_content as thinking channel -#}
239
  {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
240
+ {%- set thinking_gate = (loop.index0 > ns_turn.last_user_idx) or (preserve_thinking and message.get('tool_calls')) -%}
241
+ {%- if thinking_text and thinking_gate -%}
242
  {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
243
  {%- endif -%}
244
+
245
+ {%- if message.get('tool_calls') -%}
246
+ {%- for tool_call in message.get('tool_calls') -%}
247
  {%- set function = tool_call['function'] -%}
248
  {{- '<|tool_call>call:' + function['name'] + '{' -}}
249
  {%- if function['arguments'] is mapping -%}
 
253
  {%- set ns_args.found_first = true -%}
254
  {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
255
  {%- endfor -%}
256
+ {%- elif function['arguments'] is none -%}
257
+ {%- else -%}
258
+ {{- raise_exception(
259
+ "chat_template: tool_calls[].function.arguments must be a "
260
+ "JSON object (mapping), not a string. Deserialize arguments "
261
+ "before passing to the template."
262
+ ) -}}
263
  {%- endif -%}
264
  {{- '}<tool_call|>' -}}
265
  {%- endfor -%}
266
  {%- set ns.prev_message_type = 'tool_call' -%}
267
  {%- endif -%}
268
+
269
  {%- set ns_tr_out = namespace(flag=false) -%}
270
  {%- if message.get('tool_responses') -%}
271
  {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
272
+ {%- for tool_response in message.get('tool_responses') -%}
273
+ {{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}
274
  {%- set ns_tr_out.flag = true -%}
275
  {%- set ns.prev_message_type = 'tool_response' -%}
276
  {%- endfor -%}
 
284
  {%- else -%}
285
  {%- set follow = loop_messages[k] -%}
286
  {#- Resolve tool_call_id to function name -#}
287
+ {%- set ns_tname = namespace(name=follow.get('name') or 'unknown') -%}
288
+ {%- for tc in message.get('tool_calls') -%}
289
  {%- if tc.get('id') == follow.get('tool_call_id') -%}
290
  {%- set ns_tname.name = tc['function']['name'] -%}
291
  {%- endif -%}
 
302
  {%- endif -%}
303
  {%- endfor -%}
304
  {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
305
+ {%- for part in tool_body -%}
306
+ {%- if part.get('type') in ['image', 'image_url'] -%}
307
+ {{- '<|image|>' -}}
308
+ {%- elif part.get('type') in ['audio', 'input_audio'] -%}
309
+ {{- '<|audio|>' -}}
310
+ {%- elif part.get('type') == 'video' -%}
311
+ {{- '<|video|>' -}}
312
+ {%- endif -%}
313
+ {%- endfor -%}
314
  {%- else -%}
315
  {{- format_tool_response_block(ns_tname.name, tool_body) -}}
316
  {%- endif -%}
 
319
  {%- endif -%}
320
  {%- endfor -%}
321
  {%- endif -%}
322
+
323
+ {%- set captured_content -%}
324
+ {%- if message.get('content') is string -%}
325
  {%- if role == 'model' -%}
326
  {{- strip_thinking(message['content']) -}}
327
  {%- else -%}
328
  {{- message['content'] | trim -}}
329
  {%- endif -%}
330
+ {%- elif message.get('content') is sequence -%}
331
  {%- for item in message['content'] -%}
332
+ {%- if item.get('type') == 'text' -%}
333
  {%- if role == 'model' -%}
334
  {{- strip_thinking(item['text']) -}}
335
  {%- else -%}
336
  {{- item['text'] | trim -}}
337
  {%- endif -%}
338
+ {%- elif item.get('type') in ['image', 'image_url'] -%}
339
  {{- '<|image|>' -}}
340
+ {%- elif item.get('type') in ['audio', 'input_audio'] -%}
 
341
  {{- '<|audio|>' -}}
342
+ {%- elif item.get('type') == 'video' -%}
 
343
  {{- '<|video|>' -}}
 
344
  {%- endif -%}
345
  {%- endfor -%}
346
  {%- endif -%}
347
+ {%- endset -%}
348
+
349
+ {{- captured_content -}}
350
+ {%- set has_content = captured_content | trim | length > 0 -%}
351
+
352
+ {#- Forward-scan: find next non-tool message role for continuation detection -#}
353
+ {%- set next_nt = namespace(role=None, found=false) -%}
354
+ {%- for j in range(loop.index0 + 1, loop_messages | length) -%}
355
+ {%- if not next_nt.found -%}
356
+ {%- if loop_messages[j]['role'] != 'tool' -%}
357
+ {%- set next_nt.role = loop_messages[j]['role'] -%}
358
+ {%- set next_nt.found = true -%}
359
+ {%- endif -%}
360
+ {%- endif -%}
361
+ {%- endfor -%}
362
+
363
+ {%- set continues_into_next = (
364
+ role == 'model'
365
+ and next_nt.role == 'assistant'
366
+ and (not message.get('tool_calls') or ns_tr_out.flag)
367
+ ) -%}
368
+
369
  {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
370
  {{- '<|tool_response>' -}}
371
+ {%- elif continues_into_next -%}
372
+ {%- elif not (ns_tr_out.flag and not has_content and not next_nt.found) -%}
373
  {{- '<turn|>\n' -}}
374
  {%- endif -%}
375
+
376
+ {#- Track previous non-tool role for next iteration (avoids O(n) backward scan) -#}
377
+ {%- set ns.prev_non_tool_role = message['role'] -%}
378
  {%- endif -%}
379
  {%- endfor -%}
380
+
381
  {%- if add_generation_prompt -%}
382
  {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
383
  {{- '<|turn>model\n' -}}
384
+ {%- if not enable_thinking -%}
385
  {{- '<|channel>thought\n<channel|>' -}}
386
  {%- endif -%}
387
+ {%- elif ns.prev_message_type == 'tool_response' and enable_thinking -%}
388
+ {{- '<|channel>thought\n' -}}
389
  {%- endif -%}
390
+ {%- endif -%}
processor_config.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_ms_per_token": 40,
3
+ "audio_seq_length": 750,
4
+ "feature_extractor": {
5
+ "dither": 0.0,
6
+ "feature_extractor_type": "Gemma4AudioFeatureExtractor",
7
+ "feature_size": 128,
8
+ "fft_length": 512,
9
+ "fft_overdrive": false,
10
+ "frame_length": 320,
11
+ "hop_length": 160,
12
+ "input_scale_factor": 1.0,
13
+ "max_frequency": 8000.0,
14
+ "mel_floor": 0.001,
15
+ "min_frequency": 0.0,
16
+ "padding_side": "right",
17
+ "padding_value": 0.0,
18
+ "per_bin_mean": null,
19
+ "per_bin_stddev": null,
20
+ "preemphasis": 0.0,
21
+ "preemphasis_htk_flavor": true,
22
+ "return_attention_mask": true,
23
+ "sampling_rate": 16000
24
+ },
25
+ "image_processor": {
26
+ "do_convert_rgb": true,
27
+ "do_normalize": false,
28
+ "do_rescale": true,
29
+ "do_resize": true,
30
+ "image_mean": [
31
+ 0.0,
32
+ 0.0,
33
+ 0.0
34
+ ],
35
+ "image_processor_type": "Gemma4ImageProcessor",
36
+ "image_seq_length": 280,
37
+ "image_std": [
38
+ 1.0,
39
+ 1.0,
40
+ 1.0
41
+ ],
42
+ "max_soft_tokens": 280,
43
+ "patch_size": 16,
44
+ "pooling_kernel_size": 3,
45
+ "resample": 3,
46
+ "rescale_factor": 0.00392156862745098
47
+ },
48
+ "image_seq_length": 280,
49
+ "processor_class": "Gemma4Processor",
50
+ "video_processor": {
51
+ "do_convert_rgb": true,
52
+ "do_normalize": true,
53
+ "do_rescale": true,
54
+ "do_resize": true,
55
+ "do_sample_frames": true,
56
+ "image_mean": [
57
+ 0.0,
58
+ 0.0,
59
+ 0.0
60
+ ],
61
+ "image_std": [
62
+ 1.0,
63
+ 1.0,
64
+ 1.0
65
+ ],
66
+ "max_soft_tokens": 70,
67
+ "num_frames": 32,
68
+ "patch_size": 16,
69
+ "pooling_kernel_size": 3,
70
+ "resample": 3,
71
+ "rescale_factor": 0.00392156862745098,
72
+ "return_metadata": false,
73
+ "video_processor_type": "Gemma4VideoProcessor"
74
+ }
75
+ }
tokenizer_config.json CHANGED
@@ -17,72 +17,50 @@
17
  "<|video|>"
18
  ],
19
  "image_token": "<|image|>",
20
- "is_local": true,
21
- "local_files_only": false,
22
  "mask_token": "<mask>",
23
  "model_max_length": 1000000000000000019884624838656,
24
- "model_specific_special_tokens": {
25
- "audio_token": "<|audio|>",
26
- "boa_token": "<|audio>",
27
- "boi_token": "<|image>",
28
- "eoa_token": "<audio|>",
29
- "eoc_token": "<channel|>",
30
- "eoi_token": "<image|>",
31
- "eot_token": "<turn|>",
32
- "escape_token": "<|\"|>",
33
- "etc_token": "<tool_call|>",
34
- "etd_token": "<tool|>",
35
- "etr_token": "<tool_response|>",
36
- "image_token": "<|image|>",
37
- "soc_token": "<|channel>",
38
- "sot_token": "<|turn>",
39
- "stc_token": "<|tool_call>",
40
- "std_token": "<|tool>",
41
- "str_token": "<|tool_response>",
42
- "think_token": "<|think|>"
43
- },
44
  "pad_token": "<pad>",
45
  "padding_side": "left",
46
  "processor_class": "Gemma4Processor",
47
  "response_schema": {
 
48
  "properties": {
49
- "content": {
50
- "type": "string"
51
- },
52
  "role": {
53
  "const": "assistant"
54
  },
55
  "thinking": {
56
  "type": "string"
57
  },
 
 
 
58
  "tool_calls": {
 
 
59
  "items": {
 
60
  "properties": {
 
 
 
61
  "function": {
 
 
62
  "properties": {
63
- "arguments": {
64
- "additionalProperties": {},
65
- "type": "object",
66
- "x-parser": "gemma4-tool-call"
67
- },
68
  "name": {
69
  "type": "string"
 
 
 
 
 
70
  }
71
- },
72
- "type": "object",
73
- "x-regex": "call\\:(?P<name>\\w+)(?P<arguments>\\{.*\\})"
74
- },
75
- "type": {
76
- "const": "function"
77
  }
78
- },
79
- "type": "object"
80
- },
81
- "type": "array",
82
- "x-regex-iterator": "<\\|tool_call>(.*?)<tool_call\\|>"
83
  }
84
  },
85
- "type": "object",
86
  "x-regex": "(\\<\\|channel\\>thought\\n(?P<thinking>.*?)\\<channel\\|\\>)?(?P<tool_calls>\\<\\|tool_call\\>.*\\<tool_call\\|\\>)?(?P<content>(?:(?!\\<turn\\|\\>)(?!\\<\\|tool_response\\>).)+)?(?:\\<turn\\|\\>|\\<\\|tool_response\\>)?"
87
  },
88
  "soc_token": "<|channel>",
 
17
  "<|video|>"
18
  ],
19
  "image_token": "<|image|>",
 
 
20
  "mask_token": "<mask>",
21
  "model_max_length": 1000000000000000019884624838656,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  "pad_token": "<pad>",
23
  "padding_side": "left",
24
  "processor_class": "Gemma4Processor",
25
  "response_schema": {
26
+ "type": "object",
27
  "properties": {
 
 
 
28
  "role": {
29
  "const": "assistant"
30
  },
31
  "thinking": {
32
  "type": "string"
33
  },
34
+ "content": {
35
+ "type": "string"
36
+ },
37
  "tool_calls": {
38
+ "x-regex-iterator": "<\\|tool_call>(.*?)<tool_call\\|>",
39
+ "type": "array",
40
  "items": {
41
+ "type": "object",
42
  "properties": {
43
+ "type": {
44
+ "const": "function"
45
+ },
46
  "function": {
47
+ "type": "object",
48
+ "x-regex": "call\\:(?P<name>\\w+)(?P<arguments>\\{.*\\})",
49
  "properties": {
 
 
 
 
 
50
  "name": {
51
  "type": "string"
52
+ },
53
+ "arguments": {
54
+ "type": "object",
55
+ "x-parser": "gemma4-tool-call",
56
+ "additionalProperties": {}
57
  }
58
+ }
 
 
 
 
 
59
  }
60
+ }
61
+ }
 
 
 
62
  }
63
  },
 
64
  "x-regex": "(\\<\\|channel\\>thought\\n(?P<thinking>.*?)\\<channel\\|\\>)?(?P<tool_calls>\\<\\|tool_call\\>.*\\<tool_call\\|\\>)?(?P<content>(?:(?!\\<turn\\|\\>)(?!\\<\\|tool_response\\>).)+)?(?:\\<turn\\|\\>|\\<\\|tool_response\\>)?"
65
  },
66
  "soc_token": "<|channel>",