Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
207 changes: 207 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})