@ai-sdk/google 4.0.78 → 4.0.80

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.
@@ -923,14 +923,14 @@ The `vertexRagStore` tool accepts the following configuration options:
923
923
 
924
924
  ### Image Outputs
925
925
 
926
- Gemini models with image generation capabilities (e.g. `gemini-2.5-flash-image`) support generating images as part of a multimodal response. Images are exposed as files in the response.
926
+ Gemini models with image generation capabilities (e.g. `gemini-3.1-flash-image-preview`) support generating images as part of a multimodal response. Images are exposed as files in the response.
927
927
 
928
928
  ```ts
929
929
  import { google } from '@ai-sdk/google';
930
930
  import { generateText } from 'ai';
931
931
 
932
932
  const result = await generateText({
933
- model: google('gemini-2.5-flash-image'),
933
+ model: google('gemini-3.1-flash-image-preview'),
934
934
  prompt:
935
935
  'Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme',
936
936
  });
@@ -1088,7 +1088,7 @@ You can pass a `webhookUrl` to receive a notification when the batch reaches a t
1088
1088
  import { google } from '@ai-sdk/google';
1089
1089
  import { experimental_startBatch as startBatch } from 'ai';
1090
1090
 
1091
- const model = 'gemini-3.6-flash';
1091
+ const model = 'gemini-3.8-flash';
1092
1092
 
1093
1093
  const batch = await startBatch({
1094
1094
  provider: google,
@@ -1578,7 +1578,7 @@ import { google, type GoogleInteractionsVideoOptions } from '@ai-sdk/google';
1578
1578
  import { generateText } from 'ai';
1579
1579
 
1580
1580
  const result = await generateText({
1581
- model: google.interactions('gemini-3.7-flash'),
1581
+ model: google.interactions('gemini-3.8-flash'),
1582
1582
  messages: [
1583
1583
  {
1584
1584
  role: 'user',
@@ -1778,7 +1778,7 @@ import {
1778
1778
  import { generateText } from 'ai';
1779
1779
 
1780
1780
  const result = await generateText({
1781
- model: google.interactions('gemini-2.5-flash-image'),
1781
+ model: google.interactions('gemini-3.1-flash-image-preview'),
1782
1782
  prompt:
1783
1783
  'Tell me a three sentence bedtime story about a unicorn, accompanied by a suitable illustration.',
1784
1784
  providerOptions: {
@@ -2042,7 +2042,7 @@ You can create models that call the [Google Generative AI embeddings API](https:
2042
2042
  using the `.embedding()` factory method.
2043
2043
 
2044
2044
  ```ts
2045
- const model = google.embedding('gemini-embedding-001');
2045
+ const model = google.embedding('gemini-embedding-2');
2046
2046
  ```
2047
2047
 
2048
2048
  The Google provider sends API calls to the right endpoint based on the type of embedding:
@@ -2078,7 +2078,7 @@ import { google, type GoogleEmbeddingModelOptions } from '@ai-sdk/google';
2078
2078
  import { embedMany } from 'ai';
2079
2079
 
2080
2080
  const { embeddings } = await embedMany({
2081
- model: google.embedding('gemini-embedding-2-preview'),
2081
+ model: google.embedding('gemini-embedding-2'),
2082
2082
  values: ['sunny day at the beach', 'rainy afternoon in the city'],
2083
2083
  providerOptions: {
2084
2084
  google: {
@@ -2132,14 +2132,14 @@ output capabilities through the `:generateContent` API.
2132
2132
 
2133
2133
  ### Gemini Image Models
2134
2134
 
2135
- [Gemini image models](https://ai.google.dev/gemini-api/docs/image-generation) (e.g. `gemini-2.5-flash-image`) are technically multimodal output language models, but they can be used with the `generateImage()` function for a simpler image generation experience. Internally, the provider calls the language model API with `responseModalities: ['IMAGE']`.
2135
+ [Gemini image models](https://ai.google.dev/gemini-api/docs/image-generation) (e.g. `gemini-3.1-flash-image-preview`) are technically multimodal output language models, but they can be used with the `generateImage()` function for a simpler image generation experience. Internally, the provider calls the language model API with `responseModalities: ['IMAGE']`.
2136
2136
 
2137
2137
  ```ts
