Skip to content

Commit 9ee61bb

Browse files
committed
fix(provider): complete DeepSeek reasoning_content round-trip for multi-turn conversations
Fixes the two-layer bug where reasoning_content is dropped on conversation replay for DeepSeek thinking mode and OpenRouter-routed DeepSeek models. Three changes: 1. provider.ts: Auto-enable interleaved for reasoning models - When model.reasoning is true but interleaved is not explicitly set, default to { field: "reasoning_content" } instead of false - This triggers the interleaved transform that extracts reasoning and passes it via providerOptions 2. transform.ts: Use dynamic SDK key in interleaved transform - Replace hardcoded "openaiCompatible" with sdkKey(model.api.npm) - Fixes OpenRouter provider which expects "openrouter" key, not "openaiCompatible" (prevents key mismatch in providerOptions) 3. transform.ts: Inject reasoning_content for ALL assistant messages - New fallback transform fires when capabilities.reasoning is true - Sets reasoning_content: "" in providerOptions for every assistant message, including historical messages stored before reasoning mode was enabled (no reasoning part to extract from) - Also expands DeepSeek detection to check model.id in addition to model.api.id, covering OpenRouter-routed DeepSeek models Closes #24104 Related: #24203 (OpenRouter users still affected by PR #24218 alone) Supersedes partial fix from PR #24146 (merged but incomplete)
1 parent e29058c commit 9ee61bb

2 files changed

Lines changed: 36 additions & 4 deletions

File tree

packages/opencode/src/provider/provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ const layer: Layer.Layer<
11771177
model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
11781178
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
11791179
},
1180-
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? false,
1180+
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? (model.reasoning ? { field: "reasoning_content" } : false),
11811181
},
11821182
cost: {
11831183
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,

packages/opencode/src/provider/transform.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ function normalizeMessages(
176176
}
177177

178178
// Deepseek requires all assistant messages to have reasoning on them
179-
if (model.api.id.includes("deepseek")) {
179+
// Check both API ID and model ID to cover OpenRouter-routed DeepSeek models
180+
if (model.api.id.includes("deepseek") || model.id.includes("deepseek")) {
180181
msgs = msgs.map((msg) => {
181182
if (msg.role !== "assistant") return msg
182183
if (Array.isArray(msg.content)) {
@@ -195,6 +196,7 @@ function normalizeMessages(
195196

196197
if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
197198
const field = model.capabilities.interleaved.field
199+
const sdk = sdkKey(model.api.npm) ?? "openaiCompatible"
198200
return msgs.map((msg) => {
199201
if (msg.role === "assistant" && Array.isArray(msg.content)) {
200202
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
@@ -211,8 +213,8 @@ function normalizeMessages(
211213
content: filteredContent,
212214
providerOptions: {
213215
...msg.providerOptions,
214-
openaiCompatible: {
215-
...msg.providerOptions?.openaiCompatible,
216+
[sdk]: {
217+
...msg.providerOptions?.[sdk],
216218
[field]: reasoningText,
217219
},
218220
},
@@ -223,6 +225,36 @@ function normalizeMessages(
223225
})
224226
}
225227

228+
// When reasoning is active but interleaved is not configured, still inject empty reasoning_content
229+
// for ALL assistant messages. This covers historical messages from DB that were stored before
230+
// reasoning mode was enabled — they have no reasoning part to extract but DeepSeek's API still
231+
// requires reasoning_content on every assistant turn in thinking mode.
232+
if (model.capabilities.reasoning) {
233+
msgs = msgs.map((msg) => {
234+
if (msg.role !== "assistant") return msg
235+
if (Array.isArray(msg.content)) {
236+
const sdk = sdkKey(model.api.npm) ?? "openaiCompatible"
237+
return {
238+
...msg,
239+
providerOptions: {
240+
...msg.providerOptions,
241+
[sdk]: {
242+
...msg.providerOptions?.[sdk],
243+
reasoning_content: "",
244+
},
245+
},
246+
}
247+
}
248+
if (typeof msg.content === "string") {
249+
return {
250+
...msg,
251+
content: [{ type: "text" as const, text: msg.content }, { type: "reasoning" as const, text: "" }],
252+
}
253+
}
254+
return msg
255+
})
256+
}
257+
226258
return msgs
227259
}
228260

0 commit comments

Comments
 (0)