ai 7.0.37 → 7.0.38

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.ts +263 -40
  3. package/dist/index.js +684 -423
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/index.js +1 -1
  6. package/dist/internal/index.js.map +1 -1
  7. package/dist/test/index.d.ts +14 -2
  8. package/dist/test/index.js +15 -0
  9. package/dist/test/index.js.map +1 -1
  10. package/docs/03-ai-sdk-core/36-transcription.mdx +40 -35
  11. package/docs/03-ai-sdk-core/36-translation.mdx +196 -0
  12. package/docs/03-ai-sdk-core/index.mdx +5 -0
  13. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
  14. package/docs/07-reference/01-ai-sdk-core/11-stream-translate.mdx +176 -0
  15. package/docs/07-reference/01-ai-sdk-core/index.mdx +5 -0
  16. package/docs/07-reference/05-ai-sdk-errors/ai-no-translation-generated-error.mdx +26 -0
  17. package/docs/07-reference/05-ai-sdk-errors/index.mdx +1 -0
  18. package/package.json +4 -4
  19. package/src/error/index.ts +1 -0
  20. package/src/error/no-translation-generated-error.ts +28 -0
  21. package/src/generate-text/generate-text.ts +4 -1
  22. package/src/generate-text/stream-language-model-call.ts +14 -12
  23. package/src/generate-text/stream-text-result.ts +3 -2
  24. package/src/index.ts +1 -0
  25. package/src/model/resolve-model.ts +35 -0
  26. package/src/test/mock-speech-translation-model-v4.ts +24 -0
  27. package/src/transcribe/stream-transcribe.ts +2 -11
  28. package/src/translate/index.ts +5 -0
  29. package/src/translate/stream-translate-result.ts +146 -0
  30. package/src/translate/stream-translate.ts +374 -0
  31. package/src/types/speech-translation-model-response-metadata.ts +22 -0
  32. package/src/types/speech-translation-model.ts +11 -0
