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