diff --git a/CHANGELOG.md b/CHANGELOG.md index eca7898274..331a090bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ - CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete. - Providers: default groupPolicy to allowlist across providers and warn in doctor when groups are open. - MS Teams: add groupPolicy/groupAllowFrom gating for group chats and warn when groups are open. +- Providers: strip tool call/result ids from Gemini CLI payloads to avoid API 400s. (#756) - Gateway/WebChat: include handshake validation details in the WebSocket close reason for easier debugging; preserve close codes. - Gateway/Auth: send invalid connect responses before closing the handshake; stabilize invalid-connect auth test. - Gateway: tighten gateway listener detection. diff --git a/patches/@mariozechner__pi-ai@0.42.2.patch b/patches/@mariozechner__pi-ai@0.42.2.patch index d59a760ecc..34f40a0ed0 100644 --- a/patches/@mariozechner__pi-ai@0.42.2.patch +++ b/patches/@mariozechner__pi-ai@0.42.2.patch @@ -62,6 +62,39 @@ index f07085c64390b211340d6a826b28ea9c2e77302f..7f758532246cc7b062df48e9cec4e6c9 + } + } + } - if (output.length === 0) - continue; - messages.push(...output); + if (output.length === 0) + continue; + messages.push(...output); +diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js +index 866446158b0ee3e4c4a4f3f78c71ce72b9aab6a1..c7f9d8a0b0c7a25b62a0bb5f8a4f9d63ccad1d24 100644 +--- a/dist/providers/google-shared.js ++++ b/dist/providers/google-shared.js +@@ -52,6 +52,8 @@ export function convertMessages(model, context) { + const contents = []; + const transformedMessages = transformMessages(context.messages, model); ++ const shouldStripFunctionId = typeof model.provider === "string" && ++ model.provider.startsWith("google"); + for (const msg of transformedMessages) { + if (msg.role === "user") { +@@ -110,8 +112,8 @@ export function convertMessages(model, context) { + args: block.arguments, + }, + }; +- if (model.provider === "google-vertex" && part?.functionCall?.id) { +- delete part.functionCall.id; // Vertex AI does not support 'id' in functionCall ++ if (shouldStripFunctionId && part?.functionCall?.id) { ++ delete part.functionCall.id; // Google Gemini/Vertex do not support 'id' in functionCall + } + if (block.thoughtSignature) { + part.thoughtSignature = block.thoughtSignature; +@@ -159,8 +161,8 @@ export function convertMessages(model, context) { + ...(hasImages && supportsMultimodalFunctionResponse && { parts: imageParts }), + }, + }; +- if (model.provider === "google-vertex" && functionResponsePart.functionResponse?.id) { +- delete functionResponsePart.functionResponse.id; // Vertex AI does not support 'id' in functionResponse ++ if (shouldStripFunctionId && functionResponsePart.functionResponse?.id) { ++ delete functionResponsePart.functionResponse.id; // Google Gemini/Vertex do not support 'id' in functionResponse + } + // Cloud Code Assist API requires all function responses to be in a single user turn. + // Check if the last content is already a user turn with function responses and merge. diff --git a/src/providers/google-shared.test.ts b/src/providers/google-shared.test.ts index 0cf09fde7d..b75d4968cc 100644 --- a/src/providers/google-shared.test.ts +++ b/src/providers/google-shared.test.ts @@ -26,6 +26,20 @@ const makeModel = (id: string): Model<"google-generative-ai"> => maxTokens: 1, }) as Model<"google-generative-ai">; +const makeGeminiCliModel = (id: string): Model<"google-gemini-cli"> => + ({ + id, + name: id, + api: "google-gemini-cli", + provider: "google-gemini-cli", + baseUrl: "https://example.invalid", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1, + maxTokens: 1, + }) as Model<"google-gemini-cli">; + describe("google-shared convertTools", () => { it("preserves parameters when type is missing", () => { const tools = [ @@ -493,4 +507,70 @@ describe("google-shared convertMessages", () => { const toolCall = asRecord(toolCallPart); expect(toolCall.functionCall).toBeTruthy(); }); + + it("strips tool call and response ids for google-gemini-cli", () => { + const model = makeGeminiCliModel("gemini-3-flash"); + const context = { + messages: [ + { + role: "user", + content: "Use a tool", + }, + { + role: "assistant", + content: [ + { + type: "toolCall", + id: "call_1", + name: "myTool", + arguments: { arg: "value" }, + }, + ], + api: "google-gemini-cli", + provider: "google-gemini-cli", + model: "gemini-3-flash", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }, + stopReason: "stop", + timestamp: 0, + }, + { + role: "toolResult", + toolCallId: "call_1", + toolName: "myTool", + content: [{ type: "text", text: "Tool result" }], + isError: false, + timestamp: 0, + }, + ], + } as unknown as Context; + + const contents = convertMessages(model, context); + const parts = contents.flatMap((content) => content.parts ?? []); + const toolCallPart = parts.find( + (part) => typeof part === "object" && part !== null && "functionCall" in part, + ); + const toolResponsePart = parts.find( + (part) => + typeof part === "object" && part !== null && "functionResponse" in part, + ); + + const toolCall = asRecord(toolCallPart); + const toolResponse = asRecord(toolResponsePart); + + expect(asRecord(toolCall.functionCall).id).toBeUndefined(); + expect(asRecord(toolResponse.functionResponse).id).toBeUndefined(); + }); });