From a6d878e80419f4679ae2a191117b5a5157f22b43 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Sun, 25 Jan 2026 03:18:02 +0100 Subject: [PATCH] fix(ai): default tool call arguments to empty object for Google providers When Google providers return tool calls without an args field (common for no-argument tools), the arguments field was undefined. This breaks subsequent API calls that require tool_use.input to be present. Now defaults to {} when args is missing. Related: clawdbot/clawdbot#1509 --- .../ai/src/providers/google-gemini-cli.ts | 2 +- packages/ai/src/providers/google-vertex.ts | 2 +- packages/ai/src/providers/google.ts | 2 +- .../google-tool-call-missing-args.test.ts | 105 ++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 packages/ai/test/google-tool-call-missing-args.test.ts diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index a40d8532..2ddc98b9 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -685,7 +685,7 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGe type: "toolCall", id: toolCallId, name: part.functionCall.name || "", - arguments: part.functionCall.args as Record, + arguments: (part.functionCall.args as Record) ?? {}, ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), }; diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index a35f640a..cc8c852e 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -191,7 +191,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOpt type: "toolCall", id: toolCallId, name: part.functionCall.name || "", - arguments: part.functionCall.args as Record, + arguments: (part.functionCall.args as Record) ?? {}, ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), }; diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 79fedfa0..3d0ff796 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -179,7 +179,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions> type: "toolCall", id: toolCallId, name: part.functionCall.name || "", - arguments: part.functionCall.args as Record, + arguments: (part.functionCall.args as Record) ?? {}, ...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }), }; diff --git a/packages/ai/test/google-tool-call-missing-args.test.ts b/packages/ai/test/google-tool-call-missing-args.test.ts new file mode 100644 index 00000000..e8a2296a --- /dev/null +++ b/packages/ai/test/google-tool-call-missing-args.test.ts @@ -0,0 +1,105 @@ +import { Type } from "@sinclair/typebox"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { streamGoogleGeminiCli } from "../src/providers/google-gemini-cli.js"; +import type { Context, Model, ToolCall } from "../src/types.js"; + +const emptySchema = Type.Object({}); + +const originalFetch = global.fetch; + +afterEach(() => { + global.fetch = originalFetch; + vi.restoreAllMocks(); +}); + +describe("google providers tool call missing args", () => { + it("defaults arguments to empty object when provider omits args field", async () => { + // Simulate a tool call response where args is missing (no-arg tool) + const sse = `${[ + `data: ${JSON.stringify({ + response: { + candidates: [ + { + content: { + role: "model", + parts: [ + { + functionCall: { + name: "get_status", + // args intentionally omitted + }, + }, + ], + }, + finishReason: "STOP", + }, + ], + usageMetadata: { + promptTokenCount: 10, + candidatesTokenCount: 5, + totalTokenCount: 15, + }, + }, + })}`, + ].join("\n\n")}\n\n`; + + const encoder = new TextEncoder(); + const dataStream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(sse)); + controller.close(); + }, + }); + + const fetchMock = vi.fn(async () => { + return new Response(dataStream, { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + }); + + global.fetch = fetchMock as typeof fetch; + + const model: Model<"google-gemini-cli"> = { + id: "gemini-2.5-flash", + name: "Gemini 2.5 Flash", + api: "google-gemini-cli", + provider: "google-gemini-cli", + baseUrl: "https://cloudcode-pa.googleapis.com", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 8192, + }; + + const context: Context = { + messages: [{ role: "user", content: "Check status", timestamp: Date.now() }], + tools: [ + { + name: "get_status", + description: "Get current status", + parameters: emptySchema, + }, + ], + }; + + const stream = streamGoogleGeminiCli(model, context, { + apiKey: JSON.stringify({ token: "token", projectId: "project" }), + }); + + for await (const _ of stream) { + // consume stream + } + + const result = await stream.result(); + + expect(result.stopReason).toBe("toolUse"); + expect(result.content).toHaveLength(1); + + const toolCall = result.content[0] as ToolCall; + expect(toolCall.type).toBe("toolCall"); + expect(toolCall.name).toBe("get_status"); + expect(toolCall.arguments).toEqual({}); + }); +});