@ai-sdk/google 4.0.68 → 4.0.70

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.68",
3
+ "version": "4.0.70",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -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
+ }
@@ -38,10 +38,11 @@ import {
38
38
  convertGoogleUsage,
39
39
  type GoogleUsageMetadata,
40
40
  } from './convert-google-usage';
41
- import { convertJSONSchemaToOpenAPISchema } from './convert-json-schema-to-openapi-schema';
42
41
  import { convertToGoogleMessages } from './convert-to-google-messages';
42
+ import { downloadToolResultFiles } from './download-tool-result-files';
43
43
  import { getModelPath } from './get-model-path';
44
44
  import { googleFailedResponseHandler } from './google-error';
45
+ import { sanitizeResponseJsonSchema } from './sanitize-response-json-schema';
45
46
  import {
46
47
  googleLanguageModelOptions,
47
48
  type GoogleLanguageModelOptions,
@@ -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,
@@ -376,14 +395,14 @@ export class GoogleLanguageModel implements LanguageModelV4 {
376
395
  // response format:
377
396
  responseMimeType:
378
397
  responseFormat?.type === 'json' ? 'application/json' : undefined,
379
- responseSchema:
398
+ responseJsonSchema:
380
399
  responseFormat?.type === 'json' &&
381
400
  responseFormat.schema != null &&
382
- // Google GenAI does not support all OpenAPI Schema features,
383
- // so this is needed as an escape hatch:
401
+ // Google does not support all JSON Schema features in
402
+ // responseJsonSchema, so this is needed as an escape hatch:
384
403
  // TODO convert into provider option
385
404
  (googleOptions?.structuredOutputs ?? true)
386
- ? convertJSONSchemaToOpenAPISchema(responseFormat.schema)
405
+ ? sanitizeResponseJsonSchema(responseFormat.schema)
387
406
  : undefined,
388
407
  ...(googleOptions?.audioTimestamp && {
389
408
  audioTimestamp: googleOptions.audioTimestamp,
@@ -3,10 +3,6 @@ import {
3
3
  type LanguageModelV4CallOptions,
4
4
  type SharedV4Warning,
5
5
  } from '@ai-sdk/provider';
6
- import {
7
- convertJSONSchemaToOpenAPISchema,
8
- isRecursiveJSONSchemaReferenceError,
9
- } from './convert-json-schema-to-openapi-schema';
10
6
  import type { GoogleModelId } from './google-language-model-options';
11
7
  import { getGoogleModelCapabilities } from './google-model-capabilities';
12
8
 
@@ -18,8 +14,7 @@ type FunctionTool = Extract<
18
14
  type GoogleFunctionDeclaration = {
19
15
  name: string;
20
16
  description: string;
21
- parameters?: unknown;
22
- parametersJsonSchema?: unknown;
17
+ parametersJsonSchema: unknown;
23
18
  };
24
19
 
25
20
  export function prepareTools({
@@ -317,24 +312,9 @@ export function prepareTools({
317
312
  function prepareFunctionDeclaration(
318
313
  tool: FunctionTool,
319
314
  ): GoogleFunctionDeclaration {
320
- const declaration = {
315
+ return {
321
316
  name: tool.name,
322
317
  description: tool.description ?? '',
318
+ parametersJsonSchema: tool.inputSchema,
323
319
  };
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
320
  }
@@ -6,7 +6,6 @@ import type {
6
6
  Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
7
7
  } from '@ai-sdk/provider';
8
8
  import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
9
- import { convertJSONSchemaToOpenAPISchema } from '../convert-json-schema-to-openapi-schema';
10
9
  import { getModelPath } from '../get-model-path';
11
10
  import type { GoogleRealtimeModelOptions } from './google-realtime-model-options';
12
11
 
@@ -337,11 +336,33 @@ export class GoogleRealtimeEventMapper {
337
336
  }
338
337
  }
339
338
 
339
+ /**
340
+ * The value Gemini accepts for `functionResponse.response`.
341
+ *
342
+ * The field is typed `google.protobuf.Struct`, which is an object and nothing else, so
343
+ * a string, number, array or `null` on the wire is a protocol violation and the socket
344
+ * closes with 1007. `onToolCall` returns `unknown` and `addToolOutput` takes `unknown`,
345
+ * so those shapes reach here as perfectly valid JSON.
346
+ *
347
+ * The field's own docstring says what to do with one: "Use `output` key to specify
348
+ * function output ... If `output` and `error` keys are not specified, then whole
349
+ * `response` is treated as function output." An object is passed through unchanged and
350
+ * everything else is wrapped.
351
+ */
352
+ function toFunctionResponseStruct(value: unknown): Record<string, unknown> {
353
+ const isStruct =
354
+ typeof value === 'object' && value !== null && !Array.isArray(value);
355
+ return isStruct ? (value as Record<string, unknown>) : { output: value };
356
+ }
357
+
340
358
  async function serializeFunctionCallOutput(
341
359
  item: RealtimeModelV4FunctionCallOutput,
342
360
  ): Promise<unknown> {
343
361
  const parseResult = await safeParseJSON({ text: item.output });
344
- const response = parseResult.success ? parseResult.value : {};
362
+ const response = parseResult.success
363
+ ? toFunctionResponseStruct(parseResult.value)
364
+ : // Preserve non-JSON output in the required object wrapper.
365
+ { output: item.output };
345
366
 
346
367
  return {
347
368
  toolResponse: {
@@ -402,7 +423,7 @@ export function buildGoogleSessionConfig(
402
423
  functionDeclarations: config.tools.map(tool => ({
403
424
  name: tool.name,
404
425
  description: tool.description,
405
- parameters: convertJSONSchemaToOpenAPISchema(tool.parameters),
426
+ parametersJsonSchema: tool.parameters,
406
427
  })),
407
428
  },
408
429
  ];
@@ -0,0 +1,65 @@
1
+ import type { JSONSchema7, JSONSchema7Definition } from '@ai-sdk/provider';
2
+
3
+ /**
4
+ * Recursively replaces `const` with a single-value `enum` in the JSON Schema
5
+ * locations supported by Google because `responseJsonSchema` does not support
6
+ * `const`. All other schema properties are preserved.
7
+ */
8
+ export function sanitizeResponseJsonSchema(schema: JSONSchema7): JSONSchema7 {
9
+ const {
10
+ const: constValue,
11
+ properties,
12
+ items,
13
+ additionalProperties,
14
+ anyOf,
15
+ oneOf,
16
+ ...result
17
+ } = schema;
18
+
19
+ return {
20
+ ...result,
21
+ ...(constValue !== undefined ? { enum: [constValue] } : {}),
22
+ ...(properties != null
23
+ ? { properties: sanitizeDefinitions(properties) }
24
+ : {}),
25
+ ...(items != null
26
+ ? {
27
+ items: Array.isArray(items)
28
+ ? items.map(sanitizeDefinition)
29
+ : sanitizeDefinition(items),
30
+ }
31
+ : {}),
32
+ ...(additionalProperties != null
33
+ ? {
34
+ additionalProperties:
35
+ typeof additionalProperties === 'boolean'
36
+ ? additionalProperties
37
+ : sanitizeDefinition(additionalProperties),
38
+ }
39
+ : {}),
40
+ ...(anyOf != null ? { anyOf: anyOf.map(sanitizeDefinition) } : {}),
41
+ ...(oneOf != null ? { oneOf: oneOf.map(sanitizeDefinition) } : {}),
42
+ ...(result.$defs != null
43
+ ? { $defs: sanitizeDefinitions(result.$defs) }
44
+ : {}),
45
+ };
46
+ }
47
+
48
+ function sanitizeDefinitions(
49
+ definitions: Record<string, JSONSchema7Definition>,
50
+ ): Record<string, JSONSchema7Definition> {
51
+ return Object.fromEntries(
52
+ Object.entries(definitions).map(([name, definition]) => [
53
+ name,
54
+ sanitizeDefinition(definition),
55
+ ]),
56
+ );
57
+ }
58
+
59
+ function sanitizeDefinition(
60
+ definition: JSONSchema7Definition,
61
+ ): JSONSchema7Definition {
62
+ return typeof definition === 'boolean'
63
+ ? definition
64
+ : sanitizeResponseJsonSchema(definition);
65
+ }