From 45f768d721f0c46684c978e55b7a66cbffd0efac Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:40:32 +0000 Subject: [PATCH 1/3] fix(gemini): preserve empty tool responses --- src/api/providers/__tests__/gemini.spec.ts | 40 ++++++++++++++++++- .../transform/__tests__/gemini-format.spec.ts | 22 +++++++--- src/api/transform/gemini-format.ts | 16 +++++--- src/eslint-suppressions.json | 2 +- 4 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 701c453e3e..0c1fc6288b 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -15,6 +15,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { type ModelInfo, geminiDefaultModelId, ApiProviderError } from "@roo-code/types" import { t } from "i18next" +import type { ApiHandlerCreateMessageMetadata } from "../../index" import { GeminiHandler } from "../gemini" import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" @@ -22,13 +23,14 @@ const GEMINI_MODEL_NAME = geminiDefaultModelId describe("GeminiHandler", () => { let handler: GeminiHandler + let mockGenerateContentStream: ReturnType beforeEach(() => { // Reset mocks mockCaptureException.mockClear() // Create mock functions - const mockGenerateContentStream = vitest.fn() + mockGenerateContentStream = vitest.fn() const mockGenerateContent = vitest.fn() const mockGetGenerativeModel = vitest.fn() @@ -293,6 +295,42 @@ describe("GeminiHandler", () => { ) }) + it("should keep an empty tool result as the final user turn", async () => { + const messages: Anthropic.Messages.MessageParam[] = [ + { role: "user", content: "Run the tool" }, + { + role: "assistant", + content: [{ type: "tool_use", id: "call-1", name: "read_file", input: { path: "empty.txt" } }], + }, + { + role: "user", + content: [{ type: "tool_result", tool_use_id: "call-1", content: "" }], + }, + ] + const metadata = { + tools: [{ function: { name: "read_file", description: "", parameters: {} } }], + } satisfies ApiHandlerCreateMessageMetadata + + mockGenerateContentStream.mockResolvedValue( + asyncStreamFrom([{ candidates: [{ content: { parts: [{ text: "Done" }] } }] }]), + ) + + await collectStream(handler.createMessage(systemPrompt, messages, metadata)) + + const params = mockGenerateContentStream.mock.calls[0][0] + expect(params.contents.at(-1)).toEqual({ + role: "user", + parts: [ + { + functionResponse: { + name: "read_file", + response: { name: "read_file", content: "(empty)" }, + }, + }, + ], + }) + }) + it("should handle API errors", async () => { const mockError = new Error("Gemini API error") ;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 327a05479b..55aaf961db 100644 --- a/src/api/transform/__tests__/gemini-format.spec.ts +++ b/src/api/transform/__tests__/gemini-format.spec.ts @@ -203,22 +203,34 @@ describe("convertAnthropicMessageToGemini", () => { ]) }) - it("should handle empty tool result content", () => { + it("should preserve an empty tool result as a user function response", () => { + const toolIdToName = new Map([["calculator-123", "calculator"]]) const anthropicMessage: Anthropic.Messages.MessageParam = { role: "user", content: [ { type: "tool_result", tool_use_id: "calculator-123", - content: null as any, // Empty content + content: "", }, ], } - const result = convertAnthropicMessageToGemini(anthropicMessage) + const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName }) - // Should skip the empty tool result - expect(result).toEqual([]) + expect(result).toEqual([ + { + role: "user", + parts: [ + { + functionResponse: { + name: "calculator", + response: { name: "calculator", content: "(empty)" }, + }, + }, + ], + }, + ]) }) it("should convert a message with tool result as array with text only", () => { diff --git a/src/api/transform/gemini-format.ts b/src/api/transform/gemini-format.ts index 53bab11c52..d45e9f27ec 100644 --- a/src/api/transform/gemini-format.ts +++ b/src/api/transform/gemini-format.ts @@ -93,10 +93,6 @@ export function convertAnthropicContentToGemini( ...(functionCallSignature ? { thoughtSignature: functionCallSignature } : {}), } as Part case "tool_result": { - if (!block.content) { - return [] - } - // Get tool name from the map (built from tool_use blocks in message history). // The map must contain the tool name - if it doesn't, this indicates a bug // where the conversation history is incomplete or tool_use blocks are missing. @@ -111,7 +107,10 @@ export function convertAnthropicContentToGemini( if (typeof block.content === "string") { return { - functionResponse: { name: toolName, response: { name: toolName, content: block.content } }, + functionResponse: { + name: toolName, + response: { name: toolName, content: block.content || "(empty)" }, + }, } } @@ -137,7 +136,12 @@ export function convertAnthropicContentToGemini( // Return function response followed by any images return [ - { functionResponse: { name: toolName, response: { name: toolName, content: contentText } } }, + { + functionResponse: { + name: toolName, + response: { name: toolName, content: contentText || "(empty)" }, + }, + }, ...imageParts, ] } diff --git a/src/eslint-suppressions.json b/src/eslint-suppressions.json index 569c846c29..ee9d9e8bf8 100644 --- a/src/eslint-suppressions.json +++ b/src/eslint-suppressions.json @@ -476,7 +476,7 @@ }, "api/transform/__tests__/gemini-format.spec.ts": { "@typescript-eslint/no-explicit-any": { - "count": 6 + "count": 5 } }, "api/transform/__tests__/mistral-format.spec.ts": { From bc127e831fb998291ad197e3022ae16b6652154a Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Sat, 15 Aug 2026 11:46:13 +0000 Subject: [PATCH 2/3] test(gemini): type empty result fixture --- src/api/providers/__tests__/gemini.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 0c1fc6288b..2f19028eb7 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -308,7 +308,8 @@ describe("GeminiHandler", () => { }, ] const metadata = { - tools: [{ function: { name: "read_file", description: "", parameters: {} } }], + taskId: "test-task", + tools: [{ type: "function", function: { name: "read_file", description: "", parameters: {} } }], } satisfies ApiHandlerCreateMessageMetadata mockGenerateContentStream.mockResolvedValue( From 911fe9e7c5917c08ceeeb547c39726f0efb375b8 Mon Sep 17 00:00:00 2001 From: "@taltas" <6816042+taltas@users.noreply.github.com> Date: Sat, 15 Aug 2026 12:06:26 +0000 Subject: [PATCH 3/3] test(gemini): cover empty tool result arrays --- .../transform/__tests__/gemini-format.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/api/transform/__tests__/gemini-format.spec.ts b/src/api/transform/__tests__/gemini-format.spec.ts index 55aaf961db..407984956a 100644 --- a/src/api/transform/__tests__/gemini-format.spec.ts +++ b/src/api/transform/__tests__/gemini-format.spec.ts @@ -233,6 +233,36 @@ describe("convertAnthropicMessageToGemini", () => { ]) }) + it("should preserve an empty tool result array as a user function response", () => { + const toolIdToName = new Map([["calculator-123", "calculator"]]) + const anthropicMessage: Anthropic.Messages.MessageParam = { + role: "user", + content: [ + { + type: "tool_result", + tool_use_id: "calculator-123", + content: [], + }, + ], + } + + const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName }) + + expect(result).toEqual([ + { + role: "user", + parts: [ + { + functionResponse: { + name: "calculator", + response: { name: "calculator", content: "(empty)" }, + }, + }, + ], + }, + ]) + }) + it("should convert a message with tool result as array with text only", () => { const toolIdToName = new Map() toolIdToName.set("search-123", "search")