From f6304d4bd6cfaff88dad66b76db633f1cb44f9eb Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 8 Jan 2026 18:10:11 -0500 Subject: [PATCH] fix: ensure assistant message content is never undefined for Gemini compatibility When assistant messages contain only tool_use blocks (no text), the content field was left as undefined. While OpenAI accepts content: null with tool_calls, Gemini (via OpenRouter) strictly requires every message to have content. Changes: - Use empty string fallback for assistant message content when undefined - Add test case for Gemini compatibility Fixes ROO-425 --- .../transform/__tests__/openai-format.spec.ts | 30 +++++++++++++++++++ src/api/transform/openai-format.ts | 4 ++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/api/transform/__tests__/openai-format.spec.ts b/src/api/transform/__tests__/openai-format.spec.ts index 2e7f61c9f34..d5d44048375 100644 --- a/src/api/transform/__tests__/openai-format.spec.ts +++ b/src/api/transform/__tests__/openai-format.spec.ts @@ -225,6 +225,36 @@ describe("convertToOpenAiMessages", () => { expect(assistantMessage.tool_calls![0].id).toBe("custom_toolu_123") }) + it("should use empty string for content when assistant message has only tool calls (Gemini compatibility)", () => { + // This test ensures that assistant messages with only tool_use blocks (no text) + // have content set to "" instead of undefined. Gemini (via OpenRouter) requires + // every message to have at least one "parts" field, which fails if content is undefined. + // See: ROO-425 + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool-123", + name: "read_file", + input: { path: "test.ts" }, + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const assistantMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionAssistantMessageParam + expect(assistantMessage.role).toBe("assistant") + // Content should be an empty string, NOT undefined + expect(assistantMessage.content).toBe("") + expect(assistantMessage.tool_calls).toHaveLength(1) + expect(assistantMessage.tool_calls![0].id).toBe("tool-123") + }) + describe("mergeToolResultText option", () => { it("should merge text content into last tool message when mergeToolResultText is true", () => { const anthropicMessages: Anthropic.Messages.MessageParam[] = [ diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index de48d27a3f0..ad02be55417 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -223,7 +223,9 @@ export function convertToOpenAiMessages( reasoning_details?: any[] } = { role: "assistant", - content, + // Use empty string instead of undefined for providers like Gemini (via OpenRouter) + // that require every message to have content in the "parts" field + content: content ?? "", } // Pass through reasoning_details to preserve the original shape from the API.