fix(providers): preserve streamed content whitespace (#1324)

Co-authored-by: GoClaw Operator <operator@goclaw>
This commit is contained in:
nguyenha935andGoClaw Operator authored and GitHub committed 2026-07-02 16:24:25 +07:00
1 parent e529fb5f80
commit 7e3cdc8371
4 files changed
+62 -5

No files matched your search

+2 -4
View File
@@ -112,10 +112,8 @@ func (a *OpenAIAdapter) FromStreamChunk(data []byte) (*StreamChunk, error) {
// Text content
if delta.Content != "" {
normalized := normalizeControlOutput(delta.Content, "", nil)
sc.Content = normalized.Content
sc.Thinking = strings.TrimSpace(strings.Join([]string{sc.Thinking, normalized.Thinking}, "\n"))
hasContent = sc.Content != "" || sc.Thinking != ""
sc.Content = delta.Content
hasContent = true
}
if !hasContent {
+14
View File
@@ -185,6 +185,20 @@ func TestOpenAIAdapter_FromStreamChunk(t *testing.T) {
}
})
t.Run("content delta preserves leading whitespace", func(t *testing.T) {
raw := []byte(`{"choices":[{"delta":{"content":" xin lỗi"}}]}`)
sc, err := a.FromStreamChunk(raw)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc == nil {
t.Fatal("want stream chunk, got nil")
}
if sc.Content != " xin lỗi" {
t.Errorf("want leading whitespace preserved, got %q", sc.Content)
}
})
t.Run("reasoning_content delta", func(t *testing.T) {
raw := []byte(`{"choices":[{"delta":{"reasoning_content":"thinking..."}}]}`)
sc, err := a.FromStreamChunk(raw)
@@ -70,7 +70,6 @@ func normalizeControlOutput(content, thinking string, tools []ToolDefinition) co
out.Content, extracted = extractThinkingTags(out.Content)
out.Thinking = joinThinking(out.Thinking, extracted)
out.Content = strings.TrimSpace(out.Content)
return out
}
@@ -286,6 +286,52 @@ func TestOpenAIProviderChatStream_NormalizesSplitKimiTextToolCallBeforeEmittingC
}
}
func TestOpenAIProviderChatStream_PreservesWhitespaceAcrossContentDeltas(t *testing.T) {
events := []string{
`data: {"choices":[{"delta":{"content":"Dạ"}}]}` + "\n\n",
`data: {"choices":[{"delta":{"content":" em"}}]}` + "\n\n",
`data: {"choices":[{"delta":{"content":" xin"}}]}` + "\n\n",
`data: {"choices":[{"delta":{"content":" lỗi"}}]}` + "\n\n",
`data: {"choices":[{"delta":{"content":" anh"}}]}` + "\n\n",
`data: {"choices":[{"finish_reason":"stop","delta":{}}]}` + "\n\n",
}
srv := newOpenAISSEServer(t, events)
defer srv.Close()
p := NewOpenAIProvider("openai-compatible", "sk", srv.URL, "model")
var streamed strings.Builder
resp, err := p.ChatStream(context.Background(), ChatRequest{
Messages: []Message{{Role: "user", Content: "say it"}},
}, func(chunk StreamChunk) {
streamed.WriteString(chunk.Content)
})
if err != nil {
t.Fatalf("ChatStream: %v", err)
}
const want = "Dạ em xin lỗi anh"
if resp.Content != want {
t.Fatalf("Content = %q, want %q", resp.Content, want)
}
if got := streamed.String(); got != want {
t.Fatalf("streamed content = %q, want %q", got, want)
}
}
func TestControlOutputNormalizer_PreservesWhitespaceAroundNormalText(t *testing.T) {
n := newControlOutputNormalizer(nil)
var got strings.Builder
got.WriteString(n.Append("Dạ").Content)
got.WriteString(n.Append(" em").Content)
got.WriteString(n.Append(" xin").Content)
got.WriteString(n.Append(" lỗi").Content)
got.WriteString(n.Finish().Content)
if got.String() != "Dạ em xin lỗi" {
t.Fatalf("normalized stream content = %q, want %q", got.String(), "Dạ em xin lỗi")
}
}
func newOpenAIJSONServer(t *testing.T, body string) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {