package server import ( "testing" "text/template" ) func TestExtractThinking(t *testing.T) { tests := []struct { in, wantContent, wantThink string }{ { in: " internal world", wantThink: "internal ", wantContent: "world", }, { in: "abc", wantThink: "a", wantContent: "bc", }, { in: "no think", wantThink: "", wantContent: "no think", }, } for i, tt := range tests { parser := ThinkingParser{ OpeningTag: "", ClosingTag: "", } gotThinking, gotContent := parser.AddContent(tt.in) if gotContent != tt.wantContent || gotThinking != tt.wantThink { t.Errorf("case %d: got (%q,%q), want (%q,%q)", i, gotThinking, gotContent, tt.wantThink, tt.wantContent) } } } func TestThinkingStreaming(t *testing.T) { type step struct { input string wantThinking string wantContent string wantStateAfter thinkingState } cases := []struct { desc string skip bool steps []step }{ { desc: "content without a thinking tag", steps: []step{ { input: " abc", wantThinking: "", wantContent: " abc", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "content before a thinking tag nerfs the thinking tag", steps: []step{ { input: " abc def ghi", wantThinking: "", wantContent: " abc def ghi", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "building up a thinking tag partially", steps: []step{ { input: " a", wantThinking: "a", wantContent: "", wantStateAfter: thinkingState_Thinking, }, }, }, { desc: "partial closing tag", steps: []step{ { input: "abcdef", wantThinking: "", wantContent: "def", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "partial closing tag fakeout", steps: []step{ { input: "abcdef", wantThinking: "def", wantContent: "", wantStateAfter: thinkingState_Thinking, }, { input: "ghijkl", wantThinking: "", wantContent: "jkl", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "whitespace after thinking tag", steps: []step{ { input: " abc\n\ndef", wantThinking: "abc", wantContent: "def", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "whitespace after thinking tag (incremental)", steps: []step{ { input: " abc", wantThinking: "abc", wantContent: "", wantStateAfter: thinkingState_ThinkingDoneEatingWhitespace, }, { input: "\n\ndef", wantThinking: "", wantContent: "def", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "whitespace after thinking tag with content and more whitespace", steps: []step{ { input: " abc\n\ndef ", wantThinking: "abc", wantContent: "def ", wantStateAfter: thinkingState_ThinkingDone, }, { input: " ghi", wantThinking: "", wantContent: " ghi", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "token by token", steps: []step{ { input: "", wantThinking: "", wantContent: "", wantStateAfter: thinkingState_ThinkingStartedEatingWhitespace, }, { input: "\n", wantThinking: "", wantContent: "", wantStateAfter: thinkingState_ThinkingStartedEatingWhitespace, }, { input: "", wantThinking: "", wantContent: "", wantStateAfter: thinkingState_ThinkingDoneEatingWhitespace, }, { input: "\n\n", wantThinking: "", wantContent: "", wantStateAfter: thinkingState_ThinkingDoneEatingWhitespace, }, { input: "Hi", wantThinking: "", wantContent: "Hi", wantStateAfter: thinkingState_ThinkingDone, }, { input: " there", wantThinking: "", wantContent: " there", wantStateAfter: thinkingState_ThinkingDone, }, }, }, { desc: "leading thinking whitespace", steps: []step{ { input: " \t ", wantThinking: "", wantContent: "", wantStateAfter: thinkingState_ThinkingStartedEatingWhitespace, }, { input: " these are some ", wantThinking: "these are some ", wantContent: "", wantStateAfter: thinkingState_Thinking, }, { input: "thoughts ", wantThinking: "thoughts ", wantContent: "", wantStateAfter: thinkingState_ThinkingDoneEatingWhitespace, }, { input: " more content", wantThinking: "", wantContent: "more content", wantStateAfter: thinkingState_ThinkingDone, }, }, }, } for _, c := range cases { parser := ThinkingParser{ OpeningTag: "", ClosingTag: "", } if c.skip { continue } for i, step := range c.steps { thinking, content := parser.AddContent(step.input) if content != step.wantContent || thinking != step.wantThinking { t.Errorf("case %q (step %d): got (%q,%q), want (%q,%q)", c.desc, i, content, thinking, step.wantContent, step.wantThinking) } if parser.state != step.wantStateAfter { t.Errorf("case %q (step %d): got state %s, want %s", c.desc, i, parser.state, step.wantStateAfter) } } } } func TestInferThinkingTags(t *testing.T) { cases := []struct { desc string tmplString string wantOpeningTag string wantClosingTag string }{ { desc: "basic", tmplString: ` {{ if .Thinking}} /think {{ end }} {{- range $i, $_ := .Messages }} {{- $last := eq (len (slice $.Messages $i)) 1 -}} {{ if and $last .Thinking }} {{ .Thinking }} {{ end }} {{ end }} `, wantOpeningTag: "", wantClosingTag: "", }, { desc: "doubly nested range", tmplString: ` {{ if .Thinking}} /think {{ end }} {{- range $i, $_ := .Messages }} {{- range $j, $_ := .NotMessages }} {{- $last := eq (len (slice $.Messages $i)) 1 -}} {{ if and $last .Thinking }} {{ .Thinking }} {{ end }} {{ end }} {{ end }} `, wantOpeningTag: "", wantClosingTag: "", }, { desc: "whitespace is trimmed", tmplString: ` {{ if .Thinking}} /think {{ end }} {{- range $i, $_ := .Messages }} {{- $last := eq (len (slice $.Messages $i)) 1 -}} {{ if and $last .Thinking }} Some text before {{ .Thinking }} Some text after {{ end }} {{ end }} `, wantOpeningTag: "Some text before", wantClosingTag: "Some text after", }, { desc: "qwen3", tmplString: ` {{- if or .System .Tools .Thinking }}<|im_start|>system {{- if .System }} {{ .System }} {{- end }} {{- if .Tools }} # Tools You may call one or more functions to assist with the user query. You are provided with function signatures within XML tags: {{- range .Tools }} {"type": "function", "function": {{ .Function }}} {{- end }} For each function call, return a json object with function name and arguments within XML tags: {"name": , "arguments": } {{- end }} {{- if .Thinking }} /think {{- else }} /no_think {{- end }}<|im_end|> {{ end }} {{- range $i, $_ := .Messages }} {{- $last := eq (len (slice $.Messages $i)) 1 -}} {{- if eq .Role "user" }}<|im_start|>user {{ .Content }}<|im_end|> {{ else if eq .Role "assistant" }}<|im_start|>assistant {{ if and $last .Thinking }} {{ .Thinking }} {{ end }} {{ if .Content }}{{ .Content }} {{- else if .ToolCalls }} {{ range .ToolCalls }}{"name": "{{ .Function.Name }}", "arguments": {{ .Function.Arguments }}} {{ end }} {{- end }}{{ if not $last }}<|im_end|> {{ end }} {{- else if eq .Role "tool" }}<|im_start|>user {{ .Content }} <|im_end|> {{ end }} {{- if and (ne .Role "assistant") $last }}<|im_start|>assistant {{ end }} {{- end }} `, wantOpeningTag: "", wantClosingTag: "", }, } for _, c := range cases { tmpl := template.Must(template.New("test").Parse(c.tmplString)) openingTag, closingTag := inferThinkingTags(tmpl) if openingTag != c.wantOpeningTag || closingTag != c.wantClosingTag { t.Errorf("case %q: got (%q,%q), want (%q,%q)", c.desc, openingTag, closingTag, c.wantOpeningTag, c.wantClosingTag) } } }