Skip to content

Commit 50ed923

Browse files
authored
feat(js/plugins/google-genai): Support for gemini 3.0 thinkingLevel. (#3814)
1 parent c391070 commit 50ed923

File tree

11 files changed

+238
-32
lines changed

11 files changed

+238
-32
lines changed

js/plugins/google-genai/src/common/converters.ts

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
MessageData,
2121
ModelReference,
2222
Part,
23+
TextPart,
2324
ToolDefinition,
2425
} from 'genkit/model';
2526
import {
@@ -131,34 +132,34 @@ function toGeminiMedia(part: Part): GeminiPart {
131132
media.videoMetadata = { ...videoMetadata };
132133
}
133134

134-
return media;
135+
return maybeAddGeminiThoughtSignature(part, media);
135136
}
136137

137138
function toGeminiToolRequest(part: Part): GeminiPart {
138139
if (!part.toolRequest?.input) {
139140
throw Error('Invalid ToolRequestPart: input was missing.');
140141
}
141-
return {
142+
return maybeAddGeminiThoughtSignature(part, {
142143
functionCall: {
143144
name: part.toolRequest.name,
144145
args: part.toolRequest.input,
145146
},
146-
};
147+
});
147148
}
148149

149150
function toGeminiToolResponse(part: Part): GeminiPart {
150151
if (!part.toolResponse?.output) {
151152
throw Error('Invalid ToolResponsePart: output was missing.');
152153
}
153-
return {
154+
return maybeAddGeminiThoughtSignature(part, {
154155
functionResponse: {
155156
name: part.toolResponse.name,
156157
response: {
157158
name: part.toolResponse.name,
158159
content: part.toolResponse.output,
159160
},
160161
},
161-
};
162+
});
162163
}
163164

