From caefdfa9715e8d6e8e563bf309b5c8e550dfadf9 Mon Sep 17 00:00:00 2001 From: Hernan Javier Ardila Sanchez Date: Thu, 21 May 2026 23:09:15 +0200 Subject: [PATCH] fix(handler): capture Gemini thought_signature in non-streaming response path (#2504) (#2518) Integrated into release/v3.8.2 --- open-sse/handlers/responseTranslator.ts | 34 ++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 1fced15a87..20b4d4bcdc 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -1,4 +1,8 @@ import { FORMATS } from "../translator/formats.ts"; +import { + buildGeminiThoughtSignatureKey, + storeGeminiThoughtSignature, +} from "../services/geminiThoughtSignatureStore.ts"; type JsonRecord = Record; @@ -278,6 +282,7 @@ export function translateNonStreamingResponse( const contentParts: JsonRecord[] = []; const toolCalls: JsonRecord[] = []; let reasoningContent = ""; + let pendingThoughtSignature = ""; if (Array.isArray(content.parts)) { for (const part of content.parts) { @@ -287,6 +292,15 @@ export function translateNonStreamingResponse( continue; } + // Capture thoughtSignature from thinking parts (Gemini thinking models) + // so it can be stored alongside any subsequent functionCall part. + const partThoughtSig = toString( + partObj.thoughtSignature ?? partObj.thought_signature + ); + if (partThoughtSig) { + pendingThoughtSignature = partThoughtSig; + } + if (typeof partObj.text === "string") { textContent += partObj.text; contentParts.push({ type: "text", text: partObj.text }); @@ -309,11 +323,23 @@ export function translateNonStreamingResponse( const rawName = toString(fn.name); const restoredName = toolNameMap?.get(rawName) ?? rawName; const nativeId = toString(fn.id); + const toolCallId = + nativeId.length > 0 + ? nativeId + : `call_${toString(restoredName, "unknown")}_${Date.now()}_${toolCalls.length}`; + + // Persist the thought signature so openai-to-gemini can + // resolve it on the next turn. Use the part-level field + // (part.thoughtSignature) and fall back to any signature + // captured from an earlier thinking-only part. + const sig = partThoughtSig || pendingThoughtSignature; + if (sig) { + const sigKey = buildGeminiThoughtSignatureKey(null, toolCallId); + storeGeminiThoughtSignature(sigKey, sig); + } + toolCalls.push({ - id: - nativeId.length > 0 - ? nativeId - : `call_${toString(restoredName, "unknown")}_${Date.now()}_${toolCalls.length}`, + id: toolCallId, type: "function", function: { name: restoredName,