Fix Gemini 3.1 Pro thought_signature in tool calls
Thinking models attach a thought_signature to functionCall parts. Must be echoed back in functionResponse or API returns 400. Carry it through ToolCall -> ChatMessage -> toGeminiContents(). Made-with: Cursor
This commit is contained in:
@@ -183,6 +183,8 @@ export async function POST(request: Request) {
|
||||
content: result,
|
||||
toolCallId: tc.id,
|
||||
toolName: tc.name,
|
||||
// Echo thought_signature back — required by Gemini thinking models
|
||||
thoughtSignature: tc.thoughtSignature,
|
||||
};
|
||||
messages.push(toolMsg);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
* Uses the Gemini API (generativelanguage.googleapis.com) with the
|
||||
* existing GOOGLE_API_KEY. Drop-in upgrade to Vertex AI when needed
|
||||
* by swapping GEMINI_BASE_URL.
|
||||
*
|
||||
* NOTE: Gemini thinking models (2.5+, 3.x) attach a `thought_signature`
|
||||
* to functionCall parts. This signature MUST be echoed back in the
|
||||
* functionResponse or the API returns a 400. We carry it through our
|
||||
* ToolCall type and re-attach it when building contents[].
|
||||
*/
|
||||
|
||||
const GEMINI_API_KEY = process.env.GOOGLE_API_KEY || '';
|
||||
@@ -18,12 +23,16 @@ export interface ChatMessage {
|
||||
/** Populated when role === 'tool' */
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
/** Echo thought_signature back for tool responses (thinking models) */
|
||||
thoughtSignature?: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
/** Gemini thinking-model signature — must be echoed in functionResponse */
|
||||
thoughtSignature?: string;
|
||||
}
|
||||
|
||||
export interface ToolDefinition {
|
||||
@@ -51,20 +60,23 @@ function toGeminiContents(messages: ChatMessage[]) {
|
||||
if (msg.content) parts.push({ text: msg.content });
|
||||
if (msg.toolCalls?.length) {
|
||||
for (const tc of msg.toolCalls) {
|
||||
parts.push({ functionCall: { name: tc.name, args: tc.args, id: tc.id } });
|
||||
const fc: any = { name: tc.name, args: tc.args, id: tc.id };
|
||||
if (tc.thoughtSignature) fc.thought_signature = tc.thoughtSignature;
|
||||
parts.push({ functionCall: fc });
|
||||
}
|
||||
}
|
||||
if (parts.length) contents.push({ role: 'model', parts });
|
||||
} else if (msg.role === 'tool') {
|
||||
// Tool results must be bundled after the model turn that requested them
|
||||
const last = contents[contents.length - 1];
|
||||
const part = {
|
||||
functionResponse: {
|
||||
name: msg.toolName || 'unknown',
|
||||
id: msg.toolCallId,
|
||||
response: { content: msg.content },
|
||||
},
|
||||
const fr: any = {
|
||||
name: msg.toolName || 'unknown',
|
||||
id: msg.toolCallId,
|
||||
response: { content: msg.content },
|
||||
};
|
||||
// Echo the thought_signature back — required for Gemini thinking models
|
||||
if (msg.thoughtSignature) fr.thought_signature = msg.thoughtSignature;
|
||||
|
||||
const part = { functionResponse: fr };
|
||||
const last = contents[contents.length - 1];
|
||||
if (last?.role === 'user') {
|
||||
last.parts.push(part);
|
||||
} else {
|
||||
@@ -178,6 +190,8 @@ export async function* streamGeminiChat(opts: {
|
||||
id: part.functionCall.id || `tc-${Date.now()}`,
|
||||
name: part.functionCall.name,
|
||||
args: part.functionCall.args ?? {},
|
||||
// Carry the thought_signature so the chat route can echo it back
|
||||
thoughtSignature: part.functionCall.thought_signature,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user