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.
@@ -30,6 +30,42 @@ const language = transcript.language; // language of the transcript e.g. "en", i
30
30
  const durationInSeconds = transcript.durationInSeconds; // duration of the transcript in seconds, if available
31
31
  ```
32
32
 
33
+ ## Telemetry
34
+
35
+ `transcribe` and `experimental_streamTranscribe` accept the standard
36
+ `telemetry` option. Registered integrations receive start, end, and error
37
+ events with the provider and model, audio byte length and media type, final
38
+ transcript, warnings, response metadata, provider metadata, and usage when the
39
+ provider reports it. Because streaming transcription is experimental, custom
40
+ integrations receive its lifecycle through
41
+ `experimental_onStreamTranscriptionStart` and
42
+ `experimental_onStreamTranscriptionEnd`; errors continue to use `onError`.
43
+ Integrations that wrap or delegate to another telemetry integration must
44
+ forward these experimental callbacks explicitly for streaming transcription
45
+ to be observed.
46
+
47
+ ```ts highlight="5-7"
48
+ const transcript = await transcribe({
49
+ model: openai.transcription('gpt-4o-transcribe'),
50
+ audio: await readFile('audio.mp3'),
51
+ telemetry: {
52
+ functionId: 'transcribe-customer-call',
53
+ },
54
+ });
55
+ ```
56
+
57
+ Raw audio is not included in telemetry events. For streaming transcription,
58
+ the final byte length is emitted only after the input stream is consumed and
59
+ the transcription finishes. Use `recordInputs: false` or
60
+ `recordOutputs: false` to prevent integrations from recording sensitive input
61
+ or output fields.
62
+
63
+ For URL audio, the downloaded media type is passed to the provider and reported
64
+ on successful telemetry events. If downloading or decoding input audio fails,
65
+ the start event is still emitted before the correlated error event, but its
66
+ audio byte length and media type can be undefined because preparation did not
67
+ complete.
68
+
33
69
  ## Streaming Transcription
34
70
 
35
71
  <Note type="warning">Streaming transcription is an experimental feature.</Note>
@@ -19,6 +19,27 @@ const result = await generateSpeech({
19
19
  });
20
20
  ```
21
21
 
22
+ ## Telemetry
23
+
24
+ `generateSpeech` accepts the standard `telemetry` option. Registered
25
+ integrations receive start, end, and error events with the provider and model,
26
+ source text, generated audio byte length, media type and format, warnings,
27
+ response metadata, provider metadata, and usage when the provider reports it.
28
+ Raw generated audio is not included in telemetry events.
29
+
30
+ ```ts highlight="5-7"
31
+ const result = await generateSpeech({
32
+ model: openai.speech('gpt-4o-mini-tts'),
33
+ text: 'Hello from the AI SDK!',
34
+ telemetry: {
35
+ functionId: 'speak-greeting',
36
+ },
37
+ });
38
+ ```
39
+
40
+ Use `recordInputs: false` or `recordOutputs: false` when the text or output
41
+ metadata should not be recorded by telemetry integrations.
42
+
22
43
  To access the generated audio:
23
44
 
24
45
  ```ts
@@ -53,7 +53,10 @@ For Node.js applications (without Next.js), register the integration at the top
53
53
 
54
54
  ### Step 2: Enabling Telemetry
55
55
 
56
- Once a telemetry integration is registered, all AI SDK calls emit telemetry events by default.
56
+ Once a telemetry integration is registered, supported AI SDK calls emit
57
+ telemetry events by default. This includes text and object generation,
58
+ embeddings, reranking, speech generation, and both non-streaming and streaming
59
+ transcription.
57
60
  You can still pass `telemetry` to attach metadata (like `functionId`) or to opt out of a specific call:
58
61
 
59
62
  ```ts highlight="4-6"
@@ -71,6 +74,34 @@ By default, both inputs and outputs are recorded. You can disable them by settin
71
74
  Disabling the recording of inputs and outputs can be useful for privacy, data transfer, and performance reasons.
72
75
  You might for example want to disable recording inputs if they contain sensitive information.
