Skip to content

Commit 180604f

Browse files
authored
Merge pull request #8865 from uinstinct/gemini-thought-signature
feat: add support for gemini thought signature
2 parents 0fd1594 + 1031cbb commit 180604f

File tree

3 files changed

+122
-27
lines changed

3 files changed

+122
-27
lines changed

core/llm/llms/Gemini.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import {
2323
convertContinueToolToGeminiFunction,
2424
} from "./gemini-types";
2525

26+
interface GeminiToolCallDelta extends ToolCallDelta {
27+
extra_content?: {
28+
google?: {
29+
thought_signature?: string;
30+
};
31+
};
32+
}
33+
2634
class Gemini extends BaseLLM {
2735
static providerName = "gemini";
2836

@@ -266,18 +274,37 @@ class Gemini extends BaseLLM {
266274
? [{ text: msg.content }]
267275
: msg.content.map(this.continuePartToGeminiPart),
268276
};
269-
if (msg.toolCalls) {
270-
msg.toolCalls.forEach((toolCall) => {
271-
if (toolCall.function?.name) {
272-
assistantMsg.parts.push({
273-
functionCall: {
274-
name: toolCall.function.name,
275-
args: safeParseToolCallArgs(toolCall),
276-
},
277-
});
278-
}
279-
});
277+
278+
if (msg.toolCalls && msg.toolCalls.length) {
279+
(msg.toolCalls as GeminiToolCallDelta[]).forEach(
280+
(toolCall, index) => {
281+
if (toolCall.function?.name) {
282+
const signatureForCall =
283+
toolCall?.extra_content?.google?.thought_signature;
284+
285+
let thoughtSignature: string | undefined;
286+
if (index === 0) {
287+
if (typeof signatureForCall === "string") {
288+
thoughtSignature = signatureForCall;
289+
} else {
290+
// Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures
291+
// for histories that were not generated by Gemini or are missing signatures.
292+
thoughtSignature = "skip_thought_signature_validator";
293+
}
294+
}
295+
296+
assistantMsg.parts.push({
297+
functionCall: {
298+
name: toolCall.function.name,
299+
args: safeParseToolCallArgs(toolCall),
300+
},
301+
...(thoughtSignature && { thoughtSignature }),
302+
});
303+
}
304+
},
305+
);
280306
}
307+
281308
return assistantMsg;
282309
}
283310
return {
@@ -370,6 +397,7 @@ class Gemini extends BaseLLM {
370397
if ("text" in part) {
371398
textParts.push({ type: "text", text: part.text });
372399
} else if ("functionCall" in part) {
400+
const thoughtSignature = part.thoughtSignature;
373401
toolCalls.push({
374402
type: "function",
375403
id: part.functionCall.id ?? uuidv4(),
@@ -380,6 +408,13 @@ class Gemini extends BaseLLM {
380408
? part.functionCall.args
381409
: JSON.stringify(part.functionCall.args),
382410
},
411+
...(thoughtSignature && {
412+
extra_content: {
413+
google: {
414+
thought_signature: thoughtSignature,
415+
},
416+
},
417+
}),
383418
});
384419
} else {
385420
// Note: function responses shouldn't be streamed, images not supported

core/llm/llms/gemini-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export type GeminiFunctionCallContentPart = {
192192
name: string;
193193
args: JSONSchema7Object;
194194
};
195+
thoughtSignature?: string;
195196
};
196197

197198
export type GeminiFunctionResponseContentPart = {

packages/openai-adapters/src/apis/Gemini.ts

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ type UsageInfo = Pick<
4444
"total_tokens" | "completion_tokens" | "prompt_tokens"
4545
>;
4646

47+
interface GeminiToolCall
48+
extends OpenAI.Chat.Completions.ChatCompletionMessageFunctionToolCall {
49+
extra_content?: {
50+
google?: {
51+
thought_signature?: string;
52+
};
53+
};
54+
}
55+
56+
interface GeminiToolDelta
57+
extends OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta {
58+
extra_content?: {
59+
google?: {
60+
thought_signature?: string;
61+
};
62+
};
63+
}
64+
4765
export class GeminiApi implements BaseLlmApi {
4866
apiBase: string = "https://generativelanguage.googleapis.com/v1beta/";
4967

@@ -143,25 +161,43 @@ export class GeminiApi implements BaseLlmApi {
143161

144162
return {
145163
role: "model" as const,
146-
parts: msg.tool_calls.map((toolCall) => {
147-
// Type guard for function tool calls
148-
if (toolCall.type === "function" && "function" in toolCall) {
149-
return {
150-
functionCall: {
151-
id: includeToolCallIds ? toolCall.id : undefined,
152-
name: toolCall.function.name,
153-
args: safeParseArgs(
154-
toolCall.function.arguments,
155-
`Call: ${toolCall.function.name} ${toolCall.id}`,
156-
),
157-
},
158-
};
159-
} else {
164+
parts: (msg.tool_calls as GeminiToolCall[]).map(
165+
(toolCall, index) => {
166+
if (toolCall.type === "function" && "function" in toolCall) {
167+
let thoughtSignature: string | undefined;
168+
if (index === 0) {
169+
const rawSignature =
170+
toolCall?.extra_content?.google?.thought_signature;
171+
172+
if (
173+
typeof rawSignature === "string" &&
174+
rawSignature.length > 0
175+
) {
176+
thoughtSignature = rawSignature;
177+
} else {
178+
// Fallback per https://ai.google.dev/gemini-api/docs/thought-signatures
179+
// for histories that were not generated by Gemini or are missing signatures.
180+
thoughtSignature = "skip_thought_signature_validator";
181+
}
182+
}
183+
184+
return {
185+
functionCall: {
186+
id: includeToolCallIds ? toolCall.id : undefined,
187+
name: toolCall.function.name,
188+
args: safeParseArgs(
189+
toolCall.function.arguments,
190+
`Call: ${toolCall.function.name} ${toolCall.id}`,
191+
),
192+
},
193+
...(thoughtSignature && { thoughtSignature }),
194+
};
195+
}
160196
throw new Error(
161197
`Unsupported tool call type in Gemini: ${toolCall.type}`,
162198
);
163-
}
164-
}),
199+
},
200+
),
165201
};
166202
}
167203

@@ -328,11 +364,27 @@ export class GeminiApi implements BaseLlmApi {
328364
if (contentParts) {
329365
for (const part of contentParts) {
330366
if ("text" in part) {
367+
const thoughtSignature = part?.thoughtSignature;
368+
if (thoughtSignature) {
369+
yield chatChunkFromDelta({
370+
model,
371+
delta: {
372+
role: "assistant",
373+
extra_content: {
374+
google: {
375+
thought_signature: thoughtSignature,
376+
},
377+
},
378+
} as GeminiToolDelta,
379+
});
380+
}
381+
331382
yield chatChunk({
332383
content: part.text,
333384
model,
334385
});
335386
} else if ("functionCall" in part) {
387+
const thoughtSignature = part?.thoughtSignature;
336388
yield chatChunkFromDelta({
337389
model,
338390
delta: {
@@ -345,6 +397,13 @@ export class GeminiApi implements BaseLlmApi {
345397
name: part.functionCall.name,
346398
arguments: JSON.stringify(part.functionCall.args),
347399
},
400+
...(thoughtSignature && {
401+
extra_content: {
402+
google: {
403+
thought_signature: thoughtSignature,
404+
},
405+
},
406+
}),
348407
},
349408
],
350409
},

0 commit comments

Comments
 (0)