diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 05b9f031fe6..6be81d5e6af 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -135,7 +135,17 @@ export namespace ProviderTransform { if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field - return msgs.map((msg) => { + const isDeepSeek = model.providerID === "deepseek" || model.api.url?.includes("api.deepseek.com") + // Check if thinking mode is enabled for DeepSeek + const thinking = options.thinking as any + const isThinkingEnabled = + isDeepSeek && + (model.capabilities.reasoning || + model.api.id === "deepseek-reasoner" || + model.id.includes("deepseek-reasoner") || + (thinking && thinking.type === "enabled")) + const lastUserIndex = isThinkingEnabled ? msgs.findLastIndex((msg) => msg.role === "user") : -1 + return msgs.map((msg, index) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning") const reasoningText = reasoningParts.map((part: any) => part.text).join("") @@ -144,7 +154,8 @@ export namespace ProviderTransform { const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") // Include reasoning_content | reasoning_details directly on the message for all assistant messages - if (reasoningText) { + const shouldIncludeReasoning = reasoningText || (isThinkingEnabled && index > lastUserIndex) + if (shouldIncludeReasoning) { return { ...msg, content: filteredContent, @@ -152,7 +163,7 @@ export namespace ProviderTransform { ...msg.providerOptions, openaiCompatible: { ...(msg.providerOptions as any)?.openaiCompatible, - [field]: reasoningText, + [field]: reasoningText || "", }, }, } @@ -1009,4 +1020,6 @@ export namespace ProviderTransform { return schema as JSONSchema7 } + + // DeepSeek thinking mode fix: ensures empty reasoning_content for assistant messages after last user message } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 917d357eafa..5d4b0a55661 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -773,7 +773,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { options: {}, headers: {}, release_date: "2023-04-01", - }, + } as any, {}, ) @@ -789,6 +789,72 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...") }) + test("DeepSeek sets empty reasoning_content for assistant messages after last user message", () => { + const msgs = [ + { + role: "user", + content: "First question", + }, + { + role: "assistant", + content: [{ type: "text", text: "First answer" }], + }, + { + role: "user", + content: "Second question", + }, + { + role: "assistant", + content: [{ type: "text", text: "Second answer" }], + }, + ] as any[] + + const result = ProviderTransform.message( + msgs, + { + id: "deepseek/deepseek-chat", + providerID: "deepseek", + api: { + id: "deepseek-chat", + url: "https://api.deepseek.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "DeepSeek Chat", + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2023-04-01", + } as any, + {}, + ) + + expect(result).toHaveLength(4) + // First assistant (before last user) should NOT have reasoning_content + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() + // Second assistant (after last user) should have empty reasoning_content + expect(result[3].providerOptions?.openaiCompatible?.reasoning_content).toBe("") + }) + test("Non-DeepSeek providers leave reasoning content unchanged", () => { const msgs = [ { @@ -833,7 +899,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { options: {}, headers: {}, release_date: "2023-04-01", - }, + } as any, {}, ) @@ -843,6 +909,176 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { ]) expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() }) + + test("DeepSeek with reasoner model ID sets empty reasoning_content", () => { + const msgs = [ + { + role: "user", + content: "Question", + }, + { + role: "assistant", + content: [{ type: "text", text: "Answer" }], + }, + ] as any[] + + const result = ProviderTransform.message( + msgs, + { + id: "deepseek/deepseek-reasoner", + providerID: "deepseek", + api: { + id: "deepseek-reasoner", + url: "https://api.deepseek.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "DeepSeek Reasoner", + capabilities: { + temperature: true, + reasoning: false, // reasoning false but model is reasoner + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2023-04-01", + } as any, + {}, + ) + + expect(result).toHaveLength(2) + // Assistant after last user should have empty reasoning_content + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBe("") + }) + + test("DeepSeek with thinking option enabled sets empty reasoning_content", () => { + const msgs = [ + { + role: "user", + content: "Question", + }, + { + role: "assistant", + content: [{ type: "text", text: "Answer" }], + }, + ] as any[] + + const result = ProviderTransform.message( + msgs, + { + id: "deepseek/deepseek-chat", + providerID: "deepseek", + api: { + id: "deepseek-chat", + url: "https://api.deepseek.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "DeepSeek Chat", + capabilities: { + temperature: true, + reasoning: false, // reasoning false + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2023-04-01", + } as any, + { + thinking: { type: "enabled" }, + }, + ) + + expect(result).toHaveLength(2) + // Assistant after last user should have empty reasoning_content + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBe("") + }) + + test("DeepSeek without thinking mode does not set empty reasoning_content", () => { + const msgs = [ + { + role: "user", + content: "Question", + }, + { + role: "assistant", + content: [{ type: "text", text: "Answer" }], + }, + ] as any[] + + const result = ProviderTransform.message( + msgs, + { + id: "deepseek/deepseek-chat", + providerID: "deepseek", + api: { + id: "deepseek-chat", + url: "https://api.deepseek.com", + npm: "@ai-sdk/openai-compatible", + }, + name: "DeepSeek Chat", + capabilities: { + temperature: true, + reasoning: false, // reasoning false + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2023-04-01", + } as any, + {}, // no thinking option + ) + + expect(result).toHaveLength(2) + // Assistant should NOT have reasoning_content because thinking mode not enabled + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() + }) }) describe("ProviderTransform.message - empty image handling", () => {