@ai-sdk/google 3.0.78 → 3.0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "3.0.78",
3
+ "version": "3.0.80",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  UnsupportedFunctionalityError,
3
3
  type LanguageModelV3Prompt,
4
+ type SharedV3Warning,
4
5
  } from '@ai-sdk/provider';
5
6
  import { convertToBase64 } from '@ai-sdk/provider-utils';
6
7
  import type {
@@ -10,6 +11,48 @@ import type {
10
11
  GoogleGenerativeAIPrompt,
11
12
  } from './google-generative-ai-prompt';
12
13
 
14
+ /**
15
+ * Sentinel value Google documents for replaying functionCall parts whose
16
+ * original thoughtSignature is not available to the client.
17
+ *
18
+ * Gemini 3 models reject `functionCall` parts that lack a `thoughtSignature`
19
+ * with HTTP 400 "Function call is missing a thought_signature in functionCall
20
+ * parts." Sending this sentinel string in place of the missing signature
21
+ * makes Gemini skip the validator and continue the turn.
22
+ *
23
+ * See https://ai.google.dev/gemini-api/docs/thought-signatures.
24
+ */
25
+ export const SKIP_THOUGHT_SIGNATURE_VALIDATOR =
26
+ 'skip_thought_signature_validator';
27
+
28
+ type GoogleProviderOptions = {
29
+ thought?: unknown;
30
+ thoughtSignature?: unknown;
31
+ serverToolCallId?: unknown;
32
+ serverToolType?: unknown;
33
+ };
34
+
35
+ function getGoogleProviderOptions(
36
+ providerOptions: Record<string, GoogleProviderOptions> | undefined,
37
+ providerOptionsName: string,
38
+ ): GoogleProviderOptions | undefined {
39
+ const namespaces = [
40
+ providerOptionsName,
41
+ 'google',
42
+ 'googleVertex',
43
+ 'vertex',
44
+ ].filter((namespace, index, allNamespaces) => {
45
+ return allNamespaces.indexOf(namespace) === index;
46
+ });
47
+
48
+ for (const namespace of namespaces) {
49
+ const options = providerOptions?.[namespace];
50
+ if (options != null) {
51
+ return options;
52
+ }
53
+ }
54
+ }
55
+
13
56
  const dataUrlRegex = /^data:([^;,]+);base64,(.+)$/s;
14
57
 
15
58
  function parseBase64DataUrl(
@@ -168,17 +211,41 @@ export function convertToGoogleGenerativeAIMessages(
168
211
  prompt: LanguageModelV3Prompt,
169
212
  options?: {
170
213
  isGemmaModel?: boolean;
214
+ /**
215
+ * Whether the target model is in the Gemini 3 family. Gemini 3 enforces a
216
+ * `thoughtSignature` on every replayed `functionCall` part; when one is
217
+ * missing we inject the documented `skip_thought_signature_validator`
218
+ * sentinel and emit a warning via `onWarning` so the developer can find
219
+ * and fix the upstream serialization that lost the signature.
220
+ */
221
+ isGemini3Model?: boolean;
171
222
  providerOptionsName?: string;
172
223
  supportsFunctionResponseParts?: boolean;
224
+ /**
225
+ * Called once for the request when a Gemini 3 `functionCall` part is
226
+ * about to be sent without a `thoughtSignature` and the sentinel is
227
+ * injected.
228
+ */
229
+ onWarning?: (warning: SharedV3Warning) => void;
173
230
  },
174
231
  ): GoogleGenerativeAIPrompt {
175
232
  const systemInstructionParts: Array<{ text: string }> = [];
176
233
  const contents: Array<GoogleGenerativeAIContent> = [];
177
234
  let systemMessagesAllowed = true;
178
235
  const isGemmaModel = options?.isGemmaModel ?? false;
236
+ const isGemini3Model = options?.isGemini3Model ?? false;
179
237
  const providerOptionsName = options?.providerOptionsName ?? 'google';
180
238
  const supportsFunctionResponseParts =
181
239
  options?.supportsFunctionResponseParts ?? true;
240
+ const onWarning = options?.onWarning;
241
+
242
+ let sentinelInjected = false;
243
+ const missingSignatureToolNames: string[] = [];
244
+ const injectSkipSignature = (toolName: string) => {
245
+ missingSignatureToolNames.push(toolName);
246
+ sentinelInjected = true;
247
+ return SKIP_THOUGHT_SIGNATURE_VALIDATOR;
248
+ };
182
249
 
183
250
  for (const { role, content } of prompt) {
184
251
  switch (role) {
@@ -243,11 +310,10 @@ export function convertToGoogleGenerativeAIMessages(
243
310
  role: 'model',
244
311
  parts: content
245
312
  .map(part => {
246
- const providerOpts =
247
- part.providerOptions?.[providerOptionsName] ??
248
- (providerOptionsName !== 'google'
249
- ? part.providerOptions?.google
250
- : part.providerOptions?.vertex);
313
+ const providerOpts = getGoogleProviderOptions(
314
+ part.providerOptions,
315
+ providerOptionsName,
316
+ );
251
317
  const thoughtSignature =
252
318
  providerOpts?.thoughtSignature != null
253
319
  ? String(providerOpts.thoughtSignature)
@@ -303,6 +369,16 @@ export function convertToGoogleGenerativeAIMessages(
303
369
  ? String(providerOpts.serverToolType)
304
370
  : undefined;
305
371
 
372
+ // For Gemini 3, every replayed functionCall part must carry a
373
+ // thoughtSignature or the API returns HTTP 400. If the upstream
374
+ // serialization layer dropped the signature, inject the
375
+ // documented sentinel so the request still succeeds.
376
+ const effectiveThoughtSignature =
377
+ thoughtSignature ??
378
+ (isGemini3Model
379
+ ? injectSkipSignature(part.toolName)
380
+ : undefined);
381
+
306
382
  if (serverToolCallId && serverToolType) {
307
383
  return {
308
384
  toolCall: {
@@ -313,7 +389,7 @@ export function convertToGoogleGenerativeAIMessages(
313
389
  : part.input,
314
390
  id: serverToolCallId,
315
391
  },
316
- thoughtSignature,
392
+ thoughtSignature: effectiveThoughtSignature,
317
393
  };
318
394
  }
319
395
 
@@ -325,7 +401,7 @@ export function convertToGoogleGenerativeAIMessages(
325
401
  name: part.toolName,
326
402
  args: part.input,
327
403
  },
328
- thoughtSignature,
404
+ thoughtSignature: effectiveThoughtSignature,
329
405
  };
330
406
  }
331
407
 
@@ -371,11 +447,10 @@ export function convertToGoogleGenerativeAIMessages(
371
447
  continue;
372
448
  }
373
449
 
374
- const partProviderOpts =
375
- part.providerOptions?.[providerOptionsName] ??
376
- (providerOptionsName !== 'google'
377
- ? part.providerOptions?.google
378
- : part.providerOptions?.vertex);
450
+ const partProviderOpts = getGoogleProviderOptions(
451
+ part.providerOptions,
452
+ providerOptionsName,
453
+ );
379
454
  const serverToolCallId =
380
455
  partProviderOpts?.serverToolCallId != null
381
456
  ? String(partProviderOpts.serverToolCallId)
@@ -465,6 +540,23 @@ export function convertToGoogleGenerativeAIMessages(
465
540
  contents[0].parts.unshift({ text: systemText + '\n\n' });
466
541
  }
467
542
 
543
+ if (sentinelInjected && onWarning != null) {
544
+ const uniqueToolNames = Array.from(new Set(missingSignatureToolNames));
545
+ onWarning({
546
+ type: 'other',
547
+ message:
548
+ `Replayed ${missingSignatureToolNames.length} \`functionCall\` part(s) ` +
549
+ `for a Gemini 3 model without a \`thoughtSignature\` ` +
550
+ `(tools: ${uniqueToolNames.map(name => `\`${name}\``).join(', ')}). ` +
551
+ `Injected the documented \`skip_thought_signature_validator\` sentinel ` +
552
+ `to keep the request from failing with HTTP 400. ` +
553
+ `The likely cause is application code that drops ` +
554
+ '`providerOptions.google.thoughtSignature` when persisting or ' +
555
+ 'serializing assistant tool-call messages. ' +
556
+ 'See https://ai.google.dev/gemini-api/docs/thought-signatures.',
557
+ });
558
+ }
559
+
468
560
  return {
469
561
  systemInstruction:
470
562
  systemInstructionParts.length > 0 && !isGemmaModel
@@ -14,8 +14,8 @@ import {
14
14
  resolve,
15
15
  zodSchema,
16
16
  type FetchFunction,
17
- type Resolvable,
18
17
  type InferSchema,
18
+ type Resolvable,
19
19
  } from '@ai-sdk/provider-utils';
20
20
  import { z } from 'zod/v4';
21
21
  import { googleFailedResponseHandler } from './google-error';
@@ -25,6 +25,7 @@ import type {
25
25
  } from './google-generative-ai-image-settings';
26
26
  import { GoogleGenerativeAILanguageModel } from './google-generative-ai-language-model';
27
27
  import type { GoogleLanguageModelOptions } from './google-generative-ai-options';
28
+ import { googleSearchToolArgsBaseSchema } from './tool/google-search';
28
29
 
29
30
  interface GoogleGenerativeAIImageModelConfig {
30
31
  provider: string;
@@ -139,7 +140,17 @@ export class GoogleGenerativeAIImageModel implements ImageModelV3 {
139
140
  }
140
141
 
141
142
  if (googleOptions) {
142
- Object.assign(parameters, googleOptions);
143
+ const { googleSearch: imagenGoogleSearch, ...imagenOptions } =
144
+ googleOptions;
145
+ if (imagenGoogleSearch != null) {
146
+ warnings.push({
147
+ type: 'unsupported',
148
+ feature: 'googleSearch',
149
+ details:
150
+ 'Google Search grounding is only supported on Gemini image models.',
151
+ });
152
+ }
153
+ Object.assign(parameters, imagenOptions);
143
154
  }
144
155
 
145
156
  const body = {
@@ -257,6 +268,18 @@ export class GoogleGenerativeAIImageModel implements ImageModelV3 {
257
268
  { role: 'user', content: userContent },
258
269
  ];
259
270
 
271
+ // Parse image-model-specific provider options so we can map them onto
272
+ // the underlying language-model call. `googleSearch` is the dedicated
273
+ // escape hatch for grounding (generateImage has no `tools` parameter).
274
+ const googleImageOptions = await parseProviderOptions({
275
+ provider: 'google',
276
+ providerOptions,
277
+ schema: googleImageModelOptionsSchema,
278
+ });
279
+
280
+ const { googleSearch: _strippedGoogleSearch, ...passthroughGoogleOptions } =
281
+ providerOptions?.google ?? {};
282
+
260
283
  // Instantiate language model
261
284
  const languageModel = new GoogleGenerativeAILanguageModel(this.modelId, {
262
285
  provider: this.config.provider,
@@ -280,12 +303,23 @@ export class GoogleGenerativeAIImageModel implements ImageModelV3 {
280
303
  >['aspectRatio'],
281
304
  }
282
305
  : undefined,
283
- ...((providerOptions?.google as Omit<
306
+ ...(passthroughGoogleOptions as Omit<
284
307
  GoogleLanguageModelOptions,
285
308
  'responseModalities' | 'imageConfig'
286
- >) ?? {}),
309
+ >),
287
310
  } satisfies GoogleLanguageModelOptions,
288
311
  },
312
+ tools:
313
+ googleImageOptions?.googleSearch != null
314
+ ? [
315
+ {
316
+ type: 'provider',
317
+ id: 'google.google_search',
318
+ name: 'google_search',
319
+ args: googleImageOptions.googleSearch,
320
+ },
321
+ ]
322
+ : undefined,
289
323
  headers,
290
324
  abortSignal,
291
325
  });
@@ -300,11 +334,17 @@ export class GoogleGenerativeAIImageModel implements ImageModelV3 {
300
334
  }
301
335
  }
302
336
 
337
+ const languageModelGoogleMetadata =
338
+ (result.providerMetadata?.google as
339
+ | Record<string, unknown>
340
+ | undefined) ?? {};
341
+
303
342
  return {
304
343
  images,
305
344
  warnings,
306
345
  providerMetadata: {
307
346
  google: {
347
+ ...languageModelGoogleMetadata,
308
348
  images: images.map(() => ({})),
309
349
  },
310
350
  },
@@ -350,6 +390,17 @@ const googleImageModelOptionsSchema = lazySchema(() =>
350
390
  .enum(['dont_allow', 'allow_adult', 'allow_all'])
351
391
  .nullish(),
352
392
  aspectRatio: z.enum(['1:1', '3:4', '4:3', '9:16', '16:9']).nullish(),
393
+
394
+ /**
395
+ * Enable Google Search grounding for Gemini image models. The value is
396
+ * forwarded as the args of the `google.tools.googleSearch` provider
397
+ * tool on the underlying language-model call. Pass `{}` for defaults.
398
+ *
399
+ * `generateImage` does not accept a `tools` parameter, so this is the
400
+ * dedicated escape hatch for grounding image generation the same way
401
+ * `generateText` does.
402
+ */
403
+ googleSearch: googleSearchToolArgsBaseSchema.optional(),
353
404
  }),
354
405
  ),
355
406
  );
@@ -193,14 +193,17 @@ export class GoogleGenerativeAILanguageModel implements LanguageModelV3 {
193
193
  : googleOptions?.serviceTier;
194
194
 
195
195
  const isGemmaModel = this.modelId.toLowerCase().startsWith('gemma-');
196
- const supportsFunctionResponseParts = this.modelId.startsWith('gemini-3');
196
+ const isGemini3Model = /^gemini-3[.-]/.test(this.modelId);
197
+ const supportsFunctionResponseParts = isGemini3Model;
197
198
 
198
199
  const { contents, systemInstruction } = convertToGoogleGenerativeAIMessages(
199
200
  prompt,
200
201
  {
201
202
  isGemmaModel,
203
+ isGemini3Model,
202
204
  providerOptionsName,
203
205
  supportsFunctionResponseParts,
206
+ onWarning: warning => warnings.push(warning),
204
207
  },
205
208
  );
206
209
 
@@ -9,7 +9,7 @@ import { z } from 'zod/v4';
9
9
  // https://ai.google.dev/api/generate-content#GroundingSupport
10
10
  // https://cloud.google.com/vertex-ai/generative-ai/docs/grounding/grounding-with-google-search
11
11
 
12
- const googleSearchToolArgsBaseSchema = z
12
+ export const googleSearchToolArgsBaseSchema = z
13
13
  .object({
14
14
  searchTypes: z
15
15
  .object({