ai 7.0.12 → 7.0.14

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.
@@ -0,0 +1,309 @@
1
+ import {
2
+ UnsupportedFunctionalityError,
3
+ type Experimental_TranscriptionModelV4StreamPart,
4
+ type JSONObject,
5
+ } from '@ai-sdk/provider';
6
+ import {
7
+ DelayedPromise,
8
+ withUserAgentSuffix,
9
+ type ProviderOptions,
10
+ } from '@ai-sdk/provider-utils';
11
+ import { NoTranscriptGeneratedError } from '../error/no-transcript-generated-error';
12
+ import { logWarnings } from '../logger/log-warnings';
13
+ import { resolveTranscriptionModel } from '../model/resolve-model';
14
+ import type { TranscriptionModel } from '../types/transcription-model';
15
+ import type { TranscriptionModelResponseMetadata } from '../types/transcription-model-response-metadata';
16
+ import type { Warning } from '../types/warning';
17
+ import { asAsyncIterableStream } from '../util/async-iterable-stream';
18
+ import { VERSION } from '../version';
19
+ import type {
20
+ StreamTranscriptionResult,
21
+ TranscriptionStreamPart,
22
+ } from './stream-transcribe-result';
23
+
24
+ type TranscriptSegment = {
25
+ text: string;
26
+ startSecond: number;
27
+ endSecond: number;
28
+ };
29
+
30
+ /**
31
+ * Streams transcripts using a transcription model.
32
+ *
33
+ * @param model - The transcription model to use.
34
+ * @param audio - Raw audio chunks to transcribe.
35
+ * @param inputAudioFormat - The input audio format for the raw audio chunks.
36
+ * @param providerOptions - Additional provider-specific options.
37
+ * @param abortSignal - An optional abort signal that can be used to cancel the call.
38
+ * @param headers - Additional HTTP/WebSocket headers to send when supported by the provider.
39
+ *
40
+ * @returns A result object that contains the streaming transcript and final transcript metadata.
41
+ */
42
+ export function streamTranscribe({
43
+ model,
44
+ audio,
45
+ inputAudioFormat,
46
+ providerOptions = {},
47
+ abortSignal,
48
+ headers,
49
+ includeRawChunks,
50
+ _internal: { currentDate = () => new Date() } = {},
51
+ }: {
52
+ /**
53
+ * The transcription model to use.
54
+ */
55
+ model: TranscriptionModel;
56
+
57
+ /**
58
+ * Raw audio chunks to transcribe.
59
+ */
60
+ audio: ReadableStream<Uint8Array | string>;
61
+
62
+ /**
63
+ * The input audio format for the raw audio chunks.
64
+ */
65
+ inputAudioFormat: {
66
+ /**
67
+ * Audio format type, e.g. `audio/pcm`, `audio/pcmu`, or `audio/pcma`.
68
+ */
69
+ type: string;
70
+
71
+ /**
72
+ * Sample rate in Hz. Only applicable for formats that require a rate.
73
+ */
74
+ rate?: number;
75
+ };
76
+
77
+ /**
78
+ * Additional provider-specific options.
79
+ */
80
+ providerOptions?: ProviderOptions;
81
+
82
+ /**
83
+ * Abort signal.
84
+ */
85
+ abortSignal?: AbortSignal;
86
+
87
+ /**
88
+ * Additional headers to include in the request, if supported by the provider.
89
+ */
90
+ headers?: Record<string, string>;
91
+
92
+ /**
93
+ * When true, providers should include raw provider chunks in the stream.
94
+ */
95
+ includeRawChunks?: boolean;
96
+
97
+ /**
98
+ * Internal test hooks.
99
+ */
100
+ _internal?: {
101
+ currentDate?: () => Date;
102
+ };
103
+ }): StreamTranscriptionResult {
104
+ const resolvedModel = resolveTranscriptionModel(model);
105
+ if (!resolvedModel) {
106
+ throw new Error('Model could not be resolved');
107
+ }
108
+
109
+ const doStream = resolvedModel.doStream?.bind(resolvedModel);
110
+ if (doStream == null) {
111
+ throw new UnsupportedFunctionalityError({
112
+ functionality: 'streaming transcription',
113
+ message:
114
+ `The ${resolvedModel.provider} model "${resolvedModel.modelId}" does not support streaming transcription.` +
115
+ (typeof model === 'string'
116
+ ? ' String model IDs resolve through the global provider (AI Gateway by default),' +
117
+ ' which does not support streaming transcription yet.' +
118
+ " Pass a provider model instance instead, e.g. openai.transcription('gpt-realtime-whisper')."
119
+ : ''),
120
+ });
121
+ }
122
+
123
+ const headersWithUserAgent = withUserAgentSuffix(
124
+ headers ?? {},
125
+ `ai/${VERSION}`,
126
+ );
127
+
128
+ const textPromise = new DelayedPromise<string>();
129
+ const segmentsPromise = new DelayedPromise<Array<TranscriptSegment>>();
130
+ const languagePromise = new DelayedPromise<string | undefined>();
131
+ const durationInSecondsPromise = new DelayedPromise<number | undefined>();
132
+ const warningsPromise = new DelayedPromise<Array<Warning>>();
133
+ const responsesPromise = new DelayedPromise<
134
+ Array<TranscriptionModelResponseMetadata>
135
+ >();
136
+ const providerMetadataPromise = new DelayedPromise<
137
+ Record<string, JSONObject>
138
+ >();
139
+
140
+ const rejectPendingPromises = (error: unknown) => {
141
+ for (const promise of [
142
+ textPromise,
143
+ segmentsPromise,
144
+ languagePromise,
145
+ durationInSecondsPromise,
146
+ warningsPromise,
147
+ responsesPromise,
148
+ providerMetadataPromise,
149
+ ]) {
150
+ if (promise.isPending()) {
151
+ promise.reject(error);
152
+ }
153
+ }
154
+ };
155
+
156
+ const startedAt = currentDate();
157
+ let response: TranscriptionModelResponseMetadata | undefined;
158
+ const currentResponseMetadata = () =>
159
+ response ?? { timestamp: startedAt, modelId: resolvedModel.modelId };
160
+
161
+ const resolveWarnings = (warnings: Array<Warning>) => {
162
+ warningsPromise.resolve(warnings);
163
+ logWarnings({
164
+ warnings,
165
+ provider: resolvedModel.provider,
166
+ model: resolvedModel.modelId,
167
+ });
168
+ };
169
+
170
+ // When the consumer cancels `fullStream` early, we abort the model pipe below
171
+ // with a defined reason via this controller. Relying on the default cancel
172
+ // cascade aborts the pipe with an `undefined` reason, which surfaces as a
173
+ // spurious unhandled rejection on Node.js 26 when the transform drops chunks.
174
+ const pipeAbortController = new AbortController();
175
+
176
+ // `Transformer.cancel` is part of the Streams spec (and supported at runtime),
177
+ // but not yet reflected in the ambient `Transformer` type, so widen it here.
178
+ const transformer: Transformer<
179
+ Experimental_TranscriptionModelV4StreamPart,
180
+ TranscriptionStreamPart
181
+ > & { cancel?: (reason?: unknown) => void } = {
182
+ transform(value, controller) {
183
+ switch (value.type) {
184
+ case 'stream-start': {
185
+ resolveWarnings(value.warnings);
186
+ break;
187
+ }
188
+
189
+ case 'response-metadata': {
190
+ response = {
191
+ timestamp: value.timestamp ?? currentResponseMetadata().timestamp,
192
+ modelId: value.modelId ?? currentResponseMetadata().modelId,
193
+ headers: value.headers ?? response?.headers,
194
+ };
195
+ break;
196
+ }
197
+
198
+ case 'transcript-delta':
199
+ case 'transcript-partial':
200
+ case 'transcript-final':
201
+ case 'raw':
202
+ case 'error': {
203
+ controller.enqueue(value);
204
+ break;
205
+ }
206
+
207
+ case 'finish': {
208
+ if (!warningsPromise.isResolved()) {
209
+ resolveWarnings([]);
210
+ }
211
+
212
+ if (!value.text) {
213
+ throw new NoTranscriptGeneratedError({
214
+ responses: [currentResponseMetadata()],
215
+ });
216
+ }
217
+
218
+ textPromise.resolve(value.text);
219
+ segmentsPromise.resolve(value.segments);
220
+ languagePromise.resolve(value.language);
221
+ durationInSecondsPromise.resolve(value.durationInSeconds);
222
+ responsesPromise.resolve([currentResponseMetadata()]);
223
+ providerMetadataPromise.resolve(value.providerMetadata ?? {});
224
+ break;
225
+ }
226
+ }
227
+ },
228
+
229
+ flush() {
230
+ if (textPromise.isPending()) {
231
+ throw new NoTranscriptGeneratedError({
232
+ responses: [currentResponseMetadata()],
233
+ });
234
+ }
235
+ },
236
+
237
+ cancel(reason) {
238
+ pipeAbortController.abort(
239
+ reason ?? new Error('Transcription stream was cancelled.'),
240
+ );
241
+ },
242
+ };
243
+
244
+ const transform = new TransformStream<
245
+ Experimental_TranscriptionModelV4StreamPart,
246
+ TranscriptionStreamPart
247
+ >(transformer);
248
+
249
+ // Piping (instead of an eager read loop) preserves consumer backpressure
250
+ // and propagates cancellation of `fullStream` to the model stream.
251
+ void (async () => {
252
+ const result = await doStream({
253
+ audio,
254
+ inputAudioFormat,
255
+ providerOptions,
256
+ abortSignal,
257
+ headers: headersWithUserAgent,
258
+ includeRawChunks,
259
+ });
260
+
261
+ response = {
262
+ timestamp: result.response?.timestamp ?? startedAt,
263
+ modelId: result.response?.modelId ?? resolvedModel.modelId,
264
+ headers: result.response?.headers,
265
+ };
266
+
267
+ await result.stream.pipeTo(transform.writable, {
268
+ signal: pipeAbortController.signal,
269
+ });
270
+ })().catch(error => {
271
+ const reason =
272
+ error ?? new Error('Transcription stream was cancelled or errored.');
273
+ rejectPendingPromises(reason);
274
+ transform.writable.abort(reason).catch(() => {
275
+ // the writable is already errored when the model stream failed mid-pipe
276
+ });
277
+ });
278
+
279
+ return {
280
+ get text() {
281
+ return textPromise.promise;
282
+ },
283
+ get segments() {
284
+ return segmentsPromise.promise;
285
+ },
286
+ get language() {
287
+ return languagePromise.promise;
288
+ },
289
+ get durationInSeconds() {
290
+ return durationInSecondsPromise.promise;
291
+ },
292
+ get warnings() {
293
+ return warningsPromise.promise;
294
+ },
295
+ get responses() {
296
+ return responsesPromise.promise;
297
+ },
298
+ get providerMetadata() {
299
+ return providerMetadataPromise.promise;
300
+ },
301
+ // `transform.readable` is fresh and exclusively owned here, so attach the
302
+ // async iterator in place rather than piping through another transform.
303
+ // The extra transform (as `createAsyncIterableStream` would add) chains two
304
+ // transforms fed by the active model pipe below and surfaces a spurious
305
+ // unhandled `undefined` rejection when the consumer cancels early on
306
+ // Node.js 26.
307
+ fullStream: asAsyncIterableStream(transform.readable),
308
+ };
309
+ }
@@ -16,8 +16,29 @@ export function createAsyncIterableStream<T>(
16
16
  source: ReadableStream<T>,
17
17
  ): AsyncIterableStream<T> {
18
18
  // Pipe through a TransformStream to ensure a fresh, unlocked stream.
19
- const stream = source.pipeThrough(new TransformStream<T, T>());
19
+ return asAsyncIterableStream(source.pipeThrough(new TransformStream<T, T>()));
20
+ }
20
21
 
