From e756e497c28496f08a6fc4d7b0a6322e4787c104 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:27:40 -0500 Subject: [PATCH] fix(ai): merge parallel gemini tool results into one turn (#43814) --- packages/ai/src/protocols/gemini.ts | 7 +++- packages/ai/test/provider/gemini.test.ts | 47 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/protocols/gemini.ts b/packages/ai/src/protocols/gemini.ts index b8c1b4b95cd..ad53791da1b 100644 --- a/packages/ai/src/protocols/gemini.ts +++ b/packages/ai/src/protocols/gemini.ts @@ -379,7 +379,12 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR }, }) } - contents.push({ role: "user", parts }) + // Gemini requires every response to a parallel call batch in one user turn, + // so consecutive tool results join the open function-response turn. + const previous = contents.at(-1) + if (previous?.role === "user" && previous.parts.some((item) => "functionResponse" in item)) + contents[contents.length - 1] = { role: "user", parts: [...previous.parts, ...parts] } + else contents.push({ role: "user", parts }) } return contents diff --git a/packages/ai/test/provider/gemini.test.ts b/packages/ai/test/provider/gemini.test.ts index c1579af7595..a11591cb418 100644 --- a/packages/ai/test/provider/gemini.test.ts +++ b/packages/ai/test/provider/gemini.test.ts @@ -181,6 +181,53 @@ describe("Gemini route", () => { }), ) + it.effect("merges parallel tool results into one function-response turn", () => + Effect.gen(function* () { + const prepared = yield* compileRequest( + LLM.request({ + model, + messages: [ + Message.assistant([ + ToolCallPart.make({ id: "call_1", name: "lookup", input: { query: "weather" } }), + ToolCallPart.make({ id: "call_2", name: "lookup", input: { query: "time" } }), + ]), + Message.tool({ id: "call_1", name: "lookup", result: "sunny", resultType: "text" }), + Message.tool({ id: "call_2", name: "lookup", result: "noon", resultType: "text" }), + ], + }), + ) + + expect(prepared.body.contents).toEqual([ + { + role: "model", + parts: [ + { functionCall: { id: undefined, name: "lookup", args: { query: "weather" } } }, + { functionCall: { id: undefined, name: "lookup", args: { query: "time" } } }, + ], + }, + { + role: "user", + parts: [ + { + functionResponse: { + id: undefined, + name: "lookup", + response: { name: "lookup", content: "sunny" }, + }, + }, + { + functionResponse: { + id: undefined, + name: "lookup", + response: { name: "lookup", content: "noon" }, + }, + }, + ], + }, + ]) + }), + ) + it.effect("prepares multimodal user input and tool history", () => Effect.gen(function* () { const prepared = yield* compileRequest(