mirror of
https://github.com/likelovewant/ollama-for-amd.git
synced 2025-12-21 22:33:56 +00:00
Removing whitespace between Thinking and Content in Qwen3VL (#12838)
Eats extra whitespace at the end/beginning of content
This commit is contained in:
@@ -546,3 +546,333 @@ func TestQwen3VLThinkingParserStreamingAssistantPrefillContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwen3VLThinkingWhitespaceHandling(t *testing.T) {
|
||||
type step struct {
|
||||
input string
|
||||
wantEvents []qwenEvent
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
steps []step
|
||||
only bool
|
||||
}{
|
||||
{
|
||||
desc: "whitespace after thinking tag is trimmed",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking content</think> \n\t content starts here",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking content"},
|
||||
qwenEventContent{content: "content starts here"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace after thinking tag split across chunks",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking content</think> ",
|
||||
wantEvents: []qwenEvent{qwenEventThinkingContent{content: "thinking content"}},
|
||||
},
|
||||
{
|
||||
input: " \n\t",
|
||||
wantEvents: []qwenEvent{},
|
||||
},
|
||||
{
|
||||
input: "content",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "only whitespace after thinking tag",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking content</think> \n\t ",
|
||||
wantEvents: []qwenEvent{qwenEventThinkingContent{content: "thinking content"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple spaces and tabs after thinking",
|
||||
steps: []step{
|
||||
{
|
||||
input: "think</think> \t\t\n\n text",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "think"},
|
||||
qwenEventContent{content: "text"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "trailing whitespace before thinking tag is preserved in content",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking with spaces </think>text",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking with spaces"},
|
||||
qwenEventContent{content: "text"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace between thinking and tool call",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking</think> \n <tool_call>{\"name\":\"test\"}</tool_call>",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking"},
|
||||
qwenEventRawToolCall{raw: "{\"name\":\"test\"}"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "no whitespace after thinking tag",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking</think>content",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking"},
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "unicode whitespace after thinking tag",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking</think>\u00a0\u3000content",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking"},
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace split with partial thinking tag",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking</th",
|
||||
wantEvents: []qwenEvent{qwenEventThinkingContent{content: "thinking"}},
|
||||
},
|
||||
{
|
||||
input: "ink> \n",
|
||||
wantEvents: []qwenEvent{},
|
||||
},
|
||||
{
|
||||
input: " content",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "empty thinking tag with whitespace after",
|
||||
steps: []step{
|
||||
{
|
||||
input: "</think> \ncontent",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace inside tool call preserves trailing space",
|
||||
steps: []step{
|
||||
{
|
||||
input: "bruh</think> \n \n \n \n \n \n blahhhhhhhhhh blahhhh blahhhh \n\n\n\t\t <tool_call> tool content </tool_call> \n\n\n\n\n\n\n after",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "bruh"},
|
||||
qwenEventContent{content: "blahhhhhhhhhh blahhhh blahhhh"},
|
||||
qwenEventRawToolCall{raw: " tool content "},
|
||||
qwenEventContent{content: "after"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace inside tool call preserves trailing space",
|
||||
steps: []step{
|
||||
{
|
||||
input: "bruh</think> shdjfhksdhfj ",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "bruh"},
|
||||
qwenEventContent{content: "shdjfhksdhfj"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "another word ",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: " another word"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "<tool_call> tool content </tool_call> ",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventRawToolCall{raw: " tool content "},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "\n \n \n \n \n \n blahhhhhhhhhh blahhhh blahhhh \n\n\n\t\t <tool_call> anotha one </tool_call> \n\n\n\n\n\n\n after \n\n\n\n\n\n blep",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "blahhhhhhhhhh blahhhh blahhhh"},
|
||||
qwenEventRawToolCall{raw: " anotha one "},
|
||||
qwenEventContent{content: "after \n\n\n\n\n\n blep"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
anyOnlies := false
|
||||
for _, tc := range cases {
|
||||
if tc.only {
|
||||
anyOnlies = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if anyOnlies && !tc.only {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||
parser.Init([]api.Tool{}, nil)
|
||||
|
||||
for i, step := range tc.steps {
|
||||
parser.buffer.WriteString(step.input)
|
||||
gotEvents := parser.parseEvents()
|
||||
|
||||
if len(gotEvents) == 0 && len(step.wantEvents) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotEvents, step.wantEvents) {
|
||||
t.Errorf("step %d: input %q: got events %#v, want %#v", i, step.input, gotEvents, step.wantEvents)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQwen3VLToolCallWhitespaceHandling(t *testing.T) {
|
||||
type step struct {
|
||||
input string
|
||||
wantEvents []qwenEvent
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
steps []step
|
||||
only bool
|
||||
prefillMsg *api.Message // allows starting in content mode instead of thinking mode
|
||||
}{
|
||||
{
|
||||
desc: "whitespace inside tool call is fully preserved (with content prefill)",
|
||||
prefillMsg: &api.Message{Role: "assistant", Content: "prefill"},
|
||||
steps: []step{
|
||||
{
|
||||
input: "before<tool_call> tool content </tool_call> \n after",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "before"},
|
||||
qwenEventRawToolCall{raw: " tool content "},
|
||||
qwenEventContent{content: "after"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "whitespace after tool call trimmed across chunks (with content prefill)",
|
||||
prefillMsg: &api.Message{Role: "assistant", Content: "prefill"},
|
||||
steps: []step{
|
||||
{
|
||||
input: "before<tool_call>tool</tool_call> ",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "before"},
|
||||
qwenEventRawToolCall{raw: "tool"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "\n\t",
|
||||
wantEvents: []qwenEvent{},
|
||||
},
|
||||
{
|
||||
input: "after \n this is a song",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventContent{content: "after \n this is a song"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple tool calls with whitespace between (with content prefill)",
|
||||
prefillMsg: &api.Message{Role: "assistant", Content: "prefill"},
|
||||
steps: []step{
|
||||
{
|
||||
input: "<tool_call>first</tool_call> \n <tool_call>second</tool_call>",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventRawToolCall{raw: "first"},
|
||||
qwenEventRawToolCall{raw: "second"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "thinking with whitespace then tool call",
|
||||
steps: []step{
|
||||
{
|
||||
input: "thinking</think> \n <tool_call>tool</tool_call> \n content",
|
||||
wantEvents: []qwenEvent{
|
||||
qwenEventThinkingContent{content: "thinking"},
|
||||
qwenEventRawToolCall{raw: "tool"},
|
||||
qwenEventContent{content: "content"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
anyOnlies := false
|
||||
for _, tc := range cases {
|
||||
if tc.only {
|
||||
anyOnlies = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if anyOnlies && !tc.only {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
parser := Qwen3VLParser{hasThinkingSupport: true}
|
||||
parser.Init([]api.Tool{}, tc.prefillMsg)
|
||||
|
||||
for i, step := range tc.steps {
|
||||
parser.buffer.WriteString(step.input)
|
||||
gotEvents := parser.parseEvents()
|
||||
|
||||
if len(gotEvents) == 0 && len(step.wantEvents) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotEvents, step.wantEvents) {
|
||||
t.Errorf("step %d: input %q: got events %#v, want %#v", i, step.input, gotEvents, step.wantEvents)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user