@@ -5,6 +5,7 @@ package transformer
55import (
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.
2235func (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