Skip to content

Commit 122d8db

Browse files
committed
refactor: update Gemma4ChatHandler with latest google/gemma-4-31B-it chat template from huggingface
- Sync `Gemma4ChatHandler` logic with the upstream chat template, incorporating the new `format_tool_response_block` and OpenAI-compatible forward-scan tool resolution. Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent e73ad59 commit 122d8db

File tree

1 file changed

+123
-56
lines changed

1 file changed

+123
-56
lines changed

llama_cpp/llama_chat_format.py

Lines changed: 123 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4376,34 +4376,15 @@ class Gemma4ChatHandler(MTMDChatHandler):
43764376
" description:<|\"|>{{ value['description'] }}<|\"|>\n"
43774377
" {%- set add_comma = true -%}\n"
43784378
" {%- endif -%}\n"
4379-
" {%- if value['nullable'] %}\n"
4380-
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4381-
" nullable:true\n"
4382-
" {%- endif -%}\n"
43834379
" {%- if value['type'] | upper == 'STRING' -%}\n"
43844380
" {%- if value['enum'] -%}\n"
43854381
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
43864382
" enum:{{ format_argument(value['enum']) }}\n"
43874383
" {%- endif -%}\n"
4388-
" {%- elif value['type'] | upper == 'OBJECT' -%}\n"
4389-
" ,properties:{\n"
4390-
" {%- if value['properties'] is defined and value['properties'] is mapping -%}\n"
4391-
" {{- format_parameters(value['properties'], value['required'] | default([])) -}}\n"
4392-
" {%- elif value is mapping -%}\n"
4393-
" {{- format_parameters(value, value['required'] | default([])) -}}\n"
4394-
" {%- endif -%}\n"
4395-
" }\n"
4396-
" {%- if value['required'] -%}\n"
4397-
" ,required:[\n"
4398-
" {%- for item in value['required'] | default([]) -%}\n"
4399-
" <|\"|>{{- item -}}<|\"|>\n"
4400-
" {%- if not loop.last %},{% endif -%}\n"
4401-
" {%- endfor -%}\n"
4402-
" ]\n"
4403-
" {%- endif -%}\n"
44044384
" {%- elif value['type'] | upper == 'ARRAY' -%}\n"
44054385
" {%- if value['items'] is mapping and value['items'] -%}\n"
4406-
" ,items:{\n"
4386+
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4387+
" items:{\n"
44074388
" {%- set ns_items = namespace(found_first=false) -%}\n"
44084389
" {%- for item_key, item_value in value['items'] | dictsort -%}\n"
44094390
" {%- if item_value is not none -%}\n"
@@ -4436,6 +4417,32 @@ class Gemma4ChatHandler(MTMDChatHandler):
44364417
" }\n"
44374418
" {%- endif -%}\n"
44384419
" {%- endif -%}\n"
4420+
" {%- if value['nullable'] %}\n"
4421+
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4422+
" nullable:true\n"
4423+
" {%- endif -%}\n"
4424+
" {%- if value['type'] | upper == 'OBJECT' -%}\n"
4425+
" {%- if value['properties'] is defined and value['properties'] is mapping -%}\n"
4426+
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4427+
" properties:{\n"
4428+
" {{- format_parameters(value['properties'], value['required'] | default([])) -}}\n"
4429+
" }\n"
4430+
" {%- elif value is mapping -%}\n"
4431+
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4432+
" properties:{\n"
4433+
" {{- format_parameters(value, value['required'] | default([])) -}}\n"
4434+
" }\n"
4435+
" {%- endif -%}\n"
4436+
" {%- if value['required'] -%}\n"
4437+
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
4438+
" required:[\n"
4439+
" {%- for item in value['required'] | default([]) -%}\n"
4440+
" <|\"|>{{- item -}}<|\"|>\n"
4441+
" {%- if not loop.last %},{% endif -%}\n"
4442+
" {%- endfor -%}\n"
4443+
" ]\n"
4444+
" {%- endif -%}\n"
4445+
" {%- endif -%}\n"
44394446
" {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}\n"
44404447
" type:<|\"|>{{ value['type'] | upper }}<|\"|>}\n"
44414448
" {%- endif -%}\n"
@@ -4514,25 +4521,35 @@ class Gemma4ChatHandler(MTMDChatHandler):
45144521
" {%- endfor -%}\n"
45154522
" {{- ns.result | trim -}}\n"
45164523
"{%- endmacro -%}\n"
4517-
"\n"
4524+
"{%- macro format_tool_response_block(tool_name, response) -%}\n"
4525+
" {{- '<|tool_response>' -}}\n"
4526+
" {%- if response is mapping -%}\n"
4527+
" {{- 'response:' + tool_name + '{' -}}\n"
4528+
" {%- for key, value in response | dictsort -%}\n"
4529+
" {{- key -}}:{{- format_argument(value, escape_keys=False) -}}\n"
4530+
" {%- if not loop.last %},{% endif -%}\n"
4531+
" {%- endfor -%}\n"
4532+
" {{- '}' -}}\n"
4533+
" {%- else -%}\n"
4534+
" {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}\n"
4535+
" {%- endif -%}\n"
4536+
" {{- '<tool_response|>' -}}\n"
4537+
"{%- endmacro -%}\n"
45184538
"{%- set ns = namespace(prev_message_type=None) -%}\n"
45194539
"{%- set loop_messages = messages -%}\n"
4520-
"{{ bos_token }}\n"
4540+
"{{- bos_token -}}\n"
45214541
"{#- Handle System/Tool Definitions Block -#}\n"
45224542
"{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}\n"
45234543
" {{- '<|turn>system\\n' -}}\n"
4524-
"\n"
45254544
" {#- Inject Thinking token at the very top of the FIRST system turn -#}\n"
45264545
" {%- if enable_thinking is defined and enable_thinking -%}\n"
4527-
" {{- '<|think|>' -}}\n"
4546+
" {{- '<|think|>\\n' -}}\n"
45284547
" {%- set ns.prev_message_type = 'think' -%}\n"
45294548
" {%- endif -%}\n"
4530-
"\n"
45314549
" {%- if messages[0]['role'] in ['system', 'developer'] -%}\n"
45324550
" {{- messages[0]['content'] | trim -}}\n"
45334551
" {%- set loop_messages = messages[1:] -%}\n"
45344552
" {%- endif -%}\n"
4535-
"\n"
45364553
" {%- if tools -%}\n"
45374554
" {%- for tool in tools %}\n"
45384555
" {{- '<|tool>' -}}\n"
@@ -4541,16 +4558,41 @@ class Gemma4ChatHandler(MTMDChatHandler):
45414558
" {%- endfor %}\n"
45424559
" {%- set ns.prev_message_type = 'tool' -%}\n"
45434560
" {%- endif -%}\n"
4544-
"\n"
45454561
" {{- '<turn|>\\n' -}}\n"
45464562
"{%- endif %}\n"
4547-
"\n"
4563+
"{#- Pre-scan: find last user message index for reasoning guard -#}\n"
4564+
"{%- set ns_turn = namespace(last_user_idx=-1) -%}\n"
4565+
"{%- for i in range(loop_messages | length) -%}\n"
4566+
" {%- if loop_messages[i]['role'] == 'user' -%}\n"
4567+
" {%- set ns_turn.last_user_idx = i -%}\n"
4568+
" {%- endif -%}\n"
4569+
"{%- endfor -%}\n"
45484570
"{#- Loop through messages -#}\n"
45494571
"{%- for message in loop_messages -%}\n"
4572+
" {%- if message['role'] != 'tool' -%}\n"
45504573
" {%- set ns.prev_message_type = None -%}\n"
45514574
" {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}\n"
4575+
" {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}\n"
4576+
" {%- set prev_nt = namespace(role=None, found=false) -%}\n"
4577+
" {%- if loop.index0 > 0 -%}\n"
4578+
" {%- for j in range(loop.index0 - 1, -1, -1) -%}\n"
4579+
" {%- if not prev_nt.found -%}\n"
4580+
" {%- if loop_messages[j]['role'] != 'tool' -%}\n"
4581+
" {%- set prev_nt.role = loop_messages[j]['role'] -%}\n"
4582+
" {%- set prev_nt.found = true -%}\n"
4583+
" {%- endif -%}\n"
4584+
" {%- endif -%}\n"
4585+
" {%- endfor -%}\n"
4586+
" {%- endif -%}\n"
4587+
" {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}\n"
4588+
" {%- if not continue_same_model_turn -%}\n"
45524589
" {{- '<|turn>' + role + '\\n' }}\n"
4553-
"\n"
4590+
" {%- endif -%}\n"
4591+
" {#- Render reasoning/reasoning_content as thinking channel -#}\n"
4592+
" {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}\n"
4593+
" {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}\n"
4594+
" {{- '<|channel>thought\\n' + thinking_text + '\\n<channel|>' -}}\n"
4595+
" {%- endif -%}\n"
45544596
" {%- if message['tool_calls'] -%}\n"
45554597
" {%- for tool_call in message['tool_calls'] -%}\n"
45564598
" {%- set function = tool_call['function'] -%}\n"
@@ -4569,26 +4611,50 @@ class Gemma4ChatHandler(MTMDChatHandler):
45694611
" {%- endfor -%}\n"
45704612
" {%- set ns.prev_message_type = 'tool_call' -%}\n"
45714613
" {%- endif -%}\n"
4572-
"\n"
4573-
" {%- if message['tool_responses'] -%}\n"
4574-
" {#- Tool Response handling -#}\n"
4614+
" {%- set ns_tr_out = namespace(flag=false) -%}\n"
4615+
" {%- if message.get('tool_responses') -%}\n"
4616+
" {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}\n"
45754617
" {%- for tool_response in message['tool_responses'] -%}\n"
4576-
" {{- '<|tool_response>' -}}\n"
4577-
" {%- if tool_response['response'] is mapping -%}\n"
4578-
" {{- 'response:' + tool_response['name'] | default('unknown') + '{' -}}\n"
4579-
" {%- for key, value in tool_response['response'] | dictsort -%}\n"
4580-
" {{- key -}}:{{- format_argument(value, escape_keys=False) -}}\n"
4581-
" {%- if not loop.last %},{% endif -%}\n"
4582-
" {%- endfor -%}\n"
4583-
" {{- '}' -}}\n"
4618+
" {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}\n"
4619+
" {%- set ns_tr_out.flag = true -%}\n"
4620+
" {%- set ns.prev_message_type = 'tool_response' -%}\n"
4621+
" {%- endfor -%}\n"
4622+
" {%- elif message.get('tool_calls') -%}\n"
4623+
" {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}\n"
4624+
" {%- set ns_tool_scan = namespace(stopped=false) -%}\n"
4625+
" {%- for k in range(loop.index0 + 1, loop_messages | length) -%}\n"
4626+
" {%- if ns_tool_scan.stopped -%}\n"
4627+
" {%- elif loop_messages[k]['role'] != 'tool' -%}\n"
4628+
" {%- set ns_tool_scan.stopped = true -%}\n"
45844629
" {%- else -%}\n"
4585-
" {{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}}\n"
4630+
" {%- set follow = loop_messages[k] -%}\n"
4631+
" {#- Resolve tool_call_id to function name -#}\n"
4632+
" {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}\n"
4633+
" {%- for tc in message['tool_calls'] -%}\n"
4634+
" {%- if tc.get('id') == follow.get('tool_call_id') -%}\n"
4635+
" {%- set ns_tname.name = tc['function']['name'] -%}\n"
4636+
" {%- endif -%}\n"
4637+
" {%- endfor -%}\n"
4638+
" {#- Handle content as string or content-parts array -#}\n"
4639+
" {%- set tool_body = follow.get('content') -%}\n"
4640+
" {%- if tool_body is string -%}\n"
4641+
" {{- format_tool_response_block(ns_tname.name, tool_body) -}}\n"
4642+
" {%- elif tool_body is sequence and tool_body is not string -%}\n"
4643+
" {%- set ns_txt = namespace(s='') -%}\n"
4644+
" {%- for part in tool_body -%}\n"
4645+
" {%- if part.get('type') == 'text' -%}\n"
4646+
" {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}\n"
4647+
" {%- endif -%}\n"
4648+
" {%- endfor -%}\n"
4649+
" {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}\n"
4650+
" {%- else -%}\n"
4651+
" {{- format_tool_response_block(ns_tname.name, tool_body) -}}\n"
4652+
" {%- endif -%}\n"
4653+
" {%- set ns_tr_out.flag = true -%}\n"
4654+
" {%- set ns.prev_message_type = 'tool_response' -%}\n"
45864655
" {%- endif -%}\n"
4587-
" {{- '<tool_response|>' -}}\n"
45884656
" {%- endfor -%}\n"
4589-
" {%- set ns.prev_message_type = 'tool_response' -%}\n"
45904657
" {%- endif -%}\n"
4591-
"\n"
45924658
" {%- if message['content'] is string -%}\n"
45934659
" {%- if role == 'model' -%}\n"
45944660
" {{- strip_thinking(message['content']) -}}\n"
@@ -4605,35 +4671,36 @@ class Gemma4ChatHandler(MTMDChatHandler):
46054671
" {%- endif -%}\n"
46064672
" {%- elif item['type'] == 'image_url' -%}\n"
46074673
" {%- set url_val = item['image_url'] if item['image_url'] is string else item['image_url']['url'] -%}\n"
4608-
" {{- '\\n\\n<|image|>' + url_val + '\\n\\n' -}}\n"
4674+
" {{- '<|image|>' + url_val -}}\n"
46094675
" {%- set ns.prev_message_type = 'image' -%}\n"
46104676
" {%- elif item['type'] == 'audio_url' -%}\n"
46114677
" {%- set audio_val = item['audio_url'] if item['audio_url'] is string else item['audio_url']['url'] -%}\n"
4612-
" {{- '\\n\\n<|audio|>' + audio_val + '\\n\\n' -}}\n"
4678+
" {{- '<|audio|>' + audio_val -}}\n"
46134679
" {%- set ns.prev_message_type = 'audio' -%}\n"
46144680
" {%- elif item['type'] == 'input_audio' -%}\n"
46154681
" {%- set audio_val = item['input_audio'] if item['input_audio'] is string else ('data:audio/' + item['input_audio']['format'] + ';base64,' + item['input_audio']['data']) -%}\n"
4616-
" {{- '\\n\\n<|audio|>' + audio_val + '\\n\\n' -}}\n"
4682+
" {{- '<|audio|>' + audio_val -}}\n"
46174683
" {%- set ns.prev_message_type = 'audio' -%}\n"
46184684
# " {%- elif item['type'] == 'video_url' -%}\n"
46194685
# " {%- set video_val = item['video_url'] if item['video_url'] is string else item['video_url']['url'] -%}\n"
4620-
# " {{- '\\n\\n<|video|>' + video_val + '\\n\\n' -}}\n"
4686+
# " {{- '<|video|>' + video_val -}}\n"
46214687
# " {%- set ns.prev_message_type = 'video' -%}\n"
46224688
" {%- endif -%}\n"
46234689
" {%- endfor -%}\n"
46244690
" {%- endif -%}\n"
4625-
"\n"
4626-
" {%- if not (message['tool_responses'] and not message['content']) -%}\n"
4691+
" {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}\n"
4692+
" {{- '<|tool_response>' -}}\n"
4693+
" {%- elif not (ns_tr_out.flag and not message.get('content')) -%}\n"
46274694
" {{- '<turn|>\\n' -}}\n"
46284695
" {%- endif -%}\n"
4696+
" {%- endif -%}\n"
46294697
"{%- endfor -%}\n"
4630-
"\n"
46314698
"{%- if add_generation_prompt -%}\n"
4632-
" {%- if ns.prev_message_type != 'tool_response' -%}\n"
4699+
" {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}\n"
46334700
" {{- '<|turn>model\\n' -}}\n"
4634-
" {%- endif -%}\n"
4635-
" {%- if not enable_thinking | default(false) -%}\n"
4636-
" {{- '<|channel>thought\\n<channel|>' -}}\n"
4701+
" {%- if not enable_thinking | default(false) -%}\n"
4702+
" {{- '<|channel>thought\\n<channel|>' -}}\n"
4703+
" {%- endif -%}\n"
46374704
" {%- endif -%}\n"
46384705
"{%- endif -%}\n"
46394706
)

0 commit comments

Comments
 (0)