diff --git a/src/api/transform/__tests__/r1-format.spec.ts b/src/api/transform/__tests__/r1-format.spec.ts index 3d875e9392..7fab8bae84 100644 --- a/src/api/transform/__tests__/r1-format.spec.ts +++ b/src/api/transform/__tests__/r1-format.spec.ts @@ -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") + }) }) }) }) diff --git a/src/api/transform/r1-format.ts b/src/api/transform/r1-format.ts index 8231e24f76..839678ea60 100644 --- a/src/api/transform/r1-format.ts +++ b/src/api/transform/r1-format.ts @@ -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) }