Skip to content

Commit 10cf2cc

Browse files
committed
add thoughtSignature Support and validate contents
1 parent b144d53 commit 10cf2cc

File tree

1 file changed

+75
-15
lines changed

1 file changed

+75
-15
lines changed

packages/ai-google/src/node/google-language-model.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface ToolCallback {
3838
readonly id: string;
3939
readonly index: number;
4040
args: string;
41+
thoughtSignature?: string;
4142
}
4243
const convertMessageToPart = (message: LanguageModelMessage): Part[] | undefined => {
4344
if (LanguageModelMessage.isTextMessage(message) && message.text.length > 0) {
@@ -52,7 +53,7 @@ const convertMessageToPart = (message: LanguageModelMessage): Part[] | undefined
5253
return [{ functionResponse: { id: message.tool_use_id, name: message.name, response: { output: message.content } } }];
5354

5455
} else if (LanguageModelMessage.isThinkingMessage(message)) {
55-
return [{ thought: true }, { text: message.thinking }];
56+
return [{ thought: true, text: message.thinking }];
5657
} else if (LanguageModelMessage.isImageMessage(message) && ImageContent.isBase64(message.image)) {
5758
return [{ inlineData: { data: message.image.base64data, mimeType: message.image.mimeType } }];
5859
}
@@ -76,31 +77,61 @@ function transformToGeminiMessages(
7677
continue; // Skip system messages as they're handled separately
7778
}
7879
const resultParts = convertMessageToPart(message);
79-
if (resultParts === undefined) {
80+
if (resultParts === undefined || resultParts.length === 0) {
8081
continue;
8182
}
8283

8384
const role = toGoogleRole(message);
84-
const lastContent = contents.pop();
85+
const lastContent = contents.length > 0 ? contents[contents.length - 1] : undefined;
8586

8687
if (!lastContent) {
8788
contents.push({ role, parts: resultParts });
8889
} else if (lastContent.role !== role) {
89-
contents.push(lastContent);
9090
contents.push({ role, parts: resultParts });
9191
} else {
92-
lastContent?.parts?.push(...resultParts);
93-
contents.push(lastContent);
92+
if (lastContent.parts) {
93+
lastContent.parts.push(...resultParts);
94+
} else {
95+
lastContent.parts = resultParts;
96+
}
9497
}
9598

9699
}
97100

101+
// Ensure we have at least one content
102+
if (contents.length === 0) {
103+
throw new Error('No valid messages to send to Gemini API');
104+
}
105+
98106
return {
99107
contents: contents,
100108
systemMessage,
101109
};
102110
}
103111

112+
/**
113+
* Validates that all items in the array are proper Content objects with role and parts
114+
* @param contents Array to validate
115+
* @throws Error if validation fails
116+
*/
117+
function validateContents(contents: Content[]): void {
118+
for (let i = 0; i < contents.length; i++) {
119+
const content = contents[i];
120+
if (!content || typeof content !== 'object') {
121+
throw new Error(`Invalid content at index ${i}: not an object`);
122+
}
123+
if (!content.role || (content.role !== 'user' && content.role !== 'model')) {
124+
throw new Error(`Invalid content at index ${i}: missing or invalid role (got ${content.role})`);
125+
}
126+
if (!content.parts || !Array.isArray(content.parts)) {
127+
throw new Error(`Invalid content at index ${i}: missing or invalid parts array`);
128+
}
129+
if (content.parts.length === 0) {
130+
throw new Error(`Invalid content at index ${i}: parts array is empty`);
131+
}
132+
}
133+
}
134+
104135
export const GoogleModelIdentifier = Symbol('GoogleModelIdentifier');
105136

106137
/**
@@ -171,6 +202,12 @@ export class GoogleModel implements LanguageModel {
171202
};
172203
}
173204

205+
// Merge contents and tool messages
206+
const allContents = [...contents, ...(toolMessages ?? [])];
207+
208+
// Validate all contents before making API call
209+
validateContents(allContents);
210+
174211
// Wrap the API call in the retry mechanism
175212
const stream = await this.withRetry(async () =>
176213
genAI.models.generateContentStream({
@@ -187,7 +224,7 @@ export class GoogleModel implements LanguageModel {
187224
temperature: 1,
188225
...settings
189226
},
190-
contents: [...contents, ...(toolMessages ?? [])]
227+
contents: allContents
191228
}));
192229

193230
const that = this;
@@ -228,11 +265,16 @@ export class GoogleModel implements LanguageModel {
228265
const callId = functionCall.id ?? crypto.randomUUID().replace(/-/g, '');
229266
let toolCall = toolCallMap[callId];
230267
if (toolCall === undefined) {
268+
const candidateParts = chunk.candidates?.[0]?.content?.parts;
269+
const matchingPart = candidateParts?.find(p =>
270+
p.functionCall?.id === callId && p.thoughtSignature
271+
);
231272
toolCall = {
232273
name: functionCall.name ?? '',
233274
args: functionCall.args ? JSON.stringify(functionCall.args) : '{}',
234275
id: callId,
235-
index: functionIndex++
276+
index: functionIndex++,
277+
thoughtSignature: matchingPart?.thoughtSignature
236278
};
237279
toolCallMap[callId] = toolCall;
238280

@@ -310,18 +352,33 @@ export class GoogleModel implements LanguageModel {
310352
yield { tool_calls: calls };
311353

312354
// Format tool responses for Gemini
313-
const toolResponses: Part[] = toolResult.map(call => ({
314-
functionResponse: {
315-
id: call.id,
316-
name: call.name,
317-
response: that.formatToolCallResult(call.result)
355+
const toolResponses: Part[] = toolResult.map(call => {
356+
const toolCall = toolCallMap[call.id];
357+
const part: Part = {
358+
functionResponse: {
359+
id: call.id,
360+
name: call.name,
361+
response: that.formatToolCallResult(call.result)
362+
}
363+
};
364+
if (toolCall?.thoughtSignature) {
365+
part.thoughtSignature = toolCall.thoughtSignature;
318366
}
319-
}));
367+
return part;
368+
});
320369
const responseMessage: Content = { role: 'user', parts: toolResponses };
321370

322371
const messages = [...(toolMessages ?? [])];
323372
if (currentContent) {
324-
messages.push(currentContent);
373+
// Ensure currentContent has proper structure
374+
if (!currentContent.role) {
375+
currentContent.role = 'model';
376+
}
377+
if (!currentContent.parts || currentContent.parts.length === 0) {
378+
console.warn('currentContent has no parts, skipping');
379+
} else {
380+
messages.push(currentContent);
381+
}
325382
}
326383
messages.push(responseMessage);
327384
// Continue the conversation with tool results
@@ -374,6 +431,9 @@ export class GoogleModel implements LanguageModel {
374431
const { contents, systemMessage } = transformToGeminiMessages(request.messages);
375432
const functionDeclarations = this.createFunctionDeclarations(request);
376433

434+
// Validate contents before making API call
435+
validateContents(contents);
436+
377437
// Wrap the API call in the retry mechanism
378438
const model = await this.withRetry(async () => genAI.models.generateContent({
379439
model: this.model,

0 commit comments

Comments
 (0)