ai 7.0.122 → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/index.d.ts +7598 -7268
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +19555 -22545
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +2235 -2004
  7. package/dist/internal/index.d.ts.map +1 -0
  8. package/dist/internal/index.js +3261 -3921
  9. package/dist/internal/index.js.map +1 -1
  10. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/test/index.d.ts +294 -273
  12. package/dist/test/index.d.ts.map +1 -0
  13. package/dist/test/index.js +325 -469
  14. package/dist/test/index.js.map +1 -1
  15. package/docs/02-foundations/03-prompts.mdx +3 -1
  16. package/docs/02-foundations/06-provider-options.mdx +1 -1
  17. package/docs/02-getting-started/06-nodejs.mdx +4 -4
  18. package/docs/02-getting-started/08-tanstack-start.mdx +1 -1
  19. package/docs/03-agents/05-configuring-call-options.mdx +1 -1
  20. package/docs/03-ai-sdk-core/10-generating-structured-data.mdx +1 -1
  21. package/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx +3 -0
  22. package/docs/03-ai-sdk-core/17-mcp-apps.mdx +5 -5
  23. package/docs/03-ai-sdk-core/36-transcription.mdx +36 -0
  24. package/docs/03-ai-sdk-core/37-speech.mdx +22 -1
  25. package/docs/03-ai-sdk-core/42-batch.mdx +1 -1
  26. package/docs/03-ai-sdk-core/60-telemetry.mdx +51 -3
  27. package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +24 -0
  28. package/docs/03-ai-sdk-harnesses/05-harness-adapters.mdx +12 -12
  29. package/docs/04-ai-sdk-ui/03-chatbot-message-persistence.mdx +9 -7
  30. package/docs/04-ai-sdk-ui/03-chatbot-resume-streams.mdx +2 -2
  31. package/docs/04-ai-sdk-ui/03-chatbot-tool-usage.mdx +7 -9
  32. package/docs/04-ai-sdk-ui/05-completion.mdx +1 -1
  33. package/docs/04-ai-sdk-ui/50-stream-protocol.mdx +2 -2
  34. package/docs/05-ai-sdk-rsc/03-generative-ui-state.mdx +8 -7
  35. package/docs/05-ai-sdk-rsc/06-loading-state.mdx +19 -10
  36. package/docs/05-ai-sdk-rsc/08-error-handling.mdx +2 -0
  37. package/docs/05-ai-sdk-rsc/10-migrating-to-ui.mdx +4 -3
  38. package/docs/06-advanced/02-stopping-streams.mdx +2 -2
  39. package/docs/06-advanced/04-caching.mdx +9 -5
  40. package/docs/06-advanced/05-multiple-streamables.mdx +1 -1
  41. package/docs/06-advanced/07-rendering-ui-with-language-models.mdx +16 -16
  42. package/docs/06-advanced/09-multistep-interfaces.mdx +1 -1
  43. package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +4 -4
  44. package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +4 -4
  45. package/docs/07-reference/01-ai-sdk-core/11-stream-transcribe.mdx +7 -0
  46. package/docs/07-reference/01-ai-sdk-core/11-transcribe.mdx +7 -0
  47. package/docs/07-reference/01-ai-sdk-core/12-generate-speech.mdx +7 -0
  48. package/docs/07-reference/01-ai-sdk-core/14-upload-file.mdx +4 -4
  49. package/docs/07-reference/01-ai-sdk-core/14-upload-skill.mdx +1 -1
  50. package/docs/07-reference/01-ai-sdk-core/15-agent.mdx +1 -1
  51. package/docs/07-reference/01-ai-sdk-core/18-create-agent-ui-stream-response.mdx +1 -4
  52. package/docs/07-reference/01-ai-sdk-core/22-dynamic-tool.mdx +1 -1
  53. package/docs/07-reference/01-ai-sdk-core/23-create-mcp-client.mdx +1 -1
  54. package/docs/07-reference/01-ai-sdk-core/30-model-message.mdx +2 -2
  55. package/docs/07-reference/01-ai-sdk-core/69-add-tool-input-examples-middleware.mdx +1 -1
  56. package/docs/07-reference/01-ai-sdk-core/80-smooth-stream.mdx +1 -1
  57. package/docs/07-reference/02-ai-sdk-ui/31-convert-to-model-messages.mdx +5 -4
  58. package/docs/07-reference/02-ai-sdk-ui/40-create-ui-message-stream.mdx +3 -2
  59. package/docs/07-reference/02-ai-sdk-ui/41-create-ui-message-stream-response.mdx +7 -0
  60. package/docs/07-reference/02-ai-sdk-ui/42-pipe-ui-message-stream-to-response.mdx +7 -0
  61. package/docs/07-reference/02-ai-sdk-ui/50-direct-chat-transport.mdx +2 -2
  62. package/docs/07-reference/03-ai-sdk-rsc/01-stream-ui.mdx +2 -2
  63. package/docs/07-reference/04-ai-sdk-workflow/01-workflow-agent.mdx +1 -1
  64. package/docs/08-migration-guides/25-migration-guide-5-0-data.mdx +4 -5
  65. package/docs/08-migration-guides/26-migration-guide-5-0.mdx +2 -1
  66. package/docs/08-migration-guides/29-migration-guide-4-0.mdx +4 -4
  67. package/docs/08-migration-guides/38-migration-guide-3-2.mdx +1 -1
  68. package/docs/08-migration-guides/39-migration-guide-3-1.mdx +1 -1
  69. package/docs/09-troubleshooting/12-use-chat-an-error-occurred.mdx +8 -6
  70. package/docs/09-troubleshooting/50-react-maximum-update-depth-exceeded.mdx +6 -6
  71. package/docs/09-troubleshooting/70-high-memory-usage-with-images.mdx +1 -1
  72. package/package.json +17 -17
  73. package/src/agent/create-agent-ui-stream-response.ts +3 -0
  74. package/src/agent/pipe-agent-ui-stream-to-response.ts +3 -0
  75. package/src/generate-speech/generate-speech.ts +163 -38
  76. package/src/generate-speech/index.ts +4 -0
  77. package/src/generate-speech/speech-events.ts +91 -0
  78. package/src/generate-text/prune-messages.ts +1 -1
  79. package/src/middleware/extract-reasoning-middleware.ts +27 -11
  80. package/src/telemetry/create-telemetry-dispatcher.ts +18 -5
  81. package/src/telemetry/telemetry.ts +38 -6
  82. package/src/telemetry/tracing-channel.ts +3 -0
  83. package/src/transcribe/index.ts +6 -0
  84. package/src/transcribe/stream-transcribe.ts +182 -31
  85. package/src/transcribe/transcribe.ts +186 -33
  86. package/src/transcribe/transcription-events.ts +111 -0
  87. package/src/ui/convert-to-model-messages.ts +18 -6
  88. package/src/ui-message-stream/create-sse-stream-with-keep-alive.ts +89 -0
  89. package/src/ui-message-stream/create-ui-message-stream-response.ts +7 -1
  90. package/src/ui-message-stream/pipe-ui-message-stream-to-response.ts +7 -1
  91. package/src/ui-message-stream/ui-message-stream-response-init.ts +7 -0
