Skip to content

Commit 46790e5

Browse files
feat: Enhance DeepSeek reasoning content handling (sst#4975)
Co-authored-by: Aiden Cline <[email protected]>
1 parent 4bc3fa0 commit 46790e5

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

packages/opencode/src/provider/transform.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,46 @@ export namespace ProviderTransform {
6363
return result
6464
}
6565

66+
// DeepSeek: Handle reasoning_content for tool call continuations
67+
// - With tool calls: Include reasoning_content in providerOptions so model can continue reasoning
68+
// - Without tool calls: Strip reasoning (new turn doesn't need previous reasoning)
69+
// See: https://api-docs.deepseek.com/guides/thinking_mode
70+
if (model.providerID === "deepseek" || model.api.id.toLowerCase().includes("deepseek")) {
71+
return msgs.map((msg) => {
72+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
73+
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
74+
const hasToolCalls = msg.content.some((part: any) => part.type === "tool-call")
75+
const reasoningText = reasoningParts.map((part: any) => part.text).join("")
76+
77+
// Filter out reasoning parts from content
78+
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")
79+
80+
// If this message has tool calls and reasoning, include reasoning_content
81+
// so DeepSeek can continue reasoning after tool execution
82+
if (hasToolCalls && reasoningText) {
83+
return {
84+
...msg,
85+
content: filteredContent,
86+
providerOptions: {
87+
...msg.providerOptions,
88+
openaiCompatible: {
89+
...(msg.providerOptions as any)?.openaiCompatible,
90+
reasoning_content: reasoningText,
91+
},
92+
},
93+
}
94+
}
95+
96+
// For final answers (no tool calls), just strip reasoning
97+
return {
98+
...msg,
99+
content: filteredContent,
100+
}
101+
}
102+
return msg
103+
})
104+
}
105+
66106
return msgs
67107
}
68108

