@ai-sdk/otel 0.0.0-6b196531-20260710185421

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,1163 @@
1
+ import type { LanguageModelV4Prompt } from '@ai-sdk/provider';
2
+ import type { Context as AISDKContext } from '@ai-sdk/provider-utils';
3
+ import {
4
+ context,
5
+ SpanStatusCode,
6
+ trace,
7
+ type Attributes,
8
+ type AttributeValue,
9
+ type Context as OpenTelemetryContext,
10
+ type Span,
11
+ type Tracer,
12
+ } from '@opentelemetry/api';
13
+ import type {
14
+ EmbeddingModelCallEndEvent,
15
+ EmbedEndEvent,
16
+ EmbedStartEvent,
17
+ EmbeddingModelCallStartEvent,
18
+ GenerateObjectEndEvent,
19
+ GenerateObjectStartEvent,
20
+ GenerateObjectStepEndEvent,
21
+ GenerateObjectStepStartEvent,
22
+ GenerateTextAbortEvent,
23
+ GenerateTextEndEvent,
24
+ GenerateTextStartEvent,
25
+ GenerateTextStepEndEvent,
26
+ GenerateTextStepStartEvent,
27
+ ToolExecutionEndEvent,
28
+ ToolExecutionStartEvent,
29
+ Output,
30
+ RerankingModelCallEndEvent,
31
+ RerankEndEvent,
32
+ RerankStartEvent,
33
+ RerankingModelCallStartEvent,
34
+ InferTelemetryEvent,
35
+ Telemetry,
36
+ TelemetryOptions,
37
+ ToolSet,
38
+ } from 'ai';
39
+ import { assembleOperationName } from './assemble-operation-name';
40
+ import { getBaseTelemetryAttributes } from './get-base-telemetry-attributes';
41
+ import { sanitizeAttributeValue } from './sanitize-attribute-value';
42
+ import { stringifyForTelemetry } from './stringify-for-telemetry';
43
+
44
+ function recordSpanError(span: Span, error: unknown): void {
45
+ if (error instanceof Error) {
46
+ span.recordException({
47
+ name: error.name,
48
+ message: error.message,
49
+ stack: error.stack,
50
+ });
51
+ span.setStatus({
52
+ code: SpanStatusCode.ERROR,
53
+ message: error.message,
54
+ });
55
+ } else {
56
+ span.setStatus({ code: SpanStatusCode.ERROR });
57
+ }
58
+ }
59
+
60
+ function shouldRecord(
61
+ telemetry: TelemetryOptions | undefined,
62
+ ): telemetry is TelemetryOptions {
63
+ return telemetry?.isEnabled !== false;
64
+ }
65
+
66
+ function selectAttributes(
67
+ telemetry: TelemetryOptions | undefined,
68
+ attributes: Record<
69
+ string,
70
+ | AttributeValue
71
+ | { input: () => AttributeValue | undefined }
72
+ | { output: () => AttributeValue | undefined }
73
+ | undefined
74
+ >,
75
+ ): Attributes {
76
+ if (!shouldRecord(telemetry)) {
77
+ return {};
78
+ }
79
+
80
+ const result: Attributes = {};
81
+
82
+ for (const [key, value] of Object.entries(attributes)) {
83
+ if (value == null) continue;
84
+
85
+ if (
86
+ typeof value === 'object' &&
87
+ 'input' in value &&
88
+ typeof value.input === 'function'
89
+ ) {
90
+ if (telemetry?.recordInputs === false) continue;
91
+ const resolved = value.input();
92
+ if (resolved != null) {
93
+ const sanitized = sanitizeAttributeValue(resolved);
94
+ if (sanitized != null) result[key] = sanitized;
95
+ }
96
+ continue;
97
+ }
98
+
99
+ if (
100
+ typeof value === 'object' &&
101
+ 'output' in value &&
102
+ typeof value.output === 'function'
103
+ ) {
104
+ if (telemetry?.recordOutputs === false) continue;
105
+ const resolved = value.output();
106
+ if (resolved != null) {
107
+ const sanitized = sanitizeAttributeValue(resolved);
108
+ if (sanitized != null) result[key] = sanitized;
109
+ }
110
+ continue;
111
+ }
112
+
113
+ const sanitized = sanitizeAttributeValue(value as AttributeValue);
114
+ if (sanitized != null) result[key] = sanitized;
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ interface OtelStepStartEvent<
121
+ TOOLS extends ToolSet = ToolSet,
122
+ RUNTIME_CONTEXT extends AISDKContext = AISDKContext,
123
+ OUTPUT extends Output.Output = Output.Output,
124
+ > extends GenerateTextStepStartEvent<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
125
+ readonly promptMessages?: LanguageModelV4Prompt;
126
+ readonly stepTools?: ReadonlyArray<Record<string, unknown>>;
127
+ readonly stepToolChoice?: unknown;
128
+ }
129
+
130
+ interface CallState {
131
+ operationId: string;
132
+ telemetry: TelemetryOptions | undefined;
133
+ rootSpan: Span | undefined;
134
+ rootContext: OpenTelemetryContext | undefined;
135
+ stepSpan: Span | undefined;
136
+ stepContext: OpenTelemetryContext | undefined;
137
+ embedSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
138
+ rerankSpan: { span: Span; context: OpenTelemetryContext } | undefined;
139
+ toolSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
140
+ baseTelemetryAttributes: Attributes;
141
+ settings: Record<string, unknown>;
142
+ }
143
+
144
+ export class LegacyOpenTelemetry implements Telemetry {
145
+ private readonly callStates = new Map<string, CallState>();
146
+
147
+ /**
148
+ * The tracer to use for the telemetry data.
149
+ */
150
+ private readonly tracer: Tracer;
151
+
152
+ constructor(
153
+ options: {
154
+ tracer?: Tracer;
155
+ } = {},
156
+ ) {
157
+ this.tracer = options.tracer ?? trace.getTracer('ai');
158
+ }
159
+
160
+ private getCallState(callId: string): CallState | undefined {
161
+ return this.callStates.get(callId);
162
+ }
163
+
164
+ private cleanupCallState(callId: string): void {
165
+ this.callStates.delete(callId);
166
+ }
167
+
168
+ executeTool<T>({
169
+ callId,
170
+ toolCallId,
171
+ execute,
172
+ }: {
173
+ callId: string;
174
+ toolCallId: string;
175
+ execute: () => PromiseLike<T>;
176
+ }): PromiseLike<T> {
177
+ const toolSpanEntry = this.getCallState(callId)?.toolSpans.get(toolCallId);
178
+
179
+ if (toolSpanEntry == null) {
180
+ return execute();
181
+ }
182
+
183
+ return context.with(toolSpanEntry.context, execute);
184
+ }
185
+
186
+ /**
187
+ * Runs the provider `doGenerate`/`doStream` call with the active legacy
188
+ * model-call context.
189
+ */
190
+ executeLanguageModelCall<T>({
191
+ callId,
192
+ execute,
193
+ }: {
194
+ callId: string;
195
+ execute: () => PromiseLike<T>;
196
+ }): PromiseLike<T> {
197
+ const stepContext = this.getCallState(callId)?.stepContext;
198
+
199
+ if (stepContext == null) {
200
+ return execute();
201
+ }
202
+
203
+ return context.with(stepContext, execute);
204
+ }
205
+
206
+ onStart(
207
+ event:
208
+ | InferTelemetryEvent<GenerateTextStartEvent>
209
+ | InferTelemetryEvent<GenerateObjectStartEvent>
210
+ | InferTelemetryEvent<EmbedStartEvent>
211
+ | InferTelemetryEvent<RerankStartEvent>,
212
+ ): void {
213
+ if (
214
+ event.operationId === 'ai.embed' ||
215
+ event.operationId === 'ai.embedMany'
216
+ ) {
217
+ this.onEmbedOperationStart(event as InferTelemetryEvent<EmbedStartEvent>);
218
+ return;
219
+ }
220
+
221
+ if (event.operationId === 'ai.rerank') {
222
+ this.onRerankOperationStart(
223
+ event as InferTelemetryEvent<RerankStartEvent>,
224
+ );
225
+ return;
226
+ }
227
+
228
+ if (
229
+ event.operationId === 'ai.generateObject' ||
230
+ event.operationId === 'ai.streamObject'
231
+ ) {
232
+ this.onObjectOperationStart(
233
+ event as InferTelemetryEvent<GenerateObjectStartEvent>,
234
+ );
235
+ return;
236
+ }
237
+
238
+ this.onGenerateStart(event as InferTelemetryEvent<GenerateTextStartEvent>);
239
+ }
240
+
241
+ private onGenerateStart(
242
+ event: InferTelemetryEvent<GenerateTextStartEvent>,
243
+ ): void {
244
+ const telemetry: TelemetryOptions = {
245
+ recordInputs: event.recordInputs,
246
+ recordOutputs: event.recordOutputs,
247
+ functionId: event.functionId,
248
+ };
249
+
250
+ const settings: Record<string, unknown> = {
251
+ maxOutputTokens: event.maxOutputTokens,
252
+ temperature: event.temperature,
253
+ topP: event.topP,
254
+ topK: event.topK,
255
+ presencePenalty: event.presencePenalty,
256
+ frequencyPenalty: event.frequencyPenalty,
257
+ stopSequences: event.stopSequences,
258
+ seed: event.seed,
259
+ maxRetries: event.maxRetries,
260
+ };
261
+
262
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
263
+ model: { provider: event.provider, modelId: event.modelId },
264
+ headers: event.headers,
265
+ settings,
266
+ context: event.runtimeContext as Record<string, unknown> | undefined,
267
+ });
268
+
269
+ const attributes = selectAttributes(telemetry, {
270
+ ...assembleOperationName({
271
+ operationId: event.operationId,
272
+ telemetry,
273
+ }),
274
+ ...baseTelemetryAttributes,
275
+ 'ai.model.provider': event.provider,
276
+ 'ai.model.id': event.modelId,
277
+ 'ai.prompt': {
278
+ input: () =>
279
+ JSON.stringify({
280
+ system: event.instructions,
281
+ messages: event.messages,
282
+ }),
283
+ },
284
+ });
285
+
286
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
287
+ const rootContext = trace.setSpan(context.active(), rootSpan);
288
+
289
+ this.callStates.set(event.callId, {
290
+ operationId: event.operationId,
291
+ telemetry,
292
+ rootSpan,
293
+ rootContext,
294
+ stepSpan: undefined,
295
+ stepContext: undefined,
296
+ embedSpans: new Map(),
297
+ rerankSpan: undefined,
298
+ toolSpans: new Map(),
299
+ baseTelemetryAttributes,
300
+ settings,
301
+ });
302
+ }
303
+
304
+ private onObjectOperationStart(
305
+ event: InferTelemetryEvent<GenerateObjectStartEvent>,
306
+ ): void {
307
+ const telemetry: TelemetryOptions = {
308
+ recordInputs: event.recordInputs,
309
+ recordOutputs: event.recordOutputs,
310
+ functionId: event.functionId,
311
+ };
312
+
313
+ const settings: Record<string, unknown> = {
314
+ maxOutputTokens: event.maxOutputTokens,
315
+ temperature: event.temperature,
316
+ topP: event.topP,
317
+ topK: event.topK,
318
+ presencePenalty: event.presencePenalty,
319
+ frequencyPenalty: event.frequencyPenalty,
320
+ seed: event.seed,
321
+ maxRetries: event.maxRetries,
322
+ };
323
+
324
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
325
+ model: { provider: event.provider, modelId: event.modelId },
326
+ headers: event.headers,
327
+ settings,
328
+ context: undefined,
329
+ });
330
+
331
+ const attributes = selectAttributes(telemetry, {
332
+ ...assembleOperationName({
333
+ operationId: event.operationId,
334
+ telemetry,
335
+ }),
336
+ ...baseTelemetryAttributes,
337
+ 'ai.prompt': {
338
+ input: () =>
339
+ JSON.stringify({
340
+ system: event.system,
341
+ prompt: event.prompt,
342
+ messages: event.messages,
343
+ }),
344
+ },
345
+ 'ai.schema': event.schema
346
+ ? { input: () => JSON.stringify(event.schema) }
347
+ : undefined,
348
+ 'ai.schema.name': event.schemaName,
349
+ 'ai.schema.description': event.schemaDescription,
350
+ 'ai.settings.output': event.output,
351
+ });
352
+
353
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
354
+ const rootContext = trace.setSpan(context.active(), rootSpan);
355
+
356
+ this.callStates.set(event.callId, {
357
+ operationId: event.operationId,
358
+ telemetry,
359
+ rootSpan,
360
+ rootContext,
361
+ stepSpan: undefined,
362
+ stepContext: undefined,
363
+ embedSpans: new Map(),
364
+ rerankSpan: undefined,
365
+ toolSpans: new Map(),
366
+ baseTelemetryAttributes,
367
+ settings,
368
+ });
369
+ }
370
+
371
+ /** @deprecated */
372
+ onObjectStepStart(event: GenerateObjectStepStartEvent): void {
373
+ const state = this.getCallState(event.callId);
374
+ if (!state?.rootSpan || !state.rootContext) return;
375
+
376
+ const { telemetry } = state;
377
+
378
+ const stepOperationId =
379
+ state.operationId === 'ai.streamObject'
380
+ ? 'ai.streamObject.doStream'
381
+ : 'ai.generateObject.doGenerate';
382
+
383
+ const attributes = selectAttributes(telemetry, {
384
+ ...assembleOperationName({
385
+ operationId: stepOperationId,
386
+ telemetry,
387
+ }),
388
+ ...state.baseTelemetryAttributes,
389
+ 'ai.prompt.messages': {
390
+ input: () =>
391
+ event.promptMessages
392
+ ? stringifyForTelemetry(event.promptMessages)
393
+ : undefined,
394
+ },
395
+
396
+ 'gen_ai.system': event.provider,
397
+ 'gen_ai.request.model': event.modelId,
398
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
399
+ | number
400
+ | undefined,
401
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
402
+ | number
403
+ | undefined,
404
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
405
+ | number
406
+ | undefined,
407
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
408
+ | number
409
+ | undefined,
410
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
411
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
412
+ });
413
+
414
+ state.stepSpan = this.tracer.startSpan(
415
+ stepOperationId,
416
+ { attributes },
417
+ state.rootContext,
418
+ );
419
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
420
+ }
421
+
422
+ /** @deprecated */
423
+ onObjectStepEnd(event: GenerateObjectStepEndEvent): void {
424
+ const state = this.getCallState(event.callId);
425
+ if (!state?.stepSpan) return;
426
+
427
+ const { telemetry } = state;
428
+
429
+ state.stepSpan.setAttributes(
430
+ selectAttributes(telemetry, {
431
+ 'ai.response.finishReason': event.finishReason,
432
+ 'ai.response.object': {
433
+ output: () => {
434
+ try {
435
+ return JSON.stringify(JSON.parse(event.objectText));
436
+ } catch {
437
+ return event.objectText;
438
+ }
439
+ },
440
+ },
441
+ 'ai.response.id': event.response.id,
442
+ 'ai.response.model': event.response.modelId,
443
+ 'ai.response.timestamp': event.response.timestamp.toISOString(),
444
+ 'ai.response.providerMetadata': event.providerMetadata
445
+ ? JSON.stringify(event.providerMetadata)
446
+ : undefined,
447
+
448
+ 'ai.usage.inputTokens': event.usage.inputTokens,
449
+ 'ai.usage.outputTokens': event.usage.outputTokens,
450
+ 'ai.usage.totalTokens': event.usage.totalTokens,
451
+ 'ai.usage.reasoningTokens':
452
+ event.usage.outputTokenDetails?.reasoningTokens,
453
+ 'ai.usage.cachedInputTokens':
454
+ event.usage.inputTokenDetails?.cacheReadTokens,
455
+
456
+ 'gen_ai.response.finish_reasons': [event.finishReason],
457
+ 'gen_ai.response.id': event.response.id,
458
+ 'gen_ai.response.model': event.response.modelId,
459
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
460
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
461
+ }),
462
+ );
463
+
464
+ if (event.msToFirstChunk != null) {
465
+ state.stepSpan.addEvent('ai.stream.firstChunk', {
466
+ 'ai.stream.msToFirstChunk': event.msToFirstChunk,
467
+ });
468
+ state.stepSpan.setAttributes({
469
+ 'ai.stream.msToFirstChunk': event.msToFirstChunk,
470
+ });
471
+ }
472
+
473
+ state.stepSpan.end();
474
+ state.stepSpan = undefined;
475
+ state.stepContext = undefined;
476
+ }
477
+
478
+ private onEmbedOperationStart(
479
+ event: InferTelemetryEvent<EmbedStartEvent>,
480
+ ): void {
481
+ const telemetry: TelemetryOptions = {
482
+ recordInputs: event.recordInputs,
483
+ recordOutputs: event.recordOutputs,
484
+ functionId: event.functionId,
485
+ };
486
+
487
+ const settings: Record<string, unknown> = {
488
+ maxRetries: event.maxRetries,
489
+ };
490
+
491
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
492
+ model: { provider: event.provider, modelId: event.modelId },
493
+ headers: event.headers,
494
+ settings,
495
+ context: undefined,
496
+ });
497
+
498
+ const value = event.value;
499
+ const isMany = event.operationId === 'ai.embedMany';
500
+
501
+ const attributes = selectAttributes(telemetry, {
502
+ ...assembleOperationName({
503
+ operationId: event.operationId,
504
+ telemetry,
505
+ }),
506
+ ...baseTelemetryAttributes,
507
+ ...(isMany
508
+ ? {
509
+ 'ai.values': {
510
+ input: () => (value as string[]).map(v => JSON.stringify(v)),
511
+ },
512
+ }
513
+ : {
514
+ 'ai.value': {
515
+ input: () => JSON.stringify(value),
516
+ },
517
+ }),
518
+ });
519
+
520
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
521
+ const rootContext = trace.setSpan(context.active(), rootSpan);
522
+
523
+ this.callStates.set(event.callId, {
524
+ operationId: event.operationId,
525
+ telemetry,
526
+ rootSpan,
527
+ rootContext,
528
+ stepSpan: undefined,
529
+ stepContext: undefined,
530
+ embedSpans: new Map(),
531
+ rerankSpan: undefined,
532
+ toolSpans: new Map(),
533
+ baseTelemetryAttributes,
534
+ settings,
535
+ });
536
+ }
537
+
538
+ onStepStart(event: OtelStepStartEvent): void {
539
+ const state = this.getCallState(event.callId);
540
+ if (!state?.rootSpan || !state.rootContext) return;
541
+
542
+ const { telemetry } = state;
543
+
544
+ const stepOperationId =
545
+ state.operationId === 'ai.streamText'
546
+ ? 'ai.streamText.doStream'
547
+ : 'ai.generateText.doGenerate';
548
+
549
+ const attributes = selectAttributes(telemetry, {
550
+ ...assembleOperationName({
551
+ operationId: stepOperationId,
552
+ telemetry,
553
+ }),
554
+ ...state.baseTelemetryAttributes,
555
+ 'ai.model.provider': event.provider,
556
+ 'ai.model.id': event.modelId,
557
+
558
+ 'ai.prompt.messages': {
559
+ input: () =>
560
+ event.promptMessages
561
+ ? stringifyForTelemetry(event.promptMessages)
562
+ : undefined,
563
+ },
564
+ 'ai.prompt.tools': {
565
+ input: () => event.stepTools?.map(tool => JSON.stringify(tool)),
566
+ },
567
+ 'ai.prompt.toolChoice': {
568
+ input: () =>
569
+ event.stepToolChoice != null
570
+ ? JSON.stringify(event.stepToolChoice)
571
+ : undefined,
572
+ },
573
+
574
+ 'gen_ai.system': event.provider,
575
+ 'gen_ai.request.model': event.modelId,
576
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
577
+ | number
578
+ | undefined,
579
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
580
+ | number
581
+ | undefined,
582
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
583
+ | number
584
+ | undefined,
585
+ 'gen_ai.request.stop_sequences': state.settings.stopSequences as
586
+ | string[]
587
+ | undefined,
588
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
589
+ | number
590
+ | undefined,
591
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
592
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
593
+ });
594
+
595
+ state.stepSpan = this.tracer.startSpan(
596
+ stepOperationId,
597
+ { attributes },
598
+ state.rootContext,
599
+ );
600
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
601
+ }
602
+
603
+ onToolExecutionStart(event: ToolExecutionStartEvent<ToolSet>): void {
604
+ const state = this.getCallState(event.callId);
605
+ if (!state?.stepContext) return;
606
+
607
+ const { telemetry } = state;
608
+ const { toolCall } = event;
609
+
610
+ const attributes = selectAttributes(telemetry, {
611
+ ...assembleOperationName({
612
+ operationId: 'ai.toolCall',
613
+ telemetry,
614
+ }),
615
+ 'ai.toolCall.name': toolCall.toolName,
616
+ 'ai.toolCall.id': toolCall.toolCallId,
617
+ 'ai.toolCall.args': {
618
+ output: () => JSON.stringify(toolCall.input),
619
+ },
620
+ });
621
+
622
+ const toolSpan = this.tracer.startSpan(
623
+ 'ai.toolCall',
624
+ { attributes },
625
+ state.stepContext,
626
+ );
627
+ const toolContext = trace.setSpan(state.stepContext, toolSpan);
628
+
629
+ state.toolSpans.set(toolCall.toolCallId, {
630
+ span: toolSpan,
631
+ context: toolContext,
632
+ });
633
+ }
634
+
635
+ onToolExecutionEnd(event: ToolExecutionEndEvent<ToolSet>): void {
636
+ const state = this.getCallState(event.callId);
637
+ if (!state) return;
638
+
639
+ const toolSpanEntry = state.toolSpans.get(event.toolCall.toolCallId);
640
+ if (!toolSpanEntry) return;
641
+
642
+ const { span } = toolSpanEntry;
643
+ const { telemetry } = state;
644
+
645
+ const { toolOutput } = event;
646
+ if (toolOutput.type === 'tool-result') {
647
+ try {
648
+ span.setAttributes(
649
+ selectAttributes(telemetry, {
650
+ 'ai.toolCall.result': {
651
+ output: () => JSON.stringify(toolOutput.output),
652
+ },
653
+ }),
654
+ );
655
+ } catch {
656
+ // JSON.stringify might fail for non-serializable results
657
+ }
658
+ } else {
659
+ recordSpanError(span, toolOutput.error);
660
+ }
661
+
662
+ span.end();
663
+ state.toolSpans.delete(event.toolCall.toolCallId);
664
+ }
665
+
666
+ onStepEnd(event: GenerateTextStepEndEvent<ToolSet>): void {
667
+ const state = this.getCallState(event.callId);
668
+ if (!state?.stepSpan) return;
669
+
670
+ const { telemetry } = state;
671
+ const isStreamText = state.operationId === 'ai.streamText';
672
+
673
+ state.stepSpan.setAttributes(
674
+ selectAttributes(telemetry, {
675
+ 'ai.response.finishReason': event.finishReason,
676
+ 'ai.response.text': {
677
+ output: () => event.text ?? undefined,
678
+ },
679
+ 'ai.response.reasoning': {
680
+ output: () =>
681
+ event.reasoning.length > 0
682
+ ? event.reasoning
683
+ .filter(part => 'text' in part)
684
+ .map(part => part.text)
685
+ .join('\n')
686
+ : undefined,
687
+ },
688
+ 'ai.response.toolCalls': {
689
+ output: () =>
690
+ event.toolCalls.length > 0
691
+ ? JSON.stringify(
692
+ event.toolCalls.map(toolCall => ({
693
+ toolCallId: toolCall.toolCallId,
694
+ toolName: toolCall.toolName,
695
+ input: toolCall.input,
696
+ })),
697
+ )
698
+ : undefined,
699
+ },
700
+ 'ai.response.files': {
701
+ output: () =>
702
+ event.files.length > 0
703
+ ? JSON.stringify(
704
+ event.files.map(file => ({
705
+ type: 'file',
706
+ mediaType: file.mediaType,
707
+ data: file.base64,
708
+ })),
709
+ )
710
+ : undefined,
711
+ },
712
+ 'ai.response.id': event.response.id,
713
+ 'ai.response.model': event.response.modelId,
714
+ 'ai.response.timestamp': event.response.timestamp.toISOString(),
715
+ 'ai.response.providerMetadata': event.providerMetadata
716
+ ? JSON.stringify(event.providerMetadata)
717
+ : undefined,
718
+ 'ai.response.msToFirstChunk': isStreamText
719
+ ? event.performance.timeToFirstOutputMs
720
+ : undefined,
721
+ 'ai.response.msToFinish': isStreamText
722
+ ? event.performance.responseTimeMs
723
+ : undefined,
724
+ 'ai.response.avgOutputTokensPerSecond': isStreamText
725
+ ? event.performance.effectiveOutputTokensPerSecond
726
+ : undefined,
727
+
728
+ 'ai.usage.inputTokens': event.usage.inputTokens,
729
+ 'ai.usage.outputTokens': event.usage.outputTokens,
730
+ 'ai.usage.totalTokens': event.usage.totalTokens,
731
+ 'ai.usage.reasoningTokens':
732
+ event.usage.outputTokenDetails?.reasoningTokens,
733
+ 'ai.usage.cachedInputTokens':
734
+ event.usage.inputTokenDetails?.cacheReadTokens,
735
+ 'ai.usage.inputTokenDetails.noCacheTokens':
736
+ event.usage.inputTokenDetails?.noCacheTokens,
737
+ 'ai.usage.inputTokenDetails.cacheReadTokens':
738
+ event.usage.inputTokenDetails?.cacheReadTokens,
739
+ 'ai.usage.inputTokenDetails.cacheWriteTokens':
740
+ event.usage.inputTokenDetails?.cacheWriteTokens,
741
+ 'ai.usage.outputTokenDetails.textTokens':
742
+ event.usage.outputTokenDetails?.textTokens,
743
+ 'ai.usage.outputTokenDetails.reasoningTokens':
744
+ event.usage.outputTokenDetails?.reasoningTokens,
745
+
746
+ 'gen_ai.response.finish_reasons': [event.finishReason],
747
+ 'gen_ai.response.id': event.response.id,
748
+ 'gen_ai.response.model': event.response.modelId,
749
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
750
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
751
+ }),
752
+ );
753
+
754
+ if (isStreamText && event.performance.timeToFirstOutputMs != null) {
755
+ state.stepSpan.addEvent('ai.stream.firstChunk', {
756
+ 'ai.response.msToFirstChunk': event.performance.timeToFirstOutputMs,
757
+ });
758
+ }
759
+
760
+ if (isStreamText) {
761
+ state.stepSpan.addEvent('ai.stream.finish', {
762
+ 'ai.response.msToFinish': event.performance.responseTimeMs,
763
+ 'ai.response.avgOutputTokensPerSecond':
764
+ event.performance.effectiveOutputTokensPerSecond,
765
+ });
766
+ }
767
+
768
+ state.stepSpan.end();
769
+ state.stepSpan = undefined;
770
+ state.stepContext = undefined;
771
+ }
772
+
773
+ /** @deprecated Use `onStepEnd` instead. */
774
+ onStepFinish(event: GenerateTextStepEndEvent<ToolSet>): void {
775
+ this.onStepEnd(event);
776
+ }
777
+
778
+ onEnd(
779
+ event:
780
+ | GenerateTextEndEvent<ToolSet>
781
+ | GenerateObjectEndEvent<unknown>
782
+ | EmbedEndEvent
783
+ | RerankEndEvent,
784
+ ): void {
785
+ const state = this.getCallState(event.callId);
786
+ if (!state?.rootSpan) return;
787
+
788
+ if (
789
+ state.operationId === 'ai.embed' ||
790
+ state.operationId === 'ai.embedMany'
791
+ ) {
792
+ this.onEmbedOperationEnd(event as EmbedEndEvent);
793
+ return;
794
+ }
795
+
796
+ if (state.operationId === 'ai.rerank') {
797
+ this.onRerankOperationEnd(event as RerankEndEvent);
798
+ return;
799
+ }
800
+
801
+ if (
802
+ state.operationId === 'ai.generateObject' ||
803
+ state.operationId === 'ai.streamObject'
804
+ ) {
805
+ this.onObjectOperationEnd(event as GenerateObjectEndEvent<unknown>);
806
+ return;
807
+ }
808
+
809
+ this.onGenerateEnd(event as GenerateTextEndEvent<ToolSet>);
810
+ }
811
+
812
+ private onGenerateEnd(event: GenerateTextEndEvent<ToolSet>): void {
813
+ const state = this.getCallState(event.callId);
814
+ if (!state?.rootSpan) return;
815
+
816
+ const { telemetry } = state;
817
+
818
+ state.rootSpan.setAttributes(
819
+ selectAttributes(telemetry, {
820
+ 'ai.response.finishReason': event.finishReason,
821
+ 'ai.response.text': {
822
+ output: () => event.text ?? undefined,
823
+ },
824
+ 'ai.response.reasoning': {
825
+ output: () =>
826
+ event.finalStep.reasoning.length > 0
827
+ ? event.finalStep.reasoning
828
+ .filter(part => 'text' in part)
829
+ .map(part => part.text)
830
+ .join('\n')
831
+ : undefined,
832
+ },
833
+ 'ai.response.toolCalls': {
834
+ output: () =>
835
+ event.toolCalls.length > 0
836
+ ? JSON.stringify(
837
+ event.toolCalls.map(toolCall => ({
838
+ toolCallId: toolCall.toolCallId,
839
+ toolName: toolCall.toolName,
840
+ input: toolCall.input,
841
+ })),
842
+ )
843
+ : undefined,
844
+ },
845
+ 'ai.response.files': {
846
+ output: () =>
847
+ event.files.length > 0
848
+ ? JSON.stringify(
849
+ event.files.map(file => ({
850
+ type: 'file',
851
+ mediaType: file.mediaType,
852
+ data: file.base64,
853
+ })),
854
+ )
855
+ : undefined,
856
+ },
857
+ 'ai.response.providerMetadata': event.finalStep.providerMetadata
858
+ ? JSON.stringify(event.finalStep.providerMetadata)
859
+ : undefined,
860
+
861
+ 'ai.usage.inputTokens': event.usage.inputTokens,
862
+ 'ai.usage.outputTokens': event.usage.outputTokens,
863
+ 'ai.usage.totalTokens': event.usage.totalTokens,
864
+ 'ai.usage.reasoningTokens':
865
+ event.usage.outputTokenDetails?.reasoningTokens,
866
+ 'ai.usage.cachedInputTokens':
867
+ event.usage.inputTokenDetails?.cacheReadTokens,
868
+ 'ai.usage.inputTokenDetails.noCacheTokens':
869
+ event.usage.inputTokenDetails?.noCacheTokens,
870
+ 'ai.usage.inputTokenDetails.cacheReadTokens':
871
+ event.usage.inputTokenDetails?.cacheReadTokens,
872
+ 'ai.usage.inputTokenDetails.cacheWriteTokens':
873
+ event.usage.inputTokenDetails?.cacheWriteTokens,
874
+ 'ai.usage.outputTokenDetails.textTokens':
875
+ event.usage.outputTokenDetails?.textTokens,
876
+ 'ai.usage.outputTokenDetails.reasoningTokens':
877
+ event.usage.outputTokenDetails?.reasoningTokens,
878
+ }),
879
+ );
880
+
881
+ state.rootSpan.end();
882
+ this.cleanupCallState(event.callId);
883
+ }
884
+
885
+ private onObjectOperationEnd(event: GenerateObjectEndEvent<unknown>): void {
886
+ const state = this.getCallState(event.callId);
887
+ if (!state?.rootSpan) return;
888
+
889
+ const { telemetry } = state;
890
+
891
+ state.rootSpan.setAttributes(
892
+ selectAttributes(telemetry, {
893
+ 'ai.response.finishReason': event.finishReason,
894
+ 'ai.response.object': {
895
+ output: () =>
896
+ event.object != null ? JSON.stringify(event.object) : undefined,
897
+ },
898
+ 'ai.response.providerMetadata': event.providerMetadata
899
+ ? JSON.stringify(event.providerMetadata)
900
+ : undefined,
901
+
902
+ 'ai.usage.inputTokens': event.usage.inputTokens,
903
+ 'ai.usage.outputTokens': event.usage.outputTokens,
904
+ 'ai.usage.totalTokens': event.usage.totalTokens,
905
+ 'ai.usage.reasoningTokens':
906
+ event.usage.outputTokenDetails?.reasoningTokens,
907
+ 'ai.usage.cachedInputTokens':
908
+ event.usage.inputTokenDetails?.cacheReadTokens,
909
+ }),
910
+ );
911
+
912
+ state.rootSpan.end();
913
+ this.cleanupCallState(event.callId);
914
+ }
915
+
916
+ private onEmbedOperationEnd(event: EmbedEndEvent): void {
917
+ const state = this.getCallState(event.callId);
918
+ if (!state?.rootSpan) return;
919
+
920
+ const { telemetry } = state;
921
+ const isMany = state.operationId === 'ai.embedMany';
922
+
923
+ state.rootSpan.setAttributes(
924
+ selectAttributes(telemetry, {
925
+ ...(isMany
926
+ ? {
927
+ 'ai.embeddings': {
928
+ output: () =>
929
+ (event.embedding as number[][]).map(e => JSON.stringify(e)),
930
+ },
931
+ }
932
+ : {
933
+ 'ai.embedding': {
934
+ output: () => JSON.stringify(event.embedding),
935
+ },
936
+ }),
937
+ 'ai.usage.tokens': event.usage.tokens,
938
+ }),
939
+ );
940
+
941
+ state.rootSpan.end();
942
+ this.cleanupCallState(event.callId);
943
+ }
944
+
945
+ onEmbedStart(event: EmbeddingModelCallStartEvent): void {
946
+ const state = this.getCallState(event.callId);
947
+ if (!state?.rootSpan || !state.rootContext) return;
948
+
949
+ const { telemetry } = state;
950
+
951
+ const attributes = selectAttributes(telemetry, {
952
+ ...assembleOperationName({
953
+ operationId: event.operationId,
954
+ telemetry,
955
+ }),
956
+ ...state.baseTelemetryAttributes,
957
+ 'ai.values': {
958
+ input: () => event.values.map(v => JSON.stringify(v)),
959
+ },
960
+ });
961
+
962
+ const embedSpan = this.tracer.startSpan(
963
+ event.operationId,
964
+ { attributes },
965
+ state.rootContext,
966
+ );
967
+ const embedContext = trace.setSpan(state.rootContext, embedSpan);
968
+
969
+ state.embedSpans.set(event.embedCallId, {
970
+ span: embedSpan,
971
+ context: embedContext,
972
+ });
973
+ }
974
+
975
+ onEmbedEnd(event: EmbeddingModelCallEndEvent): void {
976
+ const state = this.getCallState(event.callId);
977
+ if (!state) return;
978
+
979
+ const embedSpanEntry = state.embedSpans.get(event.embedCallId);
980
+ if (!embedSpanEntry) return;
981
+
982
+ const { span } = embedSpanEntry;
983
+ const { telemetry } = state;
984
+
985
+ span.setAttributes(
986
+ selectAttributes(telemetry, {
987
+ 'ai.embeddings': {
988
+ output: () =>
989
+ event.embeddings.map(embedding => JSON.stringify(embedding)),
990
+ },
991
+ 'ai.usage.tokens': event.usage.tokens,
992
+ }),
993
+ );
994
+
995
+ span.end();
996
+ state.embedSpans.delete(event.embedCallId);
997
+ }
998
+
999
+ private onRerankOperationStart(
1000
+ event: InferTelemetryEvent<RerankStartEvent>,
1001
+ ): void {
1002
+ const telemetry: TelemetryOptions = {
1003
+ recordInputs: event.recordInputs,
1004
+ recordOutputs: event.recordOutputs,
1005
+ functionId: event.functionId,
1006
+ };
1007
+
1008
+ const settings: Record<string, unknown> = {
1009
+ maxRetries: event.maxRetries,
1010
+ };
1011
+
1012
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
1013
+ model: { provider: event.provider, modelId: event.modelId },
1014
+ headers: event.headers,
1015
+ settings,
1016
+ context: undefined,
1017
+ });
1018
+
1019
+ const attributes = selectAttributes(telemetry, {
1020
+ ...assembleOperationName({
1021
+ operationId: event.operationId,
1022
+ telemetry,
1023
+ }),
1024
+ ...baseTelemetryAttributes,
1025
+ 'ai.documents': {
1026
+ input: () => event.documents.map(d => JSON.stringify(d)),
1027
+ },
1028
+ });
1029
+
1030
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
1031
+ const rootContext = trace.setSpan(context.active(), rootSpan);
1032
+
1033
+ this.callStates.set(event.callId, {
1034
+ operationId: event.operationId,
1035
+ telemetry,
1036
+ rootSpan,
1037
+ rootContext,
1038
+ stepSpan: undefined,
1039
+ stepContext: undefined,
1040
+ embedSpans: new Map(),
1041
+ rerankSpan: undefined,
1042
+ toolSpans: new Map(),
1043
+ baseTelemetryAttributes,
1044
+ settings,
1045
+ });
1046
+ }
1047
+
1048
+ private onRerankOperationEnd(event: RerankEndEvent): void {
1049
+ const state = this.getCallState(event.callId);
1050
+ if (!state?.rootSpan) return;
1051
+
1052
+ state.rootSpan.end();
1053
+ this.cleanupCallState(event.callId);
1054
+ }
1055
+
1056
+ onRerankStart(event: RerankingModelCallStartEvent): void {
1057
+ const state = this.getCallState(event.callId);
1058
+ if (!state?.rootSpan || !state.rootContext) return;
1059
+
1060
+ const { telemetry } = state;
1061
+
1062
+ const attributes = selectAttributes(telemetry, {
1063
+ ...assembleOperationName({
1064
+ operationId: event.operationId,
1065
+ telemetry,
1066
+ }),
1067
+ ...state.baseTelemetryAttributes,
1068
+ 'ai.documents': {
1069
+ input: () => event.documents.map(d => JSON.stringify(d)),
1070
+ },
1071
+ });
1072
+
1073
+ const rerankSpan = this.tracer.startSpan(
1074
+ event.operationId,
1075
+ { attributes },
1076
+ state.rootContext,
1077
+ );
1078
+ const rerankContext = trace.setSpan(state.rootContext, rerankSpan);
1079
+
1080
+ state.rerankSpan = { span: rerankSpan, context: rerankContext };
1081
+ }
1082
+
1083
+ onRerankEnd(event: RerankingModelCallEndEvent): void {
1084
+ const state = this.getCallState(event.callId);
1085
+ if (!state?.rerankSpan) return;
1086
+
1087
+ const { span } = state.rerankSpan;
1088
+ const { telemetry } = state;
1089
+
1090
+ span.setAttributes(
1091
+ selectAttributes(telemetry, {
1092
+ 'ai.ranking.type': event.documentsType,
1093
+ 'ai.ranking': {
1094
+ output: () => event.ranking.map(r => JSON.stringify(r)),
1095
+ },
1096
+ }),
1097
+ );
1098
+
1099
+ span.end();
1100
+ state.rerankSpan = undefined;
1101
+ }
1102
+
1103
+ onAbort(event: GenerateTextAbortEvent<ToolSet>): void {
1104
+ const state = this.getCallState(event.callId);
1105
+ if (!state?.rootSpan) return;
1106
+
1107
+ for (const { span: toolSpan } of state.toolSpans.values()) {
1108
+ toolSpan.end();
1109
+ }
1110
+ state.toolSpans.clear();
1111
+
1112
+ if (state.stepSpan) {
1113
+ state.stepSpan.end();
1114
+ state.stepSpan = undefined;
1115
+ state.stepContext = undefined;
1116
+ }
1117
+
1118
+ for (const { span: embedSpan } of state.embedSpans.values()) {
1119
+ embedSpan.end();
1120
+ }
1121
+ state.embedSpans.clear();
1122
+
1123
+ if (state.rerankSpan) {
1124
+ state.rerankSpan.span.end();
1125
+ state.rerankSpan = undefined;
1126
+ }
1127
+
1128
+ state.rootSpan.end();
1129
+ this.cleanupCallState(event.callId);
1130
+ }
1131
+
1132
+ onError(error: unknown): void {
1133
+ const event = error as { callId?: string; error?: unknown };
1134
+ if (!event?.callId) return;
1135
+
1136
+ const state = this.getCallState(event.callId);
1137
+ if (!state?.rootSpan) return;
1138
+
1139
+ const actualError = event.error ?? error;
1140
+
1141
+ if (state.stepSpan) {
1142
+ recordSpanError(state.stepSpan, actualError);
1143
+ state.stepSpan.end();
1144
+ }
1145
+
1146
+ for (const { span: embedSpan } of state.embedSpans.values()) {
1147
+ recordSpanError(embedSpan, actualError);
1148
+ embedSpan.end();
1149
+ }
1150
+ state.embedSpans.clear();
1151
+
1152
+ if (state.rerankSpan) {
1153
+ recordSpanError(state.rerankSpan.span, actualError);
1154
+ state.rerankSpan.span.end();
1155
+ state.rerankSpan = undefined;
1156
+ }
1157
+
1158
+ recordSpanError(state.rootSpan, actualError);
1159
+
1160
+ state.rootSpan.end();
1161
+ this.cleanupCallState(event.callId);
1162
+ }
1163
+ }