Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit a2d7b64

Browse files
author
Qiang
committed
fix(openai): resolve 400 error on cross-model switching with DeepSeek V4 thinking mode
1 parent fc15778 commit a2d7b64

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

src/api/providers/openai.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
9999
const modelId = this.options.openAiModelId ?? ""
100100
const enabledR1Format = this.options.openAiR1FormatEnabled ?? false
101101
const isAzureAiInference = this._isAzureAiInference(modelUrl)
102-
const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format
102+
const deepseekReasoner =
103+
modelId.includes("deepseek-reasoner") ||
104+
modelId.includes("deepseek-v4-pro") ||
105+
modelId.includes("deepseek-v4-flash") ||
106+
enabledR1Format
103107

104108
if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) {
105109
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, metadata)
@@ -340,7 +344,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
340344
})
341345

342346
const enabledR1Format = this.options.openAiR1FormatEnabled ?? false
343-
const deepseekReasoner = id.includes("deepseek-reasoner") || enabledR1Format
347+
const deepseekReasoner =
348+
id.includes("deepseek-reasoner") ||
349+
id.includes("deepseek-v4-pro") ||
350+
id.includes("deepseek-v4-flash") ||
351+
enabledR1Format
344352

345353
return {
346354
id,

src/api/transform/openai-format.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ export function convertToOpenAiMessages(
317317
if (mapped) {
318318
;(baseMessage as any).reasoning_details = mapped
319319
}
320+
if (messageWithDetails.reasoning_content !== undefined) {
321+
;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content
322+
}
320323
}
321324

322325
openAiMessages.push(baseMessage)
@@ -494,6 +497,11 @@ export function convertToOpenAiMessages(
494497
baseMessage.reasoning_details = mapped
495498
}
496499

500+
// Pass through reasoning_content for DeepSeek V4/R1 thinking mode
501+
if (messageWithDetails.reasoning_content !== undefined) {
502+
;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content
503+
}
504+
497505
// Add tool_calls after reasoning_details
498506
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
499507
if (tool_calls.length > 0) {

src/core/task/Task.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4469,6 +4469,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
44694469

44704470
const cleanConversationHistory: (Anthropic.Messages.MessageParam | ReasoningItemForRequest)[] = []
44714471

4472+
// Check if the current model requires reasoning to be preserved (e.g., DeepSeek V4/R1).
4473+
// When preserveReasoning is true, ALL assistant messages must include reasoning_content
4474+
// to satisfy the API protocol. Messages from other models (e.g., Gemini) that lack
4475+
// reasoning_content need to be backfilled with an empty string.
4476+
const shouldPreserveForApi = this.api.getModel().info.preserveReasoning === true
4477+
44724478
for (const msg of messages) {
44734479
// Standalone reasoning: send encrypted, skip plain text
44744480
if (msg.type === "reasoning") {
@@ -4595,10 +4601,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
45954601

45964602
// Default path for regular messages (no embedded reasoning)
45974603
if (msg.role) {
4598-
cleanConversationHistory.push({
4604+
const baseMessage: any = {
45994605
role: msg.role,
46004606
content: msg.content as Anthropic.Messages.ContentBlockParam[] | string,
4601-
})
4607+
}
4608+
4609+
// DeepSeek thinking mode requires ALL assistant messages to have reasoning_content.
4610+
// If a message from another model (e.g., Gemini) lacks it, add an empty placeholder
4611+
// to prevent 400 errors when switching back to DeepSeek mid-conversation.
4612+
if (msg.role === "assistant" && shouldPreserveForApi) {
4613+
// Use a space instead of an empty string. Some proxies/gateways or
4614+
// internal SDK logic may silently strip empty string fields, causing
4615+
// DeepSeek to reject the request with 400.
4616+
baseMessage.reasoning_content = " "
4617+
}
4618+
4619+
cleanConversationHistory.push(baseMessage)
46024620
}
46034621
}
46044622

0 commit comments

Comments
 (0)