Skip to content

Commit 9fbb8a4

Browse files
committed
fix: preserve empty reasoning_content for DeepSeek V4 in non-streaming and streaming paths
Three truthy checks were dropping empty reasoning_content ('') from DeepSeek V4's thinking mode responses, causing 'must be passed back to the API' errors in multi-turn tool call chains: 1. Non-streaming parser: if (reasoning != null && reasoning.length > 0) -> Now: if (reasoning != null) to preserve empty strings 2. Streaming parser: if (reasoningContent) truthy check -> Now: if ('reasoning_text' in delta) to detect field presence 3. Outbound converter: if (part.text) reasoningText = part.text -> Now: reasoningText = part.text ?? '' to preserve empty text PR anomalyco#24146 fixed the transform.ts path; this completes the remaining cases.
1 parent 0405bc7 commit 9fbb8a4

2 files changed

Lines changed: 16 additions & 10 deletions

File tree

packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV3Pro
9595
break
9696
}
9797
case "reasoning": {
98-
if (part.text) reasoningText = part.text
98+
// Preserve empty reasoning text — some providers (e.g. DeepSeek V4)
99+
// require reasoning_content: "" to be sent back in multi-turn chains.
100+
reasoningText = part.text ?? ""
99101
break
100102
}
101103
case "tool-call": {

packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,10 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
227227
}
228228

229229
// reasoning content (Copilot uses reasoning_text):
230+
// Preserve empty reasoning_text (e.g. DeepSeek V4 returns reasoning_content: "")
231+
// because some providers require it to be sent back verbatim in multi-turn chains.
230232
const reasoning = choice.message.reasoning_text
231-
if (reasoning != null && reasoning.length > 0) {
233+
if (reasoning != null) {
232234
content.push({
233235
type: "reasoning",
234236
text: reasoning,
@@ -477,22 +479,24 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 {
477479
reasoningOpaque = delta.reasoning_opaque
478480
}
479481

480-
// enqueue reasoning before text deltas (Copilot uses reasoning_text):
482+
// enqueue reasoning before text deltas (Copilot uses reasoning_text).
483+
// Handle empty reasoning_text (e.g. DeepSeek V4 may stream reasoning_content: "").
481484
const reasoningContent = delta.reasoning_text
482-
if (reasoningContent) {
485+
if ("reasoning_text" in delta) {
483486
if (!isActiveReasoning) {
484487
controller.enqueue({
485488
type: "reasoning-start",
486489
id: "reasoning-0",
487490
})
488491
isActiveReasoning = true
489492
}
490-
491-
controller.enqueue({
492-
type: "reasoning-delta",
493-
id: "reasoning-0",
494-
delta: reasoningContent,
495-
})
493+
if (reasoningContent) {
494+
controller.enqueue({
495+
type: "reasoning-delta",
496+
id: "reasoning-0",
497+
delta: reasoningContent,
498+
})
499+
}
496500
}
497501

498502
if (delta.content) {

0 commit comments

Comments
 (0)