73
76
 
77
+ For `generateSpeech`, input telemetry contains the source text and output
78
+ telemetry contains the generated audio byte length, media type, and format.
79
+ For `transcribe` and `experimental_streamTranscribe`, telemetry contains the
80
+ input audio byte length and media type plus the output transcript. Raw audio is
81
+ never included in telemetry events. Streaming transcription reports its final
82
+ byte length when the audio stream has been consumed and the transcription
83
+ finishes.
84
+
85
+ Custom integrations receive stable `generateSpeech` and `transcribe`
86
+ operations through `onStart` and `onEnd`. To keep the experimental API isolated,
87
+ `experimental_streamTranscribe` uses
88
+ `experimental_onStreamTranscriptionStart` and
89
+ `experimental_onStreamTranscriptionEnd`; all three operations report failures
90
+ through `onError`.
91
+
92
+ An integration that wraps or delegates to another integration must explicitly
93
+ forward the experimental streaming transcription callbacks. Forwarding only
94
+ the stable `onStart` and `onEnd` callbacks does not observe
95
+ `experimental_streamTranscribe`.
96
+
97
+ The built-in OpenTelemetry integrations record numeric provider usage by
98
+ default, preserving each reported unit. The modern integration uses attributes
99
+ such as `gen_ai.usage.characters`, `gen_ai.usage.seconds`, and
100
+ `gen_ai.usage.input_tokens`; `LegacyOpenTelemetry` uses the corresponding
101
+ `ai.usage.*` attributes. The provider-native usage object remains available to
102
+ callbacks, and can also be included as the supplemental `ai.response.usage`
103
+ attribute by enabling `usage: true`.
104
+
74
105
  ### Opting out
75
106
 
76
107
  Telemetry is opt-out. To disable telemetry for a specific call, set `isEnabled: false`:
@@ -550,6 +581,23 @@ For `rerank`, the integration records spans with `CLIENT` kind:
550
581
  - `gen_ai.provider.name`: the provider
551
582
  - `gen_ai.request.model`: the model ID
552
583
 
584
+ #### generateSpeech / transcribe / experimental_streamTranscribe
585
+
586
+ Each call records a single `CLIENT` span named `{operationId} {modelId}`, where
587
+ `operationId` is `ai.generateSpeech`, `ai.transcribe`, or `ai.streamTranscribe`.
588
+
589
+ - `gen_ai.output.type`: `"speech"` for speech generation and `"text"` for transcription.
590
+ - `gen_ai.request.stream`: `true` for streaming transcription and `false` otherwise.
591
+ - `gen_ai.input.messages`: speech generation input text as a user message with a text part, when `recordInputs` is enabled.
592
+ - `gen_ai.output.messages`: the completed transcript as an assistant message with a text part, when `recordOutputs` is enabled. Streaming transcription records this once on completion.
593
+
594
+ The existing `ai.request.text` and `ai.response.text` attributes are also retained.
595
+ Audio byte length, media type, and output format remain in the
596
+ `ai.request.audio.*` and `ai.response.audio.*` attributes, subject to the
597
+ corresponding input/output recording controls. Raw audio is not captured, so
598
+ transcription input and speech output do not populate message attributes with
599
+ audio content or placeholder audio parts.
600
+
553
601
  #### experimental_evaluate
554
602
 
555
603
  For `experimental_evaluate`, the integration records two spans with `CLIENT`
@@ -88,6 +88,13 @@ console.log(await result.text);
88
88
  description:
89
89
  'When true, the provider includes raw provider chunks in the stream as `raw` parts.',
90
90
  },
91
+ {
92
+ name: 'telemetry',
93
+ type: 'TelemetryOptions',
94
+ isOptional: true,
95
+ description:
96
+ 'Telemetry configuration. Experimental lifecycle callbacks use experimental_onStreamTranscriptionStart and experimental_onStreamTranscriptionEnd. The end event is emitted after the audio stream is consumed and includes the final input byte length.',
97
+ },
91
98
  ]}
92
99
  />
93
100
 
@@ -64,6 +64,13 @@ console.log(transcript);
64
64
  isOptional: true,
