@ai-sdk/otel 1.0.0-beta.6 → 1.0.0-beta.60

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