2138
2138
  import { google } from '@ai-sdk/google';
2139
2139
  import { generateImage } from 'ai';
2140
2140
 
2141
2141
  const { image } = await generateImage({
2142
- model: google.image('gemini-2.5-flash-image'),
2142
+ model: google.image('gemini-3.1-flash-image-preview'),
2143
2143
  prompt: 'A photorealistic image of a cat wearing a wizard hat',
2144
2144
  aspectRatio: '1:1',
2145
2145
  });
@@ -2155,7 +2155,7 @@ import fs from 'node:fs';
2155
2155
  const sourceImage = fs.readFileSync('./cat.png');
2156
2156
 
2157
2157
  const { image } = await generateImage({
2158
- model: google.image('gemini-2.5-flash-image'),
2158
+ model: google.image('gemini-3.1-flash-image-preview'),
2159
2159
  prompt: {
2160
2160
  text: 'Add a small wizard hat to this cat',
2161
2161
  images: [sourceImage],
@@ -2170,7 +2170,7 @@ import { google } from '@ai-sdk/google';
2170
2170
  import { generateImage } from 'ai';
2171
2171
 
2172
2172
  const { image } = await generateImage({
2173
- model: google.image('gemini-2.5-flash-image'),
2173
+ model: google.image('gemini-3.1-flash-image-preview'),
2174
2174
  prompt: {
2175
2175
  text: 'Add a small wizard hat to this cat',
2176
2176
  images: ['https://example.com/cat.png'],
@@ -2233,45 +2233,100 @@ console.log(result.providerMetadata?.google?.groundingMetadata);
2233
2233
  You can create models that call the [Gemini text-to-speech API](https://ai.google.dev/gemini-api/docs/speech-generation)
2234
2234
  using the `.speech()` factory method.
2235
2235
 
2236
- The first argument is the model id e.g. `gemini-2.5-flash-preview-tts`.
2236
+ The first argument is the model ID, such as `gemini-3.8-flash-tts` or
2237
+ `gemini-3.8-flash-lite-tts`. Speech generation uses the `generateContent` API.
2237
2238
 
2238
2239
  ```ts
2239
- const model = google.speech('gemini-2.5-flash-preview-tts');
2240
+ const model = google.speech('gemini-3.8-flash-tts');
2240
2241
  ```
2241
2242
 
2242
2243
  The `voice` argument can be set to one of Gemini's [30 prebuilt voices](https://ai.google.dev/gemini-api/docs/speech-generation#voices)
2243
- e.g. `Kore`, `Puck`, `Zephyr`, or `Charon`. Voice names are case-sensitive. It defaults to `Kore`.
2244
+ e.g. `Kore`, `Puck`, `Zephyr`, or `Charon`. Voice names are case-sensitive.
2245
+ The default voice is `Kore`. Custom voice IDs and replication keys are not supported.
2244
2246
 
2245
2247
  ```ts highlight="6"
2246
2248
  import { generateSpeech } from 'ai';
2247
2249
  import { google } from '@ai-sdk/google';
2248
2250
 
2249
2251
  const result = await generateSpeech({
2250
- model: google.speech('gemini-2.5-flash-preview-tts'),
2252
+ model: google.speech('gemini-3.8-flash-tts'),
2251
2253
  text: 'Hello, world!',
2252
2254
  voice: 'Kore', // Gemini voice name
2253
2255
  });
2254
2256
  ```
2255
2257
 
2256
- By default the generated audio is returned as a playable WAV file (`result.audio.mediaType` is
2257
- `audio/wav`). Set `outputFormat: 'pcm'` to receive the raw signed 16-bit little-endian mono PCM
2258
- bytes instead; the sample rate is reported in `result.providerMetadata.google.sampleRate`.
2258
+ By default the generated audio is returned as a playable WAV file
2259
+ (`result.audio.mediaType` is `audio/wav`). Gemini 3.8 returns a complete WAV file;
2260
+ the provider passes those bytes through unchanged. Older models return raw PCM,
2261
+ which the provider wraps in a WAV container. Write `result.audio.uint8Array`
2262
+ directly to disk without adding another WAV header.
2263
+
2264
+ Gemini 3.8 supports the following `outputFormat` values:
2259
2265
 
2260
- Gemini honors natural-language style direction. The `instructions` argument is prepended to the
2261
- spoken text, so `instructions: 'Say cheerfully'` with `text: 'Hello'` speaks `Say cheerfully: Hello`.
2266
+ | Format | Values | Result |
2267
+ | ------ | ---------------------- | ---------------------------- |
2268
+ | WAV | `wav`, `audio/wav` | WAV with a RIFF header |
2269
+ | PCM | `pcm`, `audio/l16` | Headerless signed 16-bit PCM |
2270
+ | Mu-law | `mulaw`, `audio/mulaw` | Headerless mu-law audio |
2271
+ | A-law | `alaw`, `audio/alaw` | Headerless A-law audio |
2272
+
2273
+ These map to `generationConfig.responseFormat.audio.mimeType` in
2274
+ `generateContent`. Older models support `wav` and `pcm`. The source MIME type and
2275
+ sample rate are available in `result.providerMetadata.google`.
2276
+
2277
+ Gemini 2.5 and 3.1 model IDs retain the legacy request format. Other model IDs,
2278
+ including custom aliases, use the structured speech format.
2279
+
2280
+ ### Speech directions in Gemini 3.8
2281
+
2282
+ Gemini 3.8 treats text as a verbatim transcript. The provider sends `instructions`
2283
+ as `speechMetadata.style`, leaving the text unchanged. You can also supply
2284
+ `providerOptions.google.speechMetadata` with `style` and `speaker` fields.
2285
+ An explicit style overrides `instructions`, including an empty style string.
2286
+
2287
+ ```ts
2288
+ const result = await generateSpeech({
2289
+ model: google.speech('gemini-3.8-flash-tts'),
2290
+ text: 'Welcome back. <short pause> It is good to see you. <laugh>',
2291
+ instructions: 'Warm, relaxed, and speaking slowly',
2292
+ });
2293
+ ```
2294
+
2295
+ Put sustained delivery directions such as whispering, speaking slowly, or being
2296
+ out of breath in metadata. Keep momentary vocal events and pauses such as
2297
+ `<laugh>`, `<sigh>`, `<cough>`, `<breath>`, and `<short pause>` in the transcript.
2298
+ Avoid sound-effect tags such as applause or thuds. The provider preserves inline
2299
+ tags as written.
2300
+
2301
+ For Gemini 3.1 and earlier, the provider retains the existing behavior of
2302
+ prepending `instructions` to single-speaker text. It ignores instructions with a
2303
+ warning for older multi-speaker requests. Structured `turns` and `speechMetadata`
2304
+ require Gemini 3.8.
2262
2305
 
2263
2306
  ### Multi-speaker audio
2264
2307
 
2265
- For multi-speaker dialogue, pass a `multiSpeakerVoiceConfig` through `providerOptions`. Each speaker
2266
- name must match a name used in the input text. When set, it overrides the top-level `voice`.
2308
+ For Gemini 3.8 dialogue, pass `multiSpeakerVoiceConfig` and `turns` through
2309
+ `providerOptions.google`. Every turn must include `speechMetadata.speaker`
2310
+ matching a configured speaker; missing or unknown speakers produce an error
2311
+ before the request is sent. Speaker labels belong in metadata, not the text.
2267
2312
 
2268
- ```ts highlight="7-22"
2313
+ The `turns` array replaces the top-level text, so pass `text: ''`. The combined
2314
+ turn text must be non-empty. Passing
2315
+ nonempty top-level text with turns produces a warning. Put metadata on each turn
2316
+ instead of combining turns with top-level `speechMetadata`. Each turn's style
2317
+ overrides `instructions`; turns without a style inherit `instructions`.
2318
+
2319
+ `multiSpeakerVoiceConfig` overrides the top-level `voice`. Each `voiceConfig`
2320
+ uses `prebuiltVoiceConfig: { voiceName: 'Kore' }`. Google supports up to two
2321
+ prebuilt voices in a multi-speaker request.
2322
+
2323
+ ```ts
2269
2324
  import { generateSpeech } from 'ai';
2270
2325
  import { google, type GoogleSpeechModelOptions } from '@ai-sdk/google';
2271
2326
 
2272
2327
  const result = await generateSpeech({
2273
- model: google.speech('gemini-2.5-flash-preview-tts'),
2274
- text: 'Joe: How are you? Jane: Doing great, thanks!',
2328
+ model: google.speech('gemini-3.8-flash-lite-tts'),
2329
+ text: '',
2275
2330
  providerOptions: {
2276
2331
  google: {
2277
2332
  multiSpeakerVoiceConfig: {
@@ -2286,11 +2341,25 @@ const result = await generateSpeech({
2286
2341
  },
2287
2342
  ],
2288
2343
  },
2344
+ turns: [
2345
+ {
2346
+ text: 'How are you?',
2347
+ speechMetadata: { speaker: 'Joe', style: 'curious' },
2348
+ },
2349
+ {
2350
+ text: '<laugh> Doing great, thanks!',
2351
+ speechMetadata: { speaker: 'Jane', style: 'cheerful' },
2352
+ },
2353
+ ],
2289
2354
  } satisfies GoogleSpeechModelOptions,
2290
2355
  },
2291
2356
  });
2292
2357
  ```
2293
2358
 
2359
+ Older models continue to use labelled transcripts, such as
2360
+ `text: 'Joe: How are you? Jane: Doing great, thanks!'`, with
2361
+ `multiSpeakerVoiceConfig` and without `turns`.
2362
+
2294
2363
  <Note>
2295
2364
  Gemini TTS models do not support the `speed` or `language` options; passing
2296
2365
  them adds a warning to `result.warnings`. Language is detected automatically
@@ -2304,6 +2373,8 @@ const result = await generateSpeech({
2304
2373
  | `gemini-2.5-flash-preview-tts` | <Check /> | <Check /> |
2305
2374
  | `gemini-2.5-pro-preview-tts` | <Check /> | <Check /> |
2306
2375
  | `gemini-3.1-flash-tts-preview` | <Check /> | <Check /> |
2376
+ | `gemini-3.8-flash-tts` | <Check /> | <Check /> |
2377
+ | `gemini-3.8-flash-lite-tts` | <Check /> | <Check /> |
2307
2378
 
2308
2379
  ## Evaluation Models
2309
2380
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.78",
3
+ "version": "4.0.80",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.18",
39
- "@ai-sdk/provider-utils": "5.0.46"
39
+ "@ai-sdk/provider-utils": "5.0.47"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.1",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  UnsupportedFunctionalityError,
3
+ type JSONValue,
3
4
  type LanguageModelV4Prompt,
4
5
  type LanguageModelV4ToolResultOutput,
5
6
  type SharedV4Warning,
@@ -18,6 +19,10 @@ import type {
18
19
  GoogleFunctionResponsePart,
19
20
  GooglePrompt,
20
21
  } from './google-prompt';
22
+ import {
23
+ codeExecutionInputSchema,
24
+ codeExecutionOutputSchema,
25
+ } from './tool/code-execution';
21
26
 
22
27
  /**
23
28
  * Sentinel value Google documents for replaying functionCall parts whose
@@ -63,6 +68,30 @@ function convertUrlToolResultPart(
63
68
  };
64
69
  }
65
70
 
71
+ function containsJSONSchemaReference(value: JSONValue | undefined): boolean {
72
+ if (Array.isArray(value)) {
73
+ return value.some(containsJSONSchemaReference);
74
+ }
75
+
76
+ if (typeof value !== 'object' || value === null) {
77
+ return false;
78
+ }
79
+
80
+ return Object.entries(value).some(
81
+ ([key, nestedValue]) =>
82
+ key === '$ref' || containsJSONSchemaReference(nestedValue),
83
+ );
84
+ }
85
+
86
+ function serializeFunctionResponseContent(
87
+ value: JSONValue,
88
+ ): JSONValue | string {
89
+ // Google reserves { $ref: displayName } in structured function responses for
90
+ // multimodal parts. This conflicts with JSON Schema $ref, so serialize the
91
+ // result to preserve it without triggering Google's reference handling.
92
+ return containsJSONSchemaReference(value) ? JSON.stringify(value) : value;
93
+ }
94
+
66
95
  /*
67
96
  * Appends tool result content parts to the message using the functionResponse
68
97
  * format with support for multimodal parts (e.g. inline images/files alongside
@@ -465,6 +494,19 @@ export function convertToGoogleMessages(
465
494
  }
466
495
 
467
496
  case 'tool-call': {
497
+ if (
498
+ part.providerExecuted === true &&
499
+ part.toolName === 'code_execution'
500
+ ) {
501
+ return {
502
+ executableCode: codeExecutionInputSchema.parse(
503
+ typeof part.input === 'string'
504
+ ? secureJsonParse(part.input)
505
+ : part.input,
506
+ ),
507
+ };
508
+ }
509
+
468
510
  const serverToolCallId =
469
511
  providerOpts?.serverToolCallId != null
470
512
  ? String(providerOpts.serverToolCallId)
@@ -520,6 +562,17 @@ export function convertToGoogleMessages(
520
562
  }
521
563
 
522
564
  case 'tool-result': {
565
+ if (
566
+ part.toolName === 'code_execution' &&
567
+ part.output.type === 'json'
568
+ ) {
569
+ return {
570
+ codeExecutionResult: codeExecutionOutputSchema.parse(
571
+ part.output.value,
572
+ ),
573
+ };
574
+ }
575
+
523
576
  const serverToolCallId =
524
577
  providerOpts?.serverToolCallId != null
525
578
  ? String(providerOpts.serverToolCallId)
@@ -626,7 +679,7 @@ export function convertToGoogleMessages(
626
679
  content:
627
680
  output.type === 'execution-denied'
628
681
  ? (output.reason ?? 'Tool call execution denied.')
629
- : output.value,
682
+ : serializeFunctionResponseContent(output.value),
630
683
  },
631
684
  },
632
685
  });
@@ -5,6 +5,8 @@ import {
5
5
  import {
6
6
  combineHeaders,
7
7
  createJsonResponseHandler,
8
+ type EmbeddingModelProviderOptionsTransformer,
9
+ EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER,
8
10
  lazySchema,
9
11
  parseProviderOptions,
10
12
  postJsonToApi,
@@ -33,6 +35,28 @@ export class GoogleEmbeddingModel implements EmbeddingModelV4 {
33
35
  readonly modelId: GoogleEmbeddingModelId;
34
36
  readonly maxEmbeddingsPerCall = 100;
35
37
  readonly supportsParallelCalls = true;
38
+ readonly [EXPERIMENTAL_EMBEDDING_MODEL_PROVIDER_OPTIONS_TRANSFORMER]: EmbeddingModelProviderOptionsTransformer =
39
+ ({ providerOptions, values, startIndex, endIndex }) => {
40
+ const multimodalContent = providerOptions?.google?.content;
41
+
42
+ // Leave schema validation to doEmbed, after middleware transforms options.
43
+ if (!Array.isArray(multimodalContent)) {
44
+ return providerOptions;
45
+ }
46
+
47
+ validateMultimodalContentLength({
48
+ multimodalContent,
49
+ values,
50
+ });
51
+
52
+ return {
53
+ ...providerOptions,
54
+ google: {
55
+ ...providerOptions?.google,
56
+ content: multimodalContent.slice(startIndex, endIndex),
57
+ },
58
+ };
59
+ };
36
60
 
37
61
  private readonly config: GoogleEmbeddingConfig;
38
62
 
@@ -89,14 +113,7 @@ export class GoogleEmbeddingModel implements EmbeddingModelV4 {
89
113
 
90
114
  const multimodalContent = googleOptions?.content;
91
115
 
92
- if (
93
- multimodalContent != null &&
94
- multimodalContent.length !== values.length
95
- ) {
96
- throw new Error(
97
- `The number of multimodal content entries (${multimodalContent.length}) must match the number of values (${values.length}).`,
98
- );
99
- }
116
+ validateMultimodalContentLength({ multimodalContent, values });
100
117
 
101
118
  // For single embeddings, use the single endpoint
102
119
  if (values.length === 1) {
@@ -199,3 +216,17 @@ const googleGenerativeAISingleEmbeddingResponseSchema = lazySchema(() =>
199
216
  }),
200
217
  ),
201
218
  );
219
+
220
+ function validateMultimodalContentLength({
221
+ multimodalContent,
222
+ values,
223
+ }: {
224
+ multimodalContent: Array<unknown> | undefined;
225
+ values: Array<string>;
226
+ }) {
227
+ if (multimodalContent != null && multimodalContent.length !== values.length) {
228
+ throw new Error(
229
+ `The number of multimodal content entries (${multimodalContent.length}) must match the number of values (${values.length}).`,
230
+ );
231
+ }
232
+ }
@@ -367,5 +367,4 @@ function resolvePartialArgValue(arg: {
367
367
  const value = arg.stringValue ?? arg.numberValue ?? arg.boolValue;
368
368
  if (value != null) return { value, json: JSON.stringify(value) };
369
369
  if ('nullValue' in arg) return { value: null, json: 'null' };
370
- return undefined;
371
370
  }
@@ -59,6 +59,18 @@ export type GoogleContentPart =
59
59
  id: string;
60
60
  };
61
61
  thoughtSignature?: string;
62
+ }
63
+ | {
64
+ executableCode: {
65
+ language: string;
66
+ code: string;
67
+ };
68
+ }
69
+ | {
70
+ codeExecutionResult: {
71
+ outcome: string;
72
+ output: string;
73
+ };
62
74
  };
63
75
 
64
76
  export type GoogleFunctionResponsePart = {
@@ -4,7 +4,8 @@ import { z } from 'zod/v4';
4
4
  /**
5
5
  * Response schema for the Gemini `:generateContent` endpoint when called with
6
6
  * `responseModalities: ['AUDIO']`. The generated audio is returned as base64
7
- * encoded raw PCM in the first inline-data part.
7
+ * encoded audio in the first inline-data part (WAV by default on Gemini 3.8,
8
+ * raw PCM on older models).
8
9
  */
9
10
  export const googleSpeechResponseSchema = lazySchema(() =>
10
11
  zodSchema(
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Inspects speech input without replacing provider option validation. This is
3
+ * also used by intermediaries that need the transcript or voice kind before
4
+ * invoking a model. Model support for structured turns is validated separately.
5
+ */
6
+ export function getGoogleSpeechInput({
7
+ text,
8
+ voice,
9
+ providerOptions,
10
+ }: {
11
+ text: string;
12
+ voice?: string;
13
+ providerOptions?: Record<string, unknown>;
14
+ }): { text: string; usesCustomVoice: boolean } {
15
+ const google = providerOptions?.google;
16
+ const options =
17
+ google != null && typeof google === 'object' ? google : undefined;
18
+ const turns = options && 'turns' in options ? options.turns : undefined;
19
+ const turnTexts: string[] = [];
20
+ if (Array.isArray(turns) && turns.length > 0) {
21
+ for (const turn of turns as unknown[]) {
22
+ if (
23
+ turn == null ||
24
+ typeof turn !== 'object' ||
25
+ !('text' in turn) ||
26
+ typeof turn.text !== 'string'
27
+ ) {
28
+ break;
29
+ }
30
+ turnTexts.push(turn.text);
31
+ }
32
+ if (turnTexts.length === turns.length) {
33
+ text = turnTexts.join('');
34
+ }
35
+ }
36
+
37
+ const config =
38
+ options && 'multiSpeakerVoiceConfig' in options
39
+ ? options.multiSpeakerVoiceConfig
40
+ : undefined;
41
+ const speakers =
42
+ config != null &&
43
+ typeof config === 'object' &&
44
+ 'speakerVoiceConfigs' in config &&
45
+ Array.isArray(config.speakerVoiceConfigs)
46
+ ? (config.speakerVoiceConfigs as unknown[])
47
+ : [];
48
+
49
+ return {
50
+ text,
51
+ usesCustomVoice:
52
+ voice?.startsWith('voice_') === true ||
53
+ voice?.startsWith('voicekey_') === true ||
54
+ speakers.some(
55
+ speaker =>
56
+ speaker != null &&
57
+ typeof speaker === 'object' &&
58
+ 'voiceConfig' in speaker &&
59
+ speaker.voiceConfig != null &&
60
+ typeof speaker.voiceConfig === 'object' &&
61
+ 'voice' in speaker.voiceConfig,
62
+ ),
63
+ };
64
+ }
@@ -9,6 +9,8 @@ export type GoogleSpeechModelId =
9
9
  | 'gemini-2.5-flash-preview-tts'
10
10
  | 'gemini-2.5-pro-preview-tts'
11
11
  | 'gemini-3.1-flash-tts-preview'
12
+ | 'gemini-3.8-flash-tts'
13
+ | 'gemini-3.8-flash-lite-tts'
12
14
  | (string & {});
13
15
 
14
16
  const prebuiltVoiceConfigSchema = z.object({
@@ -17,15 +19,40 @@ const prebuiltVoiceConfigSchema = z.object({
17
19
 
18
20
  const voiceConfigSchema = z.object({
19
21
  prebuiltVoiceConfig: prebuiltVoiceConfigSchema,
22
+ voice: z.never().optional(),
23
+ });
24
+
25
+ const speechMetadataSchema = z.object({
26
+ speaker: z.string().min(1).optional(),
27
+ style: z.string().optional(),
20
28
  });
21
29
 
22
30
  export const googleSpeechProviderOptionsSchema = lazySchema(() =>
23
31
  zodSchema(
24
32
  z.object({
33
+ /** Turn-level directions for the top-level text, for Gemini 3.8 TTS. */
34
+ speechMetadata: speechMetadataSchema.optional(),
35
+
36
+ /**
37
+ * Structured transcript for Gemini 3.8 TTS. Replaces the top-level text;
38
+ * pass text: '' when using turns. Each multi-speaker turn must name a
39
+ * configured speaker in speechMetadata. Per-turn styles override instructions.
40
+ */
41
+ turns: z
42
+ .array(
43
+ z.object({
44
+ text: z.string(),
45
+ speechMetadata: speechMetadataSchema.optional(),
46
+ }),
47
+ )
48
+ .min(1)
49
+ .optional(),
50
+
25
51
  /**
26
52
  * Multi-speaker configuration for dialogue audio. When provided, this
27
53
  * overrides the top-level `voice`. The Gemini TTS API supports up to two
28
- * speakers; each speaker name must match a name used in the input text.
54
+ * speakers. For Gemini 3.8, each turn's speechMetadata.speaker must match
55
+ * a configured speaker; older models use speaker labels in the text.
29
56
  *
30
57
  * https://ai.google.dev/gemini-api/docs/speech-generation#multi-speaker
31
58
  */