From 417258434985e441e27673f4ad9a3c3f050f7e10 Mon Sep 17 00:00:00 2001 From: Stalin <161853795+0x5t4l1n@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:58:23 +0530 Subject: [PATCH] Fix DeepSeek multi-turn failure by preserving reasoning_content DeepSeek V4 requires that any reasoning_content present in an assistant message be echoed back verbatim on the next request. The stream parser was discarding it entirely, so the second turn always failed with "reasoning_content must be passed back to the API". Fixes #3266 --- .../openaichat/openaichat-backend.go | 7 +++- pkg/aiusechat/openaichat/openaichat-types.go | 41 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/pkg/aiusechat/openaichat/openaichat-backend.go b/pkg/aiusechat/openaichat/openaichat-backend.go index 635f334873..60109529da 100644 --- a/pkg/aiusechat/openaichat/openaichat-backend.go +++ b/pkg/aiusechat/openaichat/openaichat-backend.go @@ -101,6 +101,7 @@ func processChatStream( ) (*uctypes.WaveStopReason, *StoredChatMessage, error) { decoder := eventsource.NewDecoder(body) var textBuilder strings.Builder + var reasoningBuilder strings.Builder msgID := uuid.New().String() textID := uuid.New().String() var finishReason string @@ -159,6 +160,9 @@ func processChatStream( } choice := chunk.Choices[0] + if choice.Delta.ReasoningContent != "" { + reasoningBuilder.WriteString(choice.Delta.ReasoningContent) + } if choice.Delta.Content != "" { if !textStarted { _ = sseHandler.AiMsgTextStart(textID) @@ -239,7 +243,8 @@ func processChatStream( assistantMsg := &StoredChatMessage{ MessageId: msgID, Message: ChatRequestMessage{ - Role: "assistant", + Role: "assistant", + ReasoningContent: reasoningBuilder.String(), }, } diff --git a/pkg/aiusechat/openaichat/openaichat-types.go b/pkg/aiusechat/openaichat/openaichat-types.go index 18d28e3b20..82bca23fb1 100644 --- a/pkg/aiusechat/openaichat/openaichat-types.go +++ b/pkg/aiusechat/openaichat/openaichat-types.go @@ -50,29 +50,32 @@ type ChatImageUrl struct { } type ChatRequestMessage struct { - Role string `json:"role"` // "system","user","assistant","tool" - Content string `json:"-"` // plain text (used when ContentParts is nil) - ContentParts []ChatContentPart `json:"-"` // multimodal parts (used when images present) - ToolCalls []ToolCall `json:"tool_calls,omitempty"` // assistant tool-call message - ToolCallID string `json:"tool_call_id,omitempty"` // for role:"tool" - Name string `json:"name,omitempty"` // tool name on role:"tool" + Role string `json:"role"` // "system","user","assistant","tool" + Content string `json:"-"` // plain text (used when ContentParts is nil) + ContentParts []ChatContentPart `json:"-"` // multimodal parts (used when images present) + ReasoningContent string `json:"-"` // preserved for DeepSeek multi-turn (reasoning_content) + ToolCalls []ToolCall `json:"tool_calls,omitempty"` // assistant tool-call message + ToolCallID string `json:"tool_call_id,omitempty"` // for role:"tool" + Name string `json:"name,omitempty"` // tool name on role:"tool" } // chatRequestMessageJSON is the wire format for ChatRequestMessage type chatRequestMessageJSON struct { - Role string `json:"role"` - Content json.RawMessage `json:"content"` - ToolCalls []ToolCall `json:"tool_calls,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` - Name string `json:"name,omitempty"` + Role string `json:"role"` + Content json.RawMessage `json:"content"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCallID string `json:"tool_call_id,omitempty"` + Name string `json:"name,omitempty"` } func (cm ChatRequestMessage) MarshalJSON() ([]byte, error) { raw := chatRequestMessageJSON{ - Role: cm.Role, - ToolCalls: cm.ToolCalls, - ToolCallID: cm.ToolCallID, - Name: cm.Name, + Role: cm.Role, + ReasoningContent: cm.ReasoningContent, + ToolCalls: cm.ToolCalls, + ToolCallID: cm.ToolCallID, + Name: cm.Name, } if len(cm.ContentParts) > 0 { b, err := json.Marshal(cm.ContentParts) @@ -96,6 +99,7 @@ func (cm *ChatRequestMessage) UnmarshalJSON(data []byte) error { return err } cm.Role = raw.Role + cm.ReasoningContent = raw.ReasoningContent cm.ToolCalls = raw.ToolCalls cm.ToolCallID = raw.ToolCallID cm.Name = raw.Name @@ -193,9 +197,10 @@ type StreamChoice struct { // This is the important part: type ContentDelta struct { - Role string `json:"role,omitempty"` - Content string `json:"content,omitempty"` - ToolCalls []ToolCallDelta `json:"tool_calls,omitempty"` + Role string `json:"role,omitempty"` + Content string `json:"content,omitempty"` + ReasoningContent string `json:"reasoning_content,omitempty"` + ToolCalls []ToolCallDelta `json:"tool_calls,omitempty"` } type ToolCallDelta struct {