model: improvements to laguna-xs.2 parser/renderer (#16362)

This commit is contained in:
Jeffrey Morgan
2026-05-31 14:11:07 -07:00
committed by GitHub
parent be7de10c41
commit e7766a4a47
4 changed files with 269 additions and 142 deletions

View File

@@ -39,6 +39,7 @@ type LagunaParser struct {
thinkingEnabled bool
thinkingSuppressed bool
allowLeadingThinkOpen bool
atContentStart bool
}
func (p *LagunaParser) HasToolSupport() bool {
@@ -68,10 +69,28 @@ func (p *LagunaParser) Init(tools []api.Tool, lastMessage *api.Message, thinkVal
p.tools = tools
p.callIndex = 0
p.buffer.Reset()
p.thinkingEnabled = thinkValue == nil || thinkValue.Bool()
p.thinkingSuppressed = thinkValue != nil && !thinkValue.Bool()
p.state = lagunaParserStateContent
p.allowLeadingThinkOpen = false
// The prompt primes the reasoning mode: <think> when thinking is enabled
// (so the model's output begins with reasoning and no opening tag), or
// </think> otherwise. Thinking defaults off, matching the chat template.
p.thinkingEnabled = thinkValue != nil && thinkValue.Bool()
p.thinkingSuppressed = !p.thinkingEnabled
p.atContentStart = true
// When the request ends with an assistant prefill, the renderer continues
// that turn in place instead of emitting a fresh generation prompt: it has
// already written the closing </think>, so the model resumes with content
// (or a tool call), not reasoning. Start in the content state even when
// thinking is enabled, otherwise the continuation would be reported as
// thinking until done and clients would receive an empty answer.
assistantPrefill := lastMessage != nil && lastMessage.Role == "assistant"
if p.thinkingEnabled && !assistantPrefill {
p.state = lagunaParserStateThinking
p.allowLeadingThinkOpen = true
} else {
p.state = lagunaParserStateContent
p.allowLeadingThinkOpen = false
}
return tools
}
@@ -118,19 +137,28 @@ func (p *LagunaParser) consumeThinking(done bool) (bool, string) {
if p.allowLeadingThinkOpen {
trimmed := strings.TrimLeftFunc(acc, unicode.IsSpace)
if strings.HasPrefix(trimmed, lagunaThinkingOpenTag) {
// the model echoed the primed <think>; drop it
p.buffer.Reset()
p.buffer.WriteString(strings.TrimLeftFunc(strings.TrimPrefix(trimmed, lagunaThinkingOpenTag), unicode.IsSpace))
p.allowLeadingThinkOpen = false
return true, ""
}
if strings.HasPrefix(lagunaThinkingOpenTag, trimmed) && !done {
// possibly a partial opening tag; keep it (minus leading space) and wait
p.buffer.Reset()
p.buffer.WriteString(trimmed)
return false, ""
}
// reasoning begins here: drop the leading whitespace the model emits
// after the primed <think> (its trained format is "<think>\n…").
p.buffer.Reset()
p.buffer.WriteString(trimmed)
p.allowLeadingThinkOpen = false
acc = trimmed
}
if idx := strings.Index(acc, lagunaThinkingCloseTag); idx != -1 {
thinking := acc[:idx]
thinking := strings.TrimRightFunc(acc[:idx], unicode.IsSpace)
after := strings.TrimLeftFunc(acc[idx+len(lagunaThinkingCloseTag):], unicode.IsSpace)
p.buffer.Reset()
p.buffer.WriteString(after)
@@ -148,6 +176,7 @@ func (p *LagunaParser) consumeThinking(done bool) (bool, string) {
if done {
p.buffer.Reset()
p.state = lagunaParserStateContent
acc = strings.TrimRightFunc(acc, unicode.IsSpace)
return acc != "", acc
}
@@ -164,6 +193,17 @@ func (p *LagunaParser) consumeThinking(done bool) (bool, string) {
}
func (p *LagunaParser) consumeContent(done bool) (bool, string, []api.ToolCall, error) {
if p.atContentStart {
// Drop the leading whitespace the model emits before content (its trained
// format puts a newline after the primed/closed </think>).
trimmed := strings.TrimLeftFunc(p.buffer.String(), unicode.IsSpace)
p.buffer.Reset()
p.buffer.WriteString(trimmed)
if trimmed == "" {
return false, "", nil, nil
}
p.atContentStart = false
}
acc := p.buffer.String()
if p.thinkingEnabled || p.thinkingSuppressed {
if idx := strings.Index(acc, lagunaThinkingOpenTag); idx != -1 {
@@ -248,6 +288,7 @@ func (p *LagunaParser) consumeContent(done bool) (bool, string, []api.ToolCall,
}
if done {
p.buffer.Reset()
acc = strings.TrimRightFunc(acc, unicode.IsSpace)
return acc != "", acc, nil, nil
}
overlapLen := max(overlap(acc, lagunaToolCallOpenTag), overlap(acc, lagunaUserOpenTag))

View File

@@ -375,30 +375,32 @@ func TestLagunaParserUserTaggedNonToolContent(t *testing.T) {
}
}
func TestLagunaParserThinkingDefaultsOn(t *testing.T) {
func TestLagunaParserThinkingDefaultsOff(t *testing.T) {
// Thinking defaults off (matching the chat template); an emitted <think>
// block is suppressed rather than surfaced as reasoning.
parser := ParserForName("laguna")
parser.Init(nil, nil, nil)
content, thinking, calls, err := parser.Add("<think>Need to reason.</think>\nDirect answer.", true)
if err != nil {
t.Fatal(err)
}
if content != "Direct answer." || thinking != "Need to reason." || len(calls) != 0 {
if content != "Direct answer." || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserThinkingDefaultsOnWhenToolsPresent(t *testing.T) {
func TestLagunaParserThinkingDefaultsOffWhenToolsPresent(t *testing.T) {
parser := ParserForName("laguna")
parser.Init(lagunaTestTools(), nil, nil)
content, thinking, calls, err := parser.Add("<think>Need to reason.</think>\n<tool_call>get_weather\n<arg_key>location</arg_key>\n<arg_value>Paris</arg_value>\n</tool_call>", true)
if err != nil {
t.Fatal(err)
}
if thinking != "Need to reason." || len(calls) != 1 {
if thinking != "" || len(calls) != 1 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
if content != "" {
t.Fatalf("content=%q, want thinking block suppressed from content when default thinking is enabled", content)
t.Fatalf("content=%q, want the suppressed think block and the tool call both removed from content", content)
}
}
@@ -421,7 +423,7 @@ func TestLagunaParserThinkingExplicitlyDisabledDropsLeadingCloseTag(t *testing.T
if err != nil {
t.Fatal(err)
}
if content != "Tokyo\n" || thinking != "" || len(calls) != 0 {
if content != "Tokyo" || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
@@ -433,31 +435,122 @@ func TestLagunaParserThinkingEnabledDropsLeadingCloseTag(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if content != "Tokyo\n" || thinking != "" || len(calls) != 0 {
if content != "Tokyo" || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserThinkingDefaultOnDropsLeadingCloseTag(t *testing.T) {
func TestLagunaParserThinkingDefaultOffDropsLeadingCloseTag(t *testing.T) {
parser := ParserForName("laguna")
parser.Init(nil, nil, nil)
content, thinking, calls, err := parser.Add("</think>\nTokyo\n", true)
if err != nil {
t.Fatal(err)
}
if content != "Tokyo\n" || thinking != "" || len(calls) != 0 {
if content != "Tokyo" || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserThinkingEnabledUntaggedAnswerIsContent(t *testing.T) {
func TestLagunaParserThinkingEnabledUntaggedAnswerIsThinking(t *testing.T) {
// With thinking enabled the prompt primes <think>, so the model's output
// begins as reasoning even without an opening tag.
parser := ParserForName("laguna")
parser.Init(nil, nil, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("Direct answer.", true)
if err != nil {
t.Fatal(err)
}
if content != "Direct answer." || thinking != "" || len(calls) != 0 {
if content != "" || thinking != "Direct answer." || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserAssistantPrefillContinuesAsContent(t *testing.T) {
// With thinking enabled but the request ending in an assistant prefill, the
// renderer continues the turn in place after a closed </think>, so the
// untagged continuation is content, not thinking. (Regression: previously
// Init ignored lastMessage and reported it all as thinking, leaving the
// client with an empty answer.)
parser := ParserForName("laguna")
parser.Init(nil, &api.Message{Role: "assistant", Content: "The answer is"}, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add(" 42.", true)
if err != nil {
t.Fatal(err)
}
if content != "42." || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserAssistantPrefillContinuesAsToolCall(t *testing.T) {
// An assistant prefill with thinking enabled resumes past the closed
// </think>, so a tool call emitted by the continuation is parsed from the
// content state rather than being swallowed as thinking.
parser := ParserForName("laguna")
parser.Init(lagunaTestTools(), &api.Message{Role: "assistant", Thinking: "earlier reasoning"}, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("<tool_call>get_weather\n<arg_key>location</arg_key>\n<arg_value>Paris</arg_value>\n</tool_call>", true)
if err != nil {
t.Fatal(err)
}
if content != "" || thinking != "" || len(calls) != 1 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
if calls[0].Function.Name != "get_weather" {
t.Fatalf("name=%q, want get_weather", calls[0].Function.Name)
}
}
func TestLagunaParserNonAssistantLastMessageStillPrimesThinking(t *testing.T) {
// A trailing user message is not a prefill: the renderer primes <think>, so
// the untagged continuation is still reasoning.
parser := ParserForName("laguna")
parser.Init(nil, &api.Message{Role: "user", Content: "hi"}, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("Direct answer.", true)
if err != nil {
t.Fatal(err)
}
if content != "" || thinking != "Direct answer." || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserStripsLeadingContentWhitespace(t *testing.T) {
// No-think prompts prime </think>, so the model emits a leading newline
// before content; the parser drops it.
parser := ParserForName("laguna")
parser.Init(nil, nil, &api.ThinkValue{Value: false})
content, thinking, calls, err := parser.Add("\nHello! How can I help?", true)
if err != nil {
t.Fatal(err)
}
if content != "Hello! How can I help?" || thinking != "" || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}
func TestLagunaParserStripsTrailingContentWhitespace(t *testing.T) {
parser := ParserForName("laguna")
parser.Init(nil, nil, &api.ThinkValue{Value: false})
content, _, _, err := parser.Add("Hello there.\n\n", true)
if err != nil {
t.Fatal(err)
}
if content != "Hello there." {
t.Fatalf("content=%q, want trailing whitespace stripped", content)
}
}
func TestLagunaParserPrimedThinkingThenContent(t *testing.T) {
// Thinking enabled: the prompt primes <think>, so output is
// "\n{reasoning}\n</think>\n{answer}" with no opening tag.
parser := ParserForName("laguna")
parser.Init(nil, nil, &api.ThinkValue{Value: true})
content, thinking, calls, err := parser.Add("\nReasoning here.\n</think>\nThe answer.", true)
if err != nil {
t.Fatal(err)
}
if content != "The answer." || thinking != "Reasoning here." || len(calls) != 0 {
t.Fatalf("content=%q thinking=%q calls=%d", content, thinking, len(calls))
}
}

View File

@@ -2,6 +2,7 @@ package renderers
import (
"strings"
"unicode"
"github.com/ollama/ollama/api"
)
@@ -10,6 +11,10 @@ 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{}
@@ -22,40 +27,50 @@ func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think
var sb strings.Builder
sb.WriteString(lagunaBOS)
thinkingEnabled := think == nil || think.Bool()
systemMessage := ""
// 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 = strings.TrimRight(messages[0].Content, "\n")
systemMessage = messages[0].Content
}
sb.WriteString("<system>\n")
if thinkingEnabled {
sb.WriteString("You should use chain-of-thought reasoning. Put your reasoning inside <think> </think> tags before your response.")
} else {
sb.WriteString("You should respond directly without using chain-of-thought reasoning tags.")
}
if strings.TrimSpace(systemMessage) != "" {
sb.WriteByte('\n')
sb.WriteString(systemMessage)
}
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')
}
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))
}
sb.WriteString("</available_tools>\n\n")
sb.WriteString("For each function call, return a json object with function name and arguments within '<tool_call>' and '</tool_call>' tags:\n")
sb.WriteString("<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>")
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")
}
sb.WriteString("\n</system>\n")
// ── main loop ──
for i, message := range messages {
if i == 0 && firstMessageIsSystem {
continue
@@ -68,18 +83,26 @@ func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think
sb.WriteString("\n</user>\n")
case "assistant":
lastMessage := i == len(messages)-1
prefill := lastMessage && (content != "" || message.Thinking != "" || len(message.ToolCalls) > 0)
prefill := lastMessage && (strings.TrimSpace(content) != "" || strings.TrimSpace(message.Thinking) != "" || len(message.ToolCalls) > 0)
sb.WriteString("<assistant>\n")
if thinkingEnabled && message.Thinking != "" {
sb.WriteString(lagunaThoughtOpen)
sb.WriteString(message.Thinking)
sb.WriteString(lagunaThoughtClose)
sb.WriteByte('\n')
}
if strings.Trim(content, "\n") != "" {
sb.WriteString(strings.Trim(content, "\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(message.Thinking); 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)
@@ -94,6 +117,7 @@ func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think
}
sb.WriteString("</tool_call>\n")
}
if !prefill {
sb.WriteString("</assistant>\n")
}
@@ -108,8 +132,17 @@ func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think
}
}
// ── 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
}

View File

@@ -11,13 +11,20 @@ import (
"github.com/ollama/ollama/api"
)
const (
lagunaDirectDirective = "You should respond directly without using chain-of-thought reasoning tags."
lagunaThinkDirective = "You should use chain-of-thought reasoning. Put your reasoning inside <think> </think> tags before your response."
)
// lagunaToolJSON is the get_weather tool as serialized into <available_tools>,
// matching lagunaWeatherTool().
const lagunaToolJSON = `{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object", "required": ["location"], "properties": {"location": {"type": "string", "description": "City"}}}}}`
// TestLagunaRendererReferenceFlowCoverage checks the renderer against the Laguna
// chat template. Each want is byte-for-byte template output (verified by
// rendering chat_template.jinja), except that history tool-calls use the clean
// form — the template leaks Jinja indentation there.
func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
weather := lagunaWeatherTool()
think := func(v bool) *api.ThinkValue { return &api.ThinkValue{Value: v} }
// system header is always emitted; with no system message the default is used
defaultHeader := "〈|EOS|〉<system>\n\n" + lagunaDefaultSystem + "\n</system>\n"
tests := []struct {
name string
@@ -27,36 +34,21 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
want string
}{
{
name: "user_only_thinking_default_on",
name: "user_only_default",
messages: []api.Message{{Role: "user", Content: "Hello"}},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\n</system>\n" +
"<user>\nHello\n</user>\n" +
"<assistant>\n",
want: defaultHeader + "<user>\nHello\n</user>\n<assistant>\n</think>",
},
{
name: "user_only_thinking_enabled",
name: "user_only_think",
messages: []api.Message{{Role: "user", Content: "Hello"}},
think: &api.ThinkValue{Value: true},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\n</system>\n" +
"<user>\nHello\n</user>\n" +
"<assistant>\n",
think: think(true),
want: defaultHeader + "<user>\nHello\n</user>\n<assistant>\n<think>",
},
{
name: "user_only_thinking_disabled",
name: "user_only_nothink",
messages: []api.Message{{Role: "user", Content: "Hello"}},
think: &api.ThinkValue{Value: false},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaDirectDirective +
"\n</system>\n" +
"<user>\nHello\n</user>\n" +
"<assistant>\n",
think: think(false),
want: defaultHeader + "<user>\nHello\n</user>\n<assistant>\n</think>",
},
{
name: "first_system_is_header",
@@ -64,29 +56,20 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
{Role: "system", Content: "Stay concise.\n\n"},
{Role: "user", Content: "Hi"},
},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\nStay concise." +
"\n</system>\n" +
"<user>\nHi\n</user>\n" +
"<assistant>\n",
want: "〈|EOS|〉<system>\n\nStay concise.\n</system>\n" +
"<user>\nHi\n</user>\n<assistant>\n</think>",
},
{
name: "additional_system_message_renders_in_loop",
name: "additional_system",
messages: []api.Message{
{Role: "system", Content: "Primary."},
{Role: "user", Content: "Hi"},
{Role: "system", Content: "Secondary."},
},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\nPrimary." +
"\n</system>\n" +
want: "〈|EOS|〉<system>\n\nPrimary.\n</system>\n" +
"<user>\nHi\n</user>\n" +
"<system>\nSecondary.\n</system>\n" +
"<assistant>\n",
"<assistant>\n</think>",
},
{
name: "tools_in_header",
@@ -95,46 +78,32 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
{Role: "user", Content: "Weather?"},
},
tools: weather,
think: &api.ThinkValue{Value: true},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\nStay concise." +
"\n\n### Tools\n\n" +
think: think(true),
want: "〈|EOS|〉<system>\n\nStay concise.\n\n### Tools\n\n" +
"You may call functions to assist with the user query.\n" +
"All available function signatures are listed below:\n" +
"<available_tools>\n" +
`{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object", "required": ["location"], "properties": {"location": {"type": "string", "description": "City"}}}}}` + "\n" +
"</available_tools>\n\n" +
"For each function call, return a json object with function name and arguments within '<tool_call>' and '</tool_call>' tags:\n" +
"<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>" +
"<available_tools>\n" + lagunaToolJSON + "\n</available_tools>\n\n" +
"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" +
"<think> your thoughts here </think>\n" +
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n</tool_call>" +
"\n</system>\n" +
"<user>\nWeather?\n</user>\n" +
"<assistant>\n",
"<user>\nWeather?\n</user>\n<assistant>\n<think>",
},
{
name: "tools_default_thinking_on_when_unspecified",
messages: []api.Message{
{Role: "user", Content: "Weather?"},
},
tools: weather,
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\n\n### Tools\n\n" +
name: "tools_default",
messages: []api.Message{{Role: "user", Content: "Weather?"}},
tools: weather,
want: "〈|EOS|〉<system>\n\n" + lagunaDefaultSystem + "\n\n### Tools\n\n" +
"You may call functions to assist with the user query.\n" +
"All available function signatures are listed below:\n" +
"<available_tools>\n" +
`{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object", "required": ["location"], "properties": {"location": {"type": "string", "description": "City"}}}}}` + "\n" +
"</available_tools>\n\n" +
"For each function call, return a json object with function name and arguments within '<tool_call>' and '</tool_call>' tags:\n" +
"<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>" +
"<available_tools>\n" + lagunaToolJSON + "\n</available_tools>\n\n" +
"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" +
"<tool_call>function-name\n<arg_key>argument-key</arg_key>\n<arg_value>value-of-argument-key</arg_value>\n</tool_call>" +
"\n</system>\n" +
"<user>\nWeather?\n</user>\n" +
"<assistant>\n",
"<user>\nWeather?\n</user>\n<assistant>\n</think>",
},
{
name: "assistant_history_with_thinking_content_tool_and_response",
name: "assistant_history",
messages: []api.Message{
{Role: "user", Content: "Add these."},
{
@@ -154,14 +123,11 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
{Role: "tool", Content: "5"},
{Role: "user", Content: "Thanks"},
},
think: &api.ThinkValue{Value: true},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\n</system>\n" +
think: think(true),
want: defaultHeader +
"<user>\nAdd these.\n</user>\n" +
"<assistant>\n" +
"<think>Need addition.</think>\n" +
"<think>\nNeed addition.\n</think>\n" +
"Calling the tool.\n" +
"<tool_call>add\n" +
"<arg_key>a</arg_key>\n<arg_value>2</arg_value>\n" +
@@ -169,21 +135,15 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
"</tool_call>\n" +
"</assistant>\n" +
"<tool_response>\n5\n</tool_response>\n" +
"<user>\nThanks\n</user>\n" +
"<assistant>\n",
"<user>\nThanks\n</user>\n<assistant>\n<think>",
},
{
name: "final_assistant_prefill_is_continued",
name: "final_assistant_prefill",
messages: []api.Message{
{Role: "user", Content: "Complete this"},
{Role: "assistant", Content: "Partial"},
},
want: "" +
"〈|EOS|〉<system>\n" +
lagunaThinkDirective +
"\n</system>\n" +
"<user>\nComplete this\n</user>\n" +
"<assistant>\nPartial\n",
want: defaultHeader + "<user>\nComplete this\n</user>\n<assistant>\n</think>\nPartial\n",
},
}
@@ -195,7 +155,7 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) {
t.Fatal(err)
}
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatalf("renderer output mismatch (-want +got):\n%s", diff)
t.Fatalf("renderer output mismatch vs template (-want +got):\n%s", diff)
}
})
}
@@ -286,9 +246,9 @@ func renderLagunaChatTemplate(t *testing.T, python, modelDir string, messages []
t.Fatalf("failed to marshal messages: %v", err)
}
enableThinking := "True"
if think != nil && !think.Bool() {
enableThinking = "False"
enableThinking := "False"
if think != nil && think.Bool() {
enableThinking = "True"
}
script := `