From e7766a4a472ff100e589fb35fd2ed8a64be69d05 Mon Sep 17 00:00:00 2001 From: Jeffrey Morgan Date: Sun, 31 May 2026 14:11:07 -0700 Subject: [PATCH] model: improvements to laguna-xs.2 parser/renderer (#16362) --- model/parsers/laguna.go | 51 ++++++++++-- model/parsers/laguna_test.go | 115 ++++++++++++++++++++++++--- model/renderers/laguna.go | 105 ++++++++++++++++--------- model/renderers/laguna_test.go | 140 ++++++++++++--------------------- 4 files changed, 269 insertions(+), 142 deletions(-) diff --git a/model/parsers/laguna.go b/model/parsers/laguna.go index c97ba34fd..29ce5c398 100644 --- a/model/parsers/laguna.go +++ b/model/parsers/laguna.go @@ -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: when thinking is enabled + // (so the model's output begins with reasoning and no opening tag), or + // 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 , 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 ; 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 (its trained format is "\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 ). + 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)) diff --git a/model/parsers/laguna_test.go b/model/parsers/laguna_test.go index 73fb42487..8112b1641 100644 --- a/model/parsers/laguna_test.go +++ b/model/parsers/laguna_test.go @@ -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 + // block is suppressed rather than surfaced as reasoning. parser := ParserForName("laguna") parser.Init(nil, nil, nil) content, thinking, calls, err := parser.Add("Need to reason.\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("Need to reason.\nget_weather\nlocation\nParis\n", 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("\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 , 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 , 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 + // , 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("get_weather\nlocation\nParis\n", 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 , 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 , 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 , so output is + // "\n{reasoning}\n\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\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)) } } diff --git a/model/renderers/laguna.go b/model/renderers/laguna.go index 42262c074..4a3b4604f 100644 --- a/model/renderers/laguna.go +++ b/model/renderers/laguna.go @@ -2,6 +2,7 @@ package renderers import ( "strings" + "unicode" "github.com/ollama/ollama/api" ) @@ -10,6 +11,10 @@ const ( lagunaBOS = "〈|EOS|〉" lagunaThoughtOpen = "" lagunaThoughtClose = "" + + // 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 + // ( vs ), 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("\n") - if thinkingEnabled { - sb.WriteString("You should use chain-of-thought reasoning. Put your reasoning inside 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("\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("\n") + if strings.TrimSpace(systemMessage) != "" { + sb.WriteByte('\n') + sb.WriteString(strings.TrimRightFunc(systemMessage, unicode.IsSpace)) } - sb.WriteString("\n\n") - sb.WriteString("For each function call, return a json object with function name and arguments within '' and '' tags:\n") - sb.WriteString("\n{\"name\": , \"arguments\": }\n") + 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("\n") + for _, tool := range tools { + if b, err := marshalWithSpaces(tool); err == nil { + sb.Write(b) + sb.WriteByte('\n') + } + } + sb.WriteString("\n\n") + if thinkingEnabled { + sb.WriteString("Wrap your thinking in '', '' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '' and '' tags, like here:\n") + sb.WriteString(" your thoughts here \n") + } else { + sb.WriteString("For each function call, return an unescaped XML-like object with function name and arguments within '' and '' tags, like here:\n") + } + sb.WriteString("function-name\nargument-key\nvalue-of-argument-key\n") + } + sb.WriteString("\n\n") } - sb.WriteString("\n\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\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("\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 + // when there is reasoning, otherwise a bare + // marking the turn as direct. + if reasoning := strings.TrimSpace(message.Thinking); reasoning != "" { + sb.WriteString("\n") + sb.WriteString(reasoning) + sb.WriteString("\n\n") + } else { + sb.WriteString("\n") + } + + if strings.TrimSpace(content) != "" { + sb.WriteString(strings.TrimSpace(content)) sb.WriteByte('\n') } + for _, toolCall := range message.ToolCalls { sb.WriteString("") sb.WriteString(toolCall.Function.Name) @@ -94,6 +117,7 @@ func (r *LagunaRenderer) Render(messages []api.Message, tools []api.Tool, think } sb.WriteString("\n") } + if !prefill { sb.WriteString("\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 ( when thinking, else ). if len(messages) == 0 || messages[len(messages)-1].Role != "assistant" { sb.WriteString("\n") + if thinkingEnabled { + sb.WriteString(lagunaThoughtOpen) + } else { + sb.WriteString(lagunaThoughtClose) + } } + return sb.String(), nil } diff --git a/model/renderers/laguna_test.go b/model/renderers/laguna_test.go index e5cecfae7..44ad03a83 100644 --- a/model/renderers/laguna_test.go +++ b/model/renderers/laguna_test.go @@ -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 tags before your response." -) +// lagunaToolJSON is the get_weather tool as serialized into , +// 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|〉\n\n" + lagunaDefaultSystem + "\n\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|〉\n" + - lagunaThinkDirective + - "\n\n" + - "\nHello\n\n" + - "\n", + want: defaultHeader + "\nHello\n\n\n", }, { - name: "user_only_thinking_enabled", + name: "user_only_think", messages: []api.Message{{Role: "user", Content: "Hello"}}, - think: &api.ThinkValue{Value: true}, - want: "" + - "〈|EOS|〉\n" + - lagunaThinkDirective + - "\n\n" + - "\nHello\n\n" + - "\n", + think: think(true), + want: defaultHeader + "\nHello\n\n\n", }, { - name: "user_only_thinking_disabled", + name: "user_only_nothink", messages: []api.Message{{Role: "user", Content: "Hello"}}, - think: &api.ThinkValue{Value: false}, - want: "" + - "〈|EOS|〉\n" + - lagunaDirectDirective + - "\n\n" + - "\nHello\n\n" + - "\n", + think: think(false), + want: defaultHeader + "\nHello\n\n\n", }, { 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|〉\n" + - lagunaThinkDirective + - "\nStay concise." + - "\n\n" + - "\nHi\n\n" + - "\n", + want: "〈|EOS|〉\n\nStay concise.\n\n" + + "\nHi\n\n\n", }, { - 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|〉\n" + - lagunaThinkDirective + - "\nPrimary." + - "\n\n" + + want: "〈|EOS|〉\n\nPrimary.\n\n" + "\nHi\n\n" + "\nSecondary.\n\n" + - "\n", + "\n", }, { 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|〉\n" + - lagunaThinkDirective + - "\nStay concise." + - "\n\n### Tools\n\n" + + think: think(true), + want: "〈|EOS|〉\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" + - "\n" + - `{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object", "required": ["location"], "properties": {"location": {"type": "string", "description": "City"}}}}}` + "\n" + - "\n\n" + - "For each function call, return a json object with function name and arguments within '' and '' tags:\n" + - "\n{\"name\": , \"arguments\": }\n" + + "\n" + lagunaToolJSON + "\n\n\n" + + "Wrap your thinking in '', '' tags, followed by a function call. For each function call, return an unescaped XML-like object with function name and arguments within '' and '' tags, like here:\n" + + " your thoughts here \n" + + "function-name\nargument-key\nvalue-of-argument-key\n" + "\n\n" + - "\nWeather?\n\n" + - "\n", + "\nWeather?\n\n\n", }, { - name: "tools_default_thinking_on_when_unspecified", - messages: []api.Message{ - {Role: "user", Content: "Weather?"}, - }, - tools: weather, - want: "" + - "〈|EOS|〉\n" + - lagunaThinkDirective + - "\n\n### Tools\n\n" + + name: "tools_default", + messages: []api.Message{{Role: "user", Content: "Weather?"}}, + tools: weather, + want: "〈|EOS|〉\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" + - "\n" + - `{"type": "function", "function": {"name": "get_weather", "description": "Get weather", "parameters": {"type": "object", "required": ["location"], "properties": {"location": {"type": "string", "description": "City"}}}}}` + "\n" + - "\n\n" + - "For each function call, return a json object with function name and arguments within '' and '' tags:\n" + - "\n{\"name\": , \"arguments\": }\n" + + "\n" + lagunaToolJSON + "\n\n\n" + + "For each function call, return an unescaped XML-like object with function name and arguments within '' and '' tags, like here:\n" + + "function-name\nargument-key\nvalue-of-argument-key\n" + "\n\n" + - "\nWeather?\n\n" + - "\n", + "\nWeather?\n\n\n", }, { - 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|〉\n" + - lagunaThinkDirective + - "\n\n" + + think: think(true), + want: defaultHeader + "\nAdd these.\n\n" + "\n" + - "Need addition.\n" + + "\nNeed addition.\n\n" + "Calling the tool.\n" + "add\n" + "a\n2\n" + @@ -169,21 +135,15 @@ func TestLagunaRendererReferenceFlowCoverage(t *testing.T) { "\n" + "\n" + "\n5\n\n" + - "\nThanks\n\n" + - "\n", + "\nThanks\n\n\n", }, { - name: "final_assistant_prefill_is_continued", + name: "final_assistant_prefill", messages: []api.Message{ {Role: "user", Content: "Complete this"}, {Role: "assistant", Content: "Partial"}, }, - want: "" + - "〈|EOS|〉\n" + - lagunaThinkDirective + - "\n\n" + - "\nComplete this\n\n" + - "\nPartial\n", + want: defaultHeader + "\nComplete this\n\n\n\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 := `