From a78cb5f36f1315c5f4079588351227b4546fd529 Mon Sep 17 00:00:00 2001 From: skeem <215233343+skeeminator@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:48:15 -0700 Subject: [PATCH] fix(openaiShim): unconditionally set reasoning_content for DeepSeek V4 thinking mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepSeek V4 (and similar reasoning providers) require the reasoning_content field to be present on every assistant message when thinking mode is enabled — even as an empty string. The previous guard only set it when thinking text was non-empty (typeof === 'string' && trim().length > 0), causing 400 errors on multi-turn tool-call rounds. Three sites fixed in convertMessages(): - Assistant messages with thinking blocks: now always set reasoning_content (fallback to empty string when thinking text is absent) - Coalescing synthetic interrupt message: added reasoning_content: '' - String-content fallback branch: added reasoning_content: '' when preserveReasoningContent is true --- src/services/api/openaiShim.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 015c0e2e4..e258f1186 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -539,9 +539,11 @@ function convertMessages( // (harmless) or strict-reject unknown fields (harmful). if (preserveReasoningContent) { const thinkingText = (thinkingBlock as { thinking?: string } | undefined)?.thinking - if (typeof thinkingText === 'string' && thinkingText.trim().length > 0) { - assistantMsg.reasoning_content = thinkingText - } + // DeepSeek V4 (and similar reasoning providers) require reasoning_content + // to be present on every assistant message, even as empty string, when + // thinking mode is enabled. The field's presence satisfies the API contract; + // omitting it causes 400: "reasoning_content must be passed back to the API". + assistantMsg.reasoning_content = typeof thinkingText === 'string' ? thinkingText : '' } if (toolUses.length > 0) { @@ -635,6 +637,12 @@ function convertMessages( })(), } + // Reasoning providers require reasoning_content on every assistant + // message when thinking mode is active (empty string is accepted). + if (preserveReasoningContent) { + assistantMsg.reasoning_content = '' + } + if (assistantMsg.content) { result.push(assistantMsg) } @@ -659,6 +667,7 @@ function convertMessages( coalesced.push({ role: 'assistant', content: '[Tool execution interrupted by user]', + reasoning_content: '', }) }