Skip to content
Closed
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
68 changes: 53 additions & 15 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ function sdkKey(npm: string): string | undefined {
return undefined
}

function providerOptionsKey(model: Provider.Model): string {
if (model.api.npm === "@ai-sdk/openai-compatible") {
return "openaiCompatible"
}
return sdkKey(model.api.npm) ?? "openaiCompatible"
}

function normalizeMessages(
msgs: ModelMessage[],
model: Provider.Model,
Expand Down Expand Up @@ -175,8 +182,29 @@ function normalizeMessages(
return result
}

if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) {
const field = model.capabilities.interleaved.field
// Detect whether the model uses reasoning/thinking and determine the
// providerOptions key/field for reasoning_content. For models without
// interleaved configured (e.g., DeepSeek via @ai-sdk/openai-compatible),
// default to the OpenAI-compatible reasoning_content field.
const interleaved = model.capabilities.interleaved
const isInterleaved = typeof interleaved === "object" && interleaved.field
const field = isInterleaved ? interleaved.field : "reasoning_content"
const key = providerOptionsKey(model)

// Check if we need to handle reasoning parts:
// - interleaved explicitly configured, OR
// - model has reasoning capability, OR
// - messages already contain reasoning parts (from DB replay)
const hasReasoningContent =
isInterleaved ||
model.capabilities.reasoning ||
msgs.some((msg) =>
msg.role === "assistant" &&
Array.isArray(msg.content) &&
msg.content.some((part: any) => part.type === "reasoning"),
)

if (hasReasoningContent) {
return msgs.map((msg) => {
if (msg.role === "assistant" && Array.isArray(msg.content)) {
const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning")
Expand All @@ -185,24 +213,34 @@ 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
if (reasoningText) {
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
openaiCompatible: {
...msg.providerOptions?.openaiCompatible,
[field]: reasoningText,
},
// DeepSeek reasoning API requires ALL assistant messages in the conversation
// history to carry reasoning_content (even empty string). Old DB-replayed
// messages may have no reasoning parts at all — inject empty string for those.
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
[key]: {
...msg.providerOptions?.[key],
[field]: reasoningText || "",
},
}
},
}
}

// Also handle string-content assistant messages (old DB format).
// Inject empty reasoning_content to satisfy DeepSeek API requirement.
if (msg.role === "assistant" && typeof msg.content === "string") {
return {
...msg,
content: filteredContent,
providerOptions: {
...msg.providerOptions,
[key]: {
...msg.providerOptions?.[key],
[field]: "",
},
},
}
}

Expand Down
Loading