fix: preserve empty reasoning_content for DeepSeek v4 thinking mode (#399)

DeepSeek v4 in thinking mode sometimes returns reasoning_content: ""
when the model answers directly without internal reasoning. Two places
were filtering the empty string out, which dropped the thinking block
from the assistant turn entirely. The next request then omitted
reasoning_content for that prior turn, and DeepSeek rejected with
400 "reasoning_content ... must be passed back to the API".

Fix:
- openaiStreamAdapter: open a thinking block whenever reasoning_content
  is present (including ""); skip the empty thinking_delta event since
  the empty value is already conveyed by the block's initial state.
- openaiConvertMessages: preserve empty thinking blocks as
  reasoning_content: "" when serializing assistant messages back to
  the OpenAI/DeepSeek format.

Tests:
- New: empty reasoning_content opens a thinking block (adapter).
- Updated: empty thinking blocks now round-trip as reasoning_content: ""
  instead of being dropped.
- New: assistant messages with no thinking block still omit
  reasoning_content (regression guard for non-thinking models).
This commit is contained in:
ymonster
2026-05-02 14:58:29 +08:00
parent 3eba5ade1a
commit 1b10ea391a
4 changed files with 91 additions and 15 deletions

View File

@@ -468,7 +468,11 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
expect(assistant.reasoning_content).toBe('First thought.\nSecond thought.')
})
test('skips empty thinking blocks', () => {
test('preserves empty thinking blocks as reasoning_content: "" (DeepSeek v4 thinking mode)', () => {
// DeepSeek v4 thinking mode sometimes returns reasoning_content: ""
// when the model answers directly without reasoning. The empty value
// must be echoed back in the next request — otherwise DeepSeek returns
// 400 ("reasoning_content ... must be passed back"). See issue #399.
const result = anthropicMessagesToOpenAI(
[
makeUserMsg('question'),
@@ -481,7 +485,23 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
{ enableThinking: true },
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.reasoning_content).toBe('')
expect(assistant.content).toBe('Answer.')
})
test('omits reasoning_content when no thinking block is present', () => {
// No thinking block at all → no reasoning_content field on the
// OpenAI-format assistant message (relevant for non-thinking models).
const result = anthropicMessagesToOpenAI(
[
makeUserMsg('question'),
makeAssistantMsg([{ type: 'text', text: 'Answer.' }]),
],
[] as any,
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.reasoning_content).toBeUndefined()
expect(assistant.content).toBe('Answer.')
})
// ── fix: reorder tool and user messages for OpenAI API compatibility (#168) ──