Skip to content

Commit f143c13

Browse files
feat: enhance DeepSeek model handling with reasoning content and improve streaming logic
1 parent 1559f27 commit f143c13

5 files changed

Lines changed: 433 additions & 70 deletions

File tree

internal/handlers/messages.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,14 @@ func (h *MessagesHandler) handleStreaming(
310310
continue
311311
}
312312

313+
h.logger.Info("transformed request state",
314+
"model", model.ModelID,
315+
"reasoning_effort", openaiReq.ReasoningEffort,
316+
"thinking", string(openaiReq.Thinking),
317+
"has_thinking_history", transformer.HasThinkingBlocks(anthropicReq.Messages),
318+
"assistant_rc_count", countReasoningContent(openaiReq.Messages),
319+
)
320+
313321
// Get streaming body from upstream
314322
streamBody, err := h.client.GetStreamingBody(ctx, model.ModelID, openaiReq)
315323
if err != nil {
@@ -522,20 +530,12 @@ func (h *MessagesHandler) executeOpenAIRequest(
522530
return nil, fmt.Errorf("request transform failed: %w", err)
523531
}
524532

525-
// Log the transformed request for debugging
526-
reqJSON, _ := json.Marshal(openaiReq)
527-
h.logger.Debug("transformed OpenAI request", "body", string(reqJSON))
528-
529533
// Handle non-streaming.
530534
resp, err := h.client.ChatCompletionNonStreaming(ctx, model.ModelID, openaiReq)
531535
if err != nil {
532536
return nil, fmt.Errorf("chat completion failed: %w", err)
533537
}
534538

535-
// Log the raw response for debugging
536-
respJSON, _ := json.Marshal(resp)
537-
h.logger.Debug("OpenAI response", "body", string(respJSON))
538-
539539
// Transform response to Anthropic format.
540540
anthropicResp, err := h.responseTransformer.TransformResponse(resp, model.ModelID)
541541
if err != nil {
@@ -565,6 +565,17 @@ func extractTextFromBlocks(blocks []types.ContentBlock) string {
565565
return content
566566
}
567567

568+
// countReasoningContent counts how many messages in the OpenAI request have reasoning_content set.
569+
func countReasoningContent(messages []types.ChatMessage) int {
570+
count := 0
571+
for _, m := range messages {
572+
if m.ReasoningContent != nil {
573+
count++
574+
}
575+
}
576+
return count
577+
}
578+
568579
// sendError sends an error response in Anthropic format.
569580
// Safe to call multiple times - subsequent calls are no-ops.
570581
func (h *MessagesHandler) sendError(w http.ResponseWriter, statusCode int, message string, err error) {

internal/transformer/request.go

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package transformer
55
import (
66
"encoding/json"
77
"fmt"
8+
"strings"
89

910
"oc-go-cc/internal/config"
1011
"oc-go-cc/pkg/types"
@@ -18,13 +19,25 @@ func NewRequestTransformer() *RequestTransformer {
1819
return &RequestTransformer{}
1920
}
2021

22+
// isDeepSeekModel returns true for DeepSeek models that require thinking mode handling.
23+
func isDeepSeekModel(modelID string) bool {
24+
return strings.HasPrefix(modelID, "deepseek-")
25+
}
26+
27+
// needsPlaceholderReasoning returns true for providers whose validators require
28+
// a non-empty reasoning_content field on assistant tool-call messages.
29+
func needsPlaceholderReasoning(modelID string) bool {
30+
// Moonshot's validator treats an empty string as missing.
31+
return strings.HasPrefix(modelID, "kimi-")
32+
}
33+
2134
// TransformRequest converts an Anthropic MessageRequest to OpenAI ChatCompletionRequest.
2235
func (t *RequestTransformer) TransformRequest(
2336
anthropicReq *types.MessageRequest,
2437
model config.ModelConfig,
2538
) (*types.ChatCompletionRequest, error) {
2639
// Transform messages
27-
messages, err := t.transformMessages(anthropicReq)
40+
messages, err := t.transformMessages(anthropicReq, model.ModelID)
2841
if err != nil {
2942
return nil, fmt.Errorf("failed to transform messages: %w", err)
3043
}
@@ -58,11 +71,32 @@ func (t *RequestTransformer) TransformRequest(
5871
maxTokens := model.MaxTokens
5972
openaiReq.MaxTokens = &maxTokens
6073
}
61-
if model.ReasoningEffort != "" {
62-
openaiReq.ReasoningEffort = &model.ReasoningEffort
63-
}
64-
if len(model.Thinking) > 0 {
65-
openaiReq.Thinking = model.Thinking
74+
75+
// DeepSeek-v4 models always operate in thinking mode. When conversation
76+
// history contains thinking blocks (round-tripped as reasoning_content),
77+
// we MUST send thinking mode params so DeepSeek validates reasoning_content
78+
// on assistant messages. When history LACKS thinking blocks (Claude Code
79+
// dropped them), we MUST explicitly disable thinking mode so DeepSeek
80+
// doesn't require reasoning_content we can't provide.
81+
hasThinkingInHistory := HasThinkingBlocks(anthropicReq.Messages)
82+
if hasThinkingInHistory {
83+
// Thinking mode required — use model config values or defaults.
84+
if model.ReasoningEffort != "" {
85+
openaiReq.ReasoningEffort = &model.ReasoningEffort
86+
} else {
87+
defaultEffort := "high"
88+
openaiReq.ReasoningEffort = &defaultEffort
89+
}
90+
if len(model.Thinking) > 0 {
91+
openaiReq.Thinking = model.Thinking
92+
} else {
93+
openaiReq.Thinking = json.RawMessage(`{"type":"enabled"}`)
94+
}
95+
} else if len(model.Thinking) > 0 || model.ReasoningEffort != "" {
96+
// Model config wants thinking mode but history has no thinking blocks.
97+
// Explicitly disable to prevent DeepSeek from requiring reasoning_content
98+
// on assistant messages that can't provide it.
99+
openaiReq.Thinking = json.RawMessage(`{"type":"disabled"}`)
66100
}
67101

68102
// Transform tools if present
@@ -73,8 +107,26 @@ func (t *RequestTransformer) TransformRequest(
73107
return openaiReq, nil
74108
}
75109

110+
// HasThinkingBlocks returns true if any assistant message contains a
111+
// thinking content block.
112+
func HasThinkingBlocks(messages []types.Message) bool {
113+
for _, msg := range messages {
114+
if msg.Role != "assistant" {
115+
continue
116+
}
117+
for _, block := range msg.ContentBlocks() {
118+
if block.Type == "thinking" {
119+
return true
120+
}
121+
}
122+
}
123+
return false
124+
}
125+
76126
// transformMessages converts Anthropic messages to OpenAI format.
77-
func (t *RequestTransformer) transformMessages(anthropicReq *types.MessageRequest) ([]types.ChatMessage, error) {
127+
func (t *RequestTransformer) transformMessages(anthropicReq *types.MessageRequest, modelID string) ([]types.ChatMessage, error) {
128+
hasThinking := HasThinkingBlocks(anthropicReq.Messages)
129+
78130
var result []types.ChatMessage
79131

80132
// Add system message if present, preserving cache_control if available
@@ -101,7 +153,7 @@ func (t *RequestTransformer) transformMessages(anthropicReq *types.MessageReques
101153

102154
// Transform each message
103155
for _, msg := range anthropicReq.Messages {
104-
openaiMsgs, err := t.transformMessage(msg)
156+
openaiMsgs, err := t.transformMessage(msg, modelID, hasThinking)
105157
if err != nil {
106158
return nil, err
107159
}
@@ -113,14 +165,14 @@ func (t *RequestTransformer) transformMessages(anthropicReq *types.MessageReques
113165

114166
// transformMessage converts a single Anthropic message to one or more OpenAI messages.
115167
// Tool_use and tool_result require special handling to map to OpenAI's function calling format.
116-
func (t *RequestTransformer) transformMessage(msg types.Message) ([]types.ChatMessage, error) {
168+
func (t *RequestTransformer) transformMessage(msg types.Message, modelID string, hasThinkingInHistory bool) ([]types.ChatMessage, error) {
117169
blocks := msg.ContentBlocks()
118170

119171
switch msg.Role {
120172
case "user":
121173
return t.transformUserMessage(blocks)
122174
case "assistant":
123-
return t.transformAssistantMessage(blocks)
175+
return t.transformAssistantMessage(blocks, modelID, hasThinkingInHistory)
124176
default:
125177
// Fallback: concatenate all text
126178
var text string
@@ -174,7 +226,7 @@ func (t *RequestTransformer) transformUserMessage(blocks []types.ContentBlock) (
174226
}
175227

176228
// transformAssistantMessage converts an assistant message with potential tool_use blocks.
177-
func (t *RequestTransformer) transformAssistantMessage(blocks []types.ContentBlock) ([]types.ChatMessage, error) {
229+
func (t *RequestTransformer) transformAssistantMessage(blocks []types.ContentBlock, modelID string, hasThinkingInHistory bool) ([]types.ChatMessage, error) {
178230
var textParts []string
179231
var thinkingParts []string
180232
var toolCalls []types.ToolCall
@@ -184,8 +236,8 @@ func (t *RequestTransformer) transformAssistantMessage(blocks []types.ContentBlo
184236
case "text":
185237
textParts = append(textParts, block.Text)
186238
case "thinking":
187-
// Some OpenAI-compatible providers expect tool-call assistant messages
188-
// to preserve chain-of-thought in a provider-specific reasoning field.
239+
// Preserve chain-of-thought so it can be forwarded back to providers
240+
// that require reasoning_content to be preserved across turns.
189241
if block.Thinking != "" {
190242
thinkingParts = append(thinkingParts, block.Thinking)
191243
}
@@ -217,16 +269,22 @@ func (t *RequestTransformer) transformAssistantMessage(blocks []types.ContentBlo
217269
}
218270

219271
var reasoningContentPtr *string
220-
if len(toolCalls) > 0 || reasoningContent != "" {
221-
// Some providers require reasoning_content to be present on assistant
222-
// tool-call messages whenever thinking mode is enabled, even if the
223-
// upstream Anthropic history did not include an explicit thinking block.
272+
if reasoningContent != "" {
273+
// Real thinking content from the upstream history — preserve it.
274+
reasoningContentPtr = &reasoningContent
275+
} else if hasThinkingInHistory && len(toolCalls) > 0 && isDeepSeekModel(modelID) {
276+
// DeepSeek in thinking mode requires reasoning_content on ALL assistant
277+
// messages, including tool-call turns where Claude Code didn't preserve
278+
// the thinking block. Use a placeholder that won't trigger validation:
279+
// DeepSeek checks for the field's presence, not its content, when the
280+
// original thinking was stripped by the client.
281+
placeholder := " "
282+
reasoningContentPtr = &placeholder
283+
} else if len(toolCalls) > 0 && needsPlaceholderReasoning(modelID) {
224284
// Moonshot's validator treats an empty string as missing, so use a
225285
// non-empty placeholder when we must provide the field.
226-
if reasoningContent == "" {
227-
reasoningContent = " "
228-
}
229-
reasoningContentPtr = &reasoningContent
286+
placeholder := " "
287+
reasoningContentPtr = &placeholder
230288
}
231289

232290
msg := types.ChatMessage{

0 commit comments

Comments
 (0)