@ai-sdk/google 4.0.50 → 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.
@@ -0,0 +1,670 @@
1
+ import {
2
+ InvalidArgumentError,
3
+ InvalidResponseDataError,
4
+ type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
5
+ type Experimental_BatchV4Error as BatchV4Error,
6
+ type Experimental_BatchV4ItemResult as BatchV4ItemResult,
7
+ type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
8
+ type Experimental_BatchV4StartOptions as BatchV4StartOptions,
9
+ type Experimental_BatchV4StartResult as BatchV4StartResult,
10
+ type Experimental_BatchV4Status as BatchV4Status,
11
+ type LanguageModelV4GenerateResult,
12
+ } from '@ai-sdk/provider';
13
+ import {
14
+ combineHeaders,
15
+ convertAsyncIteratorToReadableStream,
16
+ createJsonLinesResponseHandler,
17
+ createJsonResponseHandler,
18
+ generateId,
19
+ getFromApi,
20
+ lazySchema,
21
+ normalizeBatchRequestCounts,
22
+ postJsonToApi,
23
+ postToApi,
24
+ resolve,
25
+ safeValidateTypes,
26
+ WORKFLOW_DESERIALIZE,
27
+ WORKFLOW_SERIALIZE,
28
+ zodSchema,
29
+ type InferSchema,
30
+ type ResponseHandler,
31
+ } from '@ai-sdk/provider-utils';
32
+ import { z } from 'zod/v4';
33
+ import { getModelPath } from './get-model-path';
34
+ import { googleFailedResponseHandler } from './google-error';
35
+ import {
36
+ GoogleLanguageModel,
37
+ responseSchema,
38
+ type GoogleLanguageModelConfig,
39
+ } from './google-language-model';
40
+ import type { GoogleModelId } from './google-language-model-options';
41
+
42
+ const googleBatchInputFileMaxBytes = 2 * 1024 * 1024 * 1024;
43
+ const googleBatchInlineCreationMaxBytes = 20_000_000;
44
+ const supportedGoogleBatchContentTypes = new Set<
45
+ LanguageModelV4GenerateResult['content'][number]['type']
46
+ >(['text', 'reasoning', 'source']);
47
+
48
+ type GoogleBatchRequest = Parameters<
49
+ BatchLanguageModelV4['experimental_doStartBatch']
50
+ >[0]['requests'][number];
51
+
52
+ const googleRpcStatusSchema = z.object({
53
+ code: z.union([z.number(), z.string()]).nullish(),
54
+ message: z.string().nullish(),
55
+ status: z.string().nullish(),
56
+ });
57
+
58
+ const googleBatchStatsSchema = z.object({
59
+ requestCount: z.union([z.string(), z.number()]).nullish(),
60
+ successfulRequestCount: z.union([z.string(), z.number()]).nullish(),
61
+ failedRequestCount: z.union([z.string(), z.number()]).nullish(),
62
+ pendingRequestCount: z.union([z.string(), z.number()]).nullish(),
63
+ });
64
+
65
+ const googleBatchOutputSchema = z.object({
66
+ responsesFile: z.string().nullish(),
67
+ inlinedResponses: z
68
+ .object({
69
+ inlinedResponses: z.array(
70
+ z.object({
71
+ metadata: z.object({
72
+ key: z.string(),
73
+ }),
74
+ response: z.unknown().nullish(),
75
+ error: googleRpcStatusSchema.nullish(),
76
+ }),
77
+ ),
78
+ })
79
+ .nullish(),
80
+ });
81
+
82
+ const googleBatchOperationSchema = lazySchema(() =>
83
+ zodSchema(
84
+ z.object({
85
+ name: z.string(),
86
+ metadata: z
87
+ .object({
88
+ state: z.string().nullish(),
89
+ createTime: z.string().nullish(),
90
+ batchStats: googleBatchStatsSchema.nullish(),
91
+ output: googleBatchOutputSchema.nullish(),
92
+ })
93
+ .nullish(),
94
+ done: z.boolean().nullish(),
95
+ error: googleRpcStatusSchema.nullish(),
96
+ response: googleBatchOutputSchema.nullish(),
97
+ }),
98
+ ),
99
+ );
100
+
101
+ type GoogleBatchOperation = InferSchema<typeof googleBatchOperationSchema>;
102
+
103
+ const googleFileUploadResponseSchema = lazySchema(() =>
104
+ zodSchema(
105
+ z.object({
106
+ file: z.object({
107
+ name: z.string(),
108
+ }),
109
+ }),
110
+ ),
111
+ );
112
+
113
+ const googleBatchResultLineSchema = lazySchema(() =>
114
+ zodSchema(
115
+ z.object({
116
+ key: z.string(),
117
+ response: z.unknown().nullish(),
118
+ error: googleRpcStatusSchema.nullish(),
119
+ }),
120
+ ),
121
+ );
122
+
123
+ type GoogleBatchResultLine = InferSchema<typeof googleBatchResultLineSchema>;
124
+
125
+ const googleBatchResponsePreviewSchema = lazySchema(() =>
126
+ zodSchema(
127
+ z.object({
128
+ candidates: z.array(z.unknown()).nullish(),
129
+ promptFeedback: z
130
+ .object({
131
+ blockReason: z.string().nullish(),
132
+ })
133
+ .nullish(),
134
+ }),
135
+ ),
136
+ );
137
+
138
+ export class GoogleBatchLanguageModel
139
+ extends GoogleLanguageModel
140
+ implements BatchLanguageModelV4
141
+ {
142
+ private readonly batchConfig: GoogleLanguageModelConfig;
143
+ private readonly batchGenerateId: () => string;
144
+
145
+ static [WORKFLOW_SERIALIZE](model: GoogleLanguageModel) {
146
+ return GoogleLanguageModel[WORKFLOW_SERIALIZE](model);
147
+ }
148
+
149
+ static [WORKFLOW_DESERIALIZE](options: {
150
+ modelId: string;
151
+ config: GoogleLanguageModelConfig;
152
+ }) {
153
+ return new GoogleBatchLanguageModel(options.modelId, options.config);
154
+ }
155
+
156
+ constructor(modelId: GoogleModelId, config: GoogleLanguageModelConfig) {
157
+ super(modelId, config);
158
+ this.batchConfig = config;
159
+ this.batchGenerateId = config.generateId ?? generateId;
160
+ }
161
+
162
+ async experimental_doStartBatch(
163
+ options: BatchV4StartOptions<GoogleBatchRequest>,
164
+ ): Promise<BatchV4StartResult> {
165
+ const warnings: BatchV4StartResult['warnings'] = [];
166
+ const displayName = `ai-sdk-batch-${this.batchGenerateId()}`;
167
+ const inlinedRequests: Array<{
168
+ request: unknown;
169
+ metadata: { key: string };
170
+ }> = [];
171
+ const inlineBatchBody = {
172
+ batch: {
173
+ displayName,
174
+ ...(options.webhookUrl != null && {
175
+ webhookConfig: { uris: [options.webhookUrl] },
176
+ }),
177
+ inputConfig: {
178
+ requests: { requests: inlinedRequests },
179
+ },
180
+ },
181
+ };
182
+ const textEncoder = new TextEncoder();
183
+ let inlineInputBytes = textEncoder.encode(
184
+ JSON.stringify(inlineBatchBody),
185
+ ).byteLength;
186
+ let fileParts: string[] | undefined;
187
+
188
+ for (const request of options.requests) {
189
+ const preparedRequest = await this.getArgs(request.options);
190
+ const inlinedRequest = {
191
+ request: preparedRequest.args,
192
+ metadata: { key: request.id },
193
+ };
194
+
195
+ if (fileParts == null) {
196
+ const requestBytes = textEncoder.encode(
197
+ JSON.stringify(inlinedRequest),
198
+ ).byteLength;
199
+ const nextInlineInputBytes =
200
+ inlineInputBytes +
201
+ requestBytes +
202
+ (inlinedRequests.length > 0 ? 1 : 0);
203
+
204
+ if (nextInlineInputBytes < googleBatchInlineCreationMaxBytes) {
205
+ inlinedRequests.push(inlinedRequest);
206
+ inlineInputBytes = nextInlineInputBytes;
207
+ } else {
208
+ fileParts = [];
209
+ for (const previousRequest of inlinedRequests) {
210
+ fileParts.push(
211
+ JSON.stringify({
212
+ key: previousRequest.metadata.key,
213
+ request: previousRequest.request,
214
+ }),
215
+ '\n',
216
+ );
217
+ }
218
+ inlinedRequests.length = 0;
219
+ fileParts.push(
220
+ JSON.stringify({
221
+ key: request.id,
222
+ request: preparedRequest.args,
223
+ }),
224
+ '\n',
225
+ );
226
+ }
227
+ } else {
228
+ fileParts.push(
229
+ JSON.stringify({
230
+ key: request.id,
231
+ request: preparedRequest.args,
232
+ }),
233
+ '\n',
234
+ );
235
+ }
236
+
237
+ for (const warning of preparedRequest.warnings) {
238
+ warnings.push({ requestId: request.id, warning });
239
+ }
240
+ }
241
+
242
+ const headers = await this.getHeaders(options.headers);
243
+ const createUrl = `${this.batchConfig.baseURL}/${getModelPath(
244
+ this.modelId,
245
+ )}:batchGenerateContent`;
246
+ let operation: GoogleBatchOperation;
247
+
248
+ if (fileParts == null) {
249
+ const { value } = await postJsonToApi({
250
+ url: createUrl,
251
+ headers,
252
+ body: inlineBatchBody,
253
+ failedResponseHandler: googleFailedResponseHandler,
254
+ successfulResponseHandler: createJsonResponseHandler(
255
+ googleBatchOperationSchema,
256
+ ),
257
+ abortSignal: options.abortSignal,
258
+ fetch: this.batchConfig.fetch,
259
+ });
260
+ operation = value;
261
+ } else {
262
+ const inputFile = new Blob(fileParts, { type: 'application/jsonl' });
263
+ // Blob snapshots the strings, so release the potentially large input array.
264
+ fileParts.length = 0;
265
+ if (inputFile.size > googleBatchInputFileMaxBytes) {
266
+ throw new InvalidArgumentError({
267
+ argument: 'requests',
268
+ message: 'Google batch input files must not exceed 2 GB.',
269
+ });
270
+ }
271
+
272
+ const { value: uploadUrl } = await postJsonToApi({
273
+ url: `${this.getBaseOrigin()}/upload/v1beta/files`,
274
+ headers: combineHeaders(headers, {
275
+ 'X-Goog-Upload-Protocol': 'resumable',
276
+ 'X-Goog-Upload-Command': 'start',
277
+ 'X-Goog-Upload-Header-Content-Length': String(inputFile.size),
278
+ 'X-Goog-Upload-Header-Content-Type': 'application/jsonl',
279
+ }),
280
+ body: {
281
+ file: {
282
+ display_name: `${displayName}-input`,
283
+ },
284
+ },
285
+ failedResponseHandler: googleFailedResponseHandler,
286
+ successfulResponseHandler: googleUploadUrlResponseHandler,
287
+ abortSignal: options.abortSignal,
288
+ fetch: this.batchConfig.fetch,
289
+ });
290
+
291
+ const { value: uploadedFile } = await postToApi({
292
+ url: uploadUrl,
293
+ headers: {
294
+ 'X-Goog-Upload-Offset': '0',
295
+ 'X-Goog-Upload-Command': 'upload, finalize',
296
+ 'Content-Type': 'application/jsonl',
297
+ },
298
+ body: {
299
+ content: inputFile,
300
+ values: {
301
+ byteLength: inputFile.size,
302
+ mediaType: 'application/jsonl',
303
+ },
304
+ },
305
+ failedResponseHandler: googleFailedResponseHandler,
306
+ successfulResponseHandler: createJsonResponseHandler(
307
+ googleFileUploadResponseSchema,
308
+ ),
309
+ abortSignal: options.abortSignal,
310
+ fetch: this.batchConfig.fetch,
311
+ });
312
+
313
+ const { value } = await postJsonToApi({
314
+ url: createUrl,
315
+ headers,
316
+ body: {
317
+ batch: {
318
+ displayName,
319
+ ...(options.webhookUrl != null && {
320
+ webhookConfig: { uris: [options.webhookUrl] },
321
+ }),
322
+ inputConfig: {
323
+ fileName: uploadedFile.file.name,
324
+ },
325
+ },
326
+ },
327
+ failedResponseHandler: googleFailedResponseHandler,
328
+ successfulResponseHandler: createJsonResponseHandler(
329
+ googleBatchOperationSchema,
330
+ ),
331
+ abortSignal: options.abortSignal,
332
+ fetch: this.batchConfig.fetch,
333
+ });
334
+ operation = value;
335
+ }
336
+
337
+ return {
338
+ batchId: operation.name,
339
+ ...convertGoogleBatchStatus(operation),
340
+ warnings,
341
+ };
342
+ }
343
+
344
+ async experimental_doGetBatchStatus(
345
+ options: BatchV4OperationOptions,
346
+ ): Promise<BatchV4Status> {
347
+ return convertGoogleBatchStatus(await this.retrieveBatch(options));
348
+ }
349
+
350
+ async experimental_doGetBatchResults(
351
+ options: BatchV4OperationOptions,
352
+ ): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
353
+ const operation = await this.retrieveBatch(options);
354
+ const batchStatus = convertGoogleBatchStatus(operation);
355
+
356
+ if (batchStatus.status === 'pending') {
357
+ throw new InvalidArgumentError({
358
+ argument: 'batchId',
359
+ message: `Google batch "${options.batchId}" is not complete.`,
360
+ });
361
+ }
362
+
363
+ const inlinedResponses =
364
+ operation.metadata?.output?.inlinedResponses?.inlinedResponses ??
365
+ operation.response?.inlinedResponses?.inlinedResponses;
366
+
367
+ if (inlinedResponses != null) {
368
+ return convertAsyncIteratorToReadableStream(
369
+ this.iterateBatchResults(
370
+ inlinedResponses.map(result => ({
371
+ key: result.metadata.key,
372
+ response: result.response,
373
+ error: result.error,
374
+ })),
375
+ ),
376
+ );
377
+ }
378
+
379
+ const responsesFile =
380
+ operation.metadata?.output?.responsesFile ??
381
+ operation.response?.responsesFile;
382
+
383
+ if (responsesFile == null) {
384
+ if (batchStatus.status === 'completed') {
385
+ throw new InvalidResponseDataError({
386
+ data: operation,
387
+ message: `Google batch "${options.batchId}" completed without batch output.`,
388
+ });
389
+ }
390
+ return new ReadableStream({
391
+ start(controller) {
392
+ controller.close();
393
+ },
394
+ });
395
+ }
396
+
397
+ const encodedResponsesFile = responsesFile
398
+ .split('/')
399
+ .map(segment => encodeURIComponent(segment))
400
+ .join('/');
401
+
402
+ const { value: lines } = await getFromApi({
403
+ url: `${this.getBaseOrigin()}/download/v1beta/${encodedResponsesFile}:download?alt=media`,
404
+ headers: await this.getHeaders(options.headers),
405
+ failedResponseHandler: googleFailedResponseHandler,
406
+ successfulResponseHandler: createJsonLinesResponseHandler(
407
+ googleBatchResultLineSchema,
408
+ ),
409
+ abortSignal: options.abortSignal,
410
+ fetch: this.batchConfig.fetch,
411
+ validateUrl: false,
412
+ });
413
+
414
+ return convertAsyncIteratorToReadableStream(
415
+ this.iterateBatchResults(lines),
416
+ );
417
+ }
418
+
419
+ private async retrieveBatch(
420
+ options: BatchV4OperationOptions,
421
+ ): Promise<GoogleBatchOperation> {
422
+ const { value: operation } = await getFromApi({
423
+ url: `${this.batchConfig.baseURL}/${options.batchId}`,
424
+ headers: await this.getHeaders(options.headers),
425
+ failedResponseHandler: googleFailedResponseHandler,
426
+ successfulResponseHandler: createJsonResponseHandler(
427
+ googleBatchOperationSchema,
428
+ ),
429
+ abortSignal: options.abortSignal,
430
+ fetch: this.batchConfig.fetch,
431
+ validateUrl: false,
432
+ });
433
+
434
+ return operation;
435
+ }
436
+
437
+ private async *iterateBatchResults(
438
+ results:
439
+ | Iterable<GoogleBatchResultLine>
440
+ | AsyncIterable<GoogleBatchResultLine>,
441
+ ): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
442
+ for await (const line of results) {
443
+ if (line.error != null) {
444
+ const error = convertGoogleRpcError(
445
+ line.error,
446
+ 'Google batch request failed.',
447
+ );
448
+ const status =
449
+ line.error.status === 'CANCELLED' || String(line.error.code) === '1'
450
+ ? 'cancelled'
451
+ : 'failed';
452
+
453
+ yield { id: line.key, status, error };
454
+ continue;
455
+ }
456
+
457
+ if (line.response == null) {
458
+ yield {
459
+ id: line.key,
460
+ status: 'failed',
461
+ error: {
462
+ message:
463
+ 'Google returned a batch result without a response or error.',
464
+ code: 'invalid_batch_result',
465
+ },
466
+ };
467
+ continue;
468
+ }
469
+
470
+ const preview = await safeValidateTypes({
471
+ value: line.response,
472
+ schema: googleBatchResponsePreviewSchema,
473
+ });
474
+ if (
475
+ preview.success &&
476
+ (preview.value.candidates == null ||
477
+ preview.value.candidates.length === 0)
478
+ ) {
479
+ const promptFeedback = preview.value.promptFeedback ?? undefined;
480
+ const blockReason = promptFeedback?.blockReason ?? undefined;
481
+ yield {
482
+ id: line.key,
483
+ status: 'failed',
484
+ error: {
485
+ message:
486
+ blockReason == null
487
+ ? 'Google returned a batch response without any candidates.'
488
+ : `Google blocked the batch request (${blockReason}).`,
489
+ code: blockReason == null ? 'invalid_response' : 'prompt_blocked',
490
+ ...(blockReason != null ? { type: blockReason } : {}),
491
+ },
492
+ ...(promptFeedback != null
493
+ ? {
494
+ providerMetadata: {
495
+ google: {
496
+ promptFeedback: {
497
+ blockReason: promptFeedback.blockReason ?? null,
498
+ },
499
+ },
500
+ },
501
+ }
502
+ : {}),
503
+ };
504
+ continue;
505
+ }
506
+
507
+ const response = await safeValidateTypes({
508
+ value: line.response,
509
+ schema: responseSchema,
510
+ });
511
+ if (!response.success) {
512
+ yield {
513
+ id: line.key,
514
+ status: 'failed',
515
+ error: {
516
+ message: 'Google returned an invalid GenerateContent batch result.',
517
+ code: 'invalid_response',
518
+ },
519
+ };
520
+ continue;
521
+ }
522
+
523
+ const result = this.convertGenerateContentResponse({
524
+ response: response.value,
525
+ warnings: [],
526
+ providerOptionsNames: ['google'],
527
+ });
528
+ const unsupportedPart = result.content.find(
529
+ part => !supportedGoogleBatchContentTypes.has(part.type),
530
+ );
531
+
532
+ if (unsupportedPart != null) {
533
+ yield {
534
+ id: line.key,
535
+ status: 'failed',
536
+ error: {
537
+ message:
538
+ `Google returned a "${unsupportedPart.type}" content block, ` +
539
+ 'but that content is not supported in AI SDK text batches.',
540
+ code: 'unsupported_content',
541
+ },
542
+ };
543
+ continue;
544
+ }
545
+
546
+ yield { id: line.key, status: 'succeeded', result };
547
+ }
548
+ }
549
+
550
+ private async getHeaders(headers?: Record<string, string | undefined>) {
551
+ return combineHeaders(
552
+ this.batchConfig.headers
553
+ ? await resolve(this.batchConfig.headers)
554
+ : undefined,
555
+ headers,
556
+ );
557
+ }
558
+
559
+ private getBaseOrigin() {
560
+ return this.batchConfig.baseURL.replace(/\/v1beta$/, '');
561
+ }
562
+ }
563
+
564
+ function convertGoogleBatchStatus(
565
+ operation: GoogleBatchOperation,
566
+ ): BatchV4Status {
567
+ const rawStatus = operation.metadata?.state ?? undefined;
568
+ const requestCounts = convertGoogleRequestCounts(
569
+ operation.metadata?.batchStats,
570
+ );
571
+ const createdAt = operation.metadata?.createTime ?? undefined;
572
+ const error =
573
+ operation.error != null
574
+ ? convertGoogleRpcError(operation.error, 'Google batch failed.')
575
+ : undefined;
576
+
577
+ return {
578
+ status: mapGoogleBatchStatus({
579
+ rawStatus,
580
+ done: operation.done ?? undefined,
581
+ hasError: error != null,
582
+ }),
583
+ ...(rawStatus != null ? { rawStatus } : {}),
584
+ ...(requestCounts != null ? { requestCounts } : {}),
585
+ ...(error != null ? { error } : {}),
586
+ ...(createdAt != null ? { createdAt } : {}),
587
+ };
588
+ }
589
+
590
+ function mapGoogleBatchStatus({
591
+ rawStatus,
592
+ done,
593
+ hasError,
594
+ }: {
595
+ rawStatus?: string;
596
+ done?: boolean;
597
+ hasError: boolean;
598
+ }): BatchV4Status['status'] {
599
+ if (hasError) {
600
+ return 'failed';
601
+ }
602
+
603
+ if (rawStatus == null) {
604
+ return done ? 'completed' : 'pending';
605
+ }
606
+
607
+ const normalizedStatus = rawStatus.replace(/^(?:BATCH|JOB)_STATE_/, '');
608
+ switch (normalizedStatus) {
609
+ case 'SUCCEEDED':
610
+ return 'completed';
611
+ case 'FAILED':
612
+ case 'CANCELLED':
613
+ case 'EXPIRED':
614
+ return 'failed';
615
+ case 'UNSPECIFIED':
616
+ case 'PENDING':
617
+ case 'RUNNING':
618
+ default:
619
+ return 'pending';
620
+ }
621
+ }
622
+
623
+ function convertGoogleRequestCounts(
624
+ counts: NonNullable<GoogleBatchOperation['metadata']>['batchStats'],
625
+ ): BatchV4Status['requestCounts'] | undefined {
626
+ const total = parseCount(counts?.requestCount);
627
+ const completed = parseCount(counts?.successfulRequestCount ?? 0);
628
+ const failed = parseCount(counts?.failedRequestCount ?? 0);
629
+ const pending = parseCount(counts?.pendingRequestCount ?? 0);
630
+
631
+ return normalizeBatchRequestCounts({
632
+ total,
633
+ pending,
634
+ completed,
635
+ failed,
636
+ });
637
+ }
638
+
639
+ function parseCount(value: string | number | null | undefined) {
640
+ const count =
641
+ typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value;
642
+ return typeof count === 'number' && Number.isSafeInteger(count) && count >= 0
643
+ ? count
644
+ : undefined;
645
+ }
646
+
647
+ function convertGoogleRpcError(
648
+ error: InferSchema<typeof googleRpcStatusSchema>,
649
+ fallbackMessage: string,
650
+ ): BatchV4Error {
651
+ return {
652
+ message: error.message ?? fallbackMessage,
653
+ ...(error.status != null ? { type: error.status } : {}),
654
+ ...(error.code != null ? { code: String(error.code) } : {}),
655
+ };
656
+ }
657
+
658
+ const googleUploadUrlResponseHandler: ResponseHandler<string> = async ({
659
+ response,
660
+ }) => {
661
+ const uploadUrl = response.headers.get('x-goog-upload-url');
662
+ if (uploadUrl == null) {
663
+ throw new InvalidResponseDataError({
664
+ data: response.headers,
665
+ message: 'Google did not return a resumable upload URL.',
666
+ });
667
+ }
668
+
669
+ return { value: uploadUrl };
670
+ };