@ai-sdk/google 4.0.0-canary.80 → 4.0.0

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.
@@ -1091,6 +1091,45 @@ Google realtime models may require provider-specific audio formats, depending
1091
1091
  on the model and modality. See [Realtime](/docs/ai-sdk-core/realtime) for the
1092
1092
  complete setup and tool calling pattern.
1093
1093
 
1094
+ ### Live Translation
1095
+
1096
+ Use `gemini-3.5-live-translate-preview` for low-latency speech-to-speech
1097
+ translation. Configure the target language with
1098
+ `providerOptions.google.translationConfig` in the realtime session config you
1099
+ use when creating the token:
1100
+
1101
+ ```ts
1102
+ import {
1103
+ google,
1104
+ type Experimental_GoogleRealtimeModelOptions as GoogleRealtimeModelOptions,
1105
+ } from '@ai-sdk/google';
1106
+
1107
+ const token = await google.experimental_realtime.getToken({
1108
+ model: 'gemini-3.5-live-translate-preview',
1109
+ sessionConfig: {
1110
+ outputModalities: ['audio'],
1111
+ inputAudioTranscription: {},
1112
+ outputAudioTranscription: {},
1113
+ providerOptions: {
1114
+ google: {
1115
+ translationConfig: {
1116
+ targetLanguageCode: 'pl',
1117
+ echoTargetLanguage: true,
1118
+ },
1119
+ } satisfies GoogleRealtimeModelOptions,
1120
+ },
1121
+ },
1122
+ });
1123
+ ```
1124
+
1125
+ Set `targetLanguageCode` to a BCP-47 language code (defaults to `en`). When
1126
+ `echoTargetLanguage` is `true`, input audio already in the target language is
1127
+ echoed instead of producing silence.
1128
+
1129
+ Gemini Live Translation accepts audio input and produces translated audio
1130
+ output. Text input, tools, and custom instructions are not supported by this
1131
+ model.
1132
+
1094
1133
  ## Interactions API
1095
1134
 
