fix: ensure reasoning_content is always present for DeepSeek thinking mode

When using DeepSeek reasoner models with context condense, the API returns
400 error "Missing reasoning_content field in the assistant message" because
condensed summary messages lack the reasoning_content field.

DeepSeek's API requires ALL assistant messages to have reasoning_content
when using thinking mode. This fix ensures reasoning_content is always
included (even if empty string) on assistant messages when mergeToolResultText
is enabled (which indicates thinking mode).

Fixes #10175
This commit is contained in:
Roo Code 2025-12-18 04:32:32 +00:00
parent 06c5c7f980
commit 0fb47f252f
2 changed files with 98 additions and 2 deletions

View file

@ -614,6 +614,86 @@ describe("convertToR1Format", () => {
// Most importantly: NO user message after tool message
expect(result.filter((m) => m.role === "user")).toHaveLength(1)
})
it("should always include empty reasoning_content on assistant messages when mergeToolResultText is true", () => {
// This simulates a condensed summary message that has no reasoning_content
const input: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Start" },
{
role: "assistant",
content: "This is a condensed summary of the previous conversation.",
},
]
const result = convertToR1Format(input, { mergeToolResultText: true })
expect(result).toHaveLength(2)
expect(result[0]).toEqual({ role: "user", content: "Start" })
// Should have empty reasoning_content for thinking models
expect((result[1] as any).reasoning_content).toBe("")
expect(result[1]).toEqual({
role: "assistant",
content: "This is a condensed summary of the previous conversation.",
reasoning_content: "",
})
})
it("should NOT add reasoning_content on assistant messages when mergeToolResultText is false", () => {
// For non-thinking models, reasoning_content should not be added
const input: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Start" },
{
role: "assistant",
content: "This is a regular response.",
},
]
// Without mergeToolResultText option (default behavior)
const result = convertToR1Format(input)
expect(result).toHaveLength(2)
// Should NOT have reasoning_content for non-thinking models
expect((result[1] as any).reasoning_content).toBeUndefined()
expect(result[1]).toEqual({
role: "assistant",
content: "This is a regular response.",
})
})
it("should include empty reasoning_content on assistant messages with array content when mergeToolResultText is true", () => {
const input: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Start" },
{
role: "assistant",
content: [{ type: "text", text: "Array content response without reasoning" }],
},
]
const result = convertToR1Format(input, { mergeToolResultText: true })
expect(result).toHaveLength(2)
// Should have empty reasoning_content for thinking models even with array content
expect((result[1] as any).reasoning_content).toBe("")
})
it("should preserve existing reasoning_content when present even with mergeToolResultText", () => {
const input = [
{ role: "user" as const, content: "Start" },
{
role: "assistant" as const,
content: "Response with reasoning",
reasoning_content: "My actual reasoning process",
},
]
const result = convertToR1Format(input as Anthropic.Messages.MessageParam[], {
mergeToolResultText: true,
})
expect(result).toHaveLength(2)
// Should preserve the actual reasoning_content, not replace with empty
expect((result[1] as any).reasoning_content).toBe("My actual reasoning process")
})
})
})
})

View file

@ -190,12 +190,21 @@ export function convertToR1Format(
// Use reasoning from content blocks if not provided at top level
const finalReasoning = reasoningContent || extractedReasoning
// For thinking models (indicated by mergeToolResultText), always include reasoning_content
// DeepSeek's reasoner API requires this field to be present on all assistant messages
const shouldAlwaysIncludeReasoningContent = options?.mergeToolResultText === true
const assistantMessage: DeepSeekAssistantMessage = {
role: "assistant",
content: textParts.length > 0 ? textParts.join("\n") : null,
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
// Preserve reasoning_content for DeepSeek interleaved thinking
...(finalReasoning && { reasoning_content: finalReasoning }),
// Always include for thinking models (even if empty) to satisfy DeepSeek's API requirement
...(finalReasoning
? { reasoning_content: finalReasoning }
: shouldAlwaysIncludeReasoningContent
? { reasoning_content: "" }
: {}),
}
// Check if we can merge with the last message (only if no tool calls)
@ -229,10 +238,17 @@ export function convertToR1Format(
;(lastMessage as DeepSeekAssistantMessage).reasoning_content = reasoningContent
}
} else {
// For thinking models (indicated by mergeToolResultText), always include reasoning_content
const shouldAlwaysIncludeReasoningContent = options?.mergeToolResultText === true
const assistantMessage: DeepSeekAssistantMessage = {
role: "assistant",
content: message.content,
...(reasoningContent && { reasoning_content: reasoningContent }),
// Always include for thinking models (even if empty) to satisfy DeepSeek's API requirement
...(reasoningContent
? { reasoning_content: reasoningContent }
: shouldAlwaysIncludeReasoningContent
? { reasoning_content: "" }
: {}),
}
result.push(assistantMessage)
}