@ai-sdk/google 4.0.67 → 4.0.69

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": "4.0.67",
3
+ "version": "4.0.69",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,8 +35,8 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.13",
39
- "@ai-sdk/provider-utils": "5.0.39"
38
+ "@ai-sdk/provider": "4.0.14",
39
+ "@ai-sdk/provider-utils": "5.0.40"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -0,0 +1,124 @@
1
+ import type {
2
+ LanguageModelV4Prompt,
3
+ LanguageModelV4ToolResultOutput,
4
+ } from '@ai-sdk/provider';
5
+ import {
6
+ detectMediaType,
7
+ downloadBlob,
8
+ isFullMediaType,
9
+ } from '@ai-sdk/provider-utils';
10
+
11
+ /**
12
+ * Vertex function responses only accept inline file data. Download remote tool
13
+ * result files before converting the prompt to the Google request format.
14
+ */
15
+ export async function downloadToolResultFiles(
16
+ prompt: LanguageModelV4Prompt,
17
+ {
18
+ abortSignal,
19
+ maxBytes,
20
+ }: {
21
+ abortSignal: AbortSignal | undefined;
22
+ maxBytes: number;
23
+ },
24
+ ): Promise<LanguageModelV4Prompt> {
25
+ const result: LanguageModelV4Prompt = [];
26
+
27
+ for (const message of prompt) {
28
+ if (message.role === 'assistant') {
29
+ const content: typeof message.content = [];
30
+
31
+ for (const part of message.content) {
32
+ content.push(
33
+ part.type === 'tool-result'
34
+ ? {
35
+ ...part,
36
+ output: await downloadToolResultOutput(part.output, {
37
+ abortSignal,
38
+ maxBytes,
39
+ }),
40
+ }
41
+ : part,
42
+ );
43
+ }
44
+
45
+ result.push({ ...message, content });
46
+ continue;
47
+ }
48
+
49
+ if (message.role === 'tool') {
50
+ const content: typeof message.content = [];
51
+
52
+ for (const part of message.content) {
53
+ if (part.type !== 'tool-result') {
54
+ content.push(part);
55
+ continue;
56
+ }
57
+
58
+ content.push({
59
+ ...part,
60
+ output: await downloadToolResultOutput(part.output, {
61
+ abortSignal,
62
+ maxBytes,
63
+ }),
64
+ });
65
+ }
66
+
67
+ result.push({ ...message, content });
68
+ continue;
69
+ }
70
+
71
+ result.push(message);
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ async function downloadToolResultOutput(
78
+ output: LanguageModelV4ToolResultOutput,
79
+ {
80
+ abortSignal,
81
+ maxBytes,
82
+ }: {
83
+ abortSignal: AbortSignal | undefined;
84
+ maxBytes: number;
85
+ },
86
+ ): Promise<LanguageModelV4ToolResultOutput> {
87
+ if (output.type !== 'content') {
88
+ return output;
89
+ }
90
+
91
+ const value: typeof output.value = [];
92
+
93
+ for (const part of output.value) {
94
+ if (part.type !== 'file' || part.data.type !== 'url') {
95
+ value.push(part);
96
+ continue;
97
+ }
98
+
99
+ const blob = await downloadBlob(part.data.url.toString(), {
100
+ abortSignal,
101
+ maxBytes,
102
+ });
103
+ const data = new Uint8Array(await blob.arrayBuffer());
104
+ const detectedMediaType = detectMediaType({
105
+ data,
106
+ topLevelType: 'image',
107
+ });
108
+
109
+ value.push({
110
+ ...part,
111
+ data: { type: 'data' as const, data },
112
+ mediaType:
113
+ detectedMediaType ??
114
+ (blob.type && !isFullMediaType(part.mediaType)
115
+ ? blob.type
116
+ : part.mediaType),
117
+ });
118
+ }
119
+
120
+ return {
121
+ ...output,
122
+ value,
123
+ };
124
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  InvalidArgumentError,
3
3
  InvalidResponseDataError,
4
+ UnsupportedFunctionalityError,
4
5
  type Experimental_BatchV4 as BatchV4,
5
6
  type Experimental_BatchV4CancelResult as BatchV4CancelResult,
6
7
  type Experimental_BatchV4Error as BatchV4Error,
@@ -10,9 +11,12 @@ import {
10
11
  type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
11
12
  type Experimental_BatchV4StartResult as BatchV4StartResult,
12
13
  type Experimental_BatchV4Status as BatchV4Status,
13
- type Experimental_TextBatchV4Request as TextBatchV4Request,
14
+ type Experimental_ImageBatchV4Request as ImageBatchV4Request,
14
15
  type Experimental_BatchV4StartOptions as BatchV4StartOptions,
15
16
  type LanguageModelV4GenerateResult,
17
+ type ImageModelV4Result,
18
+ type LanguageModelV4Prompt,
19
+ type SharedV4Warning,
16
20
  } from '@ai-sdk/provider';
17
21
  import {
18
22
  combineHeaders,
@@ -23,11 +27,13 @@ import {
23
27
  getFromApi,
24
28
  lazySchema,
25
29
  normalizeBatchRequestCounts,
30
+ parseProviderOptions,
26
31
  postJsonToApi,
27
32
  postToApi,
28
33
  resolve,
29
34
  safeValidateTypes,
30
35
  zodSchema,
36
+ convertToBase64,
31
37
  type InferSchema,
32
38
  type ResponseHandler,
33
39
  } from '@ai-sdk/provider-utils';
@@ -39,7 +45,12 @@ import {
39
45
  responseSchema,
40
46
  type GoogleLanguageModelConfig,
41
47
  } from './google-language-model';
42
- import type { GoogleModelId } from './google-language-model-options';
48
+ import {
49
+ googleLanguageModelOptions,
50
+ type GoogleModelId,
51
+ } from './google-language-model-options';
52
+ import type { GoogleImageModelId } from './google-image-settings';
53
+ import { googleImageModelOptionsSchema } from './google-image-model-options';
43
54
 
44
55
  const googleBatchInputFileMaxBytes = 2 * 1024 * 1024 * 1024;
45
56
  const googleBatchInlineCreationMaxBytes = 20_000_000;
@@ -47,7 +58,25 @@ const supportedGoogleBatchContentTypes = new Set<
47
58
  LanguageModelV4GenerateResult['content'][number]['type']
48
59
  >(['text', 'reasoning', 'source', 'tool-call', 'tool-result']);
49
60
 
50
- type GoogleBatchRequest = TextBatchV4Request<GoogleModelId>;
61
+ type GoogleImageBatchRequest = ImageBatchV4Request<GoogleImageModelId>;
62
+ type GoogleBatchModelIds = {
63
+ readonly text: GoogleModelId;
64
+ readonly image: GoogleImageModelId;
65
+ };
66
+
67
+ function assertSupportedBatchRequests(
68
+ requests: BatchV4StartOptions<GoogleBatchModelIds>['requests'],
69
+ ) {
70
+ for (const request of requests) {
71
+ const requestType = request.type;
72
+ if (requestType !== 'text' && requestType !== 'image') {
73
+ throw new UnsupportedFunctionalityError({
74
+ functionality: `batch request type: ${requestType}`,
75
+ message: `The Google Batch API does not support batch requests with type "${requestType}".`,
76
+ });
77
+ }
78
+ }
79
+ }
51
80
 
52
81
  const googleRpcStatusSchema = z.object({
53
82
  code: z.union([z.number(), z.string()]).nullish(),
@@ -150,7 +179,7 @@ const googleBatchResponsePreviewSchema = lazySchema(() =>
150
179
  ),
151
180
  );
152
181
 
153
- export class GoogleBatch implements BatchV4<{ readonly text: GoogleModelId }> {
182
+ export class GoogleBatch implements BatchV4<GoogleBatchModelIds> {
154
183
  readonly specificationVersion = 'v4' as const;
155
184
  readonly provider: string;
156
185
  readonly supportedUrls: Record<string, RegExp[]>;
@@ -169,8 +198,9 @@ export class GoogleBatch implements BatchV4<{ readonly text: GoogleModelId }> {
169
198
  }
170
199
 
171
200
  async doStartBatch(
172
- options: BatchV4StartOptions<{ text: GoogleModelId }>,
201
+ options: BatchV4StartOptions<GoogleBatchModelIds>,
173
202
  ): Promise<BatchV4StartResult> {
203
+ assertSupportedBatchRequests(options.requests);
174
204
  const modelId = getGoogleBatchModelId(options.requests);
175
205
  const warnings: BatchV4StartResult['warnings'] = [];
176
206
  const displayName = `ai-sdk-batch-${this.batchGenerateId()}`;
@@ -196,11 +226,14 @@ export class GoogleBatch implements BatchV4<{ readonly text: GoogleModelId }> {
196
226
  let fileParts: string[] | undefined;
197
227
 
198
228
  for (const request of options.requests) {
199
- const preparedRequest = await GoogleLanguageModel.prepareRequest({
200
- modelId: request.modelId,
201
- config: this.batchConfig,
202
- options: request.options,
203
- });
229
+ const preparedRequest =
230
+ request.type === 'text'
231
+ ? await GoogleLanguageModel.prepareRequest({
232
+ modelId: request.modelId,
233
+ config: this.batchConfig,
234
+ options: request.options,
235
+ })
236
+ : await this.prepareImageRequest(request);
204
237
  const inlinedRequest = {
205
238
  request: preparedRequest.args,
206
239
  metadata: { key: request.id },
@@ -602,6 +635,16 @@ export class GoogleBatch implements BatchV4<{ readonly text: GoogleModelId }> {
602
635
  warnings: [],
603
636
  providerOptionsNames: ['google'],
604
637
  });
638
+ const imageResult = convertGoogleImageBatchResult(result);
639
+ if (imageResult != null) {
640
+ yield {
641
+ type: 'image',
642
+ id: line.key,
643
+ status: 'succeeded',
644
+ result: imageResult,
645
+ };
646
+ continue;
647
+ }
605
648
  const unsupportedPart = result.content.find(
606
649
  part => !supportedGoogleBatchContentTypes.has(part.type),
607
650
  );
@@ -625,6 +668,108 @@ export class GoogleBatch implements BatchV4<{ readonly text: GoogleModelId }> {
625
668
  }
626
669
  }
627
670
 
671
+ private async prepareImageRequest(request: GoogleImageBatchRequest) {
672
+ const { prompt, n, size, aspectRatio, seed, files, mask, providerOptions } =
673
+ request.options;
674
+ const warnings: SharedV4Warning[] = [];
675
+
676
+ if (mask != null) {
677
+ throw new UnsupportedFunctionalityError({
678
+ functionality: 'mask-based image editing in Google batches',
679
+ });
680
+ }
681
+ if (n > 1) {
682
+ throw new UnsupportedFunctionalityError({
683
+ functionality: 'multiple images per Google batch request',
684
+ });
685
+ }
686
+ if (size != null) {
687
+ warnings.push({
688
+ type: 'unsupported',
689
+ feature: 'size',
690
+ details:
691
+ 'This model does not support the `size` option. Use `aspectRatio` instead.',
692
+ });
693
+ }
694
+
695
+ const userContent: Extract<
696
+ LanguageModelV4Prompt[number],
697
+ { role: 'user' }
698
+ >['content'] = [];
699
+ if (prompt != null) userContent.push({ type: 'text', text: prompt });
700
+ for (const file of files ?? []) {
701
+ userContent.push(
702
+ file.type === 'url'
703
+ ? {
704
+ type: 'file',
705
+ data: { type: 'url', url: new URL(file.url) },
706
+ mediaType: 'image/*',
707
+ }
708
+ : {
709
+ type: 'file',
710
+ data: { type: 'data', data: file.data },
711
+ mediaType: file.mediaType,
712
+ },
713
+ );
714
+ }
715
+
716
+ const googleImageOptions = await parseProviderOptions({
717
+ provider: 'google',
718
+ providerOptions,
719
+ schema: googleImageModelOptionsSchema,
720
+ });
721
+ const {
722
+ responseModalities: _responseModalities,
723
+ imageConfig: userImageConfig,
724
+ ...passthroughGoogleOptions
725
+ } = (await parseProviderOptions({
726
+ provider: 'google',
727
+ providerOptions,
728
+ schema: googleLanguageModelOptions,
729
+ })) ?? {};
730
+ const preparedGoogleOptions = await parseProviderOptions({
731
+ provider: 'google',
732
+ providerOptions: {
733
+ google: {
734
+ ...passthroughGoogleOptions,
735
+ responseModalities: ['IMAGE'],
736
+ imageConfig:
737
+ aspectRatio != null || userImageConfig != null
738
+ ? {
739
+ ...userImageConfig,
740
+ ...(aspectRatio != null ? { aspectRatio } : {}),
741
+ }
742
+ : undefined,
743
+ },
744
+ },
745
+ schema: googleLanguageModelOptions,
746
+ });
747
+
748
+ const prepared = await GoogleLanguageModel.prepareRequest({
749
+ modelId: request.modelId,
750
+ config: this.batchConfig,
751
+ options: {
752
+ prompt: [{ role: 'user', content: userContent }],
753
+ seed,
754
+ providerOptions: {
755
+ google: preparedGoogleOptions ?? { responseModalities: ['IMAGE'] },
756
+ },
757
+ tools:
758
+ googleImageOptions?.googleSearch != null
759
+ ? [
760
+ {
761
+ type: 'provider',
762
+ id: 'google.google_search',
763
+ name: 'google_search',
764
+ args: googleImageOptions.googleSearch,
765
+ },
766
+ ]
767
+ : undefined,
768
+ },
769
+ });
770
+ return { ...prepared, warnings: [...warnings, ...prepared.warnings] };
771
+ }
772
+
628
773
  private async getHeaders(headers?: Record<string, string | undefined>) {
629
774
  return combineHeaders(
630
775
  this.batchConfig.headers
@@ -748,8 +893,8 @@ const googleUploadUrlResponseHandler: ResponseHandler<string> = async ({
748
893
  };
749
894
 
750
895
  function getGoogleBatchModelId(
751
- requests: readonly GoogleBatchRequest[],
752
- ): GoogleModelId {
896
+ requests: BatchV4StartOptions<GoogleBatchModelIds>['requests'],
897
+ ): GoogleModelId | GoogleImageModelId {
753
898
  const modelId = requests[0]?.modelId;
754
899
 
755
900
  if (modelId == null) {
@@ -771,3 +916,39 @@ function getGoogleBatchModelId(
771
916
 
772
917
  return modelId;
773
918
  }
919
+
920
+ function convertGoogleImageBatchResult(
921
+ result: LanguageModelV4GenerateResult,
922
+ ): ImageModelV4Result | undefined {
923
+ const images = result.content.flatMap(part =>
924
+ part.type === 'file' &&
925
+ part.mediaType.startsWith('image/') &&
926
+ part.data.type === 'data'
927
+ ? [convertToBase64(part.data.data)]
928
+ : [],
929
+ );
930
+ if (images.length === 0) return undefined;
931
+
932
+ const googleMetadata =
933
+ (result.providerMetadata?.google as Record<string, unknown> | undefined) ??
934
+ {};
935
+ return {
936
+ images,
937
+ warnings: result.warnings,
938
+ providerMetadata: {
939
+ google: { ...googleMetadata, images: images.map(() => ({})) },
940
+ },
941
+ response: {
942
+ timestamp: new Date(),
943
+ modelId: result.response?.modelId ?? '',
944
+ headers: result.response?.headers,
945
+ },
946
+ usage: {
947
+ inputTokens: result.usage.inputTokens.total,
948
+ outputTokens: result.usage.outputTokens.total,
949
+ totalTokens:
950
+ (result.usage.inputTokens.total ?? 0) +
951
+ (result.usage.outputTokens.total ?? 0),
952
+ },
953
+ };
954
+ }
@@ -40,6 +40,7 @@ import {
40
40
  } from './convert-google-usage';
41
41
  import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema';
42
42
  import { convertToGoogleMessages } from './convert-to-google-messages';
43
+ import { downloadToolResultFiles } from './download-tool-result-files';
43
44
  import { getModelPath } from './get-model-path';
44
45
  import { googleFailedResponseHandler } from './google-error';
45
46
  import {
@@ -76,6 +77,13 @@ export type GoogleLanguageModelConfig = {
76
77
  * The supported URLs for the model.
77
78
  */
78
79
  supportedUrls?: () => LanguageModelV4['supportedUrls'];
80
+
81
+ /**
82
+ * Settings for downloading remote files in tool results before conversion.
83
+ */
84
+ downloadToolResultFiles?: {
85
+ maxBytes: number;
86
+ };
79
87
  };
80
88
 
81
89
  export class GoogleLanguageModel implements LanguageModelV4 {
@@ -132,6 +140,7 @@ export class GoogleLanguageModel implements LanguageModelV4 {
132
140
  toolChoice,
133
141
  reasoning,
134
142
  providerOptions,
143
+ abortSignal,
135
144
  },
136
145
  isStreaming = false,
137
146
  }: {
@@ -287,14 +296,24 @@ export class GoogleLanguageModel implements LanguageModelV4 {
287
296
 
288
297
  const { usesGemini3Features } = getGoogleModelCapabilities(modelId);
289
298
 
290
- const { contents, systemInstruction } = convertToGoogleMessages(prompt, {
291
- isGemmaModel,
292
- isGemini3Model: usesGemini3Features,
293
- onWarning: warning => warnings.push(warning),
294
- providerOptionsNames,
295
- supportsFunctionResponseParts: usesGemini3Features,
296
- includeFunctionCallIds: !isVertexProvider,
297
- });
299
+ const promptWithDownloadedToolResultFiles = config.downloadToolResultFiles
300
+ ? await downloadToolResultFiles(prompt, {
301
+ abortSignal,
302
+ maxBytes: config.downloadToolResultFiles.maxBytes,
303
+ })
304
+ : prompt;
305
+
306
+ const { contents, systemInstruction } = convertToGoogleMessages(
307
+ promptWithDownloadedToolResultFiles,
308
+ {
309
+ isGemmaModel,
310
+ isGemini3Model: usesGemini3Features,
311
+ onWarning: warning => warnings.push(warning),
312
+ providerOptionsNames,
313
+ supportsFunctionResponseParts: usesGemini3Features,
314
+ includeFunctionCallIds: !isVertexProvider,
315
+ },
316
+ );
298
317
 
299
318
  const {
300
319
  tools: googleTools,
@@ -61,7 +61,10 @@ export interface GoogleProvider extends ProviderV4 {
61
61
 
62
62
  chat(modelId: GoogleModelId): LanguageModelV4;
63
63
 
64
- experimental_batch(): BatchV4<{ text: GoogleModelId }>;
64
+ experimental_batch(): BatchV4<{
65
+ text: GoogleModelId;
66
+ image: GoogleImageModelId;
67
+ }>;
65
68
 
66
69
  /**
67
70
  * Creates a model for image generation.