Removing whitespace between Thinking and Content in Qwen3VL (#12838)

Eats extra whitespace at the end/beginning of content
This commit is contained in:
Grace
2025-10-29 15:14:28 -07:00
committed by GitHub
parent c88647104d
commit 0a2d92081b
3 changed files with 553 additions and 17 deletions

View File

@@ -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)
}
}
})
}
}