From 6143da70d13b2a291edc31f47fae71451a7f35c2 Mon Sep 17 00:00:00 2001 From: SAMUEL AUGUSTO GUIMARAES LOPES <80289382+Sam280903@users.noreply.github.com> Date: Thu, 13 Aug 2026 07:55:00 -0300 Subject: [PATCH] fix(mcp): persist and re-attach Gemini thoughtSignature on the direct Claude<->Gemini path (#9448) The direct Claude<->Gemini translator (claude-to-gemini.ts / gemini-to-claude.ts) never persisted the thoughtSignature Gemini returns on functionCall parts, and never re-attached one on the next turn. Gemini 3+/2.5 strictly reject a native functionCall part with no signature (400), which surfaces whenever a combo falls back onto a Gemini model mid-conversation (the fallback tool_use never went through Gemini, so no signature exists for it). - gemini-to-claude.ts: store the signature (keyed by tool_use id + connection namespace) when Gemini's response carries one, mirroring the existing gemini-to-openai.ts hub-path behavior. - claude-to-gemini.ts: resolve a stored signature for historical tool_use blocks; when none exists and the target model requires one, downgrade the tool_use/tool_result pair to inert text instead of sending a signature-less native part, matching the "context" fallback already used by the OpenAI hub path (#3358) rather than the removed fake-signature injection. Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../translator-resp-gemini-to-claude.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/translator-resp-gemini-to-claude.test.ts b/tests/unit/translator-resp-gemini-to-claude.test.ts index d1e46da020..4e63824842 100644 --- a/tests/unit/translator-resp-gemini-to-claude.test.ts +++ b/tests/unit/translator-resp-gemini-to-claude.test.ts @@ -142,6 +142,38 @@ test("Gemini -> Claude stream: STOP after prior tool use still maps to tool_use" assert.equal(result[1].type, "message_stop"); }); +test("Gemini -> Claude stream: stores thoughtSignature from a standalone part preceding functionCall", async () => { + const { getGeminiThoughtSignature, clearGeminiThoughtSignatureMemoryForTests } = await import( + "../../open-sse/services/geminiThoughtSignatureStore.ts" + ); + clearGeminiThoughtSignatureMemoryForTests(); + + const state: { signatureNamespace: string; pendingThoughtSignature?: string | null } = { + signatureNamespace: "conn-claude-1", + }; + const result = geminiToClaudeResponse( + { + responseId: "resp-sig-1", + modelVersion: "gemini-3.1-flash-lite", + candidates: [ + { + content: { + parts: [ + { thoughtSignature: "sig-claude-1" }, + { functionCall: { id: "toolu_sig_1", name: "read_file", args: { path: "/a" } } }, + ], + }, + }, + ], + }, + state + ); + + assert.equal(result[1].content_block.id, "toolu_sig_1"); + assert.equal(state.pendingThoughtSignature, null); + assert.equal(getGeminiThoughtSignature("conn-claude-1:toolu_sig_1"), "sig-claude-1"); +}); + test("Gemini -> Claude stream: response wrapper is supported and promptFeedback-only chunk is ignored", () => { const wrapped = geminiToClaudeResponse( {