@@ -82,7 +82,7 @@ export function pruneMessages({
82
82
  const keptToolCallIds: Set<string> = new Set();
83
83
  const keptApprovalIds: Set<string> = new Set();
84
84
 
85
- if (keepLastMessagesCount != null) {
85
+ if (keepLastMessagesCount != null && keepLastMessagesCount !== 0) {
86
86
  for (const message of messages.slice(-keepLastMessagesCount)) {
87
87
  if (
88
88
  (message.role === 'assistant' || message.role === 'tool') &&
@@ -116,20 +116,15 @@ export function extractReasoningMiddleware({
116
116
  return;
117
117
  }
118
118
 
119
- if (
120
- chunk.type === 'text-end' &&
121
- delayedTextStarts[chunk.id] != null
122
- ) {
123
- controller.enqueue(delayedTextStarts[chunk.id]);
124
- delete delayedTextStarts[chunk.id];
125
- }
126
-
127
- if (chunk.type !== 'text-delta') {
119
+ if (chunk.type !== 'text-delta' && chunk.type !== 'text-end') {
128
120
  controller.enqueue(chunk);
129
121
  return;
130
122
  }
131
123
 
132
- if (reasoningExtractions[chunk.id] == null) {
124
+ if (
125
+ chunk.type === 'text-delta' &&
126
+ reasoningExtractions[chunk.id] == null
127
+ ) {
133
128
  reasoningExtractions[chunk.id] = {
134
129
  isFirstReasoning: true,
135
130
  isFirstText: true,
@@ -143,7 +138,14 @@ export function extractReasoningMiddleware({
143
138
 
144
139
  const activeExtraction = reasoningExtractions[chunk.id];
145
140
 
146
- activeExtraction.buffer += chunk.delta;
141
+ if (activeExtraction == null) {
142
+ if (delayedTextStarts[chunk.id] != null) {
143
+ controller.enqueue(delayedTextStarts[chunk.id]);
144
+ delete delayedTextStarts[chunk.id];
145
+ }
146
+ controller.enqueue(chunk);
147
+ return;
148
+ }
147
149
 
148
150
  function getReasoningId() {
149
151
  return (activeExtraction.reasoningId ??= `reasoning-${reasoningIdCounter++}`);
@@ -199,6 +201,20 @@ export function extractReasoningMiddleware({
199
201
  }
200
202
  }
201
203
 
204
+ if (chunk.type === 'text-end') {
205
+ publish(activeExtraction.buffer);
206
+ activeExtraction.buffer = '';
207
+
208
+ if (delayedTextStarts[chunk.id] != null) {
209
+ controller.enqueue(delayedTextStarts[chunk.id]);
210
+ delete delayedTextStarts[chunk.id];
211
+ }
212
+ controller.enqueue(chunk);
213
+ return;
214
+ }
215
+
216
+ activeExtraction.buffer += chunk.delta;
217
+
202
218
  do {
203
219
  const nextTag = activeExtraction.isReasoning
204
220
  ? closingTag
@@ -142,7 +142,7 @@ export function createTelemetryDispatcher({
142
142
 
143
143
  const mergeTelemetryCallback = <KEY extends TelemetryCallbackKey>(
144
144
  key: KEY,
145
- ): Callback<TelemetryEvent<KEY>> => {
145
+ ): Callback<TelemetryEvent<KEY>> | undefined => {
146
146
  const integrationCallbacks = (
147
147
  integrations
148
148
  .map(integration => integration[key]?.bind(integration))
@@ -157,6 +157,10 @@ export function createTelemetryDispatcher({
157
157
  >,
158
158
  );
159
159
 
160
+ if (integrationCallbacks.length === 0) {
161
+ return undefined;
162
+ }
163
+
160
164
  const mergedIntegrationCallback = mergeCallbacks(...integrationCallbacks);
161
165
 
162
166
  return async (event: TelemetryEvent<KEY>) => {
@@ -164,6 +168,9 @@ export function createTelemetryDispatcher({
164
168
  };
165
169
  };
166
170
 
171
+ const onStepEnd = mergeTelemetryCallback('onStepEnd');
172
+ const onStepFinish = mergeTelemetryCallback('onStepFinish');
173
+
167
174
  const executeLanguageModelCallWrappers = integrations
168
175
  .map(integration => integration.executeLanguageModelCall?.bind(integration))
169
176
  .filter(Boolean) as Array<
@@ -205,10 +212,10 @@ export function createTelemetryDispatcher({
205
212
  // deprecated `onStepFinish` callback so integrations that still implement
206
213
  // only `onStepFinish` keep receiving step-end events during the deprecation
207
214
  // window.
208
- onStepEnd: mergeCallbacks(
209
- mergeTelemetryCallback('onStepEnd'),
210
- mergeTelemetryCallback('onStepFinish'),
211
- ),
215
+ onStepEnd:
216
+ onStepEnd == null && onStepFinish == null
217
+ ? undefined
218
+ : mergeCallbacks(onStepEnd, onStepFinish),
212
219
  onObjectStepStart: mergeTelemetryCallback('onObjectStepStart'),
213
220
  onObjectStepEnd: mergeTelemetryCallback('onObjectStepEnd'),
214
221
  onEmbedStart: mergeTelemetryCallback('onEmbedStart'),
@@ -227,6 +234,12 @@ export function createTelemetryDispatcher({
227
234
  experimental_onEvaluateEnd: mergeTelemetryCallback(
228
235
  'experimental_onEvaluateEnd',
229
236
  ),
237
+ experimental_onStreamTranscriptionStart: mergeTelemetryCallback(
238
+ 'experimental_onStreamTranscriptionStart',
239
+ ),
240
+ experimental_onStreamTranscriptionEnd: mergeTelemetryCallback(
241
+ 'experimental_onStreamTranscriptionEnd',
242
+ ),
230
243
  onEnd: mergeTelemetryCallback('onEnd'),
231
244
  onAbort: mergeTelemetryCallback('onAbort'),
232
245
  onError: mergeTelemetryCallback('onError'),
@@ -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
+ }