@ai-sdk/google 4.0.69 → 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.69",
3
+ "version": "4.0.70",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -38,11 +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';
43
42
  import { downloadToolResultFiles } from './download-tool-result-files';
44
43
  import { getModelPath } from './get-model-path';
45
44
  import { googleFailedResponseHandler } from './google-error';
45
+ import { sanitizeResponseJsonSchema } from './sanitize-response-json-schema';
46
46
  import {
47
47
  googleLanguageModelOptions,
48
48
  type GoogleLanguageModelOptions,
@@ -395,14 +395,14 @@ export class GoogleLanguageModel implements LanguageModelV4 {
395
395
  // response format:
396
396
  responseMimeType:
397
397
  responseFormat?.type === 'json' ? 'application/json' : undefined,
398
- responseSchema:
398
+ responseJsonSchema:
399
399
  responseFormat?.type === 'json' &&
400
400
  responseFormat.schema != null &&
401
- // Google GenAI does not support all OpenAPI Schema features,
402
- // 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:
403
403
  // TODO convert into provider option
404
404
  (googleOptions?.structuredOutputs ?? true)
405
- ? convertJSONSchemaToOpenAPISchema(responseFormat.schema)
405
+ ? sanitizeResponseJsonSchema(responseFormat.schema)
406
406
  : undefined,
407
407
  ...(googleOptions?.audioTimestamp && {
408
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
+ }
@@ -1,456 +0,0 @@
1
- import {
2
- UnsupportedFunctionalityError,
3
- type JSONSchema7,
4
- type JSONSchema7Definition,
5
- } from '@ai-sdk/provider';
6
-
7
- type JSONSchema7WithDefinitions = JSONSchema7 & {
8
- $defs?: Record<string, JSONSchema7Definition>;
9
- };
10
-
11
- type ReferenceContext = {
12
- definitions: Record<string, JSONSchema7Definition> | undefined;
13
- dollarDefinitions: Record<string, JSONSchema7Definition> | undefined;
14
- resolvingReferences: ReadonlySet<string>;
15
- };
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
-
29
- /**
30
- * Converts JSON Schema 7 to OpenAPI Schema 3.0
31
- */
32
- export function convertJSONSchemaToOpenAPISchema(
33
- jsonSchema: JSONSchema7Definition | undefined,
34
- isRoot = true,
35
- ): unknown {
36
- const rootSchema =
37
- typeof jsonSchema === 'object'
38
- ? (jsonSchema as JSONSchema7WithDefinitions)
39
- : undefined;
40
-
41
- return convertJSONSchemaDefinition(jsonSchema, isRoot, {
42
- definitions: rootSchema?.definitions,
43
- dollarDefinitions: rootSchema?.$defs,
44
- resolvingReferences: new Set(),
45
- });
46
- }
47
-
48
- function convertJSONSchemaDefinition(
49
- jsonSchema: JSONSchema7Definition | undefined,
50
- isRoot: boolean,
51
- referenceContext: ReferenceContext,
52
- ): unknown {
53
- if (jsonSchema == null) {
54
- return undefined;
55
- }
56
-
57
- if (typeof jsonSchema === 'boolean') {
58
- return { type: 'boolean', properties: {} };
59
- }
60
-
61
- if (jsonSchema.$ref != null) {
62
- return convertJSONSchemaReference({
63
- jsonSchema,
64
- reference: jsonSchema.$ref,
65
- isRoot,
66
- referenceContext,
67
- });
68
- }
69
-
70
- // Handle empty object schemas: undefined at root, preserved when nested
71
- if (isEmptyObjectSchema(jsonSchema)) {
72
- if (isRoot) {
73
- return undefined;
74
- }
75
-
76
- if (jsonSchema.description) {
77
- return { type: 'object', description: jsonSchema.description };
78
- }
79
- return { type: 'object' };
80
- }
81
-
82
- const {
83
- type,
84
- description,
85
- required,
86
- properties,
87
- items,
88
- allOf,
89
- anyOf,
90
- oneOf,
91
- format,
92
- const: constValue,
93
- minLength,
94
- minItems,
95
- maxItems,
96
- enum: enumValues,
97
- } = jsonSchema;
98
-
99
- const result: Record<string, unknown> = {};
100
-
101
- if (description) result.description = description;
102
- if (required) result.required = required;
103
- if (format) result.format = format;
104
-
105
- // Handle type
106
- if (type) {
107
- if (Array.isArray(type)) {
108
- const hasNull = type.includes('null');
109
- const nonNullTypes = type.filter(t => t !== 'null');
110
-
111
- if (nonNullTypes.length === 0) {
112
- // Only null type
113
- result.type = 'null';
114
- } else {
115
- // One or more non-null types: always use anyOf
116
- result.anyOf = nonNullTypes.map(t => ({ type: t }));
117
- if (hasNull) {
118
- result.nullable = true;
119
- }
120
- }
121
- } else {
122
- result.type = type;
123
- }
124
- }
125
-
126
- const values =
127
- enumValues ?? (constValue !== undefined ? [constValue] : undefined);
128
-
129
- if (values !== undefined) {
130
- addEnumToSchema({ values, type, result });
131
- }
132
-
133
- if (properties != null) {
134
- result.properties = Object.entries(properties).reduce(
135
- (acc, [key, value]) => {
136
- acc[key] = convertJSONSchemaDefinition(value, false, referenceContext);
137
- return acc;
138
- },
139
- {} as Record<string, unknown>,
140
- );
141
- }
142
-
143
- if (items) {
144
- result.items = Array.isArray(items)
145
- ? items.map(item =>
146
- convertJSONSchemaDefinition(item, false, referenceContext),
147
- )
148
- : convertJSONSchemaDefinition(items, false, referenceContext);
149
- }
150
-
151
- if (allOf) {
152
- result.allOf = allOf.map(item =>
153
- convertJSONSchemaDefinition(item, false, referenceContext),
154
- );
155
- }
156
- if (anyOf) {
157
- // Handle cases where anyOf includes a null type
158
- if (
159
- anyOf.some(
160
- schema => typeof schema === 'object' && schema?.type === 'null',
161
- )
162
- ) {
163
- const nonNullSchemas = anyOf.filter(
164
- schema => !(typeof schema === 'object' && schema?.type === 'null'),
165
- );
166
-
167
- if (nonNullSchemas.length === 1) {
168
- // If there's only one non-null schema, convert it and make it nullable
169
- const converted = convertJSONSchemaDefinition(
170
- nonNullSchemas[0],
171
- false,
172
- referenceContext,
173
- );
174
- if (typeof converted === 'object') {
175
- result.nullable = true;
176
- Object.assign(result, converted);
177
- }
178
- } else {
179
- // If there are multiple non-null schemas, keep them in anyOf
180
- result.anyOf = nonNullSchemas.map(item =>
181
- convertJSONSchemaDefinition(item, false, referenceContext),
182
- );
183
- result.nullable = true;
184
- }
185
- } else {
186
- result.anyOf = anyOf.map(item =>
187
- convertJSONSchemaDefinition(item, false, referenceContext),
188
- );
189
- }
190
- }
191
- if (oneOf) {
192
- result.oneOf = oneOf.map(item =>
193
- convertJSONSchemaDefinition(item, false, referenceContext),
194
- );
195
- }
196
-
197
- if (minLength !== undefined) {
198
- result.minLength = minLength;
199
- }
200
-
201
- if (minItems !== undefined) {
202
- result.minItems = minItems;
203
- }
204
-
205
- if (maxItems !== undefined) {
206
- result.maxItems = maxItems;
207
- }
208
-
209
- return result;
210
- }
211
-
212
- function convertJSONSchemaReference({
213
- jsonSchema,
214
- reference,
215
- isRoot,
216
- referenceContext,
217
- }: {
218
- jsonSchema: JSONSchema7;
219
- reference: string;
220
- isRoot: boolean;
221
- referenceContext: ReferenceContext;
222
- }): unknown {
223
- const { definition, referenceKey } = getReferencedDefinition(
224
- reference,
225
- referenceContext,
226
- );
227
-
228
- if (referenceContext.resolvingReferences.has(referenceKey)) {
229
- throw new UnsupportedFunctionalityError({
230
- functionality: `${recursiveReferenceFunctionalityPrefix} ${reference}`,
231
- message:
232
- 'Google schema conversion does not support recursive JSON Schema references.',
233
- });
234
- }
235
-
236
- const resolvingReferences = new Set(referenceContext.resolvingReferences);
237
- resolvingReferences.add(referenceKey);
238
-
239
- // Inline references instead of emitting Google's `ref` / `defs` fields.
240
- // Those fields are supported by Vertex AI's Schema representation but are
241
- // rejected by the Gemini Developer API representation used by this shared
242
- // converter.
243
- const { $ref: _reference, ...siblingSchema } = jsonSchema;
244
- const resolvedSchema =
245
- typeof definition === 'boolean'
246
- ? definition
247
- ? siblingSchema
248
- : false
249
- : { ...definition, ...siblingSchema };
250
-
251
- return convertJSONSchemaDefinition(resolvedSchema, isRoot, {
252
- ...referenceContext,
253
- resolvingReferences,
254
- });
255
- }
256
-
257
- function getReferencedDefinition(
258
- reference: string,
259
- referenceContext: ReferenceContext,
260
- ): {
261
- definition: JSONSchema7Definition;
262
- referenceKey: string;
263
- } {
264
- const definitionSources = [
265
- {
266
- prefix: '#/$defs/',
267
- definitions: referenceContext.dollarDefinitions,
268
- },
269
- {
270
- prefix: '#/definitions/',
271
- definitions: referenceContext.definitions,
272
- },
273
- ];
274
-
275
- const source = definitionSources.find(({ prefix }) =>
276
- reference.startsWith(prefix),
277
- );
278
- const encodedDefinitionName = source
279
- ? reference.slice(source.prefix.length)
280
- : undefined;
281
-
282
- if (
283
- source == null ||
284
- encodedDefinitionName == null ||
285
- encodedDefinitionName.length === 0 ||
286
- encodedDefinitionName.includes('/')
287
- ) {
288
- throwUnsupportedReference(reference);
289
- }
290
-
291
- let decodedDefinitionName: string;
292
- try {
293
- decodedDefinitionName = decodeURIComponent(encodedDefinitionName);
294
- } catch {
295
- throwUnsupportedReference(reference);
296
- }
297
-
298
- if (
299
- decodedDefinitionName.includes('/') ||
300
- /~(?![01])/u.test(decodedDefinitionName) ||
301
- source.definitions == null
302
- ) {
303
- throwUnsupportedReference(reference);
304
- }
305
-
306
- const definitionName = decodedDefinitionName.replace(/~[01]/g, match =>
307
- match === '~1' ? '/' : '~',
308
- );
309
-
310
- if (
311
- !Object.prototype.hasOwnProperty.call(source.definitions, definitionName)
312
- ) {
313
- throwUnsupportedReference(reference);
314
- }
315
-
316
- return {
317
- definition: source.definitions[definitionName],
318
- referenceKey: `${source.prefix}${definitionName}`,
319
- };
320
- }
321
-
322
- function throwUnsupportedReference(reference: string): never {
323
- throw new UnsupportedFunctionalityError({
324
- functionality: `JSON Schema reference: ${reference}`,
325
- message:
326
- 'Google schema conversion only supports references to direct children of root-level $defs or definitions.',
327
- });
328
- }
329
-
330
- type EnumValues = NonNullable<JSONSchema7['enum']>;
331
- type EnumType = 'string' | 'number' | 'integer' | 'boolean';
332
- type GoogleEnumSchema = {
333
- type?: JSONSchema7['type'];
334
- enum?: JSONSchema7['enum'];
335
- format?: JSONSchema7['format'];
336
- anyOf?: JSONSchema7['anyOf'];
337
- nullable?: boolean;
338
- };
339
-
340
- function addEnumToSchema({
341
- values,
342
- type,
343
- result,
344
- }: {
345
- values: EnumValues;
346
- type: JSONSchema7['type'];
347
- result: GoogleEnumSchema;
348
- }) {
349
- const nullable =
350
- (Array.isArray(type) && type.includes('null')) ||
351
- (type === undefined && values.includes(null));
352
-
353
- // Gemini uses nullable instead of a null enum member.
354
- const enumValues = nullable ? values.filter(value => value !== null) : values;
355
-
356
- if (values.length > 0 && values.every(value => value === null)) {
357
- const typeAllowsNull =
358
- type === undefined ||
359
- type === 'null' ||
360
- (Array.isArray(type) && type.includes('null'));
361
-
362
- if (typeAllowsNull) {
363
- result.type = 'null';
364
- if (Array.isArray(type)) {
365
- delete result.anyOf;
366
- }
367
- return;
368
- }
369
- }
370
-
371
- const enumType = getEnumType({ values: enumValues, type });
372
-
373
- if (enumType === undefined) {
374
- throw new UnsupportedFunctionalityError({
375
- functionality: 'JSON Schema enum with mixed or unsupported values',
376
- message:
377
- 'Google does not support this JSON Schema enum. Enum values must share one supported primitive type and match the schema type.',
378
- });
379
- }
380
-
381
- result.type = enumType;
382
-
383
- // The earlier type-array conversion created anyOf. The enum gives us one
384
- // concrete value type, so store that type directly.
385
- if (Array.isArray(type)) {
386
- delete result.anyOf;
387
- }
388
-
389
- if (nullable) {
390
- result.nullable = true;
391
- }
392
-
393
- if (enumType === 'string') {
394
- result.enum = enumValues;
395
- } else {
396
- result.format = 'enum';
397
- result.enum = enumValues.map(String);
398
- }
399
- }
400
-
401
- function getEnumType({
402
- values,
403
- type,
404
- }: {
405
- values: EnumValues;
406
- type: JSONSchema7['type'];
407
- }): EnumType | undefined {
408
- if (values.length === 0) {
409
- return undefined;
410
- }
411
-
412
- const typeAllows = (enumType: EnumType) =>
413
- type === undefined ||
414
- type === enumType ||
415
- (Array.isArray(type) && type.includes(enumType));
416
-
417
- if (
418
- typeAllows('string') &&
419
- values.every(value => typeof value === 'string')
420
- ) {
421
- return 'string';
422
- }
423
-
424
- if (
425
- (typeAllows('number') || typeAllows('integer')) &&
426
- values.every(value => typeof value === 'number' && Number.isFinite(value))
427
- ) {
428
- if (typeAllows('number')) {
429
- return 'number';
430
- }
431
-
432
- if (values.every(value => Number.isInteger(value))) {
433
- return 'integer';
434
- }
435
- }
436
-
437
- if (
438
- typeAllows('boolean') &&
439
- values.every(value => typeof value === 'boolean')
440
- ) {
441
- return 'boolean';
442
- }
443
-
444
- return undefined;
445
- }
446
-
447
- function isEmptyObjectSchema(jsonSchema: JSONSchema7Definition): boolean {
448
- return (
449
- jsonSchema != null &&
450
- typeof jsonSchema === 'object' &&
451
- jsonSchema.type === 'object' &&
452
- (jsonSchema.properties == null ||
453
- Object.keys(jsonSchema.properties).length === 0) &&
454
- !jsonSchema.additionalProperties
455
- );
456
- }