@ai-sdk/google 4.0.77 → 4.0.79

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.
@@ -1,4 +1,8 @@
1
- import type { SpeechModelV4, SharedV4Warning } from '@ai-sdk/provider';
1
+ import {
2
+ InvalidArgumentError,
3
+ type SpeechModelV4,
4
+ type SharedV4Warning,
5
+ } from '@ai-sdk/provider';
2
6
  import {
3
7
  combineHeaders,
4
8
  convertBase64ToUint8Array,
@@ -14,6 +18,7 @@ import {
14
18
  } from '@ai-sdk/provider-utils';
15
19
  import { googleFailedResponseHandler } from './google-error';
16
20
  import { googleSpeechResponseSchema } from './google-speech-api';
21
+ import { getGoogleSpeechInput } from './google-speech-input';
17
22
  import {
18
23
  googleSpeechProviderOptionsSchema,
19
24
  type GoogleSpeechModelId,
@@ -101,18 +106,36 @@ export class GoogleSpeechModel implements SpeechModelV4 {
101
106
  });
102
107
  }
103
108
 
109
+ // Older Gemini families require prompt-based directions. Default newer and
110
+ // custom model IDs to structured speech without enumerating their aliases.
111
+ const usesStructuredSpeech =
112
+ !this.modelId.startsWith('gemini-2.5-') &&
113
+ !this.modelId.startsWith('gemini-3.1-');
114
+
115
+ const input = getGoogleSpeechInput({
116
+ text,
117
+ voice,
118
+ providerOptions: { google: googleOptions },
119
+ });
120
+
121
+ if (input.usesCustomVoice) {
122
+ throw new InvalidArgumentError({
123
+ argument: 'voice',
124
+ message:
125
+ 'Custom voices are not supported. Use a prebuilt voice instead.',
126
+ });
127
+ }
128
+
104
129
  // Multi-speaker (provider option) takes precedence over the single voice.
105
130
  const multiSpeakerVoiceConfig = googleOptions?.multiSpeakerVoiceConfig;
106
131
  const speechConfig = multiSpeakerVoiceConfig
107
132
  ? { multiSpeakerVoiceConfig }
108
133
  : { voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } } };
109
134
 
110
- // Gemini honors natural-language style direction expressed in the prompt
111
- // text, so map `instructions` onto the spoken content. With multi-speaker
112
- // the transcript starts with speaker labels (e.g. `Joe: ...`), so prepending
113
- // instructions would corrupt that parsing — ignore them there (with a warning).
135
+ // Older models expect directions in the prompt. Prepending them to a
136
+ // labelled multi-speaker transcript would break speaker parsing.
114
137
  let promptText = text;