@@ -0,0 +1,374 @@
1
+ import type {
2
+ Experimental_SpeechTranslationModelV4StreamPart,
3
+ Experimental_SpeechTranslationModelV4Usage,
4
+ JSONObject,
5
+ SharedV4AudioFormat,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ DelayedPromise,
9
+ withUserAgentSuffix,
10
+ type ProviderOptions,
11
+ } from '@ai-sdk/provider-utils';
12
+ import { NoTranslationGeneratedError } from '../error/no-translation-generated-error';
13
+ import { logWarnings } from '../logger/log-warnings';
14
+ import { resolveSpeechTranslationModel } from '../model/resolve-model';
15
+ import type { SpeechTranslationModel } from '../types/speech-translation-model';
16
+ import type { SpeechTranslationModelResponseMetadata } from '../types/speech-translation-model-response-metadata';
17
+ import type { Warning } from '../types/warning';
18
+ import { asAsyncIterableStream } from '../util/async-iterable-stream';
19
+ import { mergeAbortSignals } from '../util/merge-abort-signals';
20
+ import { VERSION } from '../version';
21
+ import type {
22
+ StreamTranslationResult,
23
+ TranslationStreamPart,
24
+ } from './stream-translate-result';
25
+
26
+ /**
27
+ * Streams speech-to-speech translations using a speech translation model.
28
+ *
29
+ * @param model - The speech translation model to use.
30
+ * @param audio - Raw audio chunks to translate.
31
+ * @param inputAudioFormat - The input audio format for the raw audio chunks.
32
+ * @param targetLanguage - The language to translate the audio into.
33
+ * @param sourceLanguage - The language of the source audio. Auto-detected when absent.
34
+ * @param outputAudioFormat - The desired audio format for translated audio chunks.
35
+ * @param providerOptions - Additional provider-specific options.
36
+ * @param abortSignal - An optional abort signal that can be used to cancel the call.
37
+ * @param headers - Additional HTTP/WebSocket headers to send when supported by the provider.
38
+ * @param includeRawChunks - When true, providers include raw provider chunks in the stream as `raw` parts.
39
+ *
40
+ * @returns A result object that contains the streaming translation and final translation metadata.
41
+ */
42
+ export function streamTranslate({
43
+ model,
44
+ audio,
45
+ inputAudioFormat,
46
+ targetLanguage,
47
+ sourceLanguage,
48
+ outputAudioFormat,
49
+ providerOptions = {},
50
+ abortSignal,
51
+ headers,
52
+ includeRawChunks,
53
+ _internal: { currentDate = () => new Date() } = {},
54
+ }: {
55
+ /**
56
+ * The speech translation model to use.
57
+ */
58
+ model: SpeechTranslationModel;
59
+
60
+ /**
61
+ * Raw audio chunks to translate.
62
+ */
63
+ audio: ReadableStream<Uint8Array | string>;
64
+
65
+ /**
66
+ * The input audio format for the raw audio chunks.
67
+ */
68
+ inputAudioFormat: SharedV4AudioFormat;
69
+
70
+ /**
71
+ * The language to translate the audio into, as a BCP-47-style language
72
+ * tag (e.g. `en`, `es`, `fr-CA`). Supported values are provider-specific
73
+ * and validated by the provider.
74
+ */
75
+ targetLanguage: string;
76
+
77
+ /**
78
+ * The language of the source audio, as a BCP-47-style language tag.
79
+ * When absent, providers auto-detect the source language.
80
+ */
81
+ sourceLanguage?: string;
82
+
83
+ /**
84
+ * The desired audio format for translated audio chunks.
85
+ * When absent, the provider default output format is used.
86
+ */
87
+ outputAudioFormat?: SharedV4AudioFormat;
88
+
89
+ /**
90
+ * Additional provider-specific options.
91
+ */
92
+ providerOptions?: ProviderOptions;
93
+
94
+ /**
95
+ * Abort signal.
96
+ */
97
+ abortSignal?: AbortSignal;
98
+
99
+ /**
100
+ * Additional headers to include in the request, if supported by the provider.
101
+ */
102
+ headers?: Record<string, string>;
103
+
104
+ /**
105
+ * When true, providers should include raw provider chunks in the stream.
106
+ */
107
+ includeRawChunks?: boolean;
108
+
109
+ /**
110
+ * Internal test hooks.
111
+ */
112
+ _internal?: {
113
+ currentDate?: () => Date;
114
+ };
115
+ }): StreamTranslationResult {
116
+ const resolvedModel = resolveSpeechTranslationModel(model);
117
+
118
+ const doStream = resolvedModel.doStream.bind(resolvedModel);
119
+
120
+ const headersWithUserAgent = withUserAgentSuffix(
121
+ headers ?? {},
122
+ `ai/${VERSION}`,
123
+ );
124
+
125
+ const sourceTextPromise = new DelayedPromise<string>();
126
+ const translationTextPromise = new DelayedPromise<string>();
127
+ const durationInSecondsPromise = new DelayedPromise<number | undefined>();
128
+ const usagePromise = new DelayedPromise<
129
+ Experimental_SpeechTranslationModelV4Usage | undefined
130
+ >();
131
+ const warningsPromise = new DelayedPromise<Array<Warning>>();
132
+ const responsePromise =
133
+ new DelayedPromise<SpeechTranslationModelResponseMetadata>();
134
+ const providerMetadataPromise = new DelayedPromise<
135
+ Record<string, JSONObject>
136
+ >();
137
+
138
+ const rejectPendingPromises = (error: unknown) => {
139
+ for (const promise of [
140
+ sourceTextPromise,
141
+ translationTextPromise,
142
+ durationInSecondsPromise,
143
+ usagePromise,
144
+ warningsPromise,
145
+ responsePromise,
146
+ providerMetadataPromise,
147
+ ]) {
148
+ if (promise.isPending()) {
149
+ promise.reject(error);
150
+ }
151
+ }
152
+ };
153
+
154
+ const startedAt = currentDate();
155
+ let response: SpeechTranslationModelResponseMetadata | undefined;
156
+ const currentResponseMetadata = () =>
157
+ response ?? { timestamp: startedAt, modelId: resolvedModel.modelId };
158
+
159
+ const resolveWarnings = (warnings: Array<Warning>) => {
160
+ warningsPromise.resolve(warnings);
161
+ logWarnings({
162
+ warnings,
163
+ provider: resolvedModel.provider,
164
+ model: resolvedModel.modelId,
165
+ });
166
+ };
167
+
168
+ // When the consumer cancels `fullStream` early, we abort the model pipe below
169
+ // with a defined reason via this controller. Relying on the default cancel
170
+ // cascade aborts the pipe with an `undefined` reason, which surfaces as a
171
+ // spurious unhandled rejection on Node.js 26 when the transform drops chunks.
172
+ const pipeAbortController = new AbortController();
173
+
174
+ // A stream is successful when at least one `audio` part was emitted or the
175
+ // final `outputText` is non-empty (audio-only providers may report an empty
176
+ // `outputText`).
177
+ let hasAudioOutput = false;
178
+
179
+ // `Transformer.cancel` is part of the Streams spec (and supported at runtime),
180
+ // but not yet reflected in the ambient `Transformer` type, so widen it here.
181
+ const transformer: Transformer<
182
+ Experimental_SpeechTranslationModelV4StreamPart,
183
+ TranslationStreamPart
184
+ > & { cancel?: (reason?: unknown) => void } = {
185
+ transform(value, controller) {
186
+ switch (value.type) {
187
+ case 'stream-start': {
188
+ resolveWarnings(value.warnings);
189
+ break;
190
+ }
191
+
192
+ case 'response-metadata': {
193
+ response = {
194
+ timestamp: value.timestamp ?? currentResponseMetadata().timestamp,
195
+ modelId: value.modelId ?? currentResponseMetadata().modelId,
196
+ headers: value.headers ?? response?.headers,
197
+ };
198
+ break;
199
+ }
200
+
201
+ case 'audio': {
202
+ hasAudioOutput = true;
203
+ controller.enqueue(value);
204
+ break;
205
+ }
206
+
207
+ case 'output-text-delta':
208
+ case 'output-text-final':
209
+ case 'source-transcript-delta':
210
+ case 'source-transcript-partial':
211
+ case 'source-transcript-final':
212
+ case 'raw':
213
+ case 'error': {
214
+ controller.enqueue(value);
215
+ break;
216
+ }
217
+
218
+ case 'finish': {
219
+ if (!warningsPromise.isResolved()) {
220
+ resolveWarnings([]);
221
+ }
222
+
223
+ if (!hasAudioOutput && !value.outputText) {
224
+ throw new NoTranslationGeneratedError({
225
+ response: currentResponseMetadata(),
226
+ });
227
+ }
228
+
229
+ sourceTextPromise.resolve(value.sourceText);
230
+ translationTextPromise.resolve(value.outputText);
231
+ durationInSecondsPromise.resolve(value.durationInSeconds);
232
+ usagePromise.resolve(value.usage);
233
+ responsePromise.resolve(currentResponseMetadata());
234
+ providerMetadataPromise.resolve(value.providerMetadata ?? {});
235
+ break;
236
+ }
237
+
238
+ default: {
239
+ const _exhaustiveCheck: never = value;
240
+ throw new Error(`Unsupported part type: ${_exhaustiveCheck}`);
241
+ }
242
+ }
243
+ },
244
+
245
+ flush() {
246
+ if (translationTextPromise.isPending()) {
247
+ throw new NoTranslationGeneratedError({
248
+ response: currentResponseMetadata(),
249
+ });
250
+ }
251
+ },
252
+
253
+ cancel(reason) {
254
+ pipeAbortController.abort(
255
+ reason ?? new Error('Translation stream was cancelled.'),
256
+ );
257
+ },
258
+ };
259
+
260
+ const transform = new TransformStream<
261
+ Experimental_SpeechTranslationModelV4StreamPart,
262
+ TranslationStreamPart
263
+ >(transformer);
264
+
265
+ // Piping (instead of an eager read loop) preserves consumer backpressure
266
+ // and propagates cancellation of `fullStream` to the model stream.
267
+ void (async () => {
268
+ const result = await doStream({
269
+ audio,
270
+ inputAudioFormat,
271
+ targetLanguage,
272
+ sourceLanguage,
273
+ outputAudioFormat,
274
+ providerOptions,
275
+ // merged so cancelling fullStream also aborts a still-pending doStream
276
+ abortSignal: mergeAbortSignals(abortSignal, pipeAbortController.signal),
277
+ headers: headersWithUserAgent,
278
+ includeRawChunks,
279
+ });
280
+
281
+ response = {
282
+ timestamp: result.response?.timestamp ?? startedAt,
283
+ modelId: result.response?.modelId ?? resolvedModel.modelId,
284
+ headers: result.response?.headers,
285
+ };
286
+
287
+ await result.stream.pipeTo(transform.writable, {
288
+ signal: pipeAbortController.signal,
289
+ });
290
+ })().catch(error => {
291
+ const reason =
292
+ error ?? new Error('Translation stream was cancelled or errored.');
293
+ rejectPendingPromises(reason);
294
+ // When `doStream` rejects before the model stream exists (e.g. auth or
295
+ // header resolution failure), nothing has taken ownership of `audio` yet,
296
+ // so cancel it directly — otherwise an upstream producer piping into it
297
+ // hangs forever. When the model did take a reader, `audio` is locked and
298
+ // the cancel rejects, which is fine: the model's cleanup owns it then.
299
+ audio.cancel(reason).catch(() => {});
300
+ transform.writable.abort(reason).catch(() => {
301
+ // the writable is already errored when the model stream failed mid-pipe
302
+ });
303
+ });
304
+
305
+ // Translation streams can be unbounded (live microphone + raw chunks), so
306
+ // unlike streamText we cannot retain an unread tee branch for replay. The
307
+ // output stream has one owner: either fullStream, or the first result promise
308
+ // getter which claims and drains it internally.
309
+ let streamOwner: 'unclaimed' | 'full-stream' | 'result-promises' =
310
+ 'unclaimed';
311
+
312
+ function consumeStream() {
313
+ if (streamOwner === 'full-stream' || streamOwner === 'result-promises') {
314
+ return;
315
+ }
316
+ streamOwner = 'result-promises';
317
+ const reader = transform.readable.getReader();
318
+ void (async () => {
319
+ while (!(await reader.read()).done) {
320
+ // drain; results surface via the promises
321
+ }
322
+ })().catch(() => {
323
+ // stream errors reject the promises via the pipe handler above
324
+ });
325
+ }
326
+
327
+ function getFullStream() {
328
+ if (streamOwner !== 'unclaimed') {
329
+ throw new Error(
330
+ streamOwner === 'full-stream'
331
+ ? 'fullStream can only be accessed once.'
332
+ : 'fullStream cannot be accessed after a result promise.',
333
+ );
334
+ }
335
+ streamOwner = 'full-stream';
336
+ // Direct ownership preserves backpressure and cancellation: cancelling
337
+ // this stream reaches Transformer.cancel and aborts the model pipe.
338
+ return asAsyncIterableStream(transform.readable);
339
+ }
340
+
341
+ return {
342
+ get sourceText() {
343
+ consumeStream();
344
+ return sourceTextPromise.promise;
345
+ },
346
+ get translationText() {
347
+ consumeStream();
348
+ return translationTextPromise.promise;
349
+ },
350
+ get durationInSeconds() {
351
+ consumeStream();
352
+ return durationInSecondsPromise.promise;
353
+ },
354
+ get usage() {
355
+ consumeStream();
356
+ return usagePromise.promise;
357
+ },
358
+ get warnings() {
359
+ consumeStream();
360
+ return warningsPromise.promise;
361
+ },
362
+ get response() {
363
+ consumeStream();
364
+ return responsePromise.promise;
365
+ },
366
+ get providerMetadata() {
367
+ consumeStream();
368
+ return providerMetadataPromise.promise;
369
+ },
370
+ get fullStream() {
371
+ return getFullStream();
372
+ },
373
+ };
374
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Response metadata for speech translation model calls.
3
+ *
4
+ * Experimental: part of the experimental speech translation modality and may
5
+ * change in patch releases.
6
+ */
7
+ export type SpeechTranslationModelResponseMetadata = {
8
+ /**
9
+ * Timestamp for the start of the generated response.
10
+ */
11
+ timestamp: Date;
12
+
13
+ /**
14
+ * The ID of the response model that was used to generate the response.
15
+ */
16
+ modelId: string;
17
+
18
+ /**
19
+ * Response headers.
20
+ */
21
+ headers?: Record<string, string>;
22
+ };
@@ -0,0 +1,11 @@
1
+ import type { Experimental_SpeechTranslationModelV4 } from '@ai-sdk/provider';
2
+
3
+ /**
4
+ * Speech translation model that is used by the AI SDK.
5
+ *
6
+ * Experimental: part of the experimental speech translation modality and may
7
+ * change in patch releases.
8
+ */
9
+ export type SpeechTranslationModel =
10
+ | string
11
+ | Experimental_SpeechTranslationModelV4;