mirror of
https://github.com/ollama/ollama.git
synced 2026-04-17 19:54:03 +02:00
gemma4: update renderer to match new jinja template (#15490)
* gemma4: update renderer to match new jinja template Google has updated their jinja template for gemma4, and so this change gives us parity with the new template. The parsing also slightly changed upstream, so we make a small change to our parser as well. I've also corrected a few probably existing edge cases, especially around type unions. The upstream output format is weird (a stringified array), but in practice the models seem to understand it well. * gemma4: special case simple `AnyOf`s The upstream template doesn't handle `AnyOf`s, but since in the previous commit we saw type unions work reasonably well, I'm now treating very simple `AnyOf`s as type unions to help in cases where they might be used * fix lint * gemma4: prefer empty instead of `None` We can't currently distinguish between a result being not-present vs. empty. The empty case seems more important (e.g., a legitimately empty tool call) * gemma4: be more careful for tool results with missing IDs
This commit is contained in:
@@ -26,6 +26,7 @@ const (
|
||||
gemma4ThinkingCloseTag = "<channel|>"
|
||||
gemma4ToolCallOpenTag = "<|tool_call>"
|
||||
gemma4ToolCallCloseTag = "<tool_call|>"
|
||||
gemma4ToolResponseTag = "<|tool_response>"
|
||||
gemma4StringDelimiter = `<|"|>`
|
||||
)
|
||||
|
||||
@@ -326,26 +327,39 @@ func (p *Gemma4Parser) eat(done bool) ([]gemma4Event, bool) {
|
||||
|
||||
case Gemma4IgnoringPostToolCallNoise:
|
||||
// We've observed Gemma 4 occasionally emitting extra <tool_call|> tags
|
||||
// after a valid tool call. We suppress leading close tags in this immediate
|
||||
// post-tool-call state so the extra close tags do not leak into assistant
|
||||
// content. The tradeoff is that if the model intentionally begins its next
|
||||
// content span with the literal string "<tool_call|>", we will erroneously
|
||||
// treat it as noise and drop it.
|
||||
// after a valid tool call. We suppress those leading control tags in this
|
||||
// immediate post-tool-call state so they do not leak into assistant
|
||||
// content. The tradeoff is that if the model intentionally begins its next
|
||||
// content span with one of those literal strings, we will erroneously
|
||||
// treat it as noise and drop it. We also suppress a leading
|
||||
// <|tool_response> marker here because the updated upstream parser/template
|
||||
// uses it as a post-tool-call boundary.
|
||||
bufStr = strings.TrimLeftFunc(bufStr, unicode.IsSpace)
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(bufStr)
|
||||
|
||||
for strings.HasPrefix(bufStr, gemma4ToolCallCloseTag) {
|
||||
bufStr = strings.TrimLeftFunc(bufStr[len(gemma4ToolCallCloseTag):], unicode.IsSpace)
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(bufStr, gemma4ToolCallCloseTag):
|
||||
bufStr = strings.TrimLeftFunc(bufStr[len(gemma4ToolCallCloseTag):], unicode.IsSpace)
|
||||
case strings.HasPrefix(bufStr, gemma4ToolResponseTag):
|
||||
bufStr = strings.TrimLeftFunc(bufStr[len(gemma4ToolResponseTag):], unicode.IsSpace)
|
||||
default:
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(bufStr)
|
||||
goto strippedPostToolCallNoise
|
||||
}
|
||||
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(bufStr)
|
||||
}
|
||||
|
||||
strippedPostToolCallNoise:
|
||||
if bufStr == "" {
|
||||
return events, false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(gemma4ToolCallCloseTag, bufStr) {
|
||||
if strings.HasPrefix(gemma4ToolCallCloseTag, bufStr) || strings.HasPrefix(gemma4ToolResponseTag, bufStr) {
|
||||
if done {
|
||||
p.buffer.Reset()
|
||||
p.state = Gemma4CollectingContent
|
||||
|
||||
@@ -530,6 +530,77 @@ func TestGemma4Parser_IgnoresExtraToolCallCloseTags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGemma4Parser_IgnoresToolResponseBoundaryAfterToolCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
chunks []string
|
||||
expectedContent string
|
||||
}{
|
||||
{
|
||||
name: "same_chunk_without_trailing_content",
|
||||
chunks: []string{
|
||||
`<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}<tool_call|><|tool_response>`,
|
||||
},
|
||||
expectedContent: "",
|
||||
},
|
||||
{
|
||||
name: "same_chunk_before_real_content",
|
||||
chunks: []string{
|
||||
`<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}<tool_call|><|tool_response>Done.`,
|
||||
},
|
||||
expectedContent: "Done.",
|
||||
},
|
||||
{
|
||||
name: "split_across_chunks_before_real_content",
|
||||
chunks: []string{
|
||||
`<|tool_call>call:get_weather{location:<|"|>Paris<|"|>}<tool_call|><|tool_res`,
|
||||
`ponse>Done.`,
|
||||
},
|
||||
expectedContent: "Done.",
|
||||
},
|
||||
}
|
||||
|
||||
expectedToolCalls := []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: testArgs(map[string]any{
|
||||
"location": "Paris",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := &Gemma4Parser{hasThinkingSupport: false}
|
||||
parser.Init(nil, nil, nil)
|
||||
|
||||
var finalContent strings.Builder
|
||||
var finalToolCalls []api.ToolCall
|
||||
|
||||
for i, chunk := range tt.chunks {
|
||||
done := i == len(tt.chunks)-1
|
||||
content, _, toolCalls, err := parser.Add(chunk, done)
|
||||
if err != nil {
|
||||
t.Fatalf("Add() error on chunk %d: %v", i, err)
|
||||
}
|
||||
|
||||
finalContent.WriteString(content)
|
||||
finalToolCalls = append(finalToolCalls, toolCalls...)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.expectedContent, finalContent.String()); diff != "" {
|
||||
t.Errorf("content mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expectedToolCalls, finalToolCalls, argsComparer); diff != "" {
|
||||
t.Errorf("tool calls mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGemma4Parser_StreamingSplitThinkingTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -43,7 +43,7 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
||||
if hasSystemRole || len(tools) > 0 || hasThink {
|
||||
sb.WriteString("<|turn>system\n")
|
||||
if hasThink {
|
||||
sb.WriteString("<|think|>")
|
||||
sb.WriteString("<|think|>\n")
|
||||
}
|
||||
if systemMessage != "" {
|
||||
sb.WriteString(strings.TrimSpace(systemMessage))
|
||||
@@ -54,41 +54,80 @@ func (r *Gemma4Renderer) Render(messages []api.Message, tools []api.Tool, thinkV
|
||||
sb.WriteString("<turn|>\n")
|
||||
}
|
||||
|
||||
// Each message gets its own <|turn>role\n ... <turn|>\n block,
|
||||
// matching the HF chat template exactly.
|
||||
for _, message := range loopMessages {
|
||||
switch message.Role {
|
||||
case "user":
|
||||
sb.WriteString("<|turn>user\n")
|
||||
r.renderContent(&sb, message, &imageOffset, true)
|
||||
sb.WriteString("<turn|>\n")
|
||||
lastUserIdx := -1
|
||||
for i, message := range loopMessages {
|
||||
if message.Role == "user" {
|
||||
lastUserIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
case "assistant":
|
||||
sb.WriteString("<|turn>model\n")
|
||||
// Tool calls come before content (matching HF template order)
|
||||
var prevMessageType string
|
||||
|
||||
// Consecutive tool messages are folded into the preceding assistant turn,
|
||||
// and adjacent assistant messages continue in the same model turn.
|
||||
for i, message := range loopMessages {
|
||||
if message.Role == "tool" {
|
||||
continue
|
||||
}
|
||||
|
||||
messageHadContent := r.messageHasContent(message)
|
||||
prevMessageType = ""
|
||||
role := message.Role
|
||||
if role == "assistant" {
|
||||
role = "model"
|
||||
}
|
||||
|
||||
continueSameModelTurn := role == "model" && r.previousNonToolRole(loopMessages, i) == "assistant"
|
||||
if !continueSameModelTurn {
|
||||
sb.WriteString("<|turn>" + role + "\n")
|
||||
}
|
||||
|
||||
if message.Role == "assistant" && message.Thinking != "" && i > lastUserIdx && len(message.ToolCalls) > 0 {
|
||||
sb.WriteString("<|channel>thought\n")
|
||||
sb.WriteString(message.Thinking)
|
||||
sb.WriteString("\n<channel|>")
|
||||
}
|
||||
|
||||
if len(message.ToolCalls) > 0 {
|
||||
for _, tc := range message.ToolCalls {
|
||||
sb.WriteString(r.formatToolCall(tc))
|
||||
}
|
||||
// Strip thinking from history (matching HF strip_thinking macro)
|
||||
if message.Content != "" {
|
||||
sb.WriteString(stripThinking(message.Content))
|
||||
prevMessageType = "tool_call"
|
||||
}
|
||||
|
||||
toolResponsesEmitted := false
|
||||
if len(message.ToolCalls) > 0 {
|
||||
for k := i + 1; k < len(loopMessages) && loopMessages[k].Role == "tool"; k++ {
|
||||
sb.WriteString(r.formatToolResponseBlock(r.toolResponseName(loopMessages[k], message.ToolCalls), loopMessages[k].Content))
|
||||
toolResponsesEmitted = true
|
||||
prevMessageType = "tool_response"
|
||||
}
|
||||
sb.WriteString("<turn|>\n")
|
||||
|
||||
case "tool":
|
||||
sb.WriteString("<|turn>tool\n")
|
||||
sb.WriteString(strings.TrimSpace(message.Content))
|
||||
sb.WriteString("<turn|>\n")
|
||||
}
|
||||
|
||||
switch role {
|
||||
case "model":
|
||||
if message.Content != "" || len(message.Images) > 0 {
|
||||
message.Content = stripThinking(message.Content)
|
||||
r.renderContent(&sb, message, &imageOffset, false)
|
||||
}
|
||||
default:
|
||||
sb.WriteString("<|turn>" + message.Role + "\n")
|
||||
sb.WriteString(strings.TrimSpace(message.Content))
|
||||
r.renderContent(&sb, message, &imageOffset, true)
|
||||
}
|
||||
|
||||
if prevMessageType == "tool_call" && !toolResponsesEmitted {
|
||||
sb.WriteString("<|tool_response>")
|
||||
} else if !(toolResponsesEmitted && !messageHadContent) {
|
||||
sb.WriteString("<turn|>\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Generation prompt
|
||||
sb.WriteString("<|turn>model\n")
|
||||
// Generation prompt.
|
||||
if prevMessageType != "tool_response" && prevMessageType != "tool_call" {
|
||||
sb.WriteString("<|turn>model\n")
|
||||
if !hasThink {
|
||||
sb.WriteString("<|channel>thought\n<channel|>")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
@@ -130,6 +169,36 @@ func (r *Gemma4Renderer) renderContent(sb *strings.Builder, msg api.Message, ima
|
||||
sb.WriteString(content)
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) previousNonToolRole(messages []api.Message, idx int) string {
|
||||
for i := idx - 1; i >= 0; i-- {
|
||||
if messages[i].Role != "tool" {
|
||||
return messages[i].Role
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) messageHasContent(message api.Message) bool {
|
||||
return message.Content != "" || len(message.Images) > 0
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) toolResponseName(message api.Message, toolCalls []api.ToolCall) string {
|
||||
name := message.ToolName
|
||||
if name == "" {
|
||||
name = "unknown"
|
||||
}
|
||||
if message.ToolCallID != "" {
|
||||
for _, tc := range toolCalls {
|
||||
if tc.ID == message.ToolCallID {
|
||||
name = tc.Function.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) renderToolDeclaration(tool api.Tool) string {
|
||||
var sb strings.Builder
|
||||
fn := tool.Function
|
||||
@@ -144,7 +213,7 @@ func (r *Gemma4Renderer) renderToolDeclaration(tool api.Tool) string {
|
||||
|
||||
if fn.Parameters.Properties != nil && fn.Parameters.Properties.Len() > 0 {
|
||||
sb.WriteString("properties:{")
|
||||
r.writeProperties(&sb, fn.Parameters.Properties)
|
||||
r.writeTypedProperties(&sb, fn.Parameters.Properties)
|
||||
sb.WriteString("}")
|
||||
needsComma = true
|
||||
}
|
||||
@@ -178,93 +247,25 @@ func (r *Gemma4Renderer) renderToolDeclaration(tool api.Tool) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) writeProperties(sb *strings.Builder, props *api.ToolPropertiesMap) {
|
||||
keys := make([]string, 0, props.Len())
|
||||
for k := range props.All() {
|
||||
keys = append(keys, k)
|
||||
func (r *Gemma4Renderer) writeTypedProperties(sb *strings.Builder, props *api.ToolPropertiesMap) {
|
||||
if props == nil || props.Len() == 0 {
|
||||
return
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
first := true
|
||||
for _, name := range keys {
|
||||
prop, _ := props.Get(name)
|
||||
if !first {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
first = false
|
||||
|
||||
sb.WriteString(name + ":{")
|
||||
|
||||
hasContent := false
|
||||
if prop.Description != "" {
|
||||
sb.WriteString("description:" + g4Q + prop.Description + g4Q)
|
||||
hasContent = true
|
||||
}
|
||||
|
||||
if len(prop.Type) > 0 {
|
||||
typeName := strings.ToUpper(prop.Type[0])
|
||||
|
||||
switch typeName {
|
||||
case "STRING":
|
||||
if len(prop.Enum) > 0 {
|
||||
if hasContent {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("enum:[")
|
||||
for j, e := range prop.Enum {
|
||||
if j > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + fmt.Sprintf("%v", e) + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
hasContent = true
|
||||
}
|
||||
|
||||
case "OBJECT":
|
||||
// Render nested properties recursively.
|
||||
// Note: the leading comma is hardcoded (matching the template),
|
||||
// and this does NOT set hasContent — the comma before type:
|
||||
// depends only on whether description was present.
|
||||
sb.WriteString(",properties:{")
|
||||
if prop.Properties != nil && prop.Properties.Len() > 0 {
|
||||
r.writeProperties(sb, prop.Properties)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
if len(prop.Required) > 0 {
|
||||
sb.WriteString(",required:[")
|
||||
for j, req := range prop.Required {
|
||||
if j > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + req + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
|
||||
case "ARRAY":
|
||||
// Render items specification.
|
||||
// Same as OBJECT: leading comma is hardcoded, does NOT set hasContent.
|
||||
if items, ok := prop.Items.(map[string]any); ok && len(items) > 0 {
|
||||
sb.WriteString(",items:{")
|
||||
r.writeItemsSpec(sb, items)
|
||||
sb.WriteString("}")
|
||||
}
|
||||
}
|
||||
|
||||
if hasContent {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString("type:" + g4Q + typeName + g4Q)
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
}
|
||||
r.writeSchemaProperties(sb, typedSchemaPropertiesMap(props))
|
||||
}
|
||||
|
||||
// writeItemsSpec renders the items specification for array-type properties,
|
||||
func typedSchemaPropertiesMap(props *api.ToolPropertiesMap) map[string]any {
|
||||
out := make(map[string]any, props.Len())
|
||||
for key, prop := range props.All() {
|
||||
out[key] = topLevelTypedSchemaValueFromToolProperty(prop)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// writeSchemaItemsSpec renders the items specification for array-type properties,
|
||||
// matching the Jinja2 template's dictsort iteration over items.
|
||||
func (r *Gemma4Renderer) writeItemsSpec(sb *strings.Builder, items map[string]any) {
|
||||
func (r *Gemma4Renderer) writeSchemaItemsSpec(sb *strings.Builder, items map[string]any) {
|
||||
keys := make([]string, 0, len(items))
|
||||
for k := range items {
|
||||
keys = append(keys, k)
|
||||
@@ -283,16 +284,379 @@ func (r *Gemma4Renderer) writeItemsSpec(sb *strings.Builder, items map[string]an
|
||||
first = false
|
||||
|
||||
switch key {
|
||||
case "properties":
|
||||
sb.WriteString("properties:{")
|
||||
if props, ok := r.asSchemaMap(value); ok {
|
||||
r.writeSchemaProperties(sb, props)
|
||||
}
|
||||
sb.WriteString("}")
|
||||
case "required":
|
||||
sb.WriteString("required:[")
|
||||
for i, req := range normalizeStringSlice(value) {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + req + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case "type":
|
||||
if s, ok := value.(string); ok {
|
||||
sb.WriteString("type:" + g4Q + strings.ToUpper(s) + g4Q)
|
||||
typeNames := normalizeTypeNames(value)
|
||||
if len(typeNames) == 1 {
|
||||
sb.WriteString("type:" + g4Q + typeNames[0] + g4Q)
|
||||
} else if len(typeNames) > 1 {
|
||||
sb.WriteString("type:[")
|
||||
for i, typeName := range typeNames {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + typeName + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
default:
|
||||
sb.WriteString(key + ":" + r.formatArgValue(value))
|
||||
sb.WriteString(key + ":" + r.formatSchemaValue(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) writeSchemaProperties(sb *strings.Builder, props map[string]any) {
|
||||
keys := make([]string, 0, len(props))
|
||||
for k := range props {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
first := true
|
||||
for _, name := range keys {
|
||||
if isSchemaStandardKey(name) {
|
||||
continue
|
||||
}
|
||||
prop, ok := r.asSchemaMap(props[name])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !first {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
first = false
|
||||
|
||||
sb.WriteString(name + ":{")
|
||||
|
||||
addComma := false
|
||||
if description, ok := prop["description"].(string); ok && description != "" {
|
||||
sb.WriteString("description:" + g4Q + description + g4Q)
|
||||
addComma = true
|
||||
}
|
||||
|
||||
typeNames := normalizeTypeNames(prop["type"])
|
||||
typeName := ""
|
||||
if len(typeNames) > 0 {
|
||||
typeName = typeNames[0]
|
||||
}
|
||||
|
||||
switch typeName {
|
||||
case "STRING":
|
||||
if enumValues := normalizeSlice(prop["enum"]); len(enumValues) > 0 {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("enum:[")
|
||||
for i, value := range enumValues {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + fmt.Sprintf("%v", value) + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
case "ARRAY":
|
||||
if items, ok := r.asSchemaMap(prop["items"]); ok && len(items) > 0 {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("items:{")
|
||||
r.writeSchemaItemsSpec(sb, items)
|
||||
sb.WriteString("}")
|
||||
}
|
||||
}
|
||||
|
||||
if nullable, ok := prop["nullable"].(bool); ok && nullable {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("nullable:true")
|
||||
}
|
||||
|
||||
if typeName == "OBJECT" {
|
||||
if nestedProps, ok := r.asSchemaMap(prop["properties"]); ok {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("properties:{")
|
||||
r.writeSchemaProperties(sb, nestedProps)
|
||||
sb.WriteString("}")
|
||||
} else {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("properties:{")
|
||||
r.writeSchemaProperties(sb, prop)
|
||||
sb.WriteString("}")
|
||||
}
|
||||
|
||||
required := normalizeStringSlice(prop["required"])
|
||||
if len(required) > 0 {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
} else {
|
||||
addComma = true
|
||||
}
|
||||
sb.WriteString("required:[")
|
||||
for i, req := range required {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + req + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
}
|
||||
|
||||
if len(typeNames) > 0 {
|
||||
if addComma {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
if len(typeNames) == 1 {
|
||||
sb.WriteString("type:" + g4Q + typeNames[0] + g4Q)
|
||||
} else {
|
||||
sb.WriteString("type:[")
|
||||
for i, name := range typeNames {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(g4Q + name + g4Q)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) asSchemaMap(value any) (map[string]any, bool) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
return v, true
|
||||
case *api.ToolPropertiesMap:
|
||||
if v == nil {
|
||||
return nil, false
|
||||
}
|
||||
out := make(map[string]any, v.Len())
|
||||
for key, prop := range v.All() {
|
||||
out[key] = schemaValueFromToolProperty(prop)
|
||||
}
|
||||
return out, true
|
||||
case api.ToolProperty:
|
||||
return schemaValueFromToolProperty(v), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func schemaValueFromToolProperty(prop api.ToolProperty) map[string]any {
|
||||
out := make(map[string]any)
|
||||
if len(prop.Type) > 0 {
|
||||
if len(prop.Type) == 1 {
|
||||
out["type"] = prop.Type[0]
|
||||
} else {
|
||||
out["type"] = []string(prop.Type)
|
||||
}
|
||||
} else if unionTypes, ok := simpleAnyOfTypes(prop); ok {
|
||||
if len(unionTypes) == 1 {
|
||||
out["type"] = unionTypes[0]
|
||||
} else {
|
||||
out["type"] = []string(unionTypes)
|
||||
}
|
||||
}
|
||||
if prop.Description != "" {
|
||||
out["description"] = prop.Description
|
||||
}
|
||||
if len(prop.Enum) > 0 {
|
||||
out["enum"] = prop.Enum
|
||||
}
|
||||
if prop.Items != nil {
|
||||
out["items"] = prop.Items
|
||||
}
|
||||
if prop.Properties != nil {
|
||||
out["properties"] = prop.Properties
|
||||
}
|
||||
if len(prop.Required) > 0 {
|
||||
out["required"] = prop.Required
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func topLevelTypedSchemaValueFromToolProperty(prop api.ToolProperty) map[string]any {
|
||||
out := make(map[string]any)
|
||||
if len(prop.Type) > 0 {
|
||||
// api.ToolProperty intentionally models nullability through type unions
|
||||
// that include "null" rather than OpenAPI 3.0's nullable:true keyword.
|
||||
// Gemma's template accepts nullable:true as well, but our typed top-level
|
||||
// tool properties do not carry that field. For multi-type unions, the
|
||||
// template stringifies the uppercase list rather than emitting a structured
|
||||
// type array. That is odd, but we match upstream here.
|
||||
out["type"] = upstreamTypedPropertyTypeValue(prop.Type)
|
||||
} else if unionTypes, ok := simpleAnyOfTypes(prop); ok {
|
||||
// Gemma's declaration format does not have a dedicated anyOf construct, so
|
||||
// we lower simple unions of bare type branches into the same typed union
|
||||
// form used for api.PropertyType.
|
||||
out["type"] = upstreamTypedPropertyTypeValue(unionTypes)
|
||||
}
|
||||
if prop.Description != "" {
|
||||
out["description"] = prop.Description
|
||||
}
|
||||
if len(prop.Enum) > 0 {
|
||||
out["enum"] = prop.Enum
|
||||
}
|
||||
if prop.Items != nil {
|
||||
out["items"] = prop.Items
|
||||
}
|
||||
if prop.Properties != nil {
|
||||
out["properties"] = typedSchemaPropertiesMap(prop.Properties)
|
||||
}
|
||||
if len(prop.Required) > 0 {
|
||||
out["required"] = prop.Required
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func upstreamTypedPropertyTypeValue(types api.PropertyType) string {
|
||||
if len(types) == 1 {
|
||||
return types[0]
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[")
|
||||
for i, typ := range types {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString("'" + strings.ToUpper(typ) + "'")
|
||||
}
|
||||
sb.WriteString("]")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func simpleAnyOfTypes(prop api.ToolProperty) (api.PropertyType, bool) {
|
||||
if len(prop.AnyOf) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var out api.PropertyType
|
||||
seen := make(map[string]struct{})
|
||||
for _, branch := range prop.AnyOf {
|
||||
if !isBareTypeOnlyToolProperty(branch) || len(branch.Type) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
for _, typ := range branch.Type {
|
||||
if _, ok := seen[typ]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typ] = struct{}{}
|
||||
out = append(out, typ)
|
||||
}
|
||||
}
|
||||
|
||||
return out, len(out) > 0
|
||||
}
|
||||
|
||||
func isBareTypeOnlyToolProperty(prop api.ToolProperty) bool {
|
||||
return len(prop.AnyOf) == 0 &&
|
||||
len(prop.Type) > 0 &&
|
||||
prop.Items == nil &&
|
||||
prop.Description == "" &&
|
||||
len(prop.Enum) == 0 &&
|
||||
prop.Properties == nil &&
|
||||
len(prop.Required) == 0
|
||||
}
|
||||
|
||||
func isSchemaStandardKey(key string) bool {
|
||||
switch key {
|
||||
case "description", "type", "properties", "required", "nullable":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeTypeNames(value any) []string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return []string{strings.ToUpper(v)}
|
||||
case []string:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
out = append(out, strings.ToUpper(item))
|
||||
}
|
||||
return out
|
||||
case []any:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, strings.ToUpper(s))
|
||||
}
|
||||
}
|
||||
return out
|
||||
case api.PropertyType:
|
||||
return normalizeTypeNames([]string(v))
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringSlice(value any) []string {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
return append([]string(nil), v...)
|
||||
case []any:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeSlice(value any) []any {
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
return v
|
||||
case []string:
|
||||
out := make([]any, 0, len(v))
|
||||
for _, item := range v {
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatToolCall(tc api.ToolCall) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("<|tool_call>call:" + tc.Function.Name + "{")
|
||||
@@ -317,6 +681,10 @@ func (r *Gemma4Renderer) formatToolCall(tc api.ToolCall) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatToolResponseBlock(toolName, response string) string {
|
||||
return "<|tool_response>response:" + toolName + "{value:" + r.formatArgValue(response) + "}<tool_response|>"
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatArgValue(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
@@ -365,6 +733,73 @@ func (r *Gemma4Renderer) formatMapValue(m map[string]any) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatSchemaValue(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return g4Q + v + g4Q
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
if v == float64(int64(v)) {
|
||||
return fmt.Sprintf("%d", int64(v))
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
case int, int64, int32:
|
||||
return fmt.Sprintf("%d", v)
|
||||
case map[string]any:
|
||||
return r.formatSchemaMapValue(v)
|
||||
case []any:
|
||||
return r.formatSchemaArrayValue(v)
|
||||
case []string:
|
||||
out := make([]any, 0, len(v))
|
||||
for _, item := range v {
|
||||
out = append(out, item)
|
||||
}
|
||||
return r.formatSchemaArrayValue(out)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatSchemaMapValue(m map[string]any) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("{")
|
||||
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
first := true
|
||||
for _, key := range keys {
|
||||
if !first {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
first = false
|
||||
sb.WriteString(g4Q + key + g4Q + ":" + r.formatSchemaValue(m[key]))
|
||||
}
|
||||
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatSchemaArrayValue(arr []any) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[")
|
||||
for i, item := range arr {
|
||||
if i > 0 {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(r.formatSchemaValue(item))
|
||||
}
|
||||
sb.WriteString("]")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (r *Gemma4Renderer) formatArrayValue(arr []any) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
168
model/renderers/testdata/gemma4_chat_template.jinja2
vendored
168
model/renderers/testdata/gemma4_chat_template.jinja2
vendored
@@ -11,34 +11,15 @@
|
||||
description:<|"|>{{ value['description'] }}<|"|>
|
||||
{%- set add_comma = true -%}
|
||||
{%- endif -%}
|
||||
{%- if value['nullable'] %}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
nullable:true
|
||||
{%- endif -%}
|
||||
{%- if value['type'] | upper == 'STRING' -%}
|
||||
{%- if value['enum'] -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
enum:{{ format_argument(value['enum']) }}
|
||||
{%- endif -%}
|
||||
{%- elif value['type'] | upper == 'OBJECT' -%}
|
||||
,properties:{
|
||||
{%- if value['properties'] is defined and value['properties'] is mapping -%}
|
||||
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
|
||||
{%- elif value is mapping -%}
|
||||
{{- format_parameters(value, value['required'] | default([])) -}}
|
||||
{%- endif -%}
|
||||
}
|
||||
{%- if value['required'] -%}
|
||||
,required:[
|
||||
{%- for item in value['required'] | default([]) -%}
|
||||
<|"|>{{- item -}}<|"|>
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{%- endif -%}
|
||||
{%- elif value['type'] | upper == 'ARRAY' -%}
|
||||
{%- if value['items'] is mapping and value['items'] -%}
|
||||
,items:{
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
items:{
|
||||
{%- set ns_items = namespace(found_first=false) -%}
|
||||
{%- for item_key, item_value in value['items'] | dictsort -%}
|
||||
{%- if item_value is not none -%}
|
||||
@@ -71,6 +52,32 @@
|
||||
}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if value['nullable'] %}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
nullable:true
|
||||
{%- endif -%}
|
||||
{%- if value['type'] | upper == 'OBJECT' -%}
|
||||
{%- if value['properties'] is defined and value['properties'] is mapping -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
properties:{
|
||||
{{- format_parameters(value['properties'], value['required'] | default([])) -}}
|
||||
}
|
||||
{%- elif value is mapping -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
properties:{
|
||||
{{- format_parameters(value, value['required'] | default([])) -}}
|
||||
}
|
||||
{%- endif -%}
|
||||
{%- if value['required'] -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
required:[
|
||||
{%- for item in value['required'] | default([]) -%}
|
||||
<|"|>{{- item -}}<|"|>
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
]
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
|
||||
type:<|"|>{{ value['type'] | upper }}<|"|>}
|
||||
{%- endif -%}
|
||||
@@ -150,16 +157,31 @@
|
||||
{{- ns.result | trim -}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro format_tool_response_block(tool_name, response) -%}
|
||||
{{- '<|tool_response>' -}}
|
||||
{%- if response is mapping -%}
|
||||
{{- 'response:' + tool_name + '{' -}}
|
||||
{%- for key, value in response | dictsort -%}
|
||||
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
{{- '}' -}}
|
||||
{%- else -%}
|
||||
{{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}
|
||||
{%- endif -%}
|
||||
{{- '<tool_response|>' -}}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set ns = namespace(prev_message_type=None) -%}
|
||||
{%- set loop_messages = messages -%}
|
||||
{{ bos_token }}
|
||||
{{- bos_token -}}
|
||||
{#- Handle System/Tool Definitions Block -#}
|
||||
{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
|
||||
{{- '<|turn>system\n' -}}
|
||||
|
||||
{#- Inject Thinking token at the very top of the FIRST system turn -#}
|
||||
{%- if enable_thinking is defined and enable_thinking -%}
|
||||
{{- '<|think|>' -}}
|
||||
{{- '<|think|>\n' -}}
|
||||
{%- set ns.prev_message_type = 'think' -%}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -180,11 +202,41 @@
|
||||
{{- '<turn|>\n' -}}
|
||||
{%- endif %}
|
||||
|
||||
{#- Pre-scan: find last user message index for reasoning guard -#}
|
||||
{%- set ns_turn = namespace(last_user_idx=-1) -%}
|
||||
{%- for i in range(loop_messages | length) -%}
|
||||
{%- if loop_messages[i]['role'] == 'user' -%}
|
||||
{%- set ns_turn.last_user_idx = i -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{#- Loop through messages -#}
|
||||
{%- for message in loop_messages -%}
|
||||
{%- if message['role'] != 'tool' -%}
|
||||
{%- set ns.prev_message_type = None -%}
|
||||
{%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
|
||||
{#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
|
||||
{%- set prev_nt = namespace(role=None, found=false) -%}
|
||||
{%- 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 -%}
|
||||
{{- '<|turn>' + role + '\n' }}
|
||||
{%- endif -%}
|
||||
|
||||
{#- Render reasoning/reasoning_content as thinking channel -#}
|
||||
{%- 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') -%}
|
||||
{{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if message['tool_calls'] -%}
|
||||
{%- for tool_call in message['tool_calls'] -%}
|
||||
@@ -205,23 +257,49 @@
|
||||
{%- set ns.prev_message_type = 'tool_call' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if message['tool_responses'] -%}
|
||||
{#- Tool Response handling -#}
|
||||
{%- set ns_tr_out = namespace(flag=false) -%}
|
||||
{%- if message.get('tool_responses') -%}
|
||||
{#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
|
||||
{%- for tool_response in message['tool_responses'] -%}
|
||||
{{- '<|tool_response>' -}}
|
||||
{%- if tool_response['response'] is mapping -%}
|
||||
{{- 'response:' + tool_response['name'] | default('unknown') + '{' -}}
|
||||
{%- for key, value in tool_response['response'] | dictsort -%}
|
||||
{{- key -}}:{{- format_argument(value, escape_keys=False) -}}
|
||||
{%- if not loop.last %},{% endif -%}
|
||||
{%- endfor -%}
|
||||
{{- '}' -}}
|
||||
{%- else -%}
|
||||
{{- 'response:' + tool_response['name'] | default('unknown') + '{value:' + format_argument(tool_response['response'], escape_keys=False) + '}' -}}
|
||||
{%- endif -%}
|
||||
{{- '<tool_response|>' -}}
|
||||
{{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
|
||||
{%- set ns_tr_out.flag = true -%}
|
||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||
{%- endfor -%}
|
||||
{%- elif message.get('tool_calls') -%}
|
||||
{#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
|
||||
{%- set ns_tool_scan = namespace(stopped=false) -%}
|
||||
{%- for k in range(loop.index0 + 1, loop_messages | length) -%}
|
||||
{%- if ns_tool_scan.stopped -%}
|
||||
{%- elif loop_messages[k]['role'] != 'tool' -%}
|
||||
{%- set ns_tool_scan.stopped = true -%}
|
||||
{%- else -%}
|
||||
{%- set follow = loop_messages[k] -%}
|
||||
{#- Resolve tool_call_id to function name -#}
|
||||
{%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
|
||||
{%- for tc in message['tool_calls'] -%}
|
||||
{%- if tc.get('id') == follow.get('tool_call_id') -%}
|
||||
{%- set ns_tname.name = tc['function']['name'] -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{#- Handle content as string or content-parts array -#}
|
||||
{%- set tool_body = follow.get('content') -%}
|
||||
{%- if tool_body is string -%}
|
||||
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
||||
{%- elif tool_body is sequence and tool_body is not string -%}
|
||||
{%- set ns_txt = namespace(s='') -%}
|
||||
{%- for part in tool_body -%}
|
||||
{%- if part.get('type') == 'text' -%}
|
||||
{%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
|
||||
{%- else -%}
|
||||
{{- format_tool_response_block(ns_tname.name, tool_body) -}}
|
||||
{%- endif -%}
|
||||
{%- set ns_tr_out.flag = true -%}
|
||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- set ns.prev_message_type = 'tool_response' -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if message['content'] is string -%}
|
||||
@@ -239,25 +317,31 @@
|
||||
{{- item['text'] | trim -}}
|
||||
{%- endif -%}
|
||||
{%- elif item['type'] == 'image' -%}
|
||||
{{- '\n\n<|image|>\n\n' -}}
|
||||
{{- '<|image|>' -}}
|
||||
{%- set ns.prev_message_type = 'image' -%}
|
||||
{%- elif item['type'] == 'audio' -%}
|
||||
{{- '<|audio|>' -}}
|
||||
{%- set ns.prev_message_type = 'audio' -%}
|
||||
{%- elif item['type'] == 'video' -%}
|
||||
{{- '\n\n<|video|>\n\n' -}}
|
||||
{{- '<|video|>' -}}
|
||||
{%- set ns.prev_message_type = 'video' -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
|
||||
{%- if not (message['tool_responses'] and not message['content']) -%}
|
||||
{%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
|
||||
{{- '<|tool_response>' -}}
|
||||
{%- elif not (ns_tr_out.flag and not message.get('content')) -%}
|
||||
{{- '<turn|>\n' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- if add_generation_prompt -%}
|
||||
{%- if ns.prev_message_type != 'tool_response' -%}
|
||||
{%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
|
||||
{{- '<|turn>model\n' -}}
|
||||
{%- if not enable_thinking | default(false) -%}
|
||||
{{- '<|channel>thought\n<channel|>' -}}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
|
||||
Reference in New Issue
Block a user