model: align gemma4 chat template handling (#17182)
Incorporate the upstream Gemma4 chat template refinements for tool-calling stability, turn closure, and multi-turn reasoning. This updates the native renderer and checked-in HF template fixtures to keep adjacent assistant/tool continuations in the same model turn, add the post-tool thought-channel cue when thinking is enabled, and match Google's default of not replaying historical thinking before a later user turn. Also preserve null tool arguments through Gemma4 rendering/parsing and extend the Jinja2 parity coverage for these upstream behaviors.
This commit is contained in:
@@ -74,6 +74,12 @@ func (p *Gemma4Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkVal
|
|||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if lastMessage != nil && lastMessage.Role == "tool" {
|
||||||
|
p.state = Gemma4CollectingThinking
|
||||||
|
p.needsChannelNameStrip = false
|
||||||
|
return tools
|
||||||
|
}
|
||||||
|
|
||||||
if prefill && lastMessage.Content != "" {
|
if prefill && lastMessage.Content != "" {
|
||||||
p.state = Gemma4CollectingContent
|
p.state = Gemma4CollectingContent
|
||||||
return tools
|
return tools
|
||||||
|
|||||||
@@ -102,6 +102,23 @@ func TestGemma4Parser(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "tool_call_with_null_arg",
|
||||||
|
input: `<|tool_call>call:set_optional{value:null,nested:{value:null}}<tool_call|>`,
|
||||||
|
expectedToolCalls: []api.ToolCall{
|
||||||
|
{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "set_optional",
|
||||||
|
Arguments: testArgs(map[string]any{
|
||||||
|
"value": nil,
|
||||||
|
"nested": map[string]any{
|
||||||
|
"value": nil,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "tool_call_with_nested_object",
|
name: "tool_call_with_nested_object",
|
||||||
input: `<|tool_call>call:process{config:{enabled:true,name:<|"|>test<|"|>}}<tool_call|>`,
|
input: `<|tool_call>call:process{config:{enabled:true,name:<|"|>test<|"|>}}<tool_call|>`,
|
||||||
@@ -508,6 +525,26 @@ func TestGemma4Parser_Streaming(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGemma4Parser_ToolResponseContinuationStartsInThinking(t *testing.T) {
|
||||||
|
parser := &Gemma4Parser{hasThinkingSupport: true}
|
||||||
|
parser.Init(nil, &api.Message{Role: "tool", Content: "go.mod\ngo.sum\n"}, &api.ThinkValue{Value: true})
|
||||||
|
|
||||||
|
content, thinking, toolCalls, err := parser.Add("The user asked for files.<channel|>go.mod and go.sum", true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Add() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff("go.mod and go.sum", content); diff != "" {
|
||||||
|
t.Errorf("content mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff("The user asked for files.", thinking); diff != "" {
|
||||||
|
t.Errorf("thinking mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
if len(toolCalls) != 0 {
|
||||||
|
t.Fatalf("expected no tool calls, got %+v", toolCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGemma4Parser_StreamingToolCall(t *testing.T) {
|
func TestGemma4Parser_StreamingToolCall(t *testing.T) {
|
||||||
parser := &Gemma4Parser{hasThinkingSupport: false}
|
parser := &Gemma4Parser{hasThinkingSupport: false}
|
||||||
parser.Init(nil, nil, nil)
|
parser.Init(nil, nil, nil)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
|||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
imageOffset := 0
|
imageOffset := 0
|
||||||
|
|
||||||
// BOS token — Gemma 4 models have add_bos_token=false in their tokenizer
|
// BOS token: Gemma 4 models have add_bos_token=false in their tokenizer
|
||||||
// config, so the tokenizer does not auto-prepend BOS. We must emit it
|
// config, so the tokenizer does not auto-prepend BOS. We must emit it
|
||||||
// explicitly in the rendered prompt, matching the HF chat template.
|
// explicitly in the rendered prompt, matching the HF chat template.
|
||||||
sb.WriteString("<bos>")
|
sb.WriteString("<bos>")
|
||||||
@@ -67,6 +67,7 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
|||||||
}
|
}
|
||||||
|
|
||||||
var prevMessageType string
|
var prevMessageType string
|
||||||
|
var prevNonToolRole string
|
||||||
|
|
||||||
// Consecutive tool messages are folded into the preceding assistant turn,
|
// Consecutive tool messages are folded into the preceding assistant turn,
|
||||||
// and adjacent assistant messages continue in the same model turn.
|
// and adjacent assistant messages continue in the same model turn.
|
||||||
@@ -81,12 +82,12 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
|||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
|
|
||||||
continueSameModelTurn := role == "model" && r.previousNonToolRole(loopMessages, i) == "assistant"
|
continueSameModelTurn := role == "model" && prevNonToolRole == "assistant"
|
||||||
if !continueSameModelTurn {
|
if !continueSameModelTurn {
|
||||||
sb.WriteString("<|turn>" + role + "\n")
|
sb.WriteString("<|turn>" + role + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.Role == "assistant" && message.Thinking != "" && i > lastUserIdx && len(message.ToolCalls) > 0 {
|
if message.Role == "assistant" && message.Thinking != "" && i > lastUserIdx {
|
||||||
sb.WriteString("<|channel>thought\n")
|
sb.WriteString("<|channel>thought\n")
|
||||||
sb.WriteString(message.Thinking)
|
sb.WriteString(message.Thinking)
|
||||||
sb.WriteString("\n<channel|>")
|
sb.WriteString("\n<channel|>")
|
||||||
@@ -123,11 +124,15 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
|||||||
messageHadContent = r.messageHasContent(message)
|
messageHadContent = r.messageHasContent(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextNonToolRole := r.nextNonToolRole(loopMessages, i)
|
||||||
|
continuesIntoNext := role == "model" && nextNonToolRole == "assistant" && (len(message.ToolCalls) == 0 || toolResponsesEmitted)
|
||||||
if prevMessageType == "tool_call" && !toolResponsesEmitted {
|
if prevMessageType == "tool_call" && !toolResponsesEmitted {
|
||||||
sb.WriteString("<|tool_response>")
|
sb.WriteString("<|tool_response>")
|
||||||
} else if !(toolResponsesEmitted && !messageHadContent) {
|
} else if !continuesIntoNext && !(toolResponsesEmitted && !messageHadContent && nextNonToolRole == "") {
|
||||||
sb.WriteString("<turn|>\n")
|
sb.WriteString("<turn|>\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevNonToolRole = message.Role
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generation prompt.
|
// Generation prompt.
|
||||||
@@ -136,6 +141,8 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
|||||||
if r.emptyBlockOnNothink && !hasThink {
|
if r.emptyBlockOnNothink && !hasThink {
|
||||||
sb.WriteString("<|channel>thought\n<channel|>")
|
sb.WriteString("<|channel>thought\n<channel|>")
|
||||||
}
|
}
|
||||||
|
} else if prevMessageType == "tool_response" && hasThink {
|
||||||
|
sb.WriteString("<|channel>thought\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String(), nil
|
return sb.String(), nil
|
||||||
@@ -181,8 +188,8 @@ func (r *Gemma4Renderer) renderToolResponseContent(msg api.Message, imageOffset
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Gemma4Renderer) previousNonToolRole(messages []api.Message, idx int) string {
|
func (r *Gemma4Renderer) nextNonToolRole(messages []api.Message, idx int) string {
|
||||||
for i := idx - 1; i >= 0; i-- {
|
for i := idx + 1; i < len(messages); i++ {
|
||||||
if messages[i].Role != "tool" {
|
if messages[i].Role != "tool" {
|
||||||
return messages[i].Role
|
return messages[i].Role
|
||||||
}
|
}
|
||||||
@@ -699,6 +706,8 @@ func (r *Gemma4Renderer) formatToolResponseBlock(toolName, response string) stri
|
|||||||
|
|
||||||
func (r *Gemma4Renderer) formatArgValue(value any) string {
|
func (r *Gemma4Renderer) formatArgValue(value any) string {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return "null"
|
||||||
case string:
|
case string:
|
||||||
return g4Q + v + g4Q
|
return g4Q + v + g4Q
|
||||||
case bool:
|
case bool:
|
||||||
@@ -747,6 +756,8 @@ func (r *Gemma4Renderer) formatMapValue(m map[string]any) string {
|
|||||||
|
|
||||||
func (r *Gemma4Renderer) formatSchemaValue(value any) string {
|
func (r *Gemma4Renderer) formatSchemaValue(value any) string {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return "null"
|
||||||
case string:
|
case string:
|
||||||
return g4Q + v + g4Q
|
return g4Q + v + g4Q
|
||||||
case bool:
|
case bool:
|
||||||
|
|||||||
@@ -906,7 +906,7 @@ func TestGemma4RendererMatchesReference(t *testing.T) {
|
|||||||
},
|
},
|
||||||
// === Additional edge cases ported from original tests ===
|
// === Additional edge cases ported from original tests ===
|
||||||
{
|
{
|
||||||
// Assistant content with tool call — template emits tool_calls before content
|
// Assistant content with tool call: template emits tool_calls before content
|
||||||
name: "assistant_content_with_tool_call",
|
name: "assistant_content_with_tool_call",
|
||||||
messages: []api.Message{
|
messages: []api.Message{
|
||||||
{Role: "user", Content: "Weather?"},
|
{Role: "user", Content: "Weather?"},
|
||||||
@@ -988,9 +988,23 @@ func TestGemma4RendererMatchesReference(t *testing.T) {
|
|||||||
"<|turn>user\nWeather?<turn|>\n" +
|
"<|turn>user\nWeather?<turn|>\n" +
|
||||||
"<|turn>model\n<|tool_call>call:get_weather{city:" + q + "Tokyo" + q + "}<tool_call|>" +
|
"<|turn>model\n<|tool_call>call:get_weather{city:" + q + "Tokyo" + q + "}<tool_call|>" +
|
||||||
"<|tool_response>response:get_weather{value:" + q + `{"temperature": 15, "weather": "sunny"}` + q + "}<tool_response|>" +
|
"<|tool_response>response:get_weather{value:" + q + `{"temperature": 15, "weather": "sunny"}` + q + "}<tool_response|>" +
|
||||||
|
"<turn|>\n" +
|
||||||
"<|turn>user\nThanks!<turn|>\n" +
|
"<|turn>user\nThanks!<turn|>\n" +
|
||||||
"<|turn>model\n",
|
"<|turn>model\n",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "adjacent_assistants_continue_same_model_turn",
|
||||||
|
messages: []api.Message{
|
||||||
|
{Role: "user", Content: "Start"},
|
||||||
|
{Role: "assistant", Content: "One."},
|
||||||
|
{Role: "assistant", Content: "Two."},
|
||||||
|
{Role: "user", Content: "More"},
|
||||||
|
},
|
||||||
|
expected: "<bos><|turn>user\nStart<turn|>\n" +
|
||||||
|
"<|turn>model\nOne.Two.<turn|>\n" +
|
||||||
|
"<|turn>user\nMore<turn|>\n" +
|
||||||
|
"<|turn>model\n",
|
||||||
|
},
|
||||||
// === Ordering and whitespace edge cases ===
|
// === Ordering and whitespace edge cases ===
|
||||||
{
|
{
|
||||||
// Tool call arguments are sorted alphabetically
|
// Tool call arguments are sorted alphabetically
|
||||||
@@ -1127,7 +1141,7 @@ Hi<turn|>
|
|||||||
"<|turn>model\n",
|
"<|turn>model\n",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Property with no description — just type
|
// Property with no description: just type
|
||||||
name: "property_no_description",
|
name: "property_no_description",
|
||||||
messages: []api.Message{{Role: "user", Content: "Count"}},
|
messages: []api.Message{{Role: "user", Content: "Count"}},
|
||||||
tools: countTool(),
|
tools: countTool(),
|
||||||
@@ -1407,7 +1421,7 @@ Hi<turn|>
|
|||||||
|
|
||||||
// === Round 5: coding agent patterns ===
|
// === Round 5: coding agent patterns ===
|
||||||
{
|
{
|
||||||
// Chained tool calls — assistant calls tool, gets result, calls another
|
// Chained tool calls: assistant calls tool, gets result, calls another
|
||||||
// tool, gets result, then the model responds. No user messages in between.
|
// tool, gets result, then the model responds. No user messages in between.
|
||||||
name: "chained_tool_calls",
|
name: "chained_tool_calls",
|
||||||
messages: []api.Message{
|
messages: []api.Message{
|
||||||
@@ -1486,6 +1500,58 @@ Hi<turn|>
|
|||||||
"<|turn>model\n<|tool_call>call:bash{command:" + q + q + "}<tool_call|>" +
|
"<|turn>model\n<|tool_call>call:bash{command:" + q + q + "}<tool_call|>" +
|
||||||
"<|tool_response>response:bash{value:" + q + "error" + q + "}<tool_response|>",
|
"<|tool_response>response:bash{value:" + q + "error" + q + "}<tool_response|>",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "null_tool_arguments",
|
||||||
|
messages: []api.Message{
|
||||||
|
{Role: "user", Content: "Set optional"},
|
||||||
|
{Role: "assistant", ToolCalls: []api.ToolCall{{
|
||||||
|
Function: api.ToolCallFunction{Name: "set_optional", Arguments: testArgs(map[string]any{
|
||||||
|
"nested": map[string]any{"value": nil},
|
||||||
|
"value": nil,
|
||||||
|
})},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
expected: "<bos><|turn>user\nSet optional<turn|>\n" +
|
||||||
|
"<|turn>model\n<|tool_call>call:set_optional{nested:{value:null},value:null}<tool_call|><|tool_response>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "historical_tool_call_thinking_omitted_before_later_user",
|
||||||
|
messages: []api.Message{
|
||||||
|
{Role: "user", Content: "List files"},
|
||||||
|
{Role: "assistant", Thinking: "Need the file list before answering.", ToolCalls: []api.ToolCall{{
|
||||||
|
Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})},
|
||||||
|
}}},
|
||||||
|
{Role: "tool", ToolName: "bash", Content: "file1.txt"},
|
||||||
|
{Role: "user", Content: "Now answer"},
|
||||||
|
},
|
||||||
|
tools: bashSmallTool(),
|
||||||
|
think: thinkTrue(),
|
||||||
|
expected: "<bos><|turn>system\n<|think|>\n" + bashSmallDeclRef + "<turn|>\n" +
|
||||||
|
"<|turn>user\nList files<turn|>\n" +
|
||||||
|
"<|turn>model\n" +
|
||||||
|
"<|tool_call>call:bash{command:" + q + "ls" + q + "}<tool_call|>" +
|
||||||
|
"<|tool_response>response:bash{value:" + q + "file1.txt" + q + "}<tool_response|>" +
|
||||||
|
"<turn|>\n" +
|
||||||
|
"<|turn>user\nNow answer<turn|>\n" +
|
||||||
|
"<|turn>model\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "terminal_tool_response_adds_thinking_cue_when_thinking_enabled",
|
||||||
|
messages: []api.Message{
|
||||||
|
{Role: "user", Content: "List files"},
|
||||||
|
{Role: "assistant", ToolCalls: []api.ToolCall{{
|
||||||
|
Function: api.ToolCallFunction{Name: "bash", Arguments: testArgs(map[string]any{"command": "ls"})},
|
||||||
|
}}},
|
||||||
|
{Role: "tool", ToolName: "bash", Content: "file1.txt"},
|
||||||
|
},
|
||||||
|
tools: bashSmallTool(),
|
||||||
|
think: thinkTrue(),
|
||||||
|
expected: "<bos><|turn>system\n<|think|>\n" + bashSmallDeclRef + "<turn|>\n" +
|
||||||
|
"<|turn>user\nList files<turn|>\n" +
|
||||||
|
"<|turn>model\n<|tool_call>call:bash{command:" + q + "ls" + q + "}<tool_call|>" +
|
||||||
|
"<|tool_response>response:bash{value:" + q + "file1.txt" + q + "}<tool_response|>" +
|
||||||
|
"<|channel>thought\n",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyJinja2 := os.Getenv("VERIFY_JINJA2") != ""
|
verifyJinja2 := os.Getenv("VERIFY_JINJA2") != ""
|
||||||
@@ -1561,6 +1627,50 @@ func TestGemma4LargeRendererOmitsEmptyThoughtBlockWhenThinkingEnabled(t *testing
|
|||||||
assert.NotContains(t, got, "<|channel>thought\n<channel|>")
|
assert.NotContains(t, got, "<|channel>thought\n<channel|>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGemma4RendererCanonicalToolContinuations(t *testing.T) {
|
||||||
|
q := `<|"|>`
|
||||||
|
messages := []api.Message{
|
||||||
|
{Role: "user", Content: "Use the tool"},
|
||||||
|
{Role: "assistant", Content: "First."},
|
||||||
|
{Role: "assistant", Content: "Checking.", ToolCalls: []api.ToolCall{{
|
||||||
|
Function: api.ToolCallFunction{
|
||||||
|
Name: "bash",
|
||||||
|
Arguments: testArgs(map[string]any{"command": nil}),
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
{Role: "tool", ToolName: "bash", Content: "done"},
|
||||||
|
{Role: "assistant", Content: "Done."},
|
||||||
|
{Role: "user", Content: "Next"},
|
||||||
|
}
|
||||||
|
|
||||||
|
base := "<bos><|turn>system\n" + bashSmallDeclRef + "<turn|>\n" +
|
||||||
|
"<|turn>user\nUse the tool<turn|>\n" +
|
||||||
|
"<|turn>model\nFirst." +
|
||||||
|
"<|tool_call>call:bash{command:null}<tool_call|>" +
|
||||||
|
"<|tool_response>response:bash{value:" + q + "done" + q + "}<tool_response|>" +
|
||||||
|
"Checking.Done.<turn|>\n" +
|
||||||
|
"<|turn>user\nNext<turn|>\n<|turn>model\n"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
renderer *Gemma4Renderer
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "small", renderer: &Gemma4Renderer{}, want: base},
|
||||||
|
{name: "31b", renderer: &Gemma4Renderer{emptyBlockOnNothink: true}, want: base + "<|channel>thought\n<channel|>"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.renderer.Render(messages, bashSmallTool(), nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
assert.NotContains(t, got, "First.<turn|>\n<|tool_call>")
|
||||||
|
assert.NotContains(t, got, "Checking.<turn|>\nDone.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGemma4RendererMatchesJinja2ExpandedParity(t *testing.T) {
|
func TestGemma4RendererMatchesJinja2ExpandedParity(t *testing.T) {
|
||||||
if os.Getenv("VERIFY_JINJA2") == "" {
|
if os.Getenv("VERIFY_JINJA2") == "" {
|
||||||
t.Skip("set VERIFY_JINJA2=1 to run expanded Jinja2 parity checks")
|
t.Skip("set VERIFY_JINJA2=1 to run expanded Jinja2 parity checks")
|
||||||
|
|||||||
+75
-39
@@ -1,3 +1,9 @@
|
|||||||
|
{#
|
||||||
|
Template: Google Gemma 4 Canonical Chat Template
|
||||||
|
Author: Google Gemma Engineering Team
|
||||||
|
Published: 2026-07-09
|
||||||
|
Context: Fixed tool-calling loops, turn closures, and thinking content-ordering.
|
||||||
|
#}
|
||||||
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
||||||
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
||||||
{%- set ns = namespace(found_first=false) -%}
|
{%- set ns = namespace(found_first=false) -%}
|
||||||
@@ -116,7 +122,9 @@
|
|||||||
}
|
}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
{%- macro format_argument(argument, escape_keys=True) -%}
|
{%- macro format_argument(argument, escape_keys=True) -%}
|
||||||
{%- if argument is string -%}
|
{%- if argument is none -%}
|
||||||
|
{{- 'null' -}}
|
||||||
|
{%- elif argument is string -%}
|
||||||
{{- '<|"|>' + argument + '<|"|>' -}}
|
{{- '<|"|>' + argument + '<|"|>' -}}
|
||||||
{%- elif argument is boolean -%}
|
{%- elif argument is boolean -%}
|
||||||
{{- 'true' if argument else 'false' -}}
|
{{- 'true' if argument else 'false' -}}
|
||||||
@@ -172,18 +180,21 @@
|
|||||||
{{- '<tool_response|>' -}}
|
{{- '<tool_response|>' -}}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{%- set ns = namespace(prev_message_type=None) -%}
|
{#- ===== SETUP ===== -#}
|
||||||
|
{%- set ns = namespace(prev_message_type=None, prev_non_tool_role=None) -%}
|
||||||
{%- set loop_messages = messages -%}
|
{%- set loop_messages = messages -%}
|
||||||
|
{%- set enable_thinking = enable_thinking | default(false) -%}
|
||||||
|
{%- set preserve_thinking = preserve_thinking | default(false) -%}
|
||||||
{{- bos_token -}}
|
{{- bos_token -}}
|
||||||
{#- Handle System/Tool Definitions Block -#}
|
{#- Handle System/Tool Definitions Block -#}
|
||||||
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
|
{%- if enable_thinking or tools or (messages and messages[0]['role'] in ['system', 'developer']) -%}
|
||||||
{{- '<|turn>system\n' -}}
|
{{- '<|turn>system\n' -}}
|
||||||
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
||||||
{%- if enable_thinking is defined and enable_thinking -%}
|
{%- if enable_thinking -%}
|
||||||
{{- '<|think|>\n' -}}
|
{{- '<|think|>\n' -}}
|
||||||
{%- set ns.prev_message_type = 'think' -%}
|
{%- set ns.prev_message_type = 'think' -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if messages[0]['role'] in ['system', 'developer'] -%}
|
{%- if messages and messages[0]['role'] in ['system', 'developer'] -%}
|
||||||
{%- if messages[0]['content'] is string -%}
|
{%- if messages[0]['content'] is string -%}
|
||||||
{{- messages[0]['content'] | trim -}}
|
{{- messages[0]['content'] | trim -}}
|
||||||
{%- elif messages[0]['content'] is sequence -%}
|
{%- elif messages[0]['content'] is sequence -%}
|
||||||
@@ -217,31 +228,22 @@
|
|||||||
{%- if message['role'] != 'tool' -%}
|
{%- if message['role'] != 'tool' -%}
|
||||||
{%- set ns.prev_message_type = None -%}
|
{%- set ns.prev_message_type = None -%}
|
||||||
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
||||||
{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
|
{#- Detect continuation using tracked state - O(1) instead of O(n) backward scan -#}
|
||||||
{%- set prev_nt = namespace(role=None, found=false) -%}
|
{%- set continue_same_model_turn = (role == 'model' and ns.prev_non_tool_role == 'assistant') -%}
|
||||||
{%- if loop.index0 > 0 -%}
|
|
||||||
{%- for j in range(loop.index0 - 1, -1, -1) -%}
|
|
||||||
{%- if not prev_nt.found -%}
|
|
||||||
{%- if loop_messages[j]['role'] != 'tool' -%}
|
|
||||||
{%- set prev_nt.role = loop_messages[j]['role'] -%}
|
|
||||||
{%- set prev_nt.found = true -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
|
|
||||||
{%- if not continue_same_model_turn -%}
|
{%- if not continue_same_model_turn -%}
|
||||||
{{- '<|turn>' + role + '\n' }}
|
{{- '<|turn>' + role + '\n' }}
|
||||||
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- Render reasoning/reasoning_content as thinking channel -#}
|
{#- Render reasoning/reasoning_content as thinking channel -#}
|
||||||
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
||||||
{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
|
{%- set thinking_gate = (loop.index0 > ns_turn.last_user_idx) or (preserve_thinking and message.get('tool_calls')) -%}
|
||||||
|
{%- if thinking_text and thinking_gate -%}
|
||||||
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if message['tool_calls'] -%}
|
{%- if message.get('tool_calls') -%}
|
||||||
{%- for tool_call in message['tool_calls'] -%}
|
{%- for tool_call in message.get('tool_calls') -%}
|
||||||
{%- set function = tool_call['function'] -%}
|
{%- set function = tool_call['function'] -%}
|
||||||
{{- '<|tool_call>call:' + function['name'] + '{' -}}
|
{{- '<|tool_call>call:' + function['name'] + '{' -}}
|
||||||
{%- if function['arguments'] is mapping -%}
|
{%- if function['arguments'] is mapping -%}
|
||||||
@@ -251,8 +253,13 @@
|
|||||||
{%- set ns_args.found_first = true -%}
|
{%- set ns_args.found_first = true -%}
|
||||||
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- elif function['arguments'] is string -%}
|
{%- elif function['arguments'] is none -%}
|
||||||
{{- function['arguments'] -}}
|
{%- else -%}
|
||||||
|
{{- raise_exception(
|
||||||
|
"chat_template: tool_calls[].function.arguments must be a "
|
||||||
|
"JSON object (mapping), not a string. Deserialize arguments "
|
||||||
|
"before passing to the template."
|
||||||
|
) -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{- '}<tool_call|>' -}}
|
{{- '}<tool_call|>' -}}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -262,8 +269,8 @@
|
|||||||
{%- set ns_tr_out = namespace(flag=false) -%}
|
{%- set ns_tr_out = namespace(flag=false) -%}
|
||||||
{%- if message.get('tool_responses') -%}
|
{%- if message.get('tool_responses') -%}
|
||||||
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
||||||
{%- for tool_response in message['tool_responses'] -%}
|
{%- for tool_response in message.get('tool_responses') -%}
|
||||||
{{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
|
{{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}
|
||||||
{%- set ns_tr_out.flag = true -%}
|
{%- set ns_tr_out.flag = true -%}
|
||||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -277,8 +284,8 @@
|
|||||||
{%- else -%}
|
{%- else -%}
|
||||||
{%- set follow = loop_messages[k] -%}
|
{%- set follow = loop_messages[k] -%}
|
||||||
{#- Resolve tool_call_id to function name -#}
|
{#- Resolve tool_call_id to function name -#}
|
||||||
{%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
|
{%- set ns_tname = namespace(name=follow.get('name') or 'unknown') -%}
|
||||||
{%- for tc in message['tool_calls'] -%}
|
{%- for tc in message.get('tool_calls') -%}
|
||||||
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
||||||
{%- set ns_tname.name = tc['function']['name'] -%}
|
{%- set ns_tname.name = tc['function']['name'] -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -295,6 +302,15 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
||||||
|
{%- for part in tool_body -%}
|
||||||
|
{%- if part.get('type') in ['image', 'image_url'] -%}
|
||||||
|
{{- '<|image|>' -}}
|
||||||
|
{%- elif part.get('type') in ['audio', 'input_audio'] -%}
|
||||||
|
{{- '<|audio|>' -}}
|
||||||
|
{%- elif part.get('type') == 'video' -%}
|
||||||
|
{{- '<|video|>' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -305,29 +321,26 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- set captured_content -%}
|
{%- set captured_content -%}
|
||||||
{%- if message['content'] is string -%}
|
{%- if message.get('content') is string -%}
|
||||||
{%- if role == 'model' -%}
|
{%- if role == 'model' -%}
|
||||||
{{- strip_thinking(message['content']) -}}
|
{{- strip_thinking(message['content']) -}}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- message['content'] | trim -}}
|
{{- message['content'] | trim -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- elif message['content'] is sequence -%}
|
{%- elif message.get('content') is sequence -%}
|
||||||
{%- for item in message['content'] -%}
|
{%- for item in message['content'] -%}
|
||||||
{%- if item['type'] == 'text' -%}
|
{%- if item.get('type') == 'text' -%}
|
||||||
{%- if role == 'model' -%}
|
{%- if role == 'model' -%}
|
||||||
{{- strip_thinking(item['text']) -}}
|
{{- strip_thinking(item['text']) -}}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- item['text'] | trim -}}
|
{{- item['text'] | trim -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- elif item['type'] == 'image' -%}
|
{%- elif item.get('type') in ['image', 'image_url'] -%}
|
||||||
{{- '<|image|>' -}}
|
{{- '<|image|>' -}}
|
||||||
{%- set ns.prev_message_type = 'image' -%}
|
{%- elif item.get('type') in ['audio', 'input_audio'] -%}
|
||||||
{%- elif item['type'] == 'audio' -%}
|
|
||||||
{{- '<|audio|>' -}}
|
{{- '<|audio|>' -}}
|
||||||
{%- set ns.prev_message_type = 'audio' -%}
|
{%- elif item.get('type') == 'video' -%}
|
||||||
{%- elif item['type'] == 'video' -%}
|
|
||||||
{{- '<|video|>' -}}
|
{{- '<|video|>' -}}
|
||||||
{%- set ns.prev_message_type = 'video' -%}
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -336,19 +349,42 @@
|
|||||||
{{- captured_content -}}
|
{{- captured_content -}}
|
||||||
{%- set has_content = captured_content | trim | length > 0 -%}
|
{%- set has_content = captured_content | trim | length > 0 -%}
|
||||||
|
|
||||||
|
{#- Forward-scan: find next non-tool message role for continuation detection -#}
|
||||||
|
{%- set next_nt = namespace(role=None, found=false) -%}
|
||||||
|
{%- for j in range(loop.index0 + 1, loop_messages | length) -%}
|
||||||
|
{%- if not next_nt.found -%}
|
||||||
|
{%- if loop_messages[j]['role'] != 'tool' -%}
|
||||||
|
{%- set next_nt.role = loop_messages[j]['role'] -%}
|
||||||
|
{%- set next_nt.found = true -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- set continues_into_next = (
|
||||||
|
role == 'model'
|
||||||
|
and next_nt.role == 'assistant'
|
||||||
|
and (not message.get('tool_calls') or ns_tr_out.flag)
|
||||||
|
) -%}
|
||||||
|
|
||||||
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
||||||
{{- '<|tool_response>' -}}
|
{{- '<|tool_response>' -}}
|
||||||
{%- elif not (ns_tr_out.flag and not has_content) -%}
|
{%- elif continues_into_next -%}
|
||||||
|
{%- elif not (ns_tr_out.flag and not has_content and not next_nt.found) -%}
|
||||||
{{- '<turn|>\n' -}}
|
{{- '<turn|>\n' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- Track previous non-tool role for next iteration (avoids O(n) backward scan) -#}
|
||||||
|
{%- set ns.prev_non_tool_role = message['role'] -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
||||||
{%- if add_generation_prompt -%}
|
{%- if add_generation_prompt -%}
|
||||||
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
||||||
{{- '<|turn>model\n' -}}
|
{{- '<|turn>model\n' -}}
|
||||||
{%- if not enable_thinking | default(false) -%}
|
{%- if not enable_thinking -%}
|
||||||
{{- '<|channel>thought\n<channel|>' -}}
|
{{- '<|channel>thought\n<channel|>' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
{%- elif ns.prev_message_type == 'tool_response' and enable_thinking -%}
|
||||||
|
{{- '<|channel>thought\n' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
+74
-38
@@ -1,3 +1,9 @@
|
|||||||
|
{#
|
||||||
|
Template: Google Gemma 4 Canonical Chat Template
|
||||||
|
Author: Google Gemma Engineering Team
|
||||||
|
Published: 2026-07-09
|
||||||
|
Context: Fixed tool-calling loops, turn closures, and thinking content-ordering.
|
||||||
|
#}
|
||||||
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
{%- macro format_parameters(properties, required, filter_keys=false) -%}
|
||||||
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
{%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
|
||||||
{%- set ns = namespace(found_first=false) -%}
|
{%- set ns = namespace(found_first=false) -%}
|
||||||
@@ -116,7 +122,9 @@
|
|||||||
}
|
}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
{%- macro format_argument(argument, escape_keys=True) -%}
|
{%- macro format_argument(argument, escape_keys=True) -%}
|
||||||
{%- if argument is string -%}
|
{%- if argument is none -%}
|
||||||
|
{{- 'null' -}}
|
||||||
|
{%- elif argument is string -%}
|
||||||
{{- '<|"|>' + argument + '<|"|>' -}}
|
{{- '<|"|>' + argument + '<|"|>' -}}
|
||||||
{%- elif argument is boolean -%}
|
{%- elif argument is boolean -%}
|
||||||
{{- 'true' if argument else 'false' -}}
|
{{- 'true' if argument else 'false' -}}
|
||||||
@@ -172,18 +180,21 @@
|
|||||||
{{- '<tool_response|>' -}}
|
{{- '<tool_response|>' -}}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{%- set ns = namespace(prev_message_type=None) -%}
|
{#- ===== SETUP ===== -#}
|
||||||
|
{%- set ns = namespace(prev_message_type=None, prev_non_tool_role=None) -%}
|
||||||
{%- set loop_messages = messages -%}
|
{%- set loop_messages = messages -%}
|
||||||
|
{%- set enable_thinking = enable_thinking | default(false) -%}
|
||||||
|
{%- set preserve_thinking = preserve_thinking | default(false) -%}
|
||||||
{{- bos_token -}}
|
{{- bos_token -}}
|
||||||
{#- Handle System/Tool Definitions Block -#}
|
{#- Handle System/Tool Definitions Block -#}
|
||||||
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
|
{%- if enable_thinking or tools or (messages and messages[0]['role'] in ['system', 'developer']) -%}
|
||||||
{{- '<|turn>system\n' -}}
|
{{- '<|turn>system\n' -}}
|
||||||
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
||||||
{%- if enable_thinking is defined and enable_thinking -%}
|
{%- if enable_thinking -%}
|
||||||
{{- '<|think|>\n' -}}
|
{{- '<|think|>\n' -}}
|
||||||
{%- set ns.prev_message_type = 'think' -%}
|
{%- set ns.prev_message_type = 'think' -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if messages[0]['role'] in ['system', 'developer'] -%}
|
{%- if messages and messages[0]['role'] in ['system', 'developer'] -%}
|
||||||
{%- if messages[0]['content'] is string -%}
|
{%- if messages[0]['content'] is string -%}
|
||||||
{{- messages[0]['content'] | trim -}}
|
{{- messages[0]['content'] | trim -}}
|
||||||
{%- elif messages[0]['content'] is sequence -%}
|
{%- elif messages[0]['content'] is sequence -%}
|
||||||
@@ -217,31 +228,22 @@
|
|||||||
{%- if message['role'] != 'tool' -%}
|
{%- if message['role'] != 'tool' -%}
|
||||||
{%- set ns.prev_message_type = None -%}
|
{%- set ns.prev_message_type = None -%}
|
||||||
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
||||||
{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
|
{#- Detect continuation using tracked state - O(1) instead of O(n) backward scan -#}
|
||||||
{%- set prev_nt = namespace(role=None, found=false) -%}
|
{%- set continue_same_model_turn = (role == 'model' and ns.prev_non_tool_role == 'assistant') -%}
|
||||||
{%- if loop.index0 > 0 -%}
|
|
||||||
{%- for j in range(loop.index0 - 1, -1, -1) -%}
|
|
||||||
{%- if not prev_nt.found -%}
|
|
||||||
{%- if loop_messages[j]['role'] != 'tool' -%}
|
|
||||||
{%- set prev_nt.role = loop_messages[j]['role'] -%}
|
|
||||||
{%- set prev_nt.found = true -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- endfor -%}
|
|
||||||
{%- endif -%}
|
|
||||||
{%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
|
|
||||||
{%- if not continue_same_model_turn -%}
|
{%- if not continue_same_model_turn -%}
|
||||||
{{- '<|turn>' + role + '\n' }}
|
{{- '<|turn>' + role + '\n' }}
|
||||||
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{#- Render reasoning/reasoning_content as thinking channel -#}
|
{#- Render reasoning/reasoning_content as thinking channel -#}
|
||||||
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
{%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
|
||||||
{%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
|
{%- set thinking_gate = (loop.index0 > ns_turn.last_user_idx) or (preserve_thinking and message.get('tool_calls')) -%}
|
||||||
|
{%- if thinking_text and thinking_gate -%}
|
||||||
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- if message['tool_calls'] -%}
|
{%- if message.get('tool_calls') -%}
|
||||||
{%- for tool_call in message['tool_calls'] -%}
|
{%- for tool_call in message.get('tool_calls') -%}
|
||||||
{%- set function = tool_call['function'] -%}
|
{%- set function = tool_call['function'] -%}
|
||||||
{{- '<|tool_call>call:' + function['name'] + '{' -}}
|
{{- '<|tool_call>call:' + function['name'] + '{' -}}
|
||||||
{%- if function['arguments'] is mapping -%}
|
{%- if function['arguments'] is mapping -%}
|
||||||
@@ -251,8 +253,13 @@
|
|||||||
{%- set ns_args.found_first = true -%}
|
{%- set ns_args.found_first = true -%}
|
||||||
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- elif function['arguments'] is string -%}
|
{%- elif function['arguments'] is none -%}
|
||||||
{{- function['arguments'] -}}
|
{%- else -%}
|
||||||
|
{{- raise_exception(
|
||||||
|
"chat_template: tool_calls[].function.arguments must be a "
|
||||||
|
"JSON object (mapping), not a string. Deserialize arguments "
|
||||||
|
"before passing to the template."
|
||||||
|
) -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{{- '}<tool_call|>' -}}
|
{{- '}<tool_call|>' -}}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -262,8 +269,8 @@
|
|||||||
{%- set ns_tr_out = namespace(flag=false) -%}
|
{%- set ns_tr_out = namespace(flag=false) -%}
|
||||||
{%- if message.get('tool_responses') -%}
|
{%- if message.get('tool_responses') -%}
|
||||||
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
||||||
{%- for tool_response in message['tool_responses'] -%}
|
{%- for tool_response in message.get('tool_responses') -%}
|
||||||
{{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
|
{{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}
|
||||||
{%- set ns_tr_out.flag = true -%}
|
{%- set ns_tr_out.flag = true -%}
|
||||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -277,8 +284,8 @@
|
|||||||
{%- else -%}
|
{%- else -%}
|
||||||
{%- set follow = loop_messages[k] -%}
|
{%- set follow = loop_messages[k] -%}
|
||||||
{#- Resolve tool_call_id to function name -#}
|
{#- Resolve tool_call_id to function name -#}
|
||||||
{%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
|
{%- set ns_tname = namespace(name=follow.get('name') or 'unknown') -%}
|
||||||
{%- for tc in message['tool_calls'] -%}
|
{%- for tc in message.get('tool_calls') -%}
|
||||||
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
||||||
{%- set ns_tname.name = tc['function']['name'] -%}
|
{%- set ns_tname.name = tc['function']['name'] -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -295,6 +302,15 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
||||||
|
{%- for part in tool_body -%}
|
||||||
|
{%- if part.get('type') in ['image', 'image_url'] -%}
|
||||||
|
{{- '<|image|>' -}}
|
||||||
|
{%- elif part.get('type') in ['audio', 'input_audio'] -%}
|
||||||
|
{{- '<|audio|>' -}}
|
||||||
|
{%- elif part.get('type') == 'video' -%}
|
||||||
|
{{- '<|video|>' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -305,29 +321,26 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
{%- set captured_content -%}
|
{%- set captured_content -%}
|
||||||
{%- if message['content'] is string -%}
|
{%- if message.get('content') is string -%}
|
||||||
{%- if role == 'model' -%}
|
{%- if role == 'model' -%}
|
||||||
{{- strip_thinking(message['content']) -}}
|
{{- strip_thinking(message['content']) -}}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- message['content'] | trim -}}
|
{{- message['content'] | trim -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- elif message['content'] is sequence -%}
|
{%- elif message.get('content') is sequence -%}
|
||||||
{%- for item in message['content'] -%}
|
{%- for item in message['content'] -%}
|
||||||
{%- if item['type'] == 'text' -%}
|
{%- if item.get('type') == 'text' -%}
|
||||||
{%- if role == 'model' -%}
|
{%- if role == 'model' -%}
|
||||||
{{- strip_thinking(item['text']) -}}
|
{{- strip_thinking(item['text']) -}}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{{- item['text'] | trim -}}
|
{{- item['text'] | trim -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- elif item['type'] == 'image' -%}
|
{%- elif item.get('type') in ['image', 'image_url'] -%}
|
||||||
{{- '<|image|>' -}}
|
{{- '<|image|>' -}}
|
||||||
{%- set ns.prev_message_type = 'image' -%}
|
{%- elif item.get('type') in ['audio', 'input_audio'] -%}
|
||||||
{%- elif item['type'] == 'audio' -%}
|
|
||||||
{{- '<|audio|>' -}}
|
{{- '<|audio|>' -}}
|
||||||
{%- set ns.prev_message_type = 'audio' -%}
|
{%- elif item.get('type') == 'video' -%}
|
||||||
{%- elif item['type'] == 'video' -%}
|
|
||||||
{{- '<|video|>' -}}
|
{{- '<|video|>' -}}
|
||||||
{%- set ns.prev_message_type = 'video' -%}
|
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@@ -336,16 +349,39 @@
|
|||||||
{{- captured_content -}}
|
{{- captured_content -}}
|
||||||
{%- set has_content = captured_content | trim | length > 0 -%}
|
{%- set has_content = captured_content | trim | length > 0 -%}
|
||||||
|
|
||||||
|
{#- Forward-scan: find next non-tool message role for continuation detection -#}
|
||||||
|
{%- set next_nt = namespace(role=None, found=false) -%}
|
||||||
|
{%- for j in range(loop.index0 + 1, loop_messages | length) -%}
|
||||||
|
{%- if not next_nt.found -%}
|
||||||
|
{%- if loop_messages[j]['role'] != 'tool' -%}
|
||||||
|
{%- set next_nt.role = loop_messages[j]['role'] -%}
|
||||||
|
{%- set next_nt.found = true -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- set continues_into_next = (
|
||||||
|
role == 'model'
|
||||||
|
and next_nt.role == 'assistant'
|
||||||
|
and (not message.get('tool_calls') or ns_tr_out.flag)
|
||||||
|
) -%}
|
||||||
|
|
||||||
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
||||||
{{- '<|tool_response>' -}}
|
{{- '<|tool_response>' -}}
|
||||||
{%- elif not (ns_tr_out.flag and not has_content) -%}
|
{%- elif continues_into_next -%}
|
||||||
|
{%- elif not (ns_tr_out.flag and not has_content and not next_nt.found) -%}
|
||||||
{{- '<turn|>\n' -}}
|
{{- '<turn|>\n' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{#- Track previous non-tool role for next iteration (avoids O(n) backward scan) -#}
|
||||||
|
{%- set ns.prev_non_tool_role = message['role'] -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
||||||
{%- if add_generation_prompt -%}
|
{%- if add_generation_prompt -%}
|
||||||
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
||||||
{{- '<|turn>model\n' -}}
|
{{- '<|turn>model\n' -}}
|
||||||
|
{%- elif ns.prev_message_type == 'tool_response' and enable_thinking -%}
|
||||||
|
{{- '<|channel>thought\n' -}}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
Reference in New Issue
Block a user