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.