ai 7.0.123 → 7.0.124

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.
@@ -40,6 +40,16 @@ import type {
40
40
  RerankingModelCallEndEvent,
41
41
  RerankingModelCallStartEvent,
42
42
  } from '../rerank/rerank-events';
43
+ import type {
44
+ GenerateSpeechEndEvent,
45
+ GenerateSpeechStartEvent,
46
+ } from '../generate-speech/speech-events';
47
+ import type {
48
+ StreamTranscriptionEndEvent,
49
+ StreamTranscriptionStartEvent,
50
+ TranscriptionEndEvent,
51
+ TranscriptionStartEvent,
52
+ } from '../transcribe/transcription-events';
43
53
  import type { Callback } from '../util/callback';
44
54
  import type { TelemetryOptions } from '../telemetry/telemetry-options';
45
55
  import type { TelemetryTracingEventType } from './tracing-channel';
@@ -51,17 +61,25 @@ export type InferTelemetryEvent<EVENT> = EVENT &
51
61
  'integrations' | 'isEnabled' | 'includeRuntimeContext'
52
62
  >;
53
63
 
64
+ type BivariantCallback<EVENT> = {
65
+ bivarianceHack(event: EVENT): PromiseLike<void> | void;
66
+ }['bivarianceHack'];
67
+
54
68
  type OperationStartEvent =
55
69
  | GenerateTextStartEvent
56
70
  | GenerateObjectStartEvent
57
71
  | EmbedStartEvent
58
- | RerankStartEvent;
72
+ | RerankStartEvent
73
+ | GenerateSpeechStartEvent
74
+ | TranscriptionStartEvent;
59
75
 
60
76
  type OperationEndEvent =
61
77
  | GenerateTextEndEvent<ToolSet>
62
78
  | GenerateObjectEndEvent<unknown>
63
79
  | EmbedEndEvent
64
- | RerankEndEvent;
80
+ | RerankEndEvent
81
+ | GenerateSpeechEndEvent
82
+ | TranscriptionEndEvent;
65
83
 
