From 5ef90c7c914a67d0cdb11e69eae48f22212ecb5f Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 12 Jan 2026 19:12:31 -0500 Subject: [PATCH] fix: use placeholder for empty tool result content to fix Gemini API validation Gemini (via OpenRouter) requires function responses to have non-empty content in the 'parts' field. Empty content causes validation failure with error: 'Unable to submit request because it must include at least one parts field' Added defensive fix in openai-format.ts transformation layer to use '(empty)' placeholder when tool result content is empty (string, undefined, or empty array). Added 3 test cases covering the empty content scenarios. Closes ROO-517 --- .../transform/__tests__/openai-format.spec.ts | 72 +++++++++++++++++++ src/api/transform/openai-format.ts | 3 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/api/transform/__tests__/openai-format.spec.ts b/src/api/transform/__tests__/openai-format.spec.ts index d5d4404837..1523b59d3f 100644 --- a/src/api/transform/__tests__/openai-format.spec.ts +++ b/src/api/transform/__tests__/openai-format.spec.ts @@ -255,6 +255,78 @@ describe("convertToOpenAiMessages", () => { expect(assistantMessage.tool_calls![0].id).toBe("tool-123") }) + it('should use "(empty)" placeholder for tool result with empty content (Gemini compatibility)', () => { + // This test ensures that tool messages with empty content get a placeholder instead + // of an empty string. Gemini (via OpenRouter) requires function responses to have + // non-empty content in the "parts" field, and an empty string causes validation failure + // with error: "Unable to submit request because it must include at least one parts field" + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-123", + content: "", // Empty string content + }, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(toolMessage.role).toBe("tool") + expect(toolMessage.tool_call_id).toBe("tool-123") + // Content should be "(empty)" placeholder, NOT empty string + expect(toolMessage.content).toBe("(empty)") + }) + + it('should use "(empty)" placeholder for tool result with undefined content (Gemini compatibility)', () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-456", + // content is undefined/not provided + } as Anthropic.ToolResultBlockParam, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(toolMessage.role).toBe("tool") + expect(toolMessage.content).toBe("(empty)") + }) + + it('should use "(empty)" placeholder for tool result with empty array content (Gemini compatibility)', () => { + const anthropicMessages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "tool-789", + content: [], // Empty array + } as Anthropic.ToolResultBlockParam, + ], + }, + ] + + const openAiMessages = convertToOpenAiMessages(anthropicMessages) + expect(openAiMessages).toHaveLength(1) + + const toolMessage = openAiMessages[0] as OpenAI.Chat.ChatCompletionToolMessageParam + expect(toolMessage.role).toBe("tool") + expect(toolMessage.content).toBe("(empty)") + }) + 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 ad02be5541..117b81e1d4 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -116,7 +116,8 @@ export function convertToOpenAiMessages( openAiMessages.push({ role: "tool", tool_call_id: normalizeId(toolMessage.tool_use_id), - content: content, + // Use "(empty)" placeholder for empty content to satisfy providers like Gemini (via OpenRouter) + content: content || "(empty)", }) })