164165
function toGeminiReasoning(part: Part): GeminiPart {
@@ -174,21 +175,38 @@ function toGeminiReasoning(part: Part): GeminiPart {
174175

175176
function toGeminiCustom(part: Part): GeminiPart {
176177
if (part.custom?.codeExecutionResult) {
177-
return {
178+
return maybeAddGeminiThoughtSignature(part, {
178179
codeExecutionResult: part.custom.codeExecutionResult,
179-
};
180+
});
180181
}
181182
if (part.custom?.executableCode) {
182-
return {
183+
return maybeAddGeminiThoughtSignature(part, {
183184
executableCode: part.custom.executableCode,
184-
};
185+
});
185186
}
186187
throw new Error('Unsupported Custom Part type');
187188
}
188189

190+
function toGeminiText(part: Part): GeminiPart {
191+
return maybeAddGeminiThoughtSignature(part, { text: part.text ?? '' });
192+
}
193+
194+
function maybeAddGeminiThoughtSignature(
195+
part: Part,
196+
geminiPart: GeminiPart
197+
): GeminiPart {
198+
if (part.metadata?.thoughtSignature) {
199+
return {
200+
...geminiPart,
201+
thoughtSignature: part.metadata.thoughtSignature as string,
202+
};
203+
}
204+
return geminiPart;
205+
}
206+
189207
function toGeminiPart(part: Part): GeminiPart {
190-
if (part.text) {
191-
return { text: part.text };
208+
if (typeof part.text === 'string') {
209+
return toGeminiText(part);
192210
}
193211
if (part.media) {
194212
return toGeminiMedia(part);
@@ -314,13 +332,27 @@ function fromGeminiFinishReason(
314332
case 'SPII': // blocked for potentially containing Sensitive Personally Identifiable Information
315333
return 'blocked';
316334
case 'MALFORMED_FUNCTION_CALL':
335+
case 'MISSING_THOUGHT_SIGNATURE':
317336
case 'OTHER':
318337
return 'other';
319338
default:
320339
return 'unknown';
321340
}
322341
}
323342

343+
function maybeAddThoughtSignature(geminiPart: GeminiPart, part: Part): Part {
344+
if (geminiPart.thoughtSignature) {
345+
return {
346+
...part,
347+
metadata: {
348+
...part?.metadata,
349+
thoughtSignature: geminiPart.thoughtSignature,
350+
},
351+
};
352+
}
353+
return part;
354+
}
355+
324356
function fromGeminiThought(part: GeminiPart): Part {
325357
return {
326358
reasoning: part.text || '',
@@ -340,12 +372,13 @@ function fromGeminiInlineData(part: GeminiPart): Part {
340372
const { mimeType, data } = part.inlineData;
341373
// Combine data and mimeType into a data URL
342374
const dataUrl = `data:${mimeType};base64,${data}`;
343-
return {
375+
376+
return maybeAddThoughtSignature(part, {
344377
media: {
345378
url: dataUrl,
346379
contentType: mimeType,
347380
},
348-
};
381+
});
349382
}
350383

351384
function fromGeminiFileData(part: GeminiPart): Part {
@@ -359,12 +392,12 @@ function fromGeminiFileData(part: GeminiPart): Part {
359392
);
360393
}
361394

362-
return {
395+
return maybeAddThoughtSignature(part, {
363396
media: {
364397
url: part.fileData?.fileUri,
365398
contentType: part.fileData?.mimeType,
366399
},
367-
};
400+
});
368401
}
369402

370403
function fromGeminiFunctionCall(part: GeminiPart, ref: string): Part {
@@ -373,13 +406,13 @@ function fromGeminiFunctionCall(part: GeminiPart, ref: string): Part {
373406
'Invalid Gemini Function Call Part: missing function call data'
374407
);
375408
}
376-
return {
409+
return maybeAddThoughtSignature(part, {
377410
toolRequest: {
378411
name: part.functionCall.name,
379412
input: part.functionCall.args,
380413
ref,
381414
},
382-
};
415+
});
383416
}
384417

385418
function fromGeminiFunctionResponse(part: GeminiPart, ref?: string): Part {
@@ -388,46 +421,50 @@ function fromGeminiFunctionResponse(part: GeminiPart, ref?: string): Part {
388421
'Invalid Gemini Function Call Part: missing function call data'
389422
);
390423
}
391-
return {
424+
return maybeAddThoughtSignature(part, {
392425
toolResponse: {
393426
name: part.functionResponse.name.replace(/__/g, '/'), // restore slashes
394427
output: part.functionResponse.response,
395428
ref,
396429
},
397-
};
430+
});
398431
}
399432

400433
function fromExecutableCode(part: GeminiPart): Part {
401434
if (!part.executableCode) {
402435
throw new Error('Invalid GeminiPart: missing executableCode');
403436
}
404-
return {
437+
return maybeAddThoughtSignature(part, {
405438
custom: {
406439
executableCode: {
407440
language: part.executableCode.language,
408441
code: part.executableCode.code,
409442
},
410443
},
411-
};
444+
});
412445
}
413446

414447
function fromCodeExecutionResult(part: GeminiPart): Part {
415448
if (!part.codeExecutionResult) {
416449
throw new Error('Invalid GeminiPart: missing codeExecutionResult');
417450
}
418-
return {
451+
return maybeAddThoughtSignature(part, {
419452
custom: {
420453
codeExecutionResult: {
421454
outcome: part.codeExecutionResult.outcome,
422455
output: part.codeExecutionResult.output,
423456
},
424457
},
425-
};
458+
});
459+
}
460+
461+
function fromGeminiText(part: GeminiPart): Part {
462+
return maybeAddThoughtSignature(part, { text: part.text } as TextPart);
426463
}
427464

428465
function fromGeminiPart(part: GeminiPart, ref: string): Part {
429466
if (part.thought) return fromGeminiThought(part as any);
430-
if (typeof part.text === 'string') return { text: part.text };
467+
if (typeof part.text === 'string') return fromGeminiText(part);
431468
if (part.inlineData) return fromGeminiInlineData(part);
432469
if (part.fileData) return fromGeminiFileData(part);
433470
if (part.functionCall) return fromGeminiFunctionCall(part, ref);

js/plugins/google-genai/src/common/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ export enum FinishReason {
448448
SPII = 'SPII',
449449
// The function call generated by the model is invalid.
450450
MALFORMED_FUNCTION_CALL = 'MALFORMED_FUNCTION_CALL',
451+
// At least one thought signature from a previous call is missing.
452+
MISSING_THOUGHT_SIGNATURE = 'MISSING_THOUGHT_SIGNATURE',
451453
// Unknown reason.
452454
OTHER = 'OTHER',
453455
}

js/plugins/google-genai/src/common/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,10 @@ function aggregateResponses(
479479
if (part.thought) {
480480
newPart.thought = part.thought;
481481
}
482-
if (part.text) {
482+
if (part.thoughtSignature) {
483+
newPart.thoughtSignature = part.thoughtSignature;
484+
}
485+
if (typeof part.text === 'string') {
483486
newPart.text = part.text;
484487
}
485488
if (part.functionCall) {

js/plugins/google-genai/src/googleai/gemini.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({
253253
.min(0)
254254
.max(24576)
255255
.describe(
256-
'Indicates the thinking budget in tokens. 0 is DISABLED. ' +
256+
'For Gemini 2.5 - Indicates the thinking budget in tokens. 0 is DISABLED. ' +
257257
'-1 is AUTOMATIC. The default values and allowed ranges are model ' +
258258
'dependent. The thinking budget parameter gives the model guidance ' +
259259
'on the number of thinking tokens it can use when generating a ' +
@@ -262,6 +262,14 @@ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({
262262
'tasks. '
263263
)
264264
.optional(),
265+
thinkingLevel: z
266+
.enum(['LOW', 'MEDIUM', 'HIGH'])
267+
.describe(
268+
'For Gemini 3.0 - Indicates the thinking level. A higher level ' +
269+
'is associated with more detailed thinking, which is needed for solving ' +
270+
'more complex tasks.'
271+
)
272+
.optional(),
265273
})
266274
.passthrough()
267275
.optional(),
@@ -364,6 +372,7 @@ const GENERIC_GEMMA_MODEL = commonRef(
364372
);
365373

366374
const KNOWN_GEMINI_MODELS = {
375+
'gemini-3-pro-preview': commonRef('gemini-3-pro-preview'),
367376
'gemini-2.5-pro': commonRef('gemini-2.5-pro'),
368377
'gemini-2.5-flash': commonRef('gemini-2.5-flash'),
369378
'gemini-2.5-flash-lite': commonRef('gemini-2.5-flash-lite'),

js/plugins/google-genai/src/vertexai/gemini.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({
301301
.min(0)
302302
.max(24576)
303303
.describe(
304-
'Indicates the thinking budget in tokens. 0 is DISABLED. ' +
304+
'For Gemini 2.5 - Indicates the thinking budget in tokens. 0 is DISABLED. ' +
305305
'-1 is AUTOMATIC. The default values and allowed ranges are model ' +
306306
'dependent. The thinking budget parameter gives the model guidance ' +
307307
'on the number of thinking tokens it can use when generating a ' +
@@ -310,6 +310,14 @@ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({
310310
'tasks. '
311311
)
312312
.optional(),
313+
thinkingLevel: z
314+
.enum(['LOW', 'MEDIUM', 'HIGH'])
315+
.describe(
316+
'For Gemini 3.0 - Indicates the thinking level. A higher level ' +
317+
'is associated with more detailed thinking, which is needed for solving ' +
318+
'more complex tasks.'
319+
)
320+
.optional(),
313321
})
314322
.passthrough()
315323
.optional(),
@@ -376,6 +384,7 @@ function commonRef(
376384
export const GENERIC_MODEL = commonRef('gemini');
377385

378386
export const KNOWN_MODELS = {
387+
'gemini-3-pro-preview': commonRef('gemini-3-pro-preview'),
379388
'gemini-2.5-flash-lite': commonRef('gemini-2.5-flash-lite'),
380389
'gemini-2.5-pro': commonRef('gemini-2.5-pro'),
381390
'gemini-2.5-flash': commonRef('gemini-2.5-flash'),

js/plugins/google-genai/tests/common/converters_test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,41 @@ describe('fromGeminiCandidate', () => {
450450
},
451451
},
452452
},
453+
{
454+
should:
455+
'should transform gemini candidate with thoughtSignature correctly',
456+
geminiCandidate: {
457+
index: 0,
458+
content: {
459+
role: 'model',
460+
parts: [
461+
{
462+
text: 'I have a thought.',
463+
thoughtSignature: 'xyz-789',
464+
},
465+
],
466+
},
467+
finishReason: 'STOP',
468+
},
469+
expectedOutput: {
470+
index: 0,
471+
message: {
472+
role: 'model',
473+
content: [
474+
{
475+
text: 'I have a thought.',
476+
metadata: { thoughtSignature: 'xyz-789' },
477+
},
478+
],
479+
},
480+
finishReason: 'stop',
481+
finishMessage: undefined,
482+
custom: {
483+
citationMetadata: undefined,
484+
safetyRatings: undefined,
485+
},
486+
},
487+
},
453488
{
454489
should:
455490
'should transform gemini candidate to genkit candidate (function call parts) correctly',

0 commit comments

Comments
 (0)