65
65
  description: 'Additional HTTP headers for the request.',
66
66
  },
67
+ {
68
+ name: 'telemetry',
69
+ type: 'TelemetryOptions',
70
+ isOptional: true,
71
+ description:
72
+ 'Telemetry configuration. Supports functionId, recordInputs, recordOutputs, isEnabled, and per-call integrations.',
73
+ },
67
74
  {
68
75
  name: 'download',
69
76
  type: '(options: { url: URL; abortSignal?: AbortSignal }) => Promise<{ data: Uint8Array; mediaType: string | undefined }>',
@@ -124,6 +124,13 @@ const { audio } = await generateSpeech({
124
124
  isOptional: true,
125
125
  description: 'Additional HTTP headers for the request.',
126
126
  },
127
+ {
128
+ name: 'telemetry',
129
+ type: 'TelemetryOptions',
130
+ isOptional: true,
131
+ description:
132
+ 'Telemetry configuration. Supports functionId, recordInputs, recordOutputs, isEnabled, and per-call integrations.',
133
+ },
127
134
  ]}
128
135
  />
129
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai",
3
- "version": "7.0.123",
3
+ "version": "7.0.124",
4
4
  "type": "module",
5
5
  "description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
6
6
  "license": "Apache-2.0",
@@ -42,20 +42,20 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@ai-sdk/gateway": "4.0.101",
46
- "@ai-sdk/provider": "4.0.20",
47
- "@ai-sdk/provider-utils": "5.0.52"
45
+ "@ai-sdk/gateway": "4.0.102",
46
+ "@ai-sdk/provider": "4.0.21",
47
+ "@ai-sdk/provider-utils": "5.0.53"
48
48
  },
