fix: filter out empty parts in Gemini format conversion to prevent API errors

- Added filtering logic to remove empty parts from flatMap results
- Prevents "required oneof field data must have one initialized field" errors
- Added comprehensive tests for edge cases with empty content blocks
- Fixes #9816
This commit is contained in:
Roo Code 2025-12-04 13:46:02 +00:00
parent 94c997c9d6
commit 3f64b95b24
2 changed files with 138 additions and 1 deletions

View file

@ -463,4 +463,131 @@ describe("convertAnthropicMessageToGemini", () => {
},
])
})
it("should return empty array when all content blocks are skipped", () => {
const anthropicMessage: Anthropic.Messages.MessageParam = {
role: "user",
content: [
{
type: "reasoning" as any,
text: "Thinking...",
},
{
type: "thinking" as any,
text: "More thinking...",
},
],
}
const result = convertAnthropicMessageToGemini(anthropicMessage)
expect(result).toEqual([])
})
it("should handle message with only empty tool results", () => {
const anthropicMessage: Anthropic.Messages.MessageParam = {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-123",
content: null as any,
},
{
type: "tool_result",
tool_use_id: "tool-456",
content: undefined as any,
},
],
}
const result = convertAnthropicMessageToGemini(anthropicMessage)
expect(result).toEqual([])
})
it("should filter out empty parts from mixed content", () => {
const toolIdToName = new Map<string, string>()
toolIdToName.set("tool-123", "calculator")
const anthropicMessage: Anthropic.Messages.MessageParam = {
role: "assistant",
content: [
{ type: "text", text: "Let me help you" },
{
type: "tool_result",
tool_use_id: "tool-999",
content: null as any, // This should be filtered out
},
{
type: "reasoning" as any,
text: "Thinking...", // This should be filtered out
},
{
type: "tool_use",
id: "tool-123",
name: "calculator",
input: { a: 1, b: 2 },
},
],
}
const result = convertAnthropicMessageToGemini(anthropicMessage, { toolIdToName })
expect(result).toEqual([
{
role: "model",
parts: [
{ text: "Let me help you" },
{
functionCall: {
name: "calculator",
args: { a: 1, b: 2 },
},
thoughtSignature: "skip_thought_signature_validator",
},
],
},
])
})
it("should handle thoughtSignature blocks that are excluded", () => {
const anthropicMessage: Anthropic.Messages.MessageParam = {
role: "assistant",
content: [
{
type: "thoughtSignature",
thoughtSignature: "some-signature",
} as any,
{ type: "text", text: "Response text" },
],
}
// When includeThoughtSignatures is false, thoughtSignature blocks should be filtered out
const result = convertAnthropicMessageToGemini(anthropicMessage, { includeThoughtSignatures: false })
expect(result).toEqual([
{
role: "model",
parts: [{ text: "Response text" }],
},
])
})
it("should handle message with only thoughtSignature when excluded", () => {
const anthropicMessage: Anthropic.Messages.MessageParam = {
role: "assistant",
content: [
{
type: "thoughtSignature",
thoughtSignature: "some-signature",
} as any,
],
}
// When includeThoughtSignatures is false and there's only a thoughtSignature block
const result = convertAnthropicMessageToGemini(anthropicMessage, { includeThoughtSignatures: false })
expect(result).toEqual([])
})
})

View file

@ -47,7 +47,7 @@ export function convertAnthropicContentToGemini(
return [{ text: content }]
}
return content.flatMap((block): Part | Part[] => {
const parts = content.flatMap((block): Part | Part[] => {
// Handle thoughtSignature blocks first
if (isThoughtSignatureContentBlock(block)) {
if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
@ -135,6 +135,14 @@ export function convertAnthropicContentToGemini(
return []
}
})
// Filter out any empty arrays that were returned by flatMap
// This prevents sending empty parts to the Gemini API which would cause errors
return parts.filter((part): part is Part => {
// Check if the part is actually a Part object (not an empty array)
// Empty arrays from flatMap will be filtered out here
return part && typeof part === "object" && Object.keys(part).length > 0
})
}
export function convertAnthropicMessageToGemini(
@ -143,6 +151,8 @@ export function convertAnthropicMessageToGemini(
): Content[] {
const parts = convertAnthropicContentToGemini(message.content, options)
// Skip creating Content objects if there are no valid parts
// This prevents sending empty messages to the Gemini API
if (parts.length === 0) {
return []
}