1096
1135
  The [Gemini Interactions API](https://ai.google.dev/gemini-api/docs/interactions)
@@ -1977,7 +2016,7 @@ The `voice` argument can be set to one of Gemini's [30 prebuilt voices](https://
1977
2016
  e.g. `Kore`, `Puck`, `Zephyr`, or `Charon`. Voice names are case-sensitive. It defaults to `Kore`.
1978
2017
 
1979
2018
  ```ts highlight="6"
1980
- import { experimental_generateSpeech as generateSpeech } from 'ai';
2019
+ import { generateSpeech } from 'ai';
1981
2020
  import { google } from '@ai-sdk/google';
1982
2021
 
1983
2022
  const result = await generateSpeech({
@@ -2000,7 +2039,7 @@ For multi-speaker dialogue, pass a `multiSpeakerVoiceConfig` through `providerOp
2000
2039
  name must match a name used in the input text. When set, it overrides the top-level `voice`.
2001
2040
 
2002
2041
  ```ts highlight="8-23"
2003
- import { experimental_generateSpeech as generateSpeech } from 'ai';
2042
+ import { generateSpeech } from 'ai';
2004
2043
  import { google, type GoogleSpeechModelOptions } from '@ai-sdk/google';
2005
2044
 
2006
2045
  const result = await generateSpeech({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.0-canary.80",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,15 +35,15 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.0-canary.18",
39
- "@ai-sdk/provider-utils": "5.0.0-canary.47"
38
+ "@ai-sdk/provider": "4.0.0",
39
+ "@ai-sdk/provider-utils": "5.0.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "22.19.19",
43
43
  "tsup": "^8.5.1",
44
44
  "typescript": "5.8.3",
45
45
  "zod": "3.25.76",
46
- "@ai-sdk/test-server": "2.0.0-canary.6",
46
+ "@ai-sdk/test-server": "2.0.0",
47
47
  "@vercel/ai-tsconfig": "0.0.0"
48
48
  },
49
49
  "peerDependencies": {
@@ -276,6 +276,35 @@ function parsePath(rawPath: string): Array<string | number> {
276
276
  return segments;
277
277
  }
278
278
 
279
+ const hasOwn = Object.prototype.hasOwnProperty;
280
+
281
+ /**
282
+ * Checks only direct properties so path traversal never follows the prototype chain.
283
+ */
284
+ function hasOwnProperty(
285
+ obj: Record<string | number, unknown>,
286
+ key: string | number,
287
+ ): boolean {
288
+ return hasOwn.call(obj, key);
289
+ }
290
+
291
+ /**
292
+ * Defines path values as own data properties so special keys like `__proto__`
293
+ * cannot invoke prototype setters while accumulating streamed arguments.
294
+ */
295
+ function defineOwnProperty(
296
+ obj: Record<string | number, unknown>,
297
+ key: string | number,
298
+ value: unknown,
299
+ ): void {
300
+ Object.defineProperty(obj, key, {
301
+ value,
302
+ enumerable: true,
303
+ configurable: true,
304
+ writable: true,
305
+ });
306
+ }
307
+
279
308
  /**
280
309
  * Traverses a nested object along the given path segments and returns the leaf value.
281
310
  *
@@ -289,7 +318,9 @@ function getNestedValue(
289
318
  let current: unknown = obj;
290
319
  for (const pathSegment of segments) {
291
320
  if (current == null || typeof current !== 'object') return undefined;
292
- current = (current as Record<string | number, unknown>)[pathSegment];
321
+ const currentRecord = current as Record<string | number, unknown>;
322
+ if (!hasOwnProperty(currentRecord, pathSegment)) return undefined;
323
+ current = currentRecord[pathSegment];
293
324
  }
294
325
  return current;
295
326
  }
@@ -309,12 +340,16 @@ function setNestedValue(
309
340
  for (let i = 0; i < segments.length - 1; i++) {
310
341
  const pathSegment = segments[i];
311
342
  const nextSeg = segments[i + 1];
312
- if (current[pathSegment] == null) {
313
- current[pathSegment] = typeof nextSeg === 'number' ? [] : {};
343
+ if (!hasOwnProperty(current, pathSegment) || current[pathSegment] == null) {
344
+ defineOwnProperty(
345
+ current,
346
+ pathSegment,
347
+ typeof nextSeg === 'number' ? [] : {},
348
+ );
314
349
  }
315
350
  current = current[pathSegment] as Record<string | number, unknown>;
316
351
  }
317
- current[segments[segments.length - 1]] = value;
352
+ defineOwnProperty(current, segments[segments.length - 1], value);
318
353
  }
319
354
 
320
355
  /**
@@ -9,6 +9,7 @@ import {
9
9
  createJsonResponseHandler,
10
10
  delay,
11
11
  getFromApi,
12
+ isSameOrigin,
12
13
  parseProviderOptions,
13
14
  postJsonToApi,
14
15
  resolve,
@@ -263,10 +264,13 @@ export class GoogleVideoModel implements Experimental_VideoModelV4 {
263
264
  for (const generatedSample of response.generateVideoResponse
264
265
  .generatedSamples) {
265
266
  if (generatedSample.video?.uri) {
266
- // Append API key to URL for authentication during download
267
- const urlWithAuth = apiKey
268
- ? `${generatedSample.video.uri}${generatedSample.video.uri.includes('?') ? '&' : '?'}key=${apiKey}`
269
- : generatedSample.video.uri;
267
+ // Append the API key to the download URL for authentication, but only
268
+ // when the response-supplied URI stays on the provider's own origin —
269
+ // otherwise the key would leak to whatever host the response names.
270
+ const urlWithAuth =
271
+ apiKey && isSameOrigin(generatedSample.video.uri, this.config.baseURL)
272
+ ? `${generatedSample.video.uri}${generatedSample.video.uri.includes('?') ? '&' : '?'}key=${apiKey}`
273
+ : generatedSample.video.uri;
270
274
 
271
275
  videos.push({
272
276
  type: 'url',
package/src/index.ts CHANGED
@@ -56,5 +56,9 @@ export type {
56
56
  } from './google-provider';
57
57
  export { GoogleRealtimeModel as Experimental_GoogleRealtimeModel } from './realtime/google-realtime-model';
58
58
  export type { GoogleRealtimeModelConfig as Experimental_GoogleRealtimeModelConfig } from './realtime/google-realtime-model';
59
+ export type {
60
+ GoogleRealtimeModelId as Experimental_GoogleRealtimeModelId,
61
+ GoogleRealtimeModelOptions as Experimental_GoogleRealtimeModelOptions,
62
+ } from './realtime/google-realtime-model-options';
59
63
 
60
64
  export { VERSION } from './version';
@@ -459,7 +459,6 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
459
459
  const url = `${this.config.baseURL}/interactions`;
460
460
 
461
461
  const mergedHeaders = combineHeaders(
462
- INTERACTIONS_API_REVISION_HEADER,
463
462
  this.config.headers ? await resolve(this.config.headers) : undefined,
464
463
  options.headers,
465
464
  );
@@ -588,7 +587,6 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
588
587
  const url = `${this.config.baseURL}/interactions`;
589
588
 
590
589
  const mergedHeaders = combineHeaders(
591
- INTERACTIONS_API_REVISION_HEADER,
592
590
  this.config.headers ? await resolve(this.config.headers) : undefined,
593
591
  options.headers,
594
592
  );
@@ -757,15 +755,6 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
757
755
  }
758
756
  }
759
757
 
760
- /*
761
- * Pins the Interactions API revision the SDK targets. Sent on every request
762
- * the model issues so model-id calls, agent calls, polling, SSE reconnects,
763
- * and cancellation all hit the same schema.
764
- */
765
- const INTERACTIONS_API_REVISION_HEADER: Record<string, string> = {
766
- 'Api-Revision': '2026-05-20',
767
- };
768
-
769
758
  function pruneUndefined<T extends Record<string, unknown>>(obj: T): T {
770
759
  const result: Record<string, unknown> = {};
771
760
  for (const [key, value] of Object.entries(obj)) {
@@ -8,6 +8,7 @@ import type {
8
8
  import { safeParseJSON } from '@ai-sdk/provider-utils';
9
9
  import { convertJSONSchemaToOpenAPISchema } from '../convert-json-schema-to-openapi-schema';
10
10
  import { getModelPath } from '../get-model-path';
11
+ import type { GoogleRealtimeModelOptions } from './google-realtime-model-options';
11
12
 
12
13
  type GoogleRealtimeFunctionCall = {
13
14
  id: string;
@@ -38,6 +39,10 @@ type GoogleRealtimeWireEvent = {
38
39
  inputTranscription?: { text?: string };
39
40
  };
40
41
 
42
+ function isRecord(value: unknown): value is Record<string, unknown> {
43
+ return value != null && typeof value === 'object' && !Array.isArray(value);
44
+ }
45
+
41
46
  /**
42
47
  * Stateful event mapper for Google's Gemini Live API.
43
48
  *
@@ -268,6 +273,12 @@ export class GoogleRealtimeEventMapper {
268
273
  };
269
274
 
270
275
  case 'input-audio-commit':
276
+ return {
277
+ realtimeInput: {
278
+ audioStreamEnd: true,
279
+ },
280
+ };
281
+
271
282
  case 'input-audio-clear':
272
283
  case 'response-create':
273
284
  case 'response-cancel':
@@ -375,8 +386,25 @@ export function buildGoogleSessionConfig(
375
386
  setup.outputAudioTranscription = {};
376
387
  }
377
388
 
378
- if (config?.providerOptions != null) {
379
- Object.assign(setup, config.providerOptions);
389
+ if (config?.providerOptions == null) {
390
+ return setup;
391
+ }
392
+
393
+ const { google, ...providerOptions } = config.providerOptions;
394
+ Object.assign(setup, providerOptions);
395
+
396
+ const googleOptions = isRecord(google)
397
+ ? (google as GoogleRealtimeModelOptions)
398
+ : undefined;
399
+
400
+ if (googleOptions?.translationConfig != null) {
401
+ const target = isRecord(setup.generationConfig)
402
+ ? setup.generationConfig
403
+ : generationConfig;
404
+ setup.generationConfig = {
405
+ ...target,
406
+ translationConfig: googleOptions.translationConfig,
407
+ };
380
408
  }
381
409
 
382
410
  return setup;
@@ -1,3 +1,23 @@
1
1
  export type GoogleRealtimeModelId = string;
2
2
 
3
- export type GoogleRealtimeModelOptions = Record<string, never>;
3
+ export type GoogleRealtimeModelOptions = {
4
+ /**
5
+ * Gemini Live Translation configuration.
6
+ *
7
+ * Required for `gemini-3.5-live-translate-preview` when translating speech
8
+ * to a target language.
9
+ */
10
+ translationConfig?: {
11
+ /**
12
+ * BCP-47 language code of the language to translate into.
13
+ * Defaults to `en` in the Gemini API.
14
+ */
15
+ targetLanguageCode?: string;
16
+
17
+ /**
18
+ * Whether input audio already in the target language should be echoed
19
+ * instead of producing silence.
20
+ */
21
+ echoTargetLanguage?: boolean;
22
+ };
23
+ };
@@ -1,2 +1,6 @@
1
1
  export { GoogleRealtimeModel as Experimental_GoogleRealtimeModel } from './google-realtime-model';
2
2
  export type { GoogleRealtimeModelConfig as Experimental_GoogleRealtimeModelConfig } from './google-realtime-model';
3
+ export type {
4
+ GoogleRealtimeModelId as Experimental_GoogleRealtimeModelId,
5
+ GoogleRealtimeModelOptions as Experimental_GoogleRealtimeModelOptions,
6
+ } from './google-realtime-model-options';