mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
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:
@@ -439,6 +439,54 @@ describe('thinking support (reasoning_content)', () => {
|
||||
expect(blockStarts[1].content_block.type).toBe('tool_use')
|
||||
})
|
||||
|
||||
test('opens thinking block on empty reasoning_content (DeepSeek v4 direct-answer)', async () => {
|
||||
// DeepSeek v4 thinking mode sometimes streams reasoning_content: ""
|
||||
// before answering directly. We must still open a thinking block so the
|
||||
// resulting assistant message carries an (empty) thinking block — that
|
||||
// round-trips back as reasoning_content: "" in the next request,
|
||||
// satisfying DeepSeek's requirement (see issue #399).
|
||||
const events = await collectEvents([
|
||||
makeChunk({
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { reasoning_content: '' },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeChunk({
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: { content: 'Direct answer.' },
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
makeChunk({
|
||||
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
||||
}),
|
||||
])
|
||||
|
||||
// A thinking block was opened (and closed before the text block starts)
|
||||
const blockStarts = events.filter(
|
||||
e => e.type === 'content_block_start',
|
||||
) as any[]
|
||||
expect(blockStarts.length).toBe(2)
|
||||
expect(blockStarts[0].content_block.type).toBe('thinking')
|
||||
expect(blockStarts[0].content_block.thinking).toBe('')
|
||||
expect(blockStarts[1].content_block.type).toBe('text')
|
||||
|
||||
// No empty thinking_delta should be emitted — the empty string is
|
||||
// already conveyed by the thinking block's initial value.
|
||||
const thinkingDeltas = events.filter(
|
||||
e =>
|
||||
e.type === 'content_block_delta' && e.delta.type === 'thinking_delta',
|
||||
)
|
||||
expect(thinkingDeltas.length).toBe(0)
|
||||
})
|
||||
|
||||
test('thinking block index is 0, text block index is 1', async () => {
|
||||
const events = await collectEvents([
|
||||
makeChunk({
|
||||
|
||||
Reference in New Issue
Block a user