Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion src/api/providers/__tests__/gemini.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@ 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"

const GEMINI_MODEL_NAME = geminiDefaultModelId

describe("GeminiHandler", () => {
let handler: GeminiHandler
let mockGenerateContentStream: ReturnType<typeof vitest.fn>

beforeEach(() => {
// Reset mocks
mockCaptureException.mockClear()

// Create mock functions
const mockGenerateContentStream = vitest.fn()
mockGenerateContentStream = vitest.fn()
const mockGenerateContent = vitest.fn()
const mockGetGenerativeModel = vitest.fn()

Expand Down Expand Up @@ -293,6 +295,43 @@ 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 = {
taskId: "test-task",
tools: [{ type: "function", 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)
Expand Down
52 changes: 47 additions & 5 deletions src/api/transform/__tests__/gemini-format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,64 @@ 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 })

expect(result).toEqual([
{
role: "user",
parts: [
{
functionResponse: {
name: "calculator",
response: { name: "calculator", content: "(empty)" },
},
},
],
},
])
})

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 })

// 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", () => {
Expand Down
16 changes: 10 additions & 6 deletions src/api/transform/gemini-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)" },
},
}
}

Expand All @@ -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,
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading