Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model
video: model.modalities?.output?.includes("video") ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? false,
},
interleaved: model.interleaved ?? false,
interleaved: model.interleaved ?? (model.reasoning ? { field: "reasoning_content" } : false),
},
release_date: model.release_date ?? "",
variants: {},
Expand Down Expand Up @@ -1177,7 +1177,7 @@ const layer: Layer.Layer<
model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
},
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? false,
interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? (model.reasoning ? { field: "reasoning_content" } : false),
},
cost: {
input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
Expand Down
58 changes: 51 additions & 7 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ function normalizeMessages(
}

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

if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
const field = model.capabilities.interleaved.field
const sdk = sdkKey(model.api.npm) ?? "openaiCompatible"
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
Expand All @@ -203,17 +205,21 @@ function normalizeMessages(
// Filter out reasoning parts from content
const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning")

// Include reasoning_content | reasoning_details directly on the message for all assistant messages.
// Always set the field even when empty — some providers (e.g. DeepSeek) may return empty
// reasoning_content which still needs to be sent back in subsequent requests.
// Preserve existing providerOptions[field] when content no longer has reasoning parts
// (e.g. after a prior transform pass already extracted them). The @ai-sdk yG converter
// resolves reasoning_content from providerOptions → overwriting it with empty string
// on the second pass causes DeepSeek 400: "reasoning_content must be passed back".
const existingField = msg.providerOptions?.[sdk]?.[field]
const resolvedText = reasoningText || existingField || ""

return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
[sdk]: {
...msg.providerOptions?.[sdk],
[field]: resolvedText,
},
},
}
Expand All @@ -223,6 +229,44 @@ function normalizeMessages(
})
}

// When reasoning is active but interleaved is not configured, still inject empty reasoning_content
// for ALL assistant messages. This covers historical messages from DB that were stored before
// reasoning mode was enabled — they have no reasoning part to extract but DeepSeek's API still
// requires reasoning_content on every assistant turn in thinking mode.
if (model.capabilities.reasoning) {
msgs = msgs.map((msg) => {
if (msg.role !== "assistant") return msg
if (Array.isArray(msg.content)) {
const sdk = sdkKey(model.api.npm) ?? "openaiCompatible"
return {
...msg,
providerOptions: {
...msg.providerOptions,
[sdk]: {
...msg.providerOptions?.[sdk],
reasoning_content: "",
},
},
}
}
if (typeof msg.content === "string") {
const sdk = sdkKey(model.api.npm) ?? "openaiCompatible"
return {
...msg,
content: [{ type: "text" as const, text: msg.content }, { type: "reasoning" as const, text: "" }],
providerOptions: {
...msg.providerOptions,
[sdk]: {
...msg.providerOptions?.[sdk],
reasoning_content: "",
},
},
}
}
return msg
})
}

return msgs
}

Expand Down
Loading