From b00ff5bc5d8f5ce7449f44bc5ff335b99b9cbf02 Mon Sep 17 00:00:00 2001 From: lc6464 <64722907+lc6464@users.noreply.github.com> Date: Fri, 1 May 2026 15:20:35 +0800 Subject: [PATCH] fix(openai_compat): parse reasoning_content in streaming responses --- pkg/providers/openai_compat/provider.go | 18 ++++--- pkg/providers/openai_compat/provider_test.go | 51 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index be3e77a43..7003709a7 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -419,6 +419,7 @@ func parseStreamResponse( onChunk func(accumulated string), ) (*LLMResponse, error) { var textContent strings.Builder + var reasoningContent strings.Builder var finishReason string var usage *UsageInfo @@ -451,8 +452,9 @@ func parseStreamResponse( var chunk struct { Choices []struct { Delta struct { - Content string `json:"content"` - ToolCalls []struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + ToolCalls []struct { Index int `json:"index"` ID string `json:"id"` Function *struct { @@ -487,6 +489,9 @@ func parseStreamResponse( onChunk(textContent.String()) } } + if choice.Delta.ReasoningContent != "" { + reasoningContent.WriteString(choice.Delta.ReasoningContent) + } // Accumulate tool call deltas for _, tc := range choice.Delta.ToolCalls { @@ -544,10 +549,11 @@ func parseStreamResponse( } return &LLMResponse{ - Content: textContent.String(), - ToolCalls: toolCalls, - FinishReason: finishReason, - Usage: usage, + Content: textContent.String(), + ReasoningContent: reasoningContent.String(), + ToolCalls: toolCalls, + FinishReason: finishReason, + Usage: usage, }, nil } diff --git a/pkg/providers/openai_compat/provider_test.go b/pkg/providers/openai_compat/provider_test.go index 4f68fb393..fbda447b4 100644 --- a/pkg/providers/openai_compat/provider_test.go +++ b/pkg/providers/openai_compat/provider_test.go @@ -1195,6 +1195,57 @@ func TestProviderChatStream_CustomHeadersInjected(t *testing.T) { } } +func TestProviderChatStream_ParsesReasoningContent(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + _, _ = w.Write([]byte( + "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"Let me \",\"content\":\"Checking \",\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\"}}]}}]}\n\n", + )) + _, _ = w.Write([]byte( + "data: {\"choices\":[{\"delta\":{\"reasoning_content\":\"think step by step.\",\"content\":\"the weather\",\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"Hangzhou\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":10,\"completion_tokens\":6,\"total_tokens\":16}}\n\n", + )) + _, _ = w.Write([]byte("data: [DONE]\n\n")) + })) + defer server.Close() + + p := NewProvider("key", server.URL, "") + out, err := p.ChatStream( + t.Context(), + []Message{{Role: "user", Content: "weather?"}}, + nil, + "deepseek-v4-flash", + nil, + nil, + ) + if err != nil { + t.Fatalf("ChatStream() error = %v", err) + } + if out.Content != "Checking the weather" { + t.Fatalf("Content = %q, want %q", out.Content, "Checking the weather") + } + if out.ReasoningContent != "Let me think step by step." { + t.Fatalf("ReasoningContent = %q, want %q", out.ReasoningContent, "Let me think step by step.") + } + if len(out.ToolCalls) != 1 { + t.Fatalf("len(ToolCalls) = %d, want 1", len(out.ToolCalls)) + } + if out.ToolCalls[0].ID != "call_1" { + t.Fatalf("ToolCalls[0].ID = %q, want %q", out.ToolCalls[0].ID, "call_1") + } + if out.ToolCalls[0].Name != "get_weather" { + t.Fatalf("ToolCalls[0].Name = %q, want %q", out.ToolCalls[0].Name, "get_weather") + } + if out.ToolCalls[0].Arguments["city"] != "Hangzhou" { + t.Fatalf("ToolCalls[0].Arguments[city] = %v, want %q", out.ToolCalls[0].Arguments["city"], "Hangzhou") + } + if out.FinishReason != "tool_calls" { + t.Fatalf("FinishReason = %q, want %q", out.FinishReason, "tool_calls") + } + if out.Usage == nil || out.Usage.TotalTokens != 16 { + t.Fatalf("Usage = %#v, want total tokens 16", out.Usage) + } +} + type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {