From 28805d0564dc10d378abe9baa41e366876253eab Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Tue, 25 Nov 2025 02:40:44 +0530 Subject: [PATCH 1/3] feat: add support for gemini thought signature --- core/llm/llms/Gemini.ts | 29 +++++++++++- packages/openai-adapters/src/apis/Gemini.ts | 51 ++++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/core/llm/llms/Gemini.ts b/core/llm/llms/Gemini.ts index 0e0b9b243fe..d9ae2a77b94 100644 --- a/core/llm/llms/Gemini.ts +++ b/core/llm/llms/Gemini.ts @@ -266,18 +266,35 @@ class Gemini extends BaseLLM { ? [{ text: msg.content }] : msg.content.map(this.continuePartToGeminiPart), }; - if (msg.toolCalls) { - msg.toolCalls.forEach((toolCall) => { + + if (msg.toolCalls && msg.toolCalls.length) { + msg.toolCalls.forEach((toolCall, index) => { if (toolCall.function?.name) { + const signatureForCall = (toolCall as any)?.extra_content + ?.google?.thought_signature; + + let thoughtSignature: string | undefined; + if (index === 0) { + if (typeof signatureForCall === "string") { + thoughtSignature = signatureForCall; + } else { + // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures + // for histories that were not generated by Gemini or are missing signatures. + thoughtSignature = "skip_thought_signature_validator"; + } + } + assistantMsg.parts.push({ functionCall: { name: toolCall.function.name, args: safeParseToolCallArgs(toolCall), }, + ...(thoughtSignature && { thoughtSignature }), }); } }); } + return assistantMsg; } return { @@ -370,6 +387,7 @@ class Gemini extends BaseLLM { if ("text" in part) { textParts.push({ type: "text", text: part.text }); } else if ("functionCall" in part) { + const thoughtSignature = (part as any)?.thoughtSignature; toolCalls.push({ type: "function", id: part.functionCall.id ?? uuidv4(), @@ -380,6 +398,13 @@ class Gemini extends BaseLLM { ? part.functionCall.args : JSON.stringify(part.functionCall.args), }, + ...(thoughtSignature && { + extra_content: { + google: { + thought_signature: thoughtSignature, + }, + }, + }), }); } else { // Note: function responses shouldn't be streamed, images not supported diff --git a/packages/openai-adapters/src/apis/Gemini.ts b/packages/openai-adapters/src/apis/Gemini.ts index b0f32ad46e4..4f5b9837e98 100644 --- a/packages/openai-adapters/src/apis/Gemini.ts +++ b/packages/openai-adapters/src/apis/Gemini.ts @@ -143,9 +143,25 @@ export class GeminiApi implements BaseLlmApi { return { role: "model" as const, - parts: msg.tool_calls.map((toolCall) => { - // Type guard for function tool calls + parts: msg.tool_calls.map((toolCall, index) => { if (toolCall.type === "function" && "function" in toolCall) { + let thoughtSignature: string | undefined; + if (index === 0) { + const rawSignature = (toolCall as any)?.extra_content?.google + ?.thought_signature; + + if ( + typeof rawSignature === "string" && + rawSignature.length > 0 + ) { + thoughtSignature = rawSignature; + } else { + // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures + // for histories that were not generated by Gemini or are missing signatures. + thoughtSignature = "skip_thought_signature_validator"; + } + } + return { functionCall: { id: includeToolCallIds ? toolCall.id : undefined, @@ -155,12 +171,12 @@ export class GeminiApi implements BaseLlmApi { `Call: ${toolCall.function.name} ${toolCall.id}`, ), }, + ...(thoughtSignature && { thoughtSignature }), }; - } else { - throw new Error( - `Unsupported tool call type in Gemini: ${toolCall.type}`, - ); } + throw new Error( + `Unsupported tool call type in Gemini: ${toolCall.type}`, + ); }), }; } @@ -328,11 +344,27 @@ export class GeminiApi implements BaseLlmApi { if (contentParts) { for (const part of contentParts) { if ("text" in part) { + const thoughtSignature = (part as any)?.thoughtSignature; + if (thoughtSignature) { + yield chatChunkFromDelta({ + model, + delta: { + role: "assistant", + extra_content: { + google: { + thought_signature: thoughtSignature, + }, + }, + } as any, + }); + } + yield chatChunk({ content: part.text, model, }); } else if ("functionCall" in part) { + const thoughtSignature = (part as any)?.thoughtSignature; yield chatChunkFromDelta({ model, delta: { @@ -345,6 +377,13 @@ export class GeminiApi implements BaseLlmApi { name: part.functionCall.name, arguments: JSON.stringify(part.functionCall.args), }, + ...(thoughtSignature && { + extra_content: { + google: { + thought_signature: thoughtSignature, + }, + }, + }), }, ], }, From fc9c49b2181b625d6d69e53366b0aa146e5d3767 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:59:39 +0530 Subject: [PATCH 2/3] fix types in core gemini llm --- core/llm/llms/Gemini.ts | 58 ++++++++++++++++++++--------------- core/llm/llms/gemini-types.ts | 1 + 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/core/llm/llms/Gemini.ts b/core/llm/llms/Gemini.ts index d9ae2a77b94..79462607d2c 100644 --- a/core/llm/llms/Gemini.ts +++ b/core/llm/llms/Gemini.ts @@ -23,6 +23,14 @@ import { convertContinueToolToGeminiFunction, } from "./gemini-types"; +interface GeminiToolCallDelta extends ToolCallDelta { + extra_content?: { + google?: { + thought_signature?: string; + }; + }; +} + class Gemini extends BaseLLM { static providerName = "gemini"; @@ -268,31 +276,33 @@ class Gemini extends BaseLLM { }; if (msg.toolCalls && msg.toolCalls.length) { - msg.toolCalls.forEach((toolCall, index) => { - if (toolCall.function?.name) { - const signatureForCall = (toolCall as any)?.extra_content - ?.google?.thought_signature; - - let thoughtSignature: string | undefined; - if (index === 0) { - if (typeof signatureForCall === "string") { - thoughtSignature = signatureForCall; - } else { - // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures - // for histories that were not generated by Gemini or are missing signatures. - thoughtSignature = "skip_thought_signature_validator"; + (msg.toolCalls as GeminiToolCallDelta[]).forEach( + (toolCall, index) => { + if (toolCall.function?.name) { + const signatureForCall = + toolCall?.extra_content?.google?.thought_signature; + + let thoughtSignature: string | undefined; + if (index === 0) { + if (typeof signatureForCall === "string") { + thoughtSignature = signatureForCall; + } else { + // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures + // for histories that were not generated by Gemini or are missing signatures. + thoughtSignature = "skip_thought_signature_validator"; + } } - } - assistantMsg.parts.push({ - functionCall: { - name: toolCall.function.name, - args: safeParseToolCallArgs(toolCall), - }, - ...(thoughtSignature && { thoughtSignature }), - }); - } - }); + assistantMsg.parts.push({ + functionCall: { + name: toolCall.function.name, + args: safeParseToolCallArgs(toolCall), + }, + ...(thoughtSignature && { thoughtSignature }), + }); + } + }, + ); } return assistantMsg; @@ -387,7 +397,7 @@ class Gemini extends BaseLLM { if ("text" in part) { textParts.push({ type: "text", text: part.text }); } else if ("functionCall" in part) { - const thoughtSignature = (part as any)?.thoughtSignature; + const thoughtSignature = part.thoughtSignature; toolCalls.push({ type: "function", id: part.functionCall.id ?? uuidv4(), diff --git a/core/llm/llms/gemini-types.ts b/core/llm/llms/gemini-types.ts index 355d1c942f3..f1944930b37 100644 --- a/core/llm/llms/gemini-types.ts +++ b/core/llm/llms/gemini-types.ts @@ -192,6 +192,7 @@ export type GeminiFunctionCallContentPart = { name: string; args: JSONSchema7Object; }; + thoughtSignature?: string; }; export type GeminiFunctionResponseContentPart = { From 1031cbb21967c29119508aa074b1f3726b61a327 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:07:12 +0530 Subject: [PATCH 3/3] remove as any in gemini openai adapter --- packages/openai-adapters/src/apis/Gemini.ts | 92 +++++++++++++-------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/packages/openai-adapters/src/apis/Gemini.ts b/packages/openai-adapters/src/apis/Gemini.ts index 4f5b9837e98..24e2949ad98 100644 --- a/packages/openai-adapters/src/apis/Gemini.ts +++ b/packages/openai-adapters/src/apis/Gemini.ts @@ -44,6 +44,24 @@ type UsageInfo = Pick< "total_tokens" | "completion_tokens" | "prompt_tokens" >; +interface GeminiToolCall + extends OpenAI.Chat.Completions.ChatCompletionMessageFunctionToolCall { + extra_content?: { + google?: { + thought_signature?: string; + }; + }; +} + +interface GeminiToolDelta + extends OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta { + extra_content?: { + google?: { + thought_signature?: string; + }; + }; +} + export class GeminiApi implements BaseLlmApi { apiBase: string = "https://generativelanguage.googleapis.com/v1beta/"; @@ -143,41 +161,43 @@ export class GeminiApi implements BaseLlmApi { return { role: "model" as const, - parts: msg.tool_calls.map((toolCall, index) => { - if (toolCall.type === "function" && "function" in toolCall) { - let thoughtSignature: string | undefined; - if (index === 0) { - const rawSignature = (toolCall as any)?.extra_content?.google - ?.thought_signature; - - if ( - typeof rawSignature === "string" && - rawSignature.length > 0 - ) { - thoughtSignature = rawSignature; - } else { - // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures - // for histories that were not generated by Gemini or are missing signatures. - thoughtSignature = "skip_thought_signature_validator"; + parts: (msg.tool_calls as GeminiToolCall[]).map( + (toolCall, index) => { + if (toolCall.type === "function" && "function" in toolCall) { + let thoughtSignature: string | undefined; + if (index === 0) { + const rawSignature = + toolCall?.extra_content?.google?.thought_signature; + + if ( + typeof rawSignature === "string" && + rawSignature.length > 0 + ) { + thoughtSignature = rawSignature; + } else { + // Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures + // for histories that were not generated by Gemini or are missing signatures. + thoughtSignature = "skip_thought_signature_validator"; + } } - } - return { - functionCall: { - id: includeToolCallIds ? toolCall.id : undefined, - name: toolCall.function.name, - args: safeParseArgs( - toolCall.function.arguments, - `Call: ${toolCall.function.name} ${toolCall.id}`, - ), - }, - ...(thoughtSignature && { thoughtSignature }), - }; - } - throw new Error( - `Unsupported tool call type in Gemini: ${toolCall.type}`, - ); - }), + return { + functionCall: { + id: includeToolCallIds ? toolCall.id : undefined, + name: toolCall.function.name, + args: safeParseArgs( + toolCall.function.arguments, + `Call: ${toolCall.function.name} ${toolCall.id}`, + ), + }, + ...(thoughtSignature && { thoughtSignature }), + }; + } + throw new Error( + `Unsupported tool call type in Gemini: ${toolCall.type}`, + ); + }, + ), }; } @@ -344,7 +364,7 @@ export class GeminiApi implements BaseLlmApi { if (contentParts) { for (const part of contentParts) { if ("text" in part) { - const thoughtSignature = (part as any)?.thoughtSignature; + const thoughtSignature = part?.thoughtSignature; if (thoughtSignature) { yield chatChunkFromDelta({ model, @@ -355,7 +375,7 @@ export class GeminiApi implements BaseLlmApi { thought_signature: thoughtSignature, }, }, - } as any, + } as GeminiToolDelta, }); } @@ -364,7 +384,7 @@ export class GeminiApi implements BaseLlmApi { model, }); } else if ("functionCall" in part) { - const thoughtSignature = (part as any)?.thoughtSignature; + const thoughtSignature = part?.thoughtSignature; yield chatChunkFromDelta({ model, delta: {