66
84
  export interface TelemetryDispatcher {
67
85
  /**
@@ -102,6 +120,8 @@ export interface TelemetryDispatcher {
102
120
  experimental_onEvaluationModelCallStart?: Callback<EvaluationModelCallStartEvent>;
103
121
  experimental_onEvaluationModelCallEnd?: Callback<EvaluationModelCallEndEvent>;
104
122
  experimental_onEvaluateEnd?: Callback<EvaluateEndEvent>;
123
+ experimental_onStreamTranscriptionStart?: Callback<StreamTranscriptionStartEvent>;
124
+ experimental_onStreamTranscriptionEnd?: Callback<StreamTranscriptionEndEvent>;
105
125
  onEnd?: Callback<OperationEndEvent>;
106
126
  onAbort?: Callback<GenerateTextAbortEvent<ToolSet>>;
107
127
  onError?: Callback<unknown>;
@@ -117,11 +137,12 @@ export interface Telemetry {
117
137
  /**
118
138
  * Called when an operation begins. Fired for text generation
119
139
  * (generateText/streamText), object generation (generateObject/streamObject),
120
- * embedding (embed/embedMany), and reranking operations.
140
+ * embedding (embed/embedMany), reranking, speech generation, and
141
+ * non-streaming transcription operations.
121
142
  *
122
143
  * Use the `operationId` field to distinguish between operation types.
123
144
  */
124
- onStart?: Callback<InferTelemetryEvent<OperationStartEvent>>;
145
+ onStart?: BivariantCallback<InferTelemetryEvent<OperationStartEvent>>;
125
146
 
126
147
  /**
127
148
  * Called when an individual step (single LLM invocation) begins.
@@ -248,14 +269,25 @@ export interface Telemetry {
248
269
  /** Called when an experimental evaluation operation completes. */
249
270
  experimental_onEvaluateEnd?: Callback<InferTelemetryEvent<EvaluateEndEvent>>;
250
271
 
272
+ /** Called when an experimental streaming transcription operation begins. */
273
+ experimental_onStreamTranscriptionStart?: Callback<
274
+ InferTelemetryEvent<StreamTranscriptionStartEvent>
275
+ >;
276
+
277
+ /** Called when an experimental streaming transcription operation completes. */
278
+ experimental_onStreamTranscriptionEnd?: Callback<
279
+ InferTelemetryEvent<StreamTranscriptionEndEvent>
280
+ >;
281
+
251
282
  /**
252
283
  * Called when an operation completes. Fired for text generation
253
284
  * (generateText/streamText), object generation (generateObject/streamObject),
254
- * embedding (embed/embedMany), and reranking operations.
285
+ * embedding (embed/embedMany), reranking, speech generation, and
286
+ * non-streaming transcription operations.
255
287
  *
256
288
  * Use the event shape or `operationId` to distinguish between operation types.
257
289
  */
258
- onEnd?: Callback<InferTelemetryEvent<OperationEndEvent>>;
290
+ onEnd?: BivariantCallback<InferTelemetryEvent<OperationEndEvent>>;
259
291
 
260
292
  /**
261
293
  * Called when a streaming text generation operation is aborted before it
@@ -9,6 +9,9 @@ export type TelemetryTracingEventType =
9
9
  | 'embed'
10
10
  | 'embedMany'
11
11
  | 'rerank'
12
+ | 'generateSpeech'
13
+ | 'transcribe'
14
+ | 'streamTranscribe'
12
15
  | 'experimental_evaluate';
13
16
 
14
17
  export type TelemetryTracingChannelMessage<EVENT = unknown> = {
@@ -8,6 +8,12 @@ export type {
8
8
  TranscriptionStreamPart,
9
9
  } from './stream-transcribe-result';
10
10
  export type { TranscriptionResult } from './transcribe-result';
11
+ export type {
12
+ StreamTranscriptionEndEvent as Experimental_StreamTranscriptionEndEvent,
13
+ StreamTranscriptionStartEvent as Experimental_StreamTranscriptionStartEvent,
14
+ TranscriptionEndEvent,
15
+ TranscriptionStartEvent,
16
+ } from './transcription-events';
11
17
 
12
18
  // deprecated exports
13
19
 
@@ -5,6 +5,7 @@ import {
5
5
  type SharedV4AudioFormat,
6
6
  } from '@ai-sdk/provider';
7
7
  import {
8
+ createIdGenerator,
8
9
  DelayedPromise,
9
10
  withUserAgentSuffix,
10
11
  type ProviderOptions,
@@ -12,16 +13,23 @@ import {
12
13
  import { NoTranscriptGeneratedError } from '../error/no-transcript-generated-error';
13
14
  import { logWarnings } from '../logger/log-warnings';
14
15
  import { resolveTranscriptionModel } from '../model/resolve-model';
16
+ import { createTelemetryDispatcher } from '../telemetry/create-telemetry-dispatcher';
17
+ import type { TelemetryOptions } from '../telemetry/telemetry-options';
15
18
  import type { TranscriptionModel } from '../types/transcription-model';
16
19
  import type { TranscriptionModelResponseMetadata } from '../types/transcription-model-response-metadata';
17
20
  import type { Warning } from '../types/warning';
18
21
  import { asAsyncIterableStream } from '../util/async-iterable-stream';
19
22
  import { mergeAbortSignals } from '../util/merge-abort-signals';
23
+ import { notify } from '../util/notify';
20
24
  import { VERSION } from '../version';
21
25
  import type {
22
26
  StreamTranscriptionResult,
23
27
  TranscriptionStreamPart,
24
28
  } from './stream-transcribe-result';
29
+ import type {
30
+ StreamTranscriptionEndEvent,
31
+ StreamTranscriptionStartEvent,
32
+ } from './transcription-events';
25
33
 
26
34
  type TranscriptSegment = {
27
35
  text: string;
@@ -29,6 +37,11 @@ type TranscriptSegment = {
29
37
  endSecond: number;
30
38
  };
31
39
 
40
+ const originalGenerateCallId = createIdGenerator({
41
+ prefix: 'call',
42
+ size: 24,
43
+ });
44
+
32
45
  /**
33
46
  * Streams transcripts using a transcription model.
34
47
  *
@@ -38,6 +51,7 @@ type TranscriptSegment = {
38
51
  * @param providerOptions - Additional provider-specific options.
39
52
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
40
53
  * @param headers - Additional HTTP/WebSocket headers to send when supported by the provider.
54
+ * @param telemetry - Optional telemetry configuration.
41
55
  *
42
56
  * @returns A result object that contains the streaming transcript and final transcript metadata.
43
57
  */
@@ -49,7 +63,11 @@ export function streamTranscribe({
49
63
  abortSignal,
50
64
  headers,
51
65
  includeRawChunks,
52
- _internal: { currentDate = () => new Date() } = {},
66
+ telemetry,
67
+ _internal: {
68
+ currentDate = () => new Date(),
69
+ generateCallId = originalGenerateCallId,
70
+ } = {},
53
71
  }: {
54
72
  /**
55
73
  * The transcription model to use.
@@ -86,11 +104,17 @@ export function streamTranscribe({
86
104
  */
87
105
  includeRawChunks?: boolean;
88
106
 
107
+ /**
108
+ * Optional telemetry configuration.
109
+ */
110
+ telemetry?: TelemetryOptions;
111
+
89
112
  /**
90
113
  * Internal test hooks.
91
114
  */
92
115
  _internal?: {
93
116
  currentDate?: () => Date;
117
+ generateCallId?: () => string;
94
118
  };
95
119
  }): StreamTranscriptionResult {
96
120
  const resolvedModel = resolveTranscriptionModel(model);
@@ -118,6 +142,34 @@ export function streamTranscribe({
118
142
  `ai/${VERSION}`,
119
143
  );
120
144
 
145
+ const callId = generateCallId();
146
+ const telemetryDispatcher = createTelemetryDispatcher({ telemetry });
147
+ const runInTracingChannelSpan =
148
+ telemetryDispatcher.runInTracingChannelSpan ??
149
+ (async <T>({ execute }: { execute: () => PromiseLike<T> }) =>
150
+ await execute());
151
+ let audioByteLength = 0;
152
+ const countedAudio =
153
+ telemetryDispatcher.experimental_onStreamTranscriptionEnd != null
154
+ ? createByteCountingStream(audio, byteLength => {
155
+ audioByteLength += byteLength;
156
+ })
157
+ : audio;
158
+ const startEvent: StreamTranscriptionStartEvent = {
159
+ callId,
160
+ operationId: 'ai.streamTranscribe',
161
+ provider: resolvedModel.provider,
162
+ modelId: resolvedModel.modelId,
163
+ audio: {
164
+ byteLength: undefined,
165
+ mediaType: inputAudioFormat.type,
166
+ },
167
+ inputAudioFormat,
168
+ maxRetries: undefined,
169
+ headers,
170
+ providerOptions,
171
+ };
172
+
121
173
  const textPromise = new DelayedPromise<string>();
122
174
  const segmentsPromise = new DelayedPromise<Array<TranscriptSegment>>();
123
175
  const languagePromise = new DelayedPromise<string | undefined>();
@@ -129,6 +181,8 @@ export function streamTranscribe({
129
181
  const providerMetadataPromise = new DelayedPromise<
130
182
  Record<string, JSONObject>
131
183
  >();
184
+ let warnings: Array<Warning> = [];
185
+ let telemetrySettled = false;
132
186
 
133
187
  const rejectPendingPromises = (error: unknown) => {
134
188
  for (const promise of [
@@ -151,10 +205,11 @@ export function streamTranscribe({
151
205
  const currentResponseMetadata = () =>
152
206
  response ?? { timestamp: startedAt, modelId: resolvedModel.modelId };
153
207
 
154
- const resolveWarnings = (warnings: Array<Warning>) => {
155
- warningsPromise.resolve(warnings);
208
+ const resolveWarnings = (resolvedWarnings: Array<Warning>) => {
209
+ warnings = resolvedWarnings;
210
+ warningsPromise.resolve(resolvedWarnings);
156
211
  logWarnings({
157
- warnings,
212
+ warnings: resolvedWarnings,
158
213
  provider: resolvedModel.provider,
159
214
  model: resolvedModel.modelId,
160
215
  });
@@ -172,7 +227,7 @@ export function streamTranscribe({
172
227
  Experimental_TranscriptionModelV4StreamPart,
173
228
  TranscriptionStreamPart
174
229
  > & { cancel?: (reason?: unknown) => void } = {
175
- transform(value, controller) {
230
+ async transform(value, controller) {
176
231
  switch (value.type) {
177
232
  case 'stream-start': {
178
233
  resolveWarnings(value.warnings);
@@ -214,6 +269,37 @@ export function streamTranscribe({
214
269
  durationInSecondsPromise.resolve(value.durationInSeconds);
215
270
  responsesPromise.resolve([currentResponseMetadata()]);
216
271
  providerMetadataPromise.resolve(value.providerMetadata ?? {});
272
+
273
+ telemetrySettled = true;
274
+ if (
275
+ telemetryDispatcher.experimental_onStreamTranscriptionEnd != null
276
+ ) {
277
+ const endEvent: StreamTranscriptionEndEvent = {
278
+ callId,
279
+ operationId: 'ai.streamTranscribe',
280
+ provider: resolvedModel.provider,
281
+ modelId: resolvedModel.modelId,
282
+ audio: {
283
+ byteLength: audioByteLength,
284
+ mediaType: inputAudioFormat.type,
285
+ },
286
+ text: value.text,
287
+ segments: value.segments,
288
+ language: value.language,
289
+ durationInSeconds: value.durationInSeconds,
290
+ usage: value.usage,
291
+ warnings,
292
+ providerMetadata: value.providerMetadata,
293
+ response: currentResponseMetadata(),
294
+ };
295
+
296
+ await notify({
297
+ event: endEvent,
298
+ callbacks: [
299
+ telemetryDispatcher.experimental_onStreamTranscriptionEnd,
300
+ ],
301
+ });
302
+ }
217
303
  break;
218
304
  }
219
305
  }
@@ -241,36 +327,50 @@ export function streamTranscribe({
241
327
 
242
328
  // Piping (instead of an eager read loop) preserves consumer backpressure
243
329
  // and propagates cancellation of `fullStream` to the model stream.
244
- void (async () => {
245
- const result = await doStream({
246
- audio,
247
- inputAudioFormat,
248
- providerOptions,
249
- // merged so cancelling fullStream also aborts a still-pending doStream
250
- abortSignal: mergeAbortSignals(abortSignal, pipeAbortController.signal),
251
- headers: headersWithUserAgent,
252
- includeRawChunks,
253
- });
254
-
255
- response = {
256
- timestamp: result.response?.timestamp ?? startedAt,
257
- modelId: result.response?.modelId ?? resolvedModel.modelId,
258
- headers: result.response?.headers,
259
- };
260
-
261
- await result.stream.pipeTo(transform.writable, {
262
- signal: pipeAbortController.signal,
263
- });
264
- })().catch(error => {
330
+ void runInTracingChannelSpan({
331
+ type: 'streamTranscribe',
332
+ event: startEvent,
333
+ execute: async () => {
334
+ await notify({
335
+ event: startEvent,
336
+ callbacks: [
337
+ telemetryDispatcher.experimental_onStreamTranscriptionStart,
338
+ ],
339
+ });
340
+
341
+ const result = await doStream({
342
+ audio: countedAudio,
343
+ inputAudioFormat,
344
+ providerOptions,
345
+ // merged so cancelling fullStream also aborts a still-pending doStream
346
+ abortSignal: mergeAbortSignals(abortSignal, pipeAbortController.signal),
347
+ headers: headersWithUserAgent,
348
+ includeRawChunks,
349
+ });
350
+
351
+ response = {
352
+ timestamp: result.response?.timestamp ?? startedAt,
353
+ modelId: result.response?.modelId ?? resolvedModel.modelId,
354
+ headers: result.response?.headers,
355
+ };
356
+
357
+ await result.stream.pipeTo(transform.writable, {
358
+ signal: pipeAbortController.signal,
359
+ });
360
+ },
361
+ }).catch(async error => {
265
362
  const reason =
266
363
  error ?? new Error('Transcription stream was cancelled or errored.');
267
364
  rejectPendingPromises(reason);
365
+ if (!telemetrySettled) {
366
+ telemetrySettled = true;
367
+ await telemetryDispatcher.onError?.({ callId, error: reason });
368
+ }
268
369
  // When `doStream` rejects before the model stream exists (e.g. auth or
269
- // header resolution failure), nothing has taken ownership of `audio` yet,
270
- // so cancel it directly — otherwise an upstream producer piping into it
271
- // hangs forever. When the model did take a reader, `audio` is locked and
272
- // the cancel rejects, which is fine: the model's cleanup owns it then.
273
- audio.cancel(reason).catch(() => {});
370
+ // header resolution failure), nothing has taken ownership of `countedAudio`
371
+ // yet, so cancel it directly. When the model did take a reader, the cancel
372
+ // rejects and the model's cleanup owns it.
373
+ countedAudio.cancel(reason).catch(() => {});
274
374
  transform.writable.abort(reason).catch(() => {
275
375
  // the writable is already errored when the model stream failed mid-pipe
276
376
  });
@@ -346,3 +446,54 @@ export function streamTranscribe({
346
446
  },
347
447
  };
348
448
  }
449
+
450
+ function createByteCountingStream(
451
+ audio: ReadableStream<Uint8Array | string>,
452
+ onChunk: (byteLength: number) => void,
453
+ ): ReadableStream<Uint8Array | string> {
454
+ let audioReader: ReadableStreamDefaultReader<Uint8Array | string> | undefined;
455
+
456
+ return new ReadableStream<Uint8Array | string>({
457
+ async pull(controller) {
458
+ audioReader ??= audio.getReader();
459
+ const { done, value } = await audioReader.read();
460
+ if (done) {
461
+ controller.close();
462
+ return;
463
+ }
464
+
465
+ onChunk(
466
+ typeof value === 'string'
467
+ ? getBase64ByteLength(value)
468
+ : value.byteLength,
469
+ );
470
+ controller.enqueue(value);
471
+ },
472
+ async cancel(reason) {
473
+ if (audioReader != null) {
474
+ await audioReader.cancel(reason);
475
+ } else {
476
+ await audio.cancel(reason);
477
+ }
478
+ },
479
+ });
480
+ }
481
+
482
+ function getBase64ByteLength(value: string): number {
483
+ let characterCount = 0;
484
+ let lastCharacter = '';
485
+ let secondLastCharacter = '';
486
+
487
+ for (const character of value) {
488
+ if (/\s/.test(character)) {
489
+ continue;
490
+ }
491
+ characterCount++;
492
+ secondLastCharacter = lastCharacter;
493
+ lastCharacter = character;
494
+ }
495
+
496
+ const padding =
497
+ lastCharacter === '=' ? (secondLastCharacter === '=' ? 2 : 1) : 0;
498
+ return Math.floor((characterCount * 3) / 4) - padding;
499
+ }