Files
ollama/model/renderers/laguna.go
Daniel Hiltgen a18c230189 model: add Laguna v8 chat support and fix Metal inference (#17291)
Add a laguna-v8 renderer/parser matching the Laguna XS 2.1 template, and fix v2 handling of embedded thinking and structured tool arguments.

Prevent FP16 overflow in Metal's quantized routed-MoE prefill path by scaling the linear branch and folding the inverse into the routing scale. Other backends and token-generation paths are unchanged.

Add comprehensive v2/v8 Jinja parity and parser tests.
2026-07-21 16:06:29 -07:00

283 lines
8.3 KiB
Go

package renderers
import (
"strings"
"unicode"
"github.com/ollama/ollama/api"
)
const (
lagunaBOS = "〈|EOS|〉"
lagunaThoughtOpen = "<think>"
lagunaThoughtClose = "</think>"
// Default system message from the Laguna chat template, used when the
// request supplies no system message.
lagunaDefaultSystem = "You are a helpful, conversationally-fluent assistant made by Poolside. You are here to be helpful to users through natural language conversations."
)
type LagunaRenderer struct{}
func (r *LagunaRenderer) LeadingBOS() string {
return lagunaBOS
}
func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
var sb strings.Builder
sb.WriteString(lagunaBOS)
// The template signals thinking through the generation-prompt token
// (<think> vs </think>), not through the system message. It defaults off.
thinkingEnabled := think != nil && think.Bool()
// ── header (system message) ──
// The template seeds a default system message and lets an explicit leading
// system message override it. The header is emitted whenever there is a
// system message or tools to advertise.
systemMessage := lagunaDefaultSystem
firstMessageIsSystem := len(messages) > 0 && messages[0].Role == "system"
if firstMessageIsSystem {
systemMessage = messages[0].Content
}
if strings.TrimSpace(systemMessage) != "" || len(tools) > 0 {
sb.WriteString("<system>\n")
if strings.TrimSpace(systemMessage) != "" {
sb.WriteByte('\n')
sb.WriteString(strings.TrimRightFunc(systemMessage, unicode.IsSpace))
}
if len(tools) > 0 {
sb.WriteString("\n\n### Tools\n\n")
sb.WriteString("You may call functions to assist with the user query.\n")
sb.WriteString("All available function signatures are listed below:\n")
sb.WriteString("<available_tools>\n")
for _, tool := range tools {
if b, err := marshalWithSpaces(tool); err == nil {
sb.Write(b)
sb.WriteByte('\n')
}
}
sb.WriteString("</available_tools>\n\n")
if thinkingEnabled {
sb.WriteString("Wrap your thinking in '<think>', '</think>' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n")
sb.WriteString("<think> your thoughts here </think>\n")
} else {
sb.WriteString("For each function call, return an unescaped XML-like object with function name and arguments within '<tool_call>' and '</tool_call>' tags, like here:\n")
}
sb.WriteString("<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n</tool_call>")
}
sb.WriteString("\n</system>\n")
}
// ── main loop ──
for i, message := range messages {
if i == 0 && firstMessageIsSystem {
continue
}
content := message.Content
switch message.Role {
case "user":
sb.WriteString("<user>\n")
sb.WriteString(content)
sb.WriteString("\n</user>\n")
case "assistant":
content, reasoning := lagunaV2AssistantContent(message.Content, message.Thinking)
lastMessage := i == len(messages)-1
prefill := lastMessage && (strings.TrimSpace(content) != "" || strings.TrimSpace(reasoning) != "" || len(message.ToolCalls) > 0)
sb.WriteString("<assistant>\n")
// Every assistant turn opens with the reasoning block: a full
// <think>…</think> when there is reasoning, otherwise a bare
// </think> marking the turn as direct.
if reasoning := strings.TrimSpace(reasoning); reasoning != "" {
sb.WriteString("<think>\n")
sb.WriteString(reasoning)
sb.WriteString("\n</think>\n")
} else {
sb.WriteString("</think>\n")
}
if strings.TrimSpace(content) != "" {
sb.WriteString(strings.TrimSpace(content))
sb.WriteByte('\n')
}
for _, toolCall := range message.ToolCalls {
sb.WriteString("<tool_call>")
sb.WriteString(toolCall.Function.Name)
sb.WriteByte('\n')
for name, value := range toolCall.Function.Arguments.All() {
sb.WriteString("<arg_key>")
sb.WriteString(name)
sb.WriteString("</arg_key>\n")
sb.WriteString("<arg_value>")
sb.WriteString(formatLagunaToolCallArgument(value))
sb.WriteString("</arg_value>\n")
}
sb.WriteString("</tool_call>\n")
}
if !prefill {
sb.WriteString("</assistant>\n")
}
case "tool":
sb.WriteString("<tool_response>\n")
sb.WriteString(content)
sb.WriteString("\n</tool_response>\n")
case "system":
sb.WriteString("<system>\n")
sb.WriteString(content)
sb.WriteString("\n</system>\n")
}
}
// ── generation prompt ──
// Continue an assistant prefill in place; otherwise open a fresh assistant
// turn and prime the reasoning mode (<think> when thinking, else </think>).
if len(messages) == 0 || messages[len(messages)-1].Role != "assistant" {
sb.WriteString("<assistant>\n")
if thinkingEnabled {
sb.WriteString(lagunaThoughtOpen)
} else {
sb.WriteString(lagunaThoughtClose)
}
}
return sb.String(), nil
}
func lagunaV2AssistantContent(content, reasoning string) (string, string) {
parts := strings.Split(content, lagunaThoughtClose)
if len(parts) == 1 {
return content, reasoning
}
if reasoning == "" {
before := strings.TrimRight(parts[0], "\n")
if i := strings.LastIndex(before, lagunaThoughtOpen); i >= 0 {
before = before[i+len(lagunaThoughtOpen):]
}
reasoning = strings.TrimLeft(before, "\n")
}
content = strings.TrimLeft(parts[len(parts)-1], "\n")
return content, reasoning
}
type LagunaV8Renderer struct{}
func (r *LagunaV8Renderer) LeadingBOS() string {
return lagunaBOS
}
func (r *LagunaV8Renderer) Render(messages []api.Message, tools []api.Tool, think *api.ThinkValue) (string, error) {
var sb strings.Builder
sb.WriteString(lagunaBOS)
thinkingEnabled := think != nil && think.Bool()
systemMessage := lagunaDefaultSystem
firstMessageIsSystem := len(messages) > 0 && messages[0].Role == "system"
if firstMessageIsSystem {
systemMessage = messages[0].Content
}
hasSystem := strings.TrimSpace(systemMessage) != ""
if hasSystem || len(tools) > 0 || thinkingEnabled {
sb.WriteString("<system>")
if hasSystem {
sb.WriteString(strings.TrimRightFunc(systemMessage, unicode.IsSpace))
if len(tools) > 0 {
sb.WriteString("\n\n")
}
}
if len(tools) > 0 {
sb.WriteString("### Tools\n\n")
sb.WriteString("You may call functions to assist with the user query.\n")
sb.WriteString("All available function signatures are listed below:\n")
sb.WriteString("<available_tools>\n")
for _, tool := range tools {
if b, err := marshalWithSpaces(tool); err == nil {
sb.Write(b)
sb.WriteByte('\n')
}
}
sb.WriteString("</available_tools>")
}
sb.WriteString("</system>\n")
}
for i, message := range messages {
if i == 0 && firstMessageIsSystem {
continue
}
content := message.Content
switch message.Role {
case "user":
sb.WriteString("<user>")
sb.WriteString(content)
sb.WriteString("</user>\n")
case "assistant":
sb.WriteString("<assistant>")
if thinkingEnabled {
sb.WriteString(lagunaThoughtOpen)
sb.WriteString(message.Thinking)
sb.WriteString(lagunaThoughtClose)
} else {
sb.WriteString(lagunaThoughtClose)
}
if content != "" {
sb.WriteString(content)
}
for _, toolCall := range message.ToolCalls {
sb.WriteString("<tool_call>")
sb.WriteString(toolCall.Function.Name)
for name, value := range toolCall.Function.Arguments.All() {
sb.WriteString("<arg_key>")
sb.WriteString(name)
sb.WriteString("</arg_key>")
sb.WriteString("<arg_value>")
sb.WriteString(formatLagunaToolCallArgument(value))
sb.WriteString("</arg_value>")
}
sb.WriteString("</tool_call>")
}
sb.WriteString("</assistant>\n")
case "tool":
sb.WriteString("<tool_response>")
sb.WriteString(content)
sb.WriteString("</tool_response>\n")
case "system":
sb.WriteString("<system>")
sb.WriteString(content)
sb.WriteString("</system>\n")
}
}
sb.WriteString("<assistant>")
if thinkingEnabled {
sb.WriteString(lagunaThoughtOpen)
} else {
sb.WriteString(lagunaThoughtClose)
}
return sb.String(), nil
}
func formatLagunaToolCallArgument(value any) string {
switch v := value.(type) {
case string:
return v
case []byte:
return string(v)
}
if b, err := marshalWithSpaces(value); err == nil {
return string(b)
}
return formatToolCallArgument(value)
}