packages/opencode/test/provider/transform.test.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,210 @@ describe("ProviderTransform.maxOutputTokens", () => {
9696
})
9797
})
9898
})
99+
100+
describe("ProviderTransform.message - DeepSeek reasoning content", () => {
101+
test("DeepSeek with tool calls includes reasoning_content in providerOptions", () => {
102+
const msgs = [
103+
{
104+
role: "assistant",
105+
content: [
106+
{ type: "reasoning", text: "Let me think about this..." },
107+
{
108+
type: "tool-call",
109+
toolCallId: "test",
110+
toolName: "bash",
111+
input: { command: "echo hello" },
112+
},
113+
],
114+
},
115+
] as any[]
116+
117+
const result = ProviderTransform.message(msgs, {
118+
id: "deepseek/deepseek-chat",
119+
providerID: "deepseek",
120+
api: {
121+
id: "deepseek-chat",
122+
url: "https://api.deepseek.com",
123+
npm: "@ai-sdk/openai-compatible",
124+
},
125+
name: "DeepSeek Chat",
126+
capabilities: {
127+
temperature: true,
128+
reasoning: true,
129+
attachment: false,
130+
toolcall: true,
131+
input: { text: true, audio: false, image: false, video: false, pdf: false },
132+
output: { text: true, audio: false, image: false, video: false, pdf: false },
133+
},
134+
cost: {
135+
input: 0.001,
136+
output: 0.002,
137+
cache: { read: 0.0001, write: 0.0002 },
138+
},
139+
limit: {
140+
context: 128000,
141+
output: 8192,
142+
},
143+
status: "active",
144+
options: {},
145+
headers: {},
146+
})
147+
148+
expect(result).toHaveLength(1)
149+
expect(result[0].content).toEqual([
150+
{
151+
type: "tool-call",
152+
toolCallId: "test",
153+
toolName: "bash",
154+
input: { command: "echo hello" },
155+
},
156+
])
157+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Let me think about this...")
158+
})
159+
160+
test("DeepSeek without tool calls strips reasoning from content", () => {
161+
const msgs = [
162+
{
163+
role: "assistant",
164+
content: [
165+
{ type: "reasoning", text: "Let me think about this..." },
166+
{ type: "text", text: "Final answer" },
167+
],
168+
},
169+
] as any[]
170+
171+
const result = ProviderTransform.message(msgs, {
172+
id: "deepseek/deepseek-chat",
173+
providerID: "deepseek",
174+
api: {
175+
id: "deepseek-chat",
176+
url: "https://api.deepseek.com",
177+
npm: "@ai-sdk/openai-compatible",
178+
},
179+
name: "DeepSeek Chat",
180+
capabilities: {
181+
temperature: true,
182+
reasoning: true,
183+
attachment: false,
184+
toolcall: true,
185+
input: { text: true, audio: false, image: false, video: false, pdf: false },
186+
output: { text: true, audio: false, image: false, video: false, pdf: false },
187+
},
188+
cost: {
189+
input: 0.001,
190+
output: 0.002,
191+
cache: { read: 0.0001, write: 0.0002 },
192+
},
193+
limit: {
194+
context: 128000,
195+
output: 8192,
196+
},
197+
status: "active",
198+
options: {},
199+
headers: {},
200+
})
201+
202+
expect(result).toHaveLength(1)
203+
expect(result[0].content).toEqual([{ type: "text", text: "Final answer" }])
204+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
205+
})
206+
207+
test("DeepSeek model ID containing 'deepseek' matches (case insensitive)", () => {
208+
const msgs = [
209+
{
210+
role: "assistant",
211+
content: [
212+
{ type: "reasoning", text: "Thinking..." },
213+
{
214+
type: "tool-call",
215+
toolCallId: "test",
216+
toolName: "get_weather",
217+
input: { location: "Hangzhou" },
218+
},
219+
],
220+
},
221+
] as any[]
222+
223+
const result = ProviderTransform.message(msgs, {
224+
id: "someprovider/deepseek-reasoner",
225+
providerID: "someprovider",
226+
api: {
227+
id: "deepseek-reasoner",
228+
url: "https://api.someprovider.com",
229+
npm: "@ai-sdk/openai-compatible",
230+
},
231+
name: "SomeProvider DeepSeek Reasoner",
232+
capabilities: {
233+
temperature: true,
234+
reasoning: true,
235+
attachment: false,
236+
toolcall: true,
237+
input: { text: true, audio: false, image: false, video: false, pdf: false },
238+
output: { text: true, audio: false, image: false, video: false, pdf: false },
239+
},
240+
cost: {
241+
input: 0.001,
242+
output: 0.002,
243+
cache: { read: 0.0001, write: 0.0002 },
244+
},
245+
limit: {
246+
context: 128000,
247+
output: 8192,
248+
},
249+
status: "active",
250+
options: {},
251+
headers: {},
252+
})
253+
254+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBe("Thinking...")
255+
})
256+
257+
test("Non-DeepSeek providers leave reasoning content unchanged", () => {
258+
const msgs = [
259+
{
260+
role: "assistant",
261+
content: [
262+
{ type: "reasoning", text: "Should not be processed" },
263+
{ type: "text", text: "Answer" },
264+
],
265+
},
266+
] as any[]
267+
268+
const result = ProviderTransform.message(msgs, {
269+
id: "openai/gpt-4",
270+
providerID: "openai",
271+
api: {
272+
id: "gpt-4",
273+
url: "https://api.openai.com",
274+
npm: "@ai-sdk/openai",
275+
},
276+
name: "GPT-4",
277+
capabilities: {
278+
temperature: true,
279+
reasoning: false,
280+
attachment: true,
281+
toolcall: true,
282+
input: { text: true, audio: false, image: true, video: false, pdf: false },
283+
output: { text: true, audio: false, image: false, video: false, pdf: false },
284+
},
285+
cost: {
286+
input: 0.03,
287+
output: 0.06,
288+
cache: { read: 0.001, write: 0.002 },
289+
},
290+
limit: {
291+
context: 128000,
292+
output: 4096,
293+
},
294+
status: "active",
295+
options: {},
296+
headers: {},
297+
})
298+
299+
expect(result[0].content).toEqual([
300+
{ type: "reasoning", text: "Should not be processed" },
301+
{ type: "text", text: "Answer" },
302+
])
303+
expect(result[0].providerOptions?.openaiCompatible?.reasoning_content).toBeUndefined()
304+
})
305+
})

0 commit comments

Comments
 (0)