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 <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
SAMUEL AUGUSTO GUIMARAES LOPES
2026-08-13 07:55:00 -03:00
committed by GitHub
parent 9a4cca4bc2
commit 6143da70d1

View File

@@ -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(
{