diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 2d56e20c89..1008ecc5c4 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -99,7 +99,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const modelId = this.options.openAiModelId ?? "" const enabledR1Format = this.options.openAiR1FormatEnabled ?? false const isAzureAiInference = this._isAzureAiInference(modelUrl) - const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format + const deepseekReasoner = + modelId.includes("deepseek-reasoner") || + modelId.includes("deepseek-v4-pro") || + modelId.includes("deepseek-v4-flash") || + enabledR1Format if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) { yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages, metadata) @@ -340,7 +344,11 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl }) const enabledR1Format = this.options.openAiR1FormatEnabled ?? false - const deepseekReasoner = id.includes("deepseek-reasoner") || enabledR1Format + const deepseekReasoner = + id.includes("deepseek-reasoner") || + id.includes("deepseek-v4-pro") || + id.includes("deepseek-v4-flash") || + enabledR1Format return { id, diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index 8974dd599b..3596343335 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -317,6 +317,9 @@ export function convertToOpenAiMessages( if (mapped) { ;(baseMessage as any).reasoning_details = mapped } + if (messageWithDetails.reasoning_content !== undefined) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } } openAiMessages.push(baseMessage) @@ -494,6 +497,11 @@ export function convertToOpenAiMessages( baseMessage.reasoning_details = mapped } + // Pass through reasoning_content for DeepSeek V4/R1 thinking mode + if (messageWithDetails.reasoning_content !== undefined) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } + // Add tool_calls after reasoning_details // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty if (tool_calls.length > 0) { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 13da6f23b2..b68c5dcaf0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4469,6 +4469,12 @@ export class Task extends EventEmitter implements TaskLike { const cleanConversationHistory: (Anthropic.Messages.MessageParam | ReasoningItemForRequest)[] = [] + // Check if the current model requires reasoning to be preserved (e.g., DeepSeek V4/R1). + // When preserveReasoning is true, ALL assistant messages must include reasoning_content + // to satisfy the API protocol. Messages from other models (e.g., Gemini) that lack + // reasoning_content need to be backfilled with an empty string. + const shouldPreserveForApi = this.api.getModel().info.preserveReasoning === true + for (const msg of messages) { // Standalone reasoning: send encrypted, skip plain text if (msg.type === "reasoning") { @@ -4595,10 +4601,22 @@ export class Task extends EventEmitter implements TaskLike { // Default path for regular messages (no embedded reasoning) if (msg.role) { - cleanConversationHistory.push({ + const baseMessage: any = { role: msg.role, content: msg.content as Anthropic.Messages.ContentBlockParam[] | string, - }) + } + + // DeepSeek thinking mode requires ALL assistant messages to have reasoning_content. + // If a message from another model (e.g., Gemini) lacks it, add an empty placeholder + // to prevent 400 errors when switching back to DeepSeek mid-conversation. + if (msg.role === "assistant" && shouldPreserveForApi) { + // Use a space instead of an empty string. Some proxies/gateways or + // internal SDK logic may silently strip empty string fields, causing + // DeepSeek to reject the request with 400. + baseMessage.reasoning_content = " " + } + + cleanConversationHistory.push(baseMessage) } }