fix(openai_compat): parse reasoning_content in streaming responses

This commit is contained in:
lc6464
2026-05-01 15:20:35 +08:00
parent a7414608ed
commit b00ff5bc5d
2 changed files with 63 additions and 6 deletions
+12 -6
View File
@@ -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
}
@@ -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) {