115
- if (instructions != null) {
138
+ if (instructions != null && !usesStructuredSpeech) {
116
139
  if (multiSpeakerVoiceConfig) {
117
140
  warnings.push({
118
141
  type: 'unsupported',
@@ -126,6 +149,65 @@ export class GoogleSpeechModel implements SpeechModelV4 {
126
149
  }
127
150
  }
128
151
 
152
+ let parts: Array<{
153
+ text: string;
154
+ speechMetadata?: { speaker?: string; style?: string };
155
+ }> = [{ text: promptText }];
156
+
157
+ if (usesStructuredSpeech) {
158
+ if (googleOptions?.turns && googleOptions.speechMetadata) {
159
+ throw new InvalidArgumentError({
160
+ argument: 'providerOptions',
161
+ message: 'Set speechMetadata on each turn when using turns.',
162
+ });
163
+ }
164
+ if (googleOptions?.turns && text !== '') {
165
+ warnings.push({
166
+ type: 'unsupported',
167
+ feature: 'text',
168
+ details: 'Google TTS turns replace the top-level text.',
169
+ });
170
+ }
171
+ parts = (
172
+ googleOptions?.turns ?? [
173
+ { text, speechMetadata: googleOptions?.speechMetadata },
174
+ ]
175
+ ).map(part => {
176
+ const style = part.speechMetadata?.style ?? instructions;
177
+ const speaker = part.speechMetadata?.speaker;
178
+ if (
179
+ multiSpeakerVoiceConfig &&
180
+ !multiSpeakerVoiceConfig.speakerVoiceConfigs.some(
181
+ config => config.speaker === speaker,
182
+ )
183
+ ) {
184
+ throw new InvalidArgumentError({
185
+ argument: 'speechMetadata.speaker',
186
+ message:
187
+ 'Every multi-speaker turn must specify a speechMetadata.speaker matching a configured speaker.',
188
+ });
189
+ }
190
+ return {
191
+ text: part.text,
192
+ ...(style != null || speaker != null
193
+ ? { speechMetadata: { style, speaker } }
194
+ : {}),
195
+ };
196
+ });
197
+ } else if (googleOptions?.turns || googleOptions?.speechMetadata) {
198
+ throw new InvalidArgumentError({
199
+ argument: 'providerOptions',
200
+ message: 'Structured speech metadata and turns require Gemini 3.8 TTS.',
201
+ });
202
+ }
203
+
204
+ if (input.text.length === 0) {
205
+ throw new InvalidArgumentError({
206
+ argument: 'text',
207
+ message: 'Speech input must contain a non-empty transcript.',
208
+ });
209
+ }
210
+
129
211
  if (speed != null) {
130
212
  warnings.push({
131
213
  type: 'unsupported',
@@ -145,11 +227,25 @@ export class GoogleSpeechModel implements SpeechModelV4 {
145
227
  });
146
228
  }
147
229
 
148
- // Only `wav` (default, WAV-wrapped) and `pcm` (raw) are supported.
149
- let resolvedOutputFormat: 'wav' | 'pcm' = 'wav';
150
- if (outputFormat === 'pcm') {
151
- resolvedOutputFormat = 'pcm';
152
- } else if (outputFormat != null && outputFormat !== 'wav') {
230
+ const formats: Record<string, string> = usesStructuredSpeech
231
+ ? {
232
+ wav: 'AUDIO_WAV',
233
+ 'audio/wav': 'AUDIO_WAV',
234
+ pcm: 'AUDIO_L16',
235
+ 'audio/l16': 'AUDIO_L16',
236
+ mulaw: 'AUDIO_MULAW',
237
+ 'audio/mulaw': 'AUDIO_MULAW',
238
+ alaw: 'AUDIO_ALAW',
239
+ 'audio/alaw': 'AUDIO_ALAW',
240
+ }
241
+ : { wav: 'AUDIO_WAV', pcm: 'AUDIO_L16' };
242
+ let resolvedOutputFormat = 'wav';
243
+ if (
244
+ outputFormat != null &&
245
+ Object.prototype.hasOwnProperty.call(formats, outputFormat)
246
+ ) {
247
+ resolvedOutputFormat = outputFormat;
248
+ } else if (outputFormat != null) {
153
249
  warnings.push({
154
250
  type: 'unsupported',
155
251
  feature: 'outputFormat',
@@ -158,21 +254,34 @@ export class GoogleSpeechModel implements SpeechModelV4 {
158
254
  }
159
255
 
160
256
  const requestBody = {
161
- contents: [{ role: 'user', parts: [{ text: promptText }] }],
257
+ contents: [{ role: 'user', parts }],
162
258
  generationConfig: {
163
259
  responseModalities: ['AUDIO'],
164
260
  speechConfig,
261
+ ...(usesStructuredSpeech && outputFormat != null
262
+ ? {
263
+ responseFormat: {
264
+ audio: { mimeType: formats[resolvedOutputFormat] },
265
+ },
266
+ }
267
+ : {}),
165
268
  },
166
269
  };
167
270
 
168
- return { requestBody, warnings, outputFormat: resolvedOutputFormat };
271
+ return {
272
+ requestBody,
273
+ warnings,
274
+ outputFormat: formats[resolvedOutputFormat],
275
+ usesStructuredSpeech,
276
+ };
169
277
  }
170
278
 
171
279
  async doGenerate(
172
280
  options: Parameters<SpeechModelV4['doGenerate']>[0],
173
281
  ): Promise<Awaited<ReturnType<SpeechModelV4['doGenerate']>>> {
174
282
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
175
- const { requestBody, warnings, outputFormat } = await this.getArgs(options);
283
+ const { requestBody, warnings, outputFormat, usesStructuredSpeech } =
284
+ await this.getArgs(options);
176
285
 
177
286
  const {
178
287
  value: response,
@@ -211,23 +320,26 @@ export class GoogleSpeechModel implements SpeechModelV4 {
211
320
  }
212
321
 
213
322
  const sampleRate = parseSampleRate(mimeType) ?? DEFAULT_SAMPLE_RATE;
214
- const pcm =
323
+ const bytes =
215
324
  base64Audio != null
216
325
  ? convertBase64ToUint8Array(base64Audio)
217
326
  : new Uint8Array(0);
218
327
 
219
- // Gemini returns headerless raw PCM (e.g. `audio/L16;rate=24000`). Unlike
220
- // providers that return a container format (mp3/opus/wav) directly,
221
- // `generateSpeech`'s `detectMediaType` can't identify raw PCM and would
222
- // mislabel it `audio/mp3` (not playable), so wrap it in a minimal WAV header
223
- // by default; `outputFormat: 'pcm'` returns the raw bytes untouched.
224
- // Empty audio is returned as-is so the core layer throws NoSpeechGeneratedError.
328
+ // Older models return PCM, which needs a container for default WAV output.
329
+ // Gemini 3.8 returns WAV itself; adding another header corrupts that audio.
330
+ const isPcm =
331
+ /^audio\/(?:l16|pcm)(?:;|$)/i.test(mimeType ?? '') ||
332
+ (mimeType == null && !usesStructuredSpeech);
225
333
  const audio =
226
- outputFormat === 'pcm' || pcm.length === 0
227
- ? pcm
228
- : addWavHeader(pcm, sampleRate);
334
+ outputFormat === 'AUDIO_WAV' && isPcm && bytes.length > 0
335
+ ? addWavHeader(bytes, sampleRate)
336
+ : bytes;
229
337
 
230
- if (outputFormat === 'pcm' && pcm.length > 0) {
338
+ if (
339
+ outputFormat === 'AUDIO_L16' &&
340
+ bytes.length > 0 &&
341
+ !usesStructuredSpeech
342
+ ) {
231
343
  warnings.push({
232
344
  type: 'unsupported',
233
345
  feature: 'outputFormat',
@@ -1,5 +1,6 @@
1
1
  export * from '../google-language-model';
2
2
  export * from '../google-speech-model';
3
+ export { getGoogleSpeechInput } from '../google-speech-input';
3
4
  export { googleTools } from '../google-tools';
4
5
  export type { GoogleModelId } from '../google-language-model-options';
5
6
  export {
@@ -1,6 +1,18 @@
1
1
  import { createProviderExecutedToolFactory } from '@ai-sdk/provider-utils';
2
2
  import { z } from 'zod/v4';
3
3
 
4
+ export const codeExecutionInputSchema = z.object({
5
+ language: z.string().describe('The programming language of the code.'),
6
+ code: z.string().describe('The code to be executed.'),
7
+ });
8
+
9
+ export const codeExecutionOutputSchema = z.object({
10
+ outcome: z
11
+ .string()
12
+ .describe('The outcome of the execution (e.g., "OUTCOME_OK").'),
13
+ output: z.string().describe('The output from the code execution.'),
14
+ });
15
+
4
16
  /**
5
17
  * A tool that enables the model to generate and run Python code.
6
18
  *
@@ -22,14 +34,6 @@ export const codeExecution = createProviderExecutedToolFactory<
22
34
  {}
23
35
  >({
24
36
  id: 'google.code_execution',
25
- inputSchema: z.object({
26
- language: z.string().describe('The programming language of the code.'),
27
- code: z.string().describe('The code to be executed.'),
28
- }),
29
- outputSchema: z.object({
30
- outcome: z
31
- .string()
32
- .describe('The outcome of the execution (e.g., "OUTCOME_OK").'),
33
- output: z.string().describe('The output from the code execution.'),
34
- }),
37
+ inputSchema: codeExecutionInputSchema,
38
+ outputSchema: codeExecutionOutputSchema,
35
39
  });