Skip to content

Commit 558f14e

Browse files
committed
update @google/genai dependency and add thoughtSignature support
Resolves GH-16640 - update @google/genai dependency - add thoughtSignature support - follow-up: double check loop behaviour with tool calls from time to time and tool call definitions is general in combination with gemini models
1 parent f7a065c commit 558f14e

File tree

3 files changed

+71
-18
lines changed

3 files changed

+71
-18
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ai-google/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.66.0",
44
"description": "Theia - Google AI Integration",
55
"dependencies": {
6-
"@google/genai": "^1.16.0",
6+
"@google/genai": "^1.30.0",
77
"@theia/ai-core": "1.66.0",
88
"@theia/core": "1.66.0"
99
},

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

Lines changed: 66 additions & 13 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
}
@@ -101,6 +102,29 @@ function transformToGeminiMessages(
101102
};
102103
}
103104

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

106130
/**
@@ -160,7 +184,7 @@ export class GoogleModel implements LanguageModel {
160184
toolMessages?: Content[]
161185
): Promise<LanguageModelStreamResponse> {
162186
const settings = this.getSettings(request);
163-
const { contents: parts, systemMessage } = transformToGeminiMessages(request.messages);
187+
const { contents, systemMessage } = transformToGeminiMessages(request.messages);
164188
const functionDeclarations = this.createFunctionDeclarations(request);
165189

166190
const toolConfig: ToolConfig = {};
@@ -171,6 +195,12 @@ export class GoogleModel implements LanguageModel {
171195
};
172196
}
173197

198+
// Merge contents and tool messages
199+
const allContents = [...contents, ...(toolMessages ?? [])];
200+
201+
// Validate all contents before making API call
202+
validateContents(allContents);
203+
174204
// Wrap the API call in the retry mechanism
175205
const stream = await this.withRetry(async () =>
176206
genAI.models.generateContentStream({
@@ -187,7 +217,7 @@ export class GoogleModel implements LanguageModel {
187217
temperature: 1,
188218
...settings
189219
},
190-
contents: [...parts, ...(toolMessages ?? [])]
220+
contents: allContents
191221
}));
192222

193223
const that = this;
@@ -228,11 +258,16 @@ export class GoogleModel implements LanguageModel {
228258
const callId = functionCall.id ?? crypto.randomUUID().replace(/-/g, '');
229259
let toolCall = toolCallMap[callId];
230260
if (toolCall === undefined) {
261+
const candidateParts = chunk.candidates?.[0]?.content?.parts;
262+
const matchingPart = candidateParts?.find(p =>
263+
p.functionCall?.id === callId && p.thoughtSignature
264+
);
231265
toolCall = {
232266
name: functionCall.name ?? '',
233267
args: functionCall.args ? JSON.stringify(functionCall.args) : '{}',
234268
id: callId,
235-
index: functionIndex++
269+
index: functionIndex++,
270+
thoughtSignature: matchingPart?.thoughtSignature
236271
};
237272
toolCallMap[callId] = toolCall;
238273

@@ -310,18 +345,33 @@ export class GoogleModel implements LanguageModel {
310345
yield { tool_calls: calls };
311346

312347
// 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)
348+
const toolResponses: Part[] = toolResult.map(call => {
349+
const toolCall = toolCallMap[call.id];
350+
const part: Part = {
351+
functionResponse: {
352+
id: call.id,
353+
name: call.name,
354+
response: that.formatToolCallResult(call.result)
355+
}
356+
};
357+
if (toolCall?.thoughtSignature) {
358+
part.thoughtSignature = toolCall.thoughtSignature;
318359
}
319-
}));
360+
return part;
361+
});
320362
const responseMessage: Content = { role: 'user', parts: toolResponses };
321363

322364
const messages = [...(toolMessages ?? [])];
323365
if (currentContent) {
324-
messages.push(currentContent);
366+
// Ensure currentContent has proper structure
367+
if (!currentContent.role) {
368+
currentContent.role = 'model';
369+
}
370+
if (!currentContent.parts || currentContent.parts.length === 0) {
371+
console.debug('currentContent has no parts, skipping');
372+
} else {
373+
messages.push(currentContent);
374+
}
325375
}
326376
messages.push(responseMessage);
327377
// Continue the conversation with tool results
@@ -371,9 +421,12 @@ export class GoogleModel implements LanguageModel {
371421
request: UserRequest
372422
): Promise<LanguageModelTextResponse> {
373423
const settings = this.getSettings(request);
374-
const { contents: parts, systemMessage } = transformToGeminiMessages(request.messages);
424+
const { contents, systemMessage } = transformToGeminiMessages(request.messages);
375425
const functionDeclarations = this.createFunctionDeclarations(request);
376426

427+
// Validate contents before making API call
428+
validateContents(contents);
429+
377430
// Wrap the API call in the retry mechanism
378431
const model = await this.withRetry(async () => genAI.models.generateContent({
379432
model: this.model,
@@ -389,7 +442,7 @@ export class GoogleModel implements LanguageModel {
389442
}),
390443
...settings
391444
},
392-
contents: parts
445+
contents
393446
}));
394447

395448
try {

0 commit comments

Comments
 (0)