@ai-sdk/google 4.0.51 → 4.0.53

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.
@@ -1067,10 +1067,13 @@ The following Zod features are known to not work with Google:
1067
1067
  ### Text Batches
1068
1068
 
1069
1069
  <Note type="warning">
1070
- Text batch APIs are experimental and may change in future releases.
1070
+ Text batch support is experimental and the API may change in patch releases.
1071
1071
  </Note>
1072
1072
 
1073
- The Google provider supports the [Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api) for text generation.
1073
+ The Google provider supports asynchronous text generation through the
1074
+ [Gemini Batch API](https://ai.google.dev/gemini-api/docs/batch-api). Use the
1075
+ experimental text batch APIs to start a batch, poll its status, and stream its
1076
+ results:
1074
1077
 
1075
1078
  ```ts
1076
1079
  import { google } from '@ai-sdk/google';
@@ -1079,32 +1082,39 @@ import {
1079
1082
  experimental_getBatchStatus as getBatchStatus,
1080
1083
  experimental_startTextBatch as startTextBatch,
1081
1084
  } from 'ai';
1085
+ import { setTimeout } from 'node:timers/promises';
1082
1086
 
1083
1087
  const model = google('gemini-3.6-flash');
1084
1088
 
1085
1089
  const batch = await startTextBatch({
1086
1090
  model,
1087
1091
  requests: [
1088
- { id: 'france', prompt: 'What is the capital of France?' },
1089
- { id: 'germany', prompt: 'What is the capital of Germany?' },
1092
+ { id: 'capital-france', prompt: 'What is the capital of France?' },
1093
+ { id: 'capital-germany', prompt: 'What is the capital of Germany?' },
1090
1094
  ],
1091
1095
  });
1092
1096
 
1093
- // Persist `batch` and check its status later, or poll for status updates.
1094
- const { status } = await getBatchStatus({ model, batch });
1097
+ let status = batch.status;
1098
+ while (status === 'pending') {
1099
+ await setTimeout(60_000);
1100
+ ({ status } = await getBatchStatus({ model, batch }));
1101
+ }
1095
1102
 
1096
- if (status !== 'pending') {
1097
- for await (const item of getBatchResults({ model, batch })) {
1098
- if (item.status === 'succeeded') {
1099
- console.log(item.id, item.text);
1100
- } else {
1101
- console.error(item.id, item.error);
1102
- }
1103
+ for await (const item of getBatchResults({ model, batch })) {
1104
+ if (item.status === 'succeeded') {
1105
+ console.log(item.id, item.text);
1106
+ } else {
1107
+ console.error(item.id, item.error);
1103
1108
  }
1104
1109
  }
1105
1110
  ```
1106
1111
 
1107
- Starting a batch returns a serializable reference that can be persisted and used to retrieve the batch results later, or poll for status updates.
1112
+ `startTextBatch` returns a serializable batch reference. Persist this reference
1113
+ to check the batch status or retrieve its results from another process. Results
1114
+ can arrive in a different order from the input requests, so match each result by
1115
+ its `id`.
1116
+
1117
+ #### Webhooks
1108
1118
 
1109
1119
  You can pass a `webhookUrl` to receive a notification when the batch reaches a terminal state:
1110
1120
 
@@ -1112,8 +1122,8 @@ You can pass a `webhookUrl` to receive a notification when the batch reaches a t
1112
1122
  const batch = await startTextBatch({
1113
1123
  model,
1114
1124
  requests: [
1115
- { id: 'france', prompt: 'What is the capital of France?' },
1116
- { id: 'germany', prompt: 'What is the capital of Germany?' },
1125
+ { id: 'capital-france', prompt: 'What is the capital of France?' },
1126
+ { id: 'capital-germany', prompt: 'What is the capital of Germany?' },
1117
1127
  ],
1118
1128
  webhookUrl: 'https://example.com/api/google-batch-webhook',
1119
1129
  });
@@ -1121,6 +1131,8 @@ const batch = await startTextBatch({
1121
1131
 
1122
1132
  Google sends a thin event to the webhook URL. After receiving and verifying the event, use the persisted batch reference with `getBatchStatus` or `getBatchResults`. Your application is responsible for handling the webhook and [verifying Google's dynamic webhook signature](https://ai.google.dev/gemini-api/docs/webhooks#verify_dynamic_signatures_jwks).
1123
1133
 
1134
+ #### Large Batches
1135
+
1124
1136
  When the serialized batch creation body is under 20 MB, the provider sends the requests inline. At 20 MB or more, it uploads a JSONL input file through the Gemini Files API.
1125
1137
 
1126
1138
  ## Realtime Models
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.51",
3
+ "version": "4.0.53",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.8",
39
- "@ai-sdk/provider-utils": "5.0.30"
39
+ "@ai-sdk/provider-utils": "5.0.32"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -14,6 +14,18 @@ type ReferenceContext = {
14
14
  resolvingReferences: ReadonlySet<string>;
15
15
  };
16
16
 
17
+ const recursiveReferenceFunctionalityPrefix =
18
+ 'recursive JSON Schema reference:';
19
+
20
+ export function isRecursiveJSONSchemaReferenceError(
21
+ error: unknown,
22
+ ): error is UnsupportedFunctionalityError {
23
+ return (
24
+ UnsupportedFunctionalityError.isInstance(error) &&
25
+ error.functionality.startsWith(recursiveReferenceFunctionalityPrefix)
26
+ );
27
+ }
28
+
17
29
  /**
18
30
  * Converts JSON Schema 7 to OpenAPI Schema 3.0
19
31
  */
@@ -205,7 +217,7 @@ function convertJSONSchemaReference({
205
217
 
206
218
  if (referenceContext.resolvingReferences.has(referenceKey)) {
207
219
  throw new UnsupportedFunctionalityError({
208
- functionality: `recursive JSON Schema reference: ${reference}`,
220
+ functionality: `${recursiveReferenceFunctionalityPrefix} ${reference}`,
209
221
  message:
210
222
  'Google schema conversion does not support recursive JSON Schema references.',
211
223
  });
@@ -1,5 +1,4 @@
1
1
  import {
2
- EmptyResponseBodyError,
3
2
  InvalidArgumentError,
4
3
  InvalidResponseDataError,
5
4
  type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
@@ -14,11 +13,12 @@ import {
14
13
  import {
15
14
  combineHeaders,
16
15
  convertAsyncIteratorToReadableStream,
16
+ createJsonLinesResponseHandler,
17
17
  createJsonResponseHandler,
18
18
  generateId,
19
19
  getFromApi,
20
20
  lazySchema,
21
- parseJSON,
21
+ normalizeBatchRequestCounts,
22
22
  postJsonToApi,
23
23
  postToApi,
24
24
  resolve,
@@ -399,18 +399,20 @@ export class GoogleBatchLanguageModel
399
399
  .map(segment => encodeURIComponent(segment))
400
400
  .join('/');
401
401
 
402
- const { value: stream } = await getFromApi({
402
+ const { value: lines } = await getFromApi({
403
403
  url: `${this.getBaseOrigin()}/download/v1beta/${encodedResponsesFile}:download?alt=media`,
404
404
  headers: await this.getHeaders(options.headers),
405
405
  failedResponseHandler: googleFailedResponseHandler,
406
- successfulResponseHandler: rawStreamResponseHandler,
406
+ successfulResponseHandler: createJsonLinesResponseHandler(
407
+ googleBatchResultLineSchema,
408
+ ),
407
409
  abortSignal: options.abortSignal,
408
410
  fetch: this.batchConfig.fetch,
409
411
  validateUrl: false,
410
412
  });
411
413
 
412
414
  return convertAsyncIteratorToReadableStream(
413
- this.iterateBatchResults(parseJsonLines(stream)),
415
+ this.iterateBatchResults(lines),
414
416
  );
415
417
  }
416
418
 
@@ -626,22 +628,12 @@ function convertGoogleRequestCounts(
626
628
  const failed = parseCount(counts?.failedRequestCount ?? 0);
627
629
  const pending = parseCount(counts?.pendingRequestCount ?? 0);
628
630
 
629
- if (
630
- total == null ||
631
- completed == null ||
632
- failed == null ||
633
- pending == null ||
634
- completed + failed + pending !== total
635
- ) {
636
- return undefined;
637
- }
638
-
639
- return {
631
+ return normalizeBatchRequestCounts({
640
632
  total,
641
633
  pending,
642
634
  completed,
643
635
  failed,
644
- };
636
+ });
645
637
  }
646
638
 
647
639
  function parseCount(value: string | number | null | undefined) {
@@ -676,64 +668,3 @@ const googleUploadUrlResponseHandler: ResponseHandler<string> = async ({
676
668
 
677
669
  return { value: uploadUrl };
678
670
  };
679
-
680
- const rawStreamResponseHandler: ResponseHandler<
681
- ReadableStream<Uint8Array>
682
- > = async ({ response }) => {
683
- if (response.body == null) {
684
- throw new EmptyResponseBodyError();
685
- }
686
-
687
- return { value: response.body };
688
- };
689
-
690
- async function* parseJsonLines(
691
- stream: ReadableStream<Uint8Array>,
692
- ): AsyncGenerator<GoogleBatchResultLine> {
693
- const reader = stream.getReader();
694
- const decoder = new TextDecoder();
695
- let buffer = '';
696
- let finished = false;
697
-
698
- try {
699
- while (true) {
700
- const { done, value } = await reader.read();
701
-
702
- if (done) {
703
- finished = true;
704
- buffer += decoder.decode();
705
- break;
706
- }
707
-
708
- buffer += decoder.decode(value, { stream: true });
709
-
710
- let lineEnd = buffer.indexOf('\n');
711
- while (lineEnd !== -1) {
712
- const line = buffer.slice(0, lineEnd).replace(/\r$/, '');
713
- buffer = buffer.slice(lineEnd + 1);
714
-
715
- if (line.trim().length > 0) {
716
- yield await parseJSON({
717
- text: line,
718
- schema: googleBatchResultLineSchema,
719
- });
720
- }
721
-
722
- lineEnd = buffer.indexOf('\n');
723
- }
724
- }
725
-
726
- const finalLine = buffer.replace(/\r$/, '');
727
- if (finalLine.trim().length > 0) {
728
- yield await parseJSON({
729
- text: finalLine,
730
- schema: googleBatchResultLineSchema,
731
- });
732
- }
733
- } finally {
734
- if (!finished) {
735
- await reader.cancel().catch(() => {});
736
- }
737
- reader.releaseLock();
738
- }
739
- }
@@ -34,6 +34,17 @@ interface GoogleFilesConfig {
34
34
  fetch?: FetchFunction;
35
35
  }
36
36
 
37
+ function encodePathSegment(value: string): string {
38
+ const encodedValue = encodeURIComponent(value);
39
+
40
+ // URL parsing normalizes both literal and percent-encoded dot segments.
41
+ return encodedValue === '.'
42
+ ? '%252E'
43
+ : encodedValue === '..'
44
+ ? '%252E%252E'
45
+ : encodedValue;
46
+ }
47
+
37
48
  export class GoogleFiles implements FilesV4 {
38
49
  readonly specificationVersion = 'v4';
39
50
 
@@ -106,7 +117,7 @@ export class GoogleFiles implements FilesV4 {
106
117
  'X-Goog-Upload-Offset': '0',
107
118
  'X-Goog-Upload-Command': 'upload, finalize',
108
119
  },
109
- body: fileBytes,
120
+ body: ensureArrayBufferBacked(fileBytes),
110
121
  });
111
122
 
112
123
  if (!uploadResponse.ok) {
@@ -137,8 +148,14 @@ export class GoogleFiles implements FilesV4 {
137
148
 
138
149
  await delay(pollIntervalMs);
139
150
 
151
+ const fileNameMatch = /^files\/([^/]+)$/.exec(file.name);
152
+ const filePath =
153
+ fileNameMatch != null
154
+ ? `files/${encodePathSegment(fileNameMatch[1])}`
155
+ : encodePathSegment(file.name);
156
+
140
157
  const { value: fileStatus } = await getFromApi({
141
- url: `${this.config.baseURL}/${file.name}`,
158
+ url: `${this.config.baseURL}/${filePath}`,
142
159
  validateUrl: false,
143
160
  headers: combineHeaders(resolvedHeaders),
144
161
  successfulResponseHandler: createJsonResponseHandler(
@@ -182,6 +199,14 @@ export class GoogleFiles implements FilesV4 {
182
199
  }
183
200
  }
184
201
 
202
+ function ensureArrayBufferBacked(data: Uint8Array): Uint8Array<ArrayBuffer> {
203
+ if (data.buffer instanceof ArrayBuffer) {
204
+ return data as Uint8Array<ArrayBuffer>;
205
+ }
206
+
207
+ return new Uint8Array(data);
208
+ }
209
+
185
210
  type GoogleFileResource = {
186
211
  name: string;
187
212
  displayName?: string | null;
@@ -3,10 +3,25 @@ import {
3
3
  type LanguageModelV4CallOptions,
4
4
  type SharedV4Warning,
5
5
  } from '@ai-sdk/provider';
6
- import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema';
6
+ import {
7
+ convertJSONSchemaToOpenAPISchema,
8
+ isRecursiveJSONSchemaReferenceError,
9
+ } from './convert-json-schema-to-openapi-schema';
7
10
  import type { GoogleModelId } from './google-language-model-options';
8
11
  import { getGoogleModelCapabilities } from './google-model-capabilities';
9
12
 
13
+ type FunctionTool = Extract<
14
+ NonNullable<LanguageModelV4CallOptions['tools']>[number],
15
+ { type: 'function' }
16
+ >;
17
+
18
+ type GoogleFunctionDeclaration = {
19
+ name: string;
20
+ description: string;
21
+ parameters?: unknown;
22
+ parametersJsonSchema?: unknown;
23
+ };
24
+
10
25
  export function prepareTools({
11
26
  tools,
12
27
  toolChoice,
@@ -21,11 +36,7 @@ export function prepareTools({
21
36
  tools:
22
37
  | Array<
23
38
  | {
24
- functionDeclarations: Array<{
25
- name: string;
26
- description: string;
27
- parameters: unknown;
28
- }>;
39
+ functionDeclarations: GoogleFunctionDeclaration[];
29
40
  }
30
41
  | Record<string, any>
31
42
  >
@@ -172,18 +183,10 @@ export function prepareTools({
172
183
  });
173
184
 
174
185
  if (hasFunctionTools && usesGemini3Features && googleTools.length > 0) {
175
- const functionDeclarations: Array<{
176
- name: string;
177
- description: string;
178
- parameters: unknown;
179
- }> = [];
186
+ const functionDeclarations: GoogleFunctionDeclaration[] = [];
180
187
  for (const tool of tools) {
181
188
  if (tool.type === 'function') {
182
- functionDeclarations.push({
183
- name: tool.name,
184
- description: tool.description ?? '',
185
- parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
186
- });
189
+ functionDeclarations.push(prepareFunctionDeclaration(tool));
187
190
  }
188
191
  }
189
192
 
@@ -238,11 +241,7 @@ export function prepareTools({
238
241
  for (const tool of tools) {
239
242
  switch (tool.type) {
240
243
  case 'function':
241
- functionDeclarations.push({
242
- name: tool.name,
243
- description: tool.description ?? '',
244
- parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
245
- });
244
+ functionDeclarations.push(prepareFunctionDeclaration(tool));
246
245
  if (tool.strict === true) {
247
246
  hasStrictTools = true;
248
247
  }
@@ -314,3 +313,28 @@ export function prepareTools({
314
313
  }
315
314
  }
316
315
  }
316
+
317
+ function prepareFunctionDeclaration(
318
+ tool: FunctionTool,
319
+ ): GoogleFunctionDeclaration {
320
+ const declaration = {
321
+ name: tool.name,
322
+ description: tool.description ?? '',
323
+ };
324
+
325
+ try {
326
+ return {
327
+ ...declaration,
328
+ parameters: convertJSONSchemaToOpenAPISchema(tool.inputSchema),
329
+ };
330
+ } catch (error) {
331
+ if (!isRecursiveJSONSchemaReferenceError(error)) {
332
+ throw error;
333
+ }
334
+
335
+ return {
336
+ ...declaration,
337
+ parametersJsonSchema: tool.inputSchema,
338
+ };
339
+ }
340
+ }
@@ -6,7 +6,10 @@ import type {
6
6
  SharedV4ProviderMetadata,
7
7
  SharedV4Warning,
8
8
  } from '@ai-sdk/provider';
9
- import type { ParseResult } from '@ai-sdk/provider-utils';
9
+ import {
10
+ createProviderStreamError,
11
+ type ParseResult,
12
+ } from '@ai-sdk/provider-utils';
10
13
  import type {
11
14
  GoogleInteractionsEvent,
12
15
  GoogleInteractionsUsage,
@@ -821,10 +824,15 @@ export function buildGoogleInteractionsStreamTransform({
821
824
  { event_type: 'error' }
822
825
  >;
823
826
  finishStatus = 'failed';
824
- const errorPayload = event.error ?? {
825
- message: 'Unknown interaction error',
826
- };
827
- controller.enqueue({ type: 'error', error: errorPayload });
827
+ controller.enqueue({
828
+ type: 'error',
829
+ error: createProviderStreamError({
830
+ message: event.error?.message ?? 'Unknown interaction error',
831
+ type: event.event_type,
832
+ code: event.error?.code ?? undefined,
833
+ data: event,
834
+ }),
835
+ });
828
836
  break;
829
837
  }
830
838