49
49
  "devDependencies": {
50
- "@ai-sdk/amazon-bedrock": "5.0.102",
51
- "@ai-sdk/deepseek": "3.0.57",
52
- "@ai-sdk/google": "4.0.86",
53
- "@ai-sdk/groq": "4.0.53",
54
- "@ai-sdk/huggingface": "2.0.60",
55
- "@ai-sdk/moonshotai": "3.0.61",
56
- "@ai-sdk/openai": "4.0.82",
50
+ "@ai-sdk/amazon-bedrock": "5.0.103",
51
+ "@ai-sdk/deepseek": "3.0.58",
52
+ "@ai-sdk/google": "4.0.87",
53
+ "@ai-sdk/groq": "4.0.54",
54
+ "@ai-sdk/huggingface": "2.0.61",
55
+ "@ai-sdk/moonshotai": "3.0.62",
56
+ "@ai-sdk/openai": "4.0.83",
57
57
  "@ai-sdk/test-server": "2.0.3",
58
- "@ai-sdk/xai": "5.0.13",
58
+ "@ai-sdk/xai": "5.0.14",
59
59
  "@edge-runtime/vm": "^5.0.0",
60
60
  "@smithy/eventstream-codec": "^4.3.3",
61
61
  "@smithy/util-utf8": "^4.3.3",
@@ -1,5 +1,6 @@
1
1
  import type { JSONObject } from '@ai-sdk/provider';
2
2
  import {
3
+ createIdGenerator,
3
4
  detectMediaType,
4
5
  withUserAgentSuffix,
5
6
  type ProviderOptions,
@@ -7,16 +8,28 @@ import {
7
8
  import { NoSpeechGeneratedError } from '../error/no-speech-generated-error';
8
9
  import { logWarnings } from '../logger/log-warnings';
9
10
  import { resolveSpeechModel } from '../model/resolve-model';
11
+ import { createTelemetryDispatcher } from '../telemetry/create-telemetry-dispatcher';
12
+ import type { TelemetryOptions } from '../telemetry/telemetry-options';
10
13
  import type { SpeechModel } from '../types/speech-model';
11
14
  import type { SpeechModelResponseMetadata } from '../types/speech-model-response-metadata';
12
15
  import type { Warning } from '../types/warning';
13
16
  import { prepareRetries } from '../util/prepare-retries';
17
+ import { notify } from '../util/notify';
14
18
  import { VERSION } from '../version';
15
19
  import type { SpeechResult } from './generate-speech-result';
16
20
  import {
17
21
  DefaultGeneratedAudioFile,
18
22
  type GeneratedAudioFile,
19
23
  } from './generated-audio-file';
24
+ import type {
25
+ GenerateSpeechEndEvent,
26
+ GenerateSpeechStartEvent,
27
+ } from './speech-events';
28
+
29
+ const originalGenerateCallId = createIdGenerator({
30
+ prefix: 'call',
31
+ size: 24,
32
+ });
20
33
  /**
21
34
  * Generates speech audio using a speech model.
22
35
  *
@@ -32,6 +45,7 @@ import {
32
45
  * @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
33
46
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
34
47
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
48
+ * @param telemetry - Optional telemetry configuration.
35
49
  *
36
50
  * @returns A result object that contains the generated audio data.
37
51
  */
@@ -47,6 +61,8 @@ export async function generateSpeech({
47
61
  maxRetries: maxRetriesArg,
48
62
  abortSignal,
49
63
  headers,
64
+ telemetry,
65
+ _internal: { generateCallId = originalGenerateCallId } = {},
50
66
  }: {
51
67
  /**
52
68
  * The speech model to use.
@@ -115,6 +131,18 @@ export async function generateSpeech({
115
131
  * Only applicable for HTTP-based providers.
116
132
  */
117
133
  headers?: Record<string, string>;
134
+
135
+ /**
136
+ * Optional telemetry configuration.
137
+ */
138
+ telemetry?: TelemetryOptions;
139
+
140
+ /**
141
+ * Internal. For test use only. May change without notice.
142
+ */
143
+ _internal?: {
144
+ generateCallId?: () => string;
145
+ };
118
146
  }): Promise<SpeechResult> {
119
147
  const resolvedModel = resolveSpeechModel(model);
120
148
  if (!resolvedModel) {
@@ -126,55 +154,152 @@ export async function generateSpeech({
126
154
  `ai/${VERSION}`,
127
155
  );
128
156
 
129
- const { retry } = prepareRetries({
157
+ const { maxRetries, retry } = prepareRetries({
130
158
  maxRetries: maxRetriesArg,
131
159
  abortSignal,
132
160
  });
133
161
 
134
- const result = await retry(() =>
135
- resolvedModel.doGenerate({
136
- text,
137
- voice,
138
- outputFormat,
139
- instructions,
140
- speed,
141
- language,
142
- abortSignal,
143
- headers: headersWithUserAgent,
144
- providerOptions,
145
- }),
146
- );
147
-
148
- if (!result.audio || result.audio.length === 0) {
149
- throw new NoSpeechGeneratedError({ responses: [result.response] });
150
- }
162
+ const callId = generateCallId();
163
+ const telemetryDispatcher = createTelemetryDispatcher({ telemetry });
164
+ const runInTracingChannelSpan =
165
+ telemetryDispatcher.runInTracingChannelSpan ??
166
+ (async <T>({ execute }: { execute: () => PromiseLike<T> }) =>
167
+ await execute());
151
168
 
152
- logWarnings({
153
- warnings: result.warnings,
169
+ const startEvent: GenerateSpeechStartEvent = {
170
+ callId,
171
+ operationId: 'ai.generateSpeech',
154
172
  provider: resolvedModel.provider,
155
- model: resolvedModel.modelId,
156
- });
173
+ modelId: resolvedModel.modelId,
174
+ text,
175
+ voice,
176
+ outputFormat,
177
+ instructions,
178
+ speed,
179
+ language,
180
+ maxRetries,
181
+ headers,
182
+ providerOptions,
183
+ };
157
184
 
158
- const detectedMediaType = detectMediaType({
159
- data: result.audio,
160
- topLevelType: 'audio',
161
- });
185
+ return await runInTracingChannelSpan({
186
+ type: 'generateSpeech',
187
+ event: startEvent,
188
+ execute: async () => {
189
+ await notify({
190
+ event: startEvent,
191
+ callbacks: [telemetryDispatcher.onStart],
192
+ });
193
+
194
+ try {
195
+ const result = await retry(() =>
196
+ resolvedModel.doGenerate({
197
+ text,
198
+ voice,
199
+ outputFormat,
200
+ instructions,
201
+ speed,
202
+ language,
203
+ abortSignal,
204
+ headers: headersWithUserAgent,
205
+ providerOptions,
206
+ }),
207
+ );
208
+
209
+ if (!result.audio || result.audio.length === 0) {
210
+ throw new NoSpeechGeneratedError({ responses: [result.response] });
211
+ }
162
212
 
163
- return new DefaultSpeechResult({
164
- audio: new DefaultGeneratedAudioFile({
165
- data: result.audio,
166
- mediaType:
167
- detectedMediaType ??
168
- getResponseAudioMediaType(result.response.headers) ??
169
- getOutputFormatMediaType(outputFormat) ??
170
- 'audio/mp3',
171
- }),
172
- warnings: result.warnings,
173
- responses: [result.response],
174
- providerMetadata: result.providerMetadata,
213
+ logWarnings({
214
+ warnings: result.warnings,
215
+ provider: resolvedModel.provider,
216
+ model: resolvedModel.modelId,
217
+ });
218
+
219
+ const detectedMediaType = detectMediaType({
220
+ data: result.audio,
221
+ topLevelType: 'audio',
222
+ });
223
+
224
+ const audio = new DefaultGeneratedAudioFile({
225
+ data: result.audio,
226
+ mediaType:
227
+ detectedMediaType ??
228
+ getResponseAudioMediaType(result.response.headers) ??
229
+ getOutputFormatMediaType(outputFormat) ??
230
+ 'audio/mp3',
231
+ });
232
+
233
+ if (telemetryDispatcher.onEnd != null) {
234
+ const endEvent: GenerateSpeechEndEvent = {
235
+ callId,
236
+ operationId: 'ai.generateSpeech',
237
+ provider: resolvedModel.provider,
238
+ modelId: resolvedModel.modelId,
239
+ text,
240
+ audio: {
241
+ byteLength: getBase64OrBinaryByteLength(result.audio),
242
+ mediaType: audio.mediaType,
243
+ format: audio.format,
244
+ },
245
+ usage: result.usage,
246
+ warnings: result.warnings,
247
+ providerMetadata: result.providerMetadata,
248
+ response: getTelemetryResponseMetadata(result.response),
249
+ };
250
+
251
+ await notify({
252
+ event: endEvent,
253
+ callbacks: [telemetryDispatcher.onEnd],
254
+ });
255
+ }
256
+
257
+ return new DefaultSpeechResult({
258
+ audio,
259
+ warnings: result.warnings,
260
+ responses: [result.response],
261
+ providerMetadata: result.providerMetadata,
262
+ });
263
+ } catch (error) {
264
+ await telemetryDispatcher.onError?.({ callId, error });
265
+ throw error;
266
+ }
267
+ },
175
268
  });
176
269
  }
177
270
 
271
+ function getBase64OrBinaryByteLength(audio: string | Uint8Array): number {
272
+ if (audio instanceof Uint8Array) {
273
+ return audio.byteLength;
274
+ }
275
+
276
+ let characterCount = 0;
277
+ let lastCharacter = '';
278
+ let secondLastCharacter = '';
279
+ for (const character of audio) {
280
+ if (/\s/.test(character)) {
281
+ continue;
282
+ }
283
+ characterCount++;
284
+ secondLastCharacter = lastCharacter;
285
+ lastCharacter = character;
286
+ }
287
+
288
+ const padding =
289
+ lastCharacter === '=' ? (secondLastCharacter === '=' ? 2 : 1) : 0;
290
+ return Math.floor((characterCount * 3) / 4) - padding;
291
+ }
292
+
293
+ function getTelemetryResponseMetadata(
294
+ response: SpeechModelResponseMetadata,
295
+ ): SpeechModelResponseMetadata {
296
+ return {
297
+ timestamp: response.timestamp,
298
+ modelId: response.modelId,
299
+ headers: response.headers,
300
+ };
301
+ }
302
+
178
303
  function getResponseAudioMediaType(
179
304
  headers: Record<string, string> | undefined,
180
305
  ): string | undefined {
@@ -4,6 +4,10 @@ import { generateSpeech } from './generate-speech';
4
4
  export { generateSpeech } from './generate-speech';
5
5
  export type { SpeechResult } from './generate-speech-result';
6
6
  export type { GeneratedAudioFile } from './generated-audio-file';
7
+ export type {
8
+ GenerateSpeechEndEvent,
9
+ GenerateSpeechStartEvent,
10
+ } from './speech-events';
7
11
 
8
12
  // deprecated exports
9
13
 
@@ -0,0 +1,91 @@
1
+ import type { JSONObject } from '@ai-sdk/provider';
2
+ import type { ProviderOptions } from '@ai-sdk/provider-utils';
3
+ import type { ProviderMetadata } from '../types';
4
+ import type { SpeechModelResponseMetadata } from '../types/speech-model-response-metadata';
5
+ import type { Warning } from '../types/warning';
6
+
7
+ /**
8
+ * Event passed to telemetry integrations when speech generation begins.
9
+ */
10
+ export type GenerateSpeechStartEvent = {
11
+ /** Unique identifier for this speech generation call. */
12
+ readonly callId: string;
13
+
14
+ /** Identifies the operation type (`ai.generateSpeech`). */
15
+ readonly operationId: 'ai.generateSpeech';
16
+
17
+ /** The provider identifier. */
18
+ readonly provider: string;
19
+
20
+ /** The specific model identifier. */
21
+ readonly modelId: string;
22
+
23
+ /** The text to convert to speech. */
24
+ readonly text: string;
25
+
26
+ /** The requested voice. */
27
+ readonly voice: string | undefined;
28
+
29
+ /** The requested output format. */
30
+ readonly outputFormat: string | undefined;
31
+
32
+ /** Additional speech instructions. */
33
+ readonly instructions: string | undefined;
34
+
35
+ /** The requested speech speed. */
36
+ readonly speed: number | undefined;
37
+
38
+ /** The requested language. */
39
+ readonly language: string | undefined;
40
+
41
+ /** Maximum number of retries for failed requests. */
42
+ readonly maxRetries: number;
43
+
44
+ /** Additional HTTP headers sent with the request. */
45
+ readonly headers: Record<string, string> | undefined;
46
+
47
+ /** Additional provider-specific options. */
48
+ readonly providerOptions: ProviderOptions;
49
+ };
50
+
51
+ /**
52
+ * Event passed to telemetry integrations when speech generation completes.
53
+ *
54
+ * Raw audio is intentionally not included. Integrations receive only its size,
55
+ * media type, and format.
56
+ */
57
+ export type GenerateSpeechEndEvent = {
58
+ /** Unique identifier for this speech generation call. */
59
+ readonly callId: string;
60
+
61
+ /** Identifies the operation type (`ai.generateSpeech`). */
62
+ readonly operationId: 'ai.generateSpeech';
63
+
64
+ /** The provider identifier. */
65
+ readonly provider: string;
66
+
67
+ /** The specific model identifier. */
68
+ readonly modelId: string;
69
+
70
+ /** The text that was converted to speech. */
71
+ readonly text: string;
72
+
73
+ /** Metadata about the generated audio. */
74
+ readonly audio: {
75
+ readonly byteLength: number;
76
+ readonly mediaType: string;
77
+ readonly format: string;
78
+ };
79
+
80
+ /** Usage reported by the provider, when available. */
81
+ readonly usage: JSONObject | undefined;
82
+
83
+ /** Warnings from the speech model. */
84
+ readonly warnings: Array<Warning>;
85
+
86
+ /** Optional provider-specific metadata. */
87
+ readonly providerMetadata: ProviderMetadata | undefined;
88
+
89
+ /** Response metadata from the provider. */
90
+ readonly response: SpeechModelResponseMetadata;
91
+ };
@@ -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'),