@ai-sdk/google 3.0.114 → 3.0.116

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": "3.0.114",
3
+ "version": "3.0.116",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@ai-sdk/provider": "3.0.15",
40
- "@ai-sdk/provider-utils": "4.0.48"
40
+ "@ai-sdk/provider-utils": "4.0.49"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -100,10 +100,6 @@ function convertJSONSchemaDefinition(
100
100
  if (required) result.required = required;
101
101
  if (format) result.format = format;
102
102
 
103
- if (constValue !== undefined) {
104
- result.enum = [constValue];
105
- }
106
-
107
103
  // Handle type
108
104
  if (type) {
109
105
  if (Array.isArray(type)) {
@@ -125,9 +121,11 @@ function convertJSONSchemaDefinition(
125
121
  }
126
122
  }
127
123
 
128
- // Handle enum
129
- if (enumValues !== undefined) {
130
- result.enum = enumValues;
124
+ const values =
125
+ enumValues ?? (constValue !== undefined ? [constValue] : undefined);
126
+
127
+ if (values !== undefined) {
128
+ addEnumToSchema({ values, type, result });
131
129
  }
132
130
 
133
131
  if (properties != null) {
@@ -201,6 +199,123 @@ function convertJSONSchemaDefinition(
201
199
  return result;
202
200
  }
203
201
 
202
+ type EnumValues = NonNullable<JSONSchema7['enum']>;
203
+ type EnumType = 'string' | 'number' | 'integer' | 'boolean';
204
+ type GoogleEnumSchema = {
205
+ type?: JSONSchema7['type'];
206
+ enum?: JSONSchema7['enum'];
207
+ format?: JSONSchema7['format'];
208
+ anyOf?: JSONSchema7['anyOf'];
209
+ nullable?: boolean;
210
+ };
211
+
212
+ function addEnumToSchema({
213
+ values,
214
+ type,
215
+ result,
216
+ }: {
217
+ values: EnumValues;
218
+ type: JSONSchema7['type'];
219
+ result: GoogleEnumSchema;
220
+ }) {
221
+ const nullable =
222
+ (Array.isArray(type) && type.includes('null')) ||
223
+ (type === undefined && values.includes(null));
224
+
225
+ // Gemini uses nullable instead of a null enum member.
226
+ const enumValues = nullable ? values.filter(value => value !== null) : values;
227
+
228
+ if (values.length > 0 && values.every(value => value === null)) {
229
+ const typeAllowsNull =
230
+ type === undefined ||
231
+ type === 'null' ||
232
+ (Array.isArray(type) && type.includes('null'));
233
+
234
+ if (typeAllowsNull) {
235
+ result.type = 'null';
236
+ if (Array.isArray(type)) {
237
+ delete result.anyOf;
238
+ }
239
+ return;
240
+ }
241
+ }
242
+
243
+ const enumType = getEnumType({ values: enumValues, type });
244
+
245
+ if (enumType === undefined) {
246
+ throw new UnsupportedFunctionalityError({
247
+ functionality: 'JSON Schema enum with mixed or unsupported values',
248
+ message:
249
+ 'Google does not support this JSON Schema enum. Enum values must share one supported primitive type and match the schema type.',
250
+ });
251
+ }
252
+
253
+ result.type = enumType;
254
+
255
+ // The earlier type-array conversion created anyOf. The enum gives us one
256
+ // concrete value type, so store that type directly.
257
+ if (Array.isArray(type)) {
258
+ delete result.anyOf;
259
+ }
260
+
261
+ if (nullable) {
262
+ result.nullable = true;
263
+ }
264
+
265
+ if (enumType === 'string') {
266
+ result.enum = enumValues;
267
+ } else {
268
+ result.format = 'enum';
269
+ result.enum = enumValues.map(String);
270
+ }
271
+ }
272
+
273
+ function getEnumType({
274
+ values,
275
+ type,
276
+ }: {
277
+ values: EnumValues;
278
+ type: JSONSchema7['type'];
279
+ }): EnumType | undefined {
280
+ if (values.length === 0) {
281
+ return undefined;
282
+ }
283
+
284
+ const typeAllows = (enumType: EnumType) =>
285
+ type === undefined ||
286
+ type === enumType ||
287
+ (Array.isArray(type) && type.includes(enumType));
288
+
289
+ if (
290
+ typeAllows('string') &&
291
+ values.every(value => typeof value === 'string')
292
+ ) {
293
+ return 'string';
294
+ }
295
+
296
+ if (
297
+ (typeAllows('number') || typeAllows('integer')) &&
298
+ values.every(value => typeof value === 'number' && Number.isFinite(value))
299
+ ) {
300
+ if (typeAllows('number')) {
301
+ return 'number';
302
+ }
303
+
304
+ if (values.every(value => Number.isInteger(value))) {
305
+ return 'integer';
306
+ }
307
+ }
308
+
309
+ if (
310
+ typeAllows('boolean') &&
311
+ values.every(value => typeof value === 'boolean')
312
+ ) {
313
+ return 'boolean';
314
+ }
315
+
316
+ return undefined;
317
+ }
318
+
204
319
  function convertJSONSchemaReference({
205
320
  jsonSchema,
206
321
  reference,
@@ -318,6 +433,7 @@ function throwUnsupportedReference(reference: string): never {
318
433
  'Google schema conversion only supports references to direct children of root-level $defs or definitions.',
319
434
  });
320
435
  }
436
+
321
437
  function isEmptyObjectSchema(jsonSchema: JSONSchema7Definition): boolean {
322
438
  return (
323
439
  jsonSchema != null &&
@@ -4,6 +4,7 @@ import type {
4
4
  ImageModelV3,
5
5
  LanguageModelV3,
6
6
  ProviderV3,
7
+ TranscriptionModelV3,
7
8
  } from '@ai-sdk/provider';
8
9
  import {
9
10
  generateId,
@@ -32,6 +33,8 @@ import {
32
33
  } from './interactions/google-interactions-language-model';
33
34
  import type { GoogleInteractionsModelId } from './interactions/google-interactions-language-model-options';
34
35
  import type { GoogleInteractionsAgentName } from './interactions/google-interactions-agent';
36
+ import { GoogleTranscriptionModel } from './transcription/google-transcription-model';
37
+ import type { GoogleTranscriptionModelId } from './transcription/google-transcription-model-options';
35
38
 
36
39
  export interface GoogleGenerativeAIProvider extends ProviderV3 {
37
40
  (modelId: GoogleGenerativeAIModelId): LanguageModelV3;
@@ -75,6 +78,17 @@ export interface GoogleGenerativeAIProvider extends ProviderV3 {
75
78
  modelId: GoogleGenerativeAIEmbeddingModelId,
76
79
  ): EmbeddingModelV3;
77
80
 
81
+ /**
82
+ * Creates a model for transcription (speech-to-text), e.g.
83
+ * `gemini-3.5-transcribe`.
84
+ */
85
+ transcription(modelId: GoogleTranscriptionModelId): TranscriptionModelV3;
86
+
87
+ /**
88
+ * Creates a model for transcription (speech-to-text).
89
+ */
90
+ transcriptionModel(modelId: GoogleTranscriptionModelId): TranscriptionModelV3;
91
+
78
92
  /**
79
93
  * Creates a model for video generation.
80
94
  */
@@ -245,6 +259,14 @@ export function createGoogleGenerativeAI(
245
259
  fetch: options.fetch,
246
260
  });
247
261
 
262
+ const createTranscriptionModel = (modelId: GoogleTranscriptionModelId) =>
263
+ new GoogleTranscriptionModel(modelId, {
264
+ provider: `${providerName}.transcription`,
265
+ baseURL,
266
+ headers: getHeaders,
267
+ fetch: options.fetch,
268
+ });
269
+
248
270
  const createVideoModel = (modelId: GoogleGenerativeAIVideoModelId) =>
249
271
  new GoogleGenerativeAIVideoModel(modelId, {
250
272
  provider: providerName,
@@ -291,6 +313,8 @@ export function createGoogleGenerativeAI(
291
313
  provider.textEmbeddingModel = createEmbeddingModel;
292
314
  provider.image = createImageModel;
293
315
  provider.imageModel = createImageModel;
316
+ provider.transcription = createTranscriptionModel;
317
+ provider.transcriptionModel = createTranscriptionModel;
294
318
  provider.video = createVideoModel;
295
319
  provider.videoModel = createVideoModel;
296
320
  provider.interactions = createInteractionsModel;
package/src/index.ts CHANGED
@@ -27,6 +27,11 @@ export type {
27
27
  } from './interactions/google-interactions-language-model-options';
28
28
  export type { GoogleInteractionsProviderMetadata } from './interactions/google-interactions-provider-metadata';
29
29
  export type { GoogleInteractionsAgentName } from './interactions/google-interactions-agent';
30
+ export { GoogleTranscriptionModel } from './transcription/google-transcription-model';
31
+ export type {
32
+ GoogleTranscriptionModelId,
33
+ GoogleTranscriptionModelOptions,
34
+ } from './transcription/google-transcription-model-options';
30
35
  export { createGoogleGenerativeAI, google } from './google-provider';
31
36
  export type {
32
37
  GoogleGenerativeAIProvider,
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ export type GoogleTranscriptionModelId =
4
+ | 'gemini-3.5-transcribe'
5
+ | 'gemini-3.5-transcribe-live'
6
+ | (string & {});
7
+
8
+ /**
9
+ * Speech recognition options for Gemini transcription models
10
+ * (`gemini-3.5-transcribe`). Maps onto Google's `AudioTranscriptionConfig`.
11
+ * The live variant (`gemini-3.5-transcribe-live`) requires streaming
12
+ * transcription, which is only available in AI SDK v7.
13
+ */
14
+ export const googleTranscriptionModelOptions = z.object({
15
+ /**
16
+ * BCP-47 language codes providing hints about the languages present in the
17
+ * audio. If omitted or empty, defaults to automatic language detection.
18
+ */
19
+ languageCodes: z.array(z.string()).optional(),
20
+
21
+ /**
22
+ * Custom vocabulary phrases, which bias the speech recognition model
23
+ * toward recognizing specific terms.
24
+ */
25
+ customVocabulary: z.array(z.string()).optional(),
26
+
27
+ /**
28
+ * Enables word-level timestamp generation.
29
+ */
30
+ wordTimestamp: z.boolean().optional(),
31
+
32
+ /**
33
+ * Enables speaker diarization.
34
+ */
35
+ diarization: z.boolean().optional(),
36
+
37
+ /**
38
+ * Transcription output formatting mode.
39
+ *
40
+ * - `VERBATIM` (default): exact literal transcript preserving filler
41
+ * words, repetitions, and false starts.
42
+ * - `SMART`: cleans up and structures the transcript in real time —
43
+ * disfluency removal, inline self-corrections, structured formatting
44
+ * (lists, numbers, dates, paragraph breaks), and grammar/casing polish.
45
+ */
46
+ mode: z.enum(['SMART', 'VERBATIM']).optional(),
47
+ });
48
+
49
+ export type GoogleTranscriptionModelOptions = z.infer<
50
+ typeof googleTranscriptionModelOptions
51
+ >;
@@ -0,0 +1,243 @@
1
+ import {
2
+ InvalidArgumentError,
3
+ type JSONObject,
4
+ type SharedV3Warning,
5
+ type TranscriptionModelV3,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ combineHeaders,
9
+ convertToBase64,
10
+ createJsonResponseHandler,
11
+ parseProviderOptions,
12
+ postJsonToApi,
13
+ resolve,
14
+ type FetchFunction,
15
+ type Resolvable,
16
+ } from '@ai-sdk/provider-utils';
17
+ import { z } from 'zod/v4';
18
+ import { googleFailedResponseHandler } from '../google-error';
19
+ import {
20
+ googleTranscriptionModelOptions,
21
+ type GoogleTranscriptionModelId,
22
+ type GoogleTranscriptionModelOptions,
23
+ } from './google-transcription-model-options';
24
+
25
+ /**
26
+ * Live transcription (`*-live` model variants) requires streaming support,
27
+ * which is only available in AI SDK v7 (transcription specification v4).
28
+ */
29
+ function isLiveTranscriptionModelId(modelId: string): boolean {
30
+ return modelId.includes('-live');
31
+ }
32
+
33
+ interface GoogleTranscriptionModelConfig {
34
+ provider: string;
35
+ baseURL: string;
36
+ headers?: Resolvable<Record<string, string | undefined>>;
37
+ fetch?: FetchFunction;
38
+ _internal?: {
39
+ currentDate?: () => Date;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Gemini transcription (speech-to-text) via the Interactions API
45
+ * (e.g. `gemini-3.5-transcribe`).
46
+ *
47
+ * @see https://ai.google.dev/gemini-api/docs/transcribe
48
+ */
49
+ export class GoogleTranscriptionModel implements TranscriptionModelV3 {
50
+ readonly specificationVersion = 'v3';
51
+
52
+ get provider(): string {
53
+ return this.config.provider;
54
+ }
55
+
56
+ constructor(
57
+ readonly modelId: GoogleTranscriptionModelId,
58
+ private readonly config: GoogleTranscriptionModelConfig,
59
+ ) {}
60
+
61
+ private async parseOptions(
62
+ providerOptions: Record<string, unknown> | undefined,
63
+ ): Promise<GoogleTranscriptionModelOptions | undefined> {
64
+ return parseProviderOptions({
65
+ provider: 'google',
66
+ providerOptions,
67
+ schema: googleTranscriptionModelOptions,
68
+ });
69
+ }
70
+
71
+ async doGenerate(
72
+ options: Parameters<TranscriptionModelV3['doGenerate']>[0],
73
+ ): Promise<Awaited<ReturnType<TranscriptionModelV3['doGenerate']>>> {
74
+ if (isLiveTranscriptionModelId(this.modelId)) {
75
+ throw new InvalidArgumentError({
76
+ argument: 'modelId',
77
+ message:
78
+ `Model '${this.modelId}' only supports streaming transcription, ` +
79
+ `which requires AI SDK v7. Use a unary model such as 'gemini-3.5-transcribe'.`,
80
+ });
81
+ }
82
+
83
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
84
+ const warnings: SharedV3Warning[] = [];
85
+ const googleOptions = await this.parseOptions(options.providerOptions);
86
+ const transcriptionConfig = buildTranscriptionConfig(googleOptions);
87
+
88
+ // Unary transcription is served by the Interactions API
89
+ // (https://ai.google.dev/gemini-api/docs/transcribe).
90
+ const requestBody = {
91
+ model: this.modelId,
92
+ input: [
93
+ {
94
+ type: 'audio',
95
+ data: convertToBase64(options.audio),
96
+ mime_type: options.mediaType,
97
+ },
98
+ ],
99
+ ...(transcriptionConfig != null
100
+ ? { generation_config: { transcription_config: transcriptionConfig } }
101
+ : {}),
102
+ };
103
+
104
+ const {
105
+ value: response,
106
+ responseHeaders,
107
+ rawValue: rawResponse,
108
+ } = await postJsonToApi({
109
+ url: `${this.config.baseURL}/interactions`,
110
+ headers: combineHeaders(
111
+ this.config.headers ? await resolve(this.config.headers) : undefined,
112
+ options.headers,
113
+ ),
114
+ body: requestBody,
115
+ failedResponseHandler: googleFailedResponseHandler,
116
+ successfulResponseHandler: createJsonResponseHandler(
117
+ googleInteractionsTranscriptionResponseSchema,
118
+ ),
119
+ abortSignal: options.abortSignal,
120
+ fetch: this.config.fetch,
121
+ });
122
+
123
+ let text = '';
124
+ const segments: Array<{
125
+ text: string;
126
+ startSecond: number;
127
+ endSecond: number;
128
+ }> = [];
129
+ for (const step of response.steps ?? []) {
130
+ for (const content of step.content ?? []) {
131
+ if (content.type !== 'text' || content.text == null) continue;
132
+ text += content.text;
133
+ for (const annotation of content.annotations ?? []) {
134
+ if (annotation.type !== 'word_info') continue;
135
+ const startSecond = parseOffsetSeconds(annotation.start_offset);
136
+ const endSecond = parseOffsetSeconds(annotation.end_offset);
137
+ if (
138
+ annotation.text == null ||
139
+ startSecond == null ||
140
+ endSecond == null
141
+ ) {
142
+ continue;
143
+ }
144
+ segments.push({ text: annotation.text, startSecond, endSecond });
145
+ }
146
+ }
147
+ }
148
+
149
+ return {
150
+ text,
151
+ segments,
152
+ language: undefined,
153
+ durationInSeconds: undefined,
154
+ warnings,
155
+ response: {
156
+ timestamp: currentDate,
157
+ modelId: this.modelId,
158
+ headers: responseHeaders,
159
+ body: rawResponse,
160
+ },
161
+ ...(response.usage != null
162
+ ? {
163
+ providerMetadata: {
164
+ google: { usage: response.usage as JSONObject },
165
+ },
166
+ }
167
+ : {}),
168
+ };
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Builds the Interactions API `transcription_config` (snake_case wire) from
174
+ * provider options; returns undefined when no options are set. Diarization
175
+ * and word timestamps are expressed inside the `mode` object per
176
+ * https://ai.google.dev/gemini-api/docs/transcribe.
177
+ */
178
+ function buildTranscriptionConfig(
179
+ options: GoogleTranscriptionModelOptions | undefined,
180
+ ): Record<string, unknown> | undefined {
181
+ if (options == null) return undefined;
182
+ const config: Record<string, unknown> = {};
183
+ if (options.languageCodes != null) {
184
+ config.language_codes = options.languageCodes;
185
+ }
186
+ if (options.customVocabulary != null) {
187
+ config.custom_vocabulary = options.customVocabulary;
188
+ }
189
+ if (
190
+ options.mode != null ||
191
+ options.diarization === true ||
192
+ options.wordTimestamp === true
193
+ ) {
194
+ config.mode = {
195
+ type: (options.mode ?? 'VERBATIM').toLowerCase(),
196
+ ...(options.diarization === true ? { diarization_mode: 'speaker' } : {}),
197
+ ...(options.wordTimestamp === true
198
+ ? { timestamp_granularities: ['word'] }
199
+ : {}),
200
+ };
201
+ }
202
+ return Object.keys(config).length > 0 ? config : undefined;
203
+ }
204
+
205
+ /** Parses a Google duration offset such as `"1s"` or `"9.400s"` to seconds. */
206
+ function parseOffsetSeconds(
207
+ offset: string | undefined | null,
208
+ ): number | undefined {
209
+ if (offset == null) return undefined;
210
+ const parsed = Number.parseFloat(offset);
211
+ return Number.isFinite(parsed) ? parsed : undefined;
212
+ }
213
+
214
+ const googleInteractionsWordAnnotationSchema = z.object({
215
+ type: z.string().nullish(),
216
+ text: z.string().nullish(),
217
+ speaker: z.string().nullish(),
218
+ start_offset: z.string().nullish(),
219
+ end_offset: z.string().nullish(),
220
+ });
221
+
222
+ const googleInteractionsTranscriptionResponseSchema = z.object({
223
+ status: z.string().nullish(),
224
+ steps: z
225
+ .array(
226
+ z.object({
227
+ type: z.string().nullish(),
228
+ content: z
229
+ .array(
230
+ z.object({
231
+ type: z.string().nullish(),
232
+ text: z.string().nullish(),
233
+ annotations: z
234
+ .array(googleInteractionsWordAnnotationSchema)
235
+ .nullish(),
236
+ }),
237
+ )
238
+ .nullish(),
239
+ }),
240
+ )
241
+ .nullish(),
242
+ usage: z.record(z.string(), z.unknown()).nullish(),
243
+ });