summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJakub Matjanowski <[email protected]>2025-12-04 04:31:36 +0100
committerGitHub <[email protected]>2025-12-03 21:31:36 -0600
commit46790e57e9abfa363c3030d57d033f4b13b5c91e (patch)
tree5977c1df1f945de5b3cf9a9c735b29d2794580bc
parent4bc3fa08261c1e778c08b5b7bc75bbad95fb2323 (diff)
downloadopencode-46790e57e9abfa363c3030d57d033f4b13b5c91e.tar.gz
opencode-46790e57e9abfa363c3030d57d033f4b13b5c91e.zip
feat: Enhance DeepSeek reasoning content handling (#4975)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/provider/transform.ts40
-rw-r--r--packages/opencode/test/provider/transform.test.ts207
2 files changed, 247 insertions, 0 deletions
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index c703a57e1..09dfd69a3 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -63,6 +63,46 @@ export namespace ProviderTransform {
return result
}
+ // DeepSeek: Handle reasoning_content for tool call continuations
+ // - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning
+ // - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning)
+ // See: https://api-docs.deepseek.com/guides/thinking_mode
+ if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) {
+ return msgs.map((msg) => {
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
+ const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
+ const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call")
+ const reasoningText = reasoningParts.map((part: any) => part.text).join("")
+
+ // Filter out reasoning parts from content
+ const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
+
+ // If this message has tool calls and reasoning, include reasoning_content
+ // so DeepSeek can continue reasoning after tool execution
+ if (hasToolCalls && reasoningText) {
+ return {
+ ...msg,
+ content: filteredContent,
+ providerOptions: {
+ ...msg.providerOptions,
+ openaiCompatible: {
+ ...(msg.providerOptions as any)?.openaiCompatible,
+ reasoning_content: reasoningText,
+ },
+ },
+ }
+ }
+
+ // For final answers (no tool calls), just strip reasoning
+ return {
+ ...msg,
+ content: filteredContent,
+ }
+ }
+ return msg
+ })
+ }
+
return msgs
}
diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts
index e6080d54c..648f108bd 100644
--- a/packages/opencode/test/provider/transform.test.ts
+++ b/packages/opencode/test/provider/transform.test.ts
@@ -96,3 +96,210 @@ describe("ProviderTransform.maxOutputTokens", () => {
})
})
})
+
+describe("ProviderTransform.message - DeepSeek reasoning content", () => {
+ test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "Let me think about this..." },
+ {
+ type: "tool-call",
+ toolCallId: "test",
+ toolName: "bash",
+ input: { command: "echo hello" },
+ },
+ ],
+ },
+ ] 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 },
+ },
+ cost: {
+ input: 0.001,
+ output: 0.002,
+ cache: { read: 0.0001, write: 0.0002 },
+ },
+ limit: {
+ context: 128000,
+ output: 8192,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ })
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toEqual([
+ {
+ type: "tool-call",
+ toolCallId: "test",
+ toolName: "bash",
+ input: { command: "echo hello" },
+ },
+ ])
+ expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
+ })
+
+ test("DeepSeek without tool calls strips reasoning from content", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "Let me think about this..." },
+ { type: "text", text: "Final 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 },
+ },
+ cost: {
+ input: 0.001,
+ output: 0.002,
+ cache: { read: 0.0001, write: 0.0002 },
+ },
+ limit: {
+ context: 128000,
+ output: 8192,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ })
+
+ expect(result).toHaveLength(1)
+ expect(result[0].content).toEqual([{ type: "text", text: "Final answer" }])
+ expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
+ })
+
+ test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "Thinking..." },
+ {
+ type: "tool-call",
+ toolCallId: "test",
+ toolName: "get_weather",
+ input: { location: "Hangzhou" },
+ },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, {
+ id: "someprovider/deepseek-reasoner",
+ providerID: "someprovider",
+ api: {
+ id: "deepseek-reasoner",
+ url: "https://api.someprovider.com",
+ npm: "@ai-sdk/openai-compatible",
+ },
+ name: "SomeProvider 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 },
+ },
+ cost: {
+ input: 0.001,
+ output: 0.002,
+ cache: { read: 0.0001, write: 0.0002 },
+ },
+ limit: {
+ context: 128000,
+ output: 8192,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ })
+
+ expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking...")
+ })
+
+ test("Non-DeepSeek providers leave reasoning content unchanged", () => {
+ const msgs = [
+ {
+ role: "assistant",
+ content: [
+ { type: "reasoning", text: "Should not be processed" },
+ { type: "text", text: "Answer" },
+ ],
+ },
+ ] as any[]
+
+ const result = ProviderTransform.message(msgs, {
+ id: "openai/gpt-4",
+ providerID: "openai",
+ api: {
+ id: "gpt-4",
+ url: "https://api.openai.com",
+ npm: "@ai-sdk/openai",
+ },
+ name: "GPT-4",
+ capabilities: {
+ temperature: true,
+ reasoning: false,
+ attachment: true,
+ toolcall: true,
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
+ },
+ cost: {
+ input: 0.03,
+ output: 0.06,
+ cache: { read: 0.001, write: 0.002 },
+ },
+ limit: {
+ context: 128000,
+ output: 4096,
+ },
+ status: "active",
+ options: {},
+ headers: {},
+ })
+
+ expect(result[0].content).toEqual([
+ { type: "reasoning", text: "Should not be processed" },
+ { type: "text", text: "Answer" },
+ ])
+ expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
+ })
+})