22
+ /**
23
+ * Attaches the async iterator protocol to an existing ReadableStream in place,
24
+ * turning it into an AsyncIterableStream without piping through an additional
25
+ * TransformStream.
26
+ *
27
+ * Use this when the stream is already known to be fresh and exclusively owned
28
+ * (e.g. the readable side of a TransformStream created for this consumer).
29
+ * Adding an extra `pipeThrough` in that situation creates a chain of two
30
+ * transforms fed by an active upstream pipe, which can surface a spurious
31
+ * unhandled `undefined` rejection when the consumer cancels early (observed on
32
+ * Node.js 26). {@link createAsyncIterableStream} wraps this after adding a
33
+ * fresh transform for callers that may pass shared or locked streams.
34
+ *
35
+ * @template T The type of the stream's chunks.
36
+ * @param stream The ReadableStream to augment. It must be fresh and unlocked.
37
+ * @returns The same stream, augmented with the async iterator protocol.
38
+ */
39
+ export function asAsyncIterableStream<T>(
40
+ stream: ReadableStream<T>,
41
+ ): AsyncIterableStream<T> {
21
42
  /**
22
43
  * Implements the async iterator protocol for the stream.
23
44
  * Ensures proper cleanup (cancelling and releasing the reader) on completion, early exit, or error.