From e2e25abdbd3b13f5ae9a63a82b5a4d3024df2255 Mon Sep 17 00:00:00 2001 From: OpenCode Assistant Date: Tue, 10 Mar 2026 15:44:25 +0100 Subject: [PATCH 1/3] transform to DeepSeek API fix and aditional test cases --- packages/opencode/src/provider/transform.ts | 13 +- .../opencode/test/provider/transform.test.ts | 203 ++++++++++++++++++ 2 files changed, 213 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 05b9f031fe6..3cb13dc43d8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -135,7 +135,12 @@ export namespace ProviderTransform { if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field - return msgs.map((msg) => { + const thinking = options?.thinking as any + const isDeepSeekReasoningFormat = model.api.id.includes("deepseek") + && (model.id.includes("reasoner") || thinking?.type === "enabled") + const lastUserIndex = isDeepSeekReasoningFormat ? 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("") @@ -143,8 +148,10 @@ export namespace ProviderTransform { // Filter out reasoning parts from content const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") + const isDeepSeekReasoningChain = isDeepSeekReasoningFormat && index > lastUserIndex + // Include reasoning_content | reasoning_details directly on the message for all assistant messages - if (reasoningText) { + if ((reasoningText && !isDeepSeekReasoningChain) || isDeepSeekReasoningChain) { return { ...msg, content: filteredContent, @@ -152,7 +159,7 @@ export namespace ProviderTransform { ...msg.providerOptions, openaiCompatible: { ...(msg.providerOptions as any)?.openaiCompatible, - [field]: reasoningText, + [field]: reasoningText || "", }, }, } diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 917d357eafa..c9873d37cde 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -843,6 +843,209 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { ]) expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() }) + + test("DeepSeek reasoning chain after last user message", () => { + const msgs = [ + { role: "user", content: [{ type: "text", text: "First question" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "Thinking about first question" }, + { type: "text", text: "First answer" } + ] + }, + { role: "user", content: [{ type: "text", text: "Second question" }] }, + { + role: "assistant", + content: [ + { type: "reasoning", text: "Chain reasoning after last user" }, + { type: "text", text: "Second 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: 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", + }, + {}, + ) + + // First assistant message (before last user) should have reasoning_content + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking about first question") + expect(result[1].content).toEqual([{ type: "text", text: "First answer" }]) + + // Second assistant message (after last user) should also have reasoning_content (chain) + expect(result[3].providerOptions?.openaiCompatible?.reasoning_content).toBe("Chain reasoning after last user") + expect(result[3].content).toEqual([{ type: "text", text: "Second answer" }]) + }) + + test("DeepSeek with thinking options enabled", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Thinking with options enabled" }, + { 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: 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", + }, + { thinking: { type: "enabled" } }, + ) + + expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking with options enabled") + expect(result[0].content).toEqual([{ type: "text", text: "Answer" }]) + }) + + test("DeepSeek with empty reasoning text", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "" }, + { type: "text", text: "Answer without reasoning" }, + ], + }, + ] 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: 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", + }, + {}, + ) + + expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("") + expect(result[0].content).toEqual([{ type: "text", text: "Answer without reasoning" }]) + }) + + test("DeepSeek non-reasoner model without thinking options", () => { + const msgs = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Should not be processed" }, + { 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, + 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: false, + }, + 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", + }, + {}, + ) + + expect(result[0].content).toEqual([ + { type: "reasoning", text: "Should not be processed" }, + { type: "text", text: "Answer" }, + ]) + expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() + }) }) describe("ProviderTransform.message - empty image handling", () => { From 2d8055d8c92bf88f9dcdde84f4bcc7de33430864 Mon Sep 17 00:00:00 2001 From: OpenCode Assistant Date: Mon, 9 Mar 2026 12:52:42 +0100 Subject: [PATCH 2/3] fix: DeepSeek API thinking mode handling - Set empty reasoning_content for assistant messages after last user message - Only apply when thinking mode is enabled (reasoner model or thinking option) - Add comprehensive tests for all thinking mode conditions --- packages/opencode/src/provider/transform.ts | 20 +- .../opencode/test/provider/transform.test.ts | 233 ++++++++++-------- 2 files changed, 145 insertions(+), 108 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 3cb13dc43d8..8680744f257 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -135,11 +135,16 @@ export namespace ProviderTransform { if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field - const thinking = options?.thinking as any - const isDeepSeekReasoningFormat = model.api.id.includes("deepseek") - && (model.id.includes("reasoner") || thinking?.type === "enabled") - const lastUserIndex = isDeepSeekReasoningFormat ? msgs.findLastIndex((msg) => msg.role === "user") : -1 - + 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") @@ -148,10 +153,9 @@ export namespace ProviderTransform { // Filter out reasoning parts from content const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") - const isDeepSeekReasoningChain = isDeepSeekReasoningFormat && index > lastUserIndex - // Include reasoning_content | reasoning_details directly on the message for all assistant messages - if ((reasoningText && !isDeepSeekReasoningChain) || isDeepSeekReasoningChain) { + const shouldIncludeReasoning = reasoningText || (isThinkingEnabled && index > lastUserIndex) + if (shouldIncludeReasoning) { return { ...msg, content: filteredContent, diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index c9873d37cde..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, {}, ) @@ -844,23 +910,15 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() }) - test("DeepSeek reasoning chain after last user message", () => { + test("DeepSeek with reasoner model ID sets empty reasoning_content", () => { const msgs = [ - { role: "user", content: [{ type: "text", text: "First question" }] }, - { - role: "assistant", - content: [ - { type: "reasoning", text: "Thinking about first question" }, - { type: "text", text: "First answer" } - ] + { + role: "user", + content: "Question", }, - { role: "user", content: [{ type: "text", text: "Second question" }] }, - { - role: "assistant", - content: [ - { type: "reasoning", text: "Chain reasoning after last user" }, - { type: "text", text: "Second answer" } - ] + { + role: "assistant", + content: [{ type: "text", text: "Answer" }], }, ] as any[] @@ -877,7 +935,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { name: "DeepSeek Reasoner", capabilities: { temperature: true, - reasoning: true, + reasoning: false, // reasoning false but model is reasoner attachment: false, toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false }, @@ -886,33 +944,37 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { field: "reasoning_content", }, }, - cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, - limit: { context: 128000, output: 8192 }, + 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, {}, ) - // First assistant message (before last user) should have reasoning_content - expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking about first question") - expect(result[1].content).toEqual([{ type: "text", text: "First answer" }]) - - // Second assistant message (after last user) should also have reasoning_content (chain) - expect(result[3].providerOptions?.openaiCompatible?.reasoning_content).toBe("Chain reasoning after last user") - expect(result[3].content).toEqual([{ type: "text", text: "Second answer" }]) + 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 options enabled", () => { + test("DeepSeek with thinking option enabled sets empty reasoning_content", () => { const msgs = [ + { + role: "user", + content: "Question", + }, { role: "assistant", - content: [ - { type: "reasoning", text: "Thinking with options enabled" }, - { type: "text", text: "Answer" }, - ], + content: [{ type: "text", text: "Answer" }], }, ] as any[] @@ -929,7 +991,7 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { name: "DeepSeek Chat", capabilities: { temperature: true, - reasoning: true, + reasoning: false, // reasoning false attachment: false, toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false }, @@ -938,75 +1000,39 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { 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", - }, - { thinking: { type: "enabled" } }, - ) - - expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking with options enabled") - expect(result[0].content).toEqual([{ type: "text", text: "Answer" }]) - }) - - test("DeepSeek with empty reasoning text", () => { - const msgs = [ - { - role: "assistant", - content: [ - { type: "reasoning", text: "" }, - { type: "text", text: "Answer without reasoning" }, - ], - }, - ] 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", + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, }, - name: "DeepSeek Reasoner", - 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", - }, + limit: { + context: 128000, + output: 8192, }, - 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[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("") - expect(result[0].content).toEqual([{ type: "text", text: "Answer without reasoning" }]) + expect(result).toHaveLength(2) + // Assistant after last user should have empty reasoning_content + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBe("") }) - test("DeepSeek non-reasoner model without thinking options", () => { + test("DeepSeek without thinking mode does not set empty reasoning_content", () => { const msgs = [ + { + role: "user", + content: "Question", + }, { role: "assistant", - content: [ - { type: "reasoning", text: "Should not be processed" }, - { type: "text", text: "Answer" }, - ], + content: [{ type: "text", text: "Answer" }], }, ] as any[] @@ -1023,28 +1049,35 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => { name: "DeepSeek Chat", capabilities: { temperature: true, - reasoning: false, + 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: false, + interleaved: { + field: "reasoning_content", + }, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 128000, + output: 8192, }, - 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[0].content).toEqual([ - { type: "reasoning", text: "Should not be processed" }, - { type: "text", text: "Answer" }, - ]) - expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() + expect(result).toHaveLength(2) + // Assistant should NOT have reasoning_content because thinking mode not enabled + expect(result[1].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined() }) }) From cb163a10a053cc297eceafc218dfb97e8a1dcaae Mon Sep 17 00:00:00 2001 From: OpenCode Assistant Date: Sun, 15 Mar 2026 01:14:03 +0100 Subject: [PATCH 3/3] chore: add clarifying comment for DeepSeek thinking mode fix --- packages/opencode/src/provider/transform.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8680744f257..6be81d5e6af 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -1020,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 }