@ai-sdk/otel 1.0.0-beta.4 → 1.0.0-beta.40

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,924 @@
1
+ import { LanguageModelV4Prompt } from '@ai-sdk/provider';
2
+ import type { Context as AISDKContext } from '@ai-sdk/provider-utils';
3
+ import {
4
+ Attributes,
5
+ AttributeValue,
6
+ context,
7
+ Context as OpenTelemetryContext,
8
+ Span,
9
+ SpanKind,
10
+ SpanStatusCode,
11
+ trace,
12
+ Tracer,
13
+ } from '@opentelemetry/api';
14
+ import type {
15
+ EmbedFinishEvent,
16
+ EmbedOnFinishEvent,
17
+ EmbedOnStartEvent,
18
+ EmbedStartEvent,
19
+ ObjectOnFinishEvent,
20
+ ObjectOnStartEvent,
21
+ ObjectOnStepFinishEvent,
22
+ ObjectOnStepStartEvent,
23
+ OnChunkEvent,
24
+ OnFinishEvent,
25
+ OnStartEvent,
26
+ OnStepFinishEvent,
27
+ OnStepStartEvent,
28
+ OnToolCallFinishEvent,
29
+ OnToolCallStartEvent,
30
+ OutputInterface as Output,
31
+ RerankFinishEvent,
32
+ RerankOnFinishEvent,
33
+ RerankOnStartEvent,
34
+ RerankStartEvent,
35
+ TelemetryIntegration,
36
+ TelemetrySettings,
37
+ ToolSet,
38
+ } from 'ai';
39
+ import {
40
+ formatInputMessages,
41
+ formatModelMessages,
42
+ formatObjectOutputMessages,
43
+ formatOutputMessages,
44
+ formatSystemInstructions,
45
+ mapOperationName,
46
+ mapProviderName,
47
+ } from './gen-ai-format-messages';
48
+
49
+ function recordSpanError(span: Span, error: unknown): void {
50
+ if (error instanceof Error) {
51
+ span.recordException({
52
+ name: error.name,
53
+ message: error.message,
54
+ stack: error.stack,
55
+ });
56
+ span.setStatus({
57
+ code: SpanStatusCode.ERROR,
58
+ message: error.message,
59
+ });
60
+ } else {
61
+ span.setStatus({ code: SpanStatusCode.ERROR });
62
+ }
63
+ }
64
+
65
+ function shouldRecord(
66
+ telemetry: TelemetrySettings | undefined,
67
+ ): telemetry is TelemetrySettings {
68
+ return telemetry?.isEnabled === true;
69
+ }
70
+
71
+ function selectAttributes(
72
+ telemetry: TelemetrySettings | undefined,
73
+ attributes: Record<
74
+ string,
75
+ | AttributeValue
76
+ | { input: () => AttributeValue | undefined }
77
+ | { output: () => AttributeValue | undefined }
78
+ | undefined
79
+ >,
80
+ ): Attributes {
81
+ if (!shouldRecord(telemetry)) {
82
+ return {};
83
+ }
84
+
85
+ const result: Attributes = {};
86
+
87
+ for (const [key, value] of Object.entries(attributes)) {
88
+ if (value == null) continue;
89
+
90
+ if (
91
+ typeof value === 'object' &&
92
+ 'input' in value &&
93
+ typeof value.input === 'function'
94
+ ) {
95
+ if (telemetry?.recordInputs === false) continue;
96
+ const resolved = value.input();
97
+ if (resolved != null) result[key] = resolved;
98
+ continue;
99
+ }
100
+
101
+ if (
102
+ typeof value === 'object' &&
103
+ 'output' in value &&
104
+ typeof value.output === 'function'
105
+ ) {
106
+ if (telemetry?.recordOutputs === false) continue;
107
+ const resolved = value.output();
108
+ if (resolved != null) result[key] = resolved;
109
+ continue;
110
+ }
111
+
112
+ result[key] = value as AttributeValue;
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ interface OtelStepStartEvent<
119
+ TOOLS extends ToolSet = ToolSet,
120
+ USER_CONTEXT extends AISDKContext = AISDKContext,
121
+ OUTPUT extends Output = Output,
122
+ > extends OnStepStartEvent<TOOLS, USER_CONTEXT, OUTPUT> {
123
+ readonly promptMessages?: LanguageModelV4Prompt;
124
+ readonly stepTools?: ReadonlyArray<Record<string, unknown>>;
125
+ readonly stepToolChoice?: unknown;
126
+ }
127
+
128
+ interface CallState {
129
+ operationId: string;
130
+ telemetry: TelemetrySettings | undefined;
131
+ rootSpan: Span | undefined;
132
+ rootContext: OpenTelemetryContext | undefined;
133
+ stepSpan: Span | undefined;
134
+ stepContext: OpenTelemetryContext | undefined;
135
+ embedSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
136
+ rerankSpan: { span: Span; context: OpenTelemetryContext } | undefined;
137
+ toolSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
138
+ settings: Record<string, unknown>;
139
+ provider: string;
140
+ modelId: string;
141
+ }
142
+
143
+ export class GenAIOpenTelemetryIntegration implements TelemetryIntegration {
144
+ private readonly callStates = new Map<string, CallState>();
145
+
146
+ private readonly tracer: Tracer;
147
+
148
+ constructor(
149
+ options: {
150
+ tracer?: Tracer;
151
+ } = {},
152
+ ) {
153
+ this.tracer = options.tracer ?? trace.getTracer('gen_ai');
154
+ }
155
+
156
+ private getCallState(callId: string): CallState | undefined {
157
+ return this.callStates.get(callId);
158
+ }
159
+
160
+ private cleanupCallState(callId: string): void {
161
+ this.callStates.delete(callId);
162
+ }
163
+
164
+ executeTool<T>({
165
+ callId,
166
+ toolCallId,
167
+ execute,
168
+ }: {
169
+ callId: string;
170
+ toolCallId: string;
171
+ execute: () => PromiseLike<T>;
172
+ }): PromiseLike<T> {
173
+ const toolSpanEntry = this.getCallState(callId)?.toolSpans.get(toolCallId);
174
+
175
+ if (toolSpanEntry == null) {
176
+ return execute();
177
+ }
178
+
179
+ return context.with(toolSpanEntry.context, execute);
180
+ }
181
+
182
+ onStart(
183
+ event:
184
+ | OnStartEvent
185
+ | ObjectOnStartEvent
186
+ | EmbedOnStartEvent
187
+ | RerankOnStartEvent,
188
+ ): void {
189
+ if (event.isEnabled !== true) return;
190
+
191
+ if (
192
+ event.operationId === 'ai.embed' ||
193
+ event.operationId === 'ai.embedMany'
194
+ ) {
195
+ this.onEmbedOperationStart(event as EmbedOnStartEvent);
196
+ return;
197
+ }
198
+
199
+ if (event.operationId === 'ai.rerank') {
200
+ this.onRerankOperationStart(event as RerankOnStartEvent);
201
+ return;
202
+ }
203
+
204
+ if (
205
+ event.operationId === 'ai.generateObject' ||
206
+ event.operationId === 'ai.streamObject'
207
+ ) {
208
+ this.onObjectOperationStart(event as ObjectOnStartEvent);
209
+ return;
210
+ }
211
+
212
+ this.onGenerateStart(event as OnStartEvent);
213
+ }
214
+
215
+ private onGenerateStart(event: OnStartEvent): void {
216
+ const telemetry: TelemetrySettings = {
217
+ isEnabled: event.isEnabled,
218
+ recordInputs: event.recordInputs,
219
+ recordOutputs: event.recordOutputs,
220
+ functionId: event.functionId,
221
+ };
222
+
223
+ const settings: Record<string, unknown> = {
224
+ maxOutputTokens: event.maxOutputTokens,
225
+ temperature: event.temperature,
226
+ topP: event.topP,
227
+ topK: event.topK,
228
+ presencePenalty: event.presencePenalty,
229
+ frequencyPenalty: event.frequencyPenalty,
230
+ stopSequences: event.stopSequences,
231
+ seed: event.seed,
232
+ maxRetries: event.maxRetries,
233
+ };
234
+
235
+ const providerName = mapProviderName(event.provider);
236
+ const operationName = mapOperationName(event.operationId);
237
+
238
+ const attributes = selectAttributes(telemetry, {
239
+ 'gen_ai.operation.name': operationName,
240
+ 'gen_ai.provider.name': providerName,
241
+ 'gen_ai.request.model': event.modelId,
242
+ 'gen_ai.agent.name': telemetry.functionId,
243
+ 'gen_ai.request.frequency_penalty': event.frequencyPenalty,
244
+ 'gen_ai.request.max_tokens': event.maxOutputTokens,
245
+ 'gen_ai.request.presence_penalty': event.presencePenalty,
246
+ 'gen_ai.request.temperature': (event.temperature ?? undefined) as
247
+ | number
248
+ | undefined,
249
+ 'gen_ai.request.top_k': event.topK,
250
+ 'gen_ai.request.top_p': event.topP,
251
+ 'gen_ai.request.stop_sequences': event.stopSequences,
252
+ 'gen_ai.request.seed': event.seed,
253
+ 'gen_ai.system_instructions': event.system
254
+ ? {
255
+ input: () =>
256
+ JSON.stringify(formatSystemInstructions(event.system!)),
257
+ }
258
+ : undefined,
259
+ 'gen_ai.input.messages': {
260
+ input: () =>
261
+ JSON.stringify(
262
+ formatModelMessages({
263
+ prompt: event.prompt,
264
+ messages: event.messages,
265
+ }),
266
+ ),
267
+ },
268
+ });
269
+
270
+ const spanName = `${operationName} ${event.modelId}`;
271
+ const rootSpan = this.tracer.startSpan(spanName, {
272
+ attributes,
273
+ kind: SpanKind.INTERNAL,
274
+ });
275
+ const rootContext = trace.setSpan(context.active(), rootSpan);
276
+
277
+ this.callStates.set(event.callId, {
278
+ operationId: event.operationId,
279
+ telemetry,
280
+ rootSpan,
281
+ rootContext,
282
+ stepSpan: undefined,
283
+ stepContext: undefined,
284
+ embedSpans: new Map(),
285
+ rerankSpan: undefined,
286
+ toolSpans: new Map(),
287
+ settings,
288
+ provider: event.provider,
289
+ modelId: event.modelId,
290
+ });
291
+ }
292
+
293
+ private onObjectOperationStart(event: ObjectOnStartEvent): void {
294
+ const telemetry: TelemetrySettings = {
295
+ isEnabled: event.isEnabled,
296
+ recordInputs: event.recordInputs,
297
+ recordOutputs: event.recordOutputs,
298
+ functionId: event.functionId,
299
+ };
300
+
301
+ const settings: Record<string, unknown> = {
302
+ maxOutputTokens: event.maxOutputTokens,
303
+ temperature: event.temperature,
304
+ topP: event.topP,
305
+ topK: event.topK,
306
+ presencePenalty: event.presencePenalty,
307
+ frequencyPenalty: event.frequencyPenalty,
308
+ seed: event.seed,
309
+ maxRetries: event.maxRetries,
310
+ };
311
+
312
+ const providerName = mapProviderName(event.provider);
313
+ const operationName = mapOperationName(event.operationId);
314
+
315
+ const attributes = selectAttributes(telemetry, {
316
+ 'gen_ai.operation.name': operationName,
317
+ 'gen_ai.provider.name': providerName,
318
+ 'gen_ai.request.model': event.modelId,
319
+ 'gen_ai.agent.name': telemetry.functionId,
320
+ 'gen_ai.output.type': 'json',
321
+ 'gen_ai.request.frequency_penalty': event.frequencyPenalty,
322
+ 'gen_ai.request.max_tokens': event.maxOutputTokens,
323
+ 'gen_ai.request.presence_penalty': event.presencePenalty,
324
+ 'gen_ai.request.temperature': (event.temperature ?? undefined) as
325
+ | number
326
+ | undefined,
327
+ 'gen_ai.request.top_k': event.topK,
328
+ 'gen_ai.request.top_p': event.topP,
329
+ 'gen_ai.request.seed': event.seed,
330
+ 'gen_ai.system_instructions': event.system
331
+ ? {
332
+ input: () =>
333
+ JSON.stringify(formatSystemInstructions(event.system!)),
334
+ }
335
+ : undefined,
336
+ 'gen_ai.input.messages': {
337
+ input: () =>
338
+ JSON.stringify(
339
+ formatModelMessages({
340
+ prompt: event.prompt,
341
+ messages: event.messages,
342
+ }),
343
+ ),
344
+ },
345
+ });
346
+
347
+ const spanName = `${operationName} ${event.modelId}`;
348
+ const rootSpan = this.tracer.startSpan(spanName, {
349
+ attributes,
350
+ kind: SpanKind.INTERNAL,
351
+ });
352
+ const rootContext = trace.setSpan(context.active(), rootSpan);
353
+
354
+ this.callStates.set(event.callId, {
355
+ operationId: event.operationId,
356
+ telemetry,
357
+ rootSpan,
358
+ rootContext,
359
+ stepSpan: undefined,
360
+ stepContext: undefined,
361
+ embedSpans: new Map(),
362
+ rerankSpan: undefined,
363
+ toolSpans: new Map(),
364
+ settings,
365
+ provider: event.provider,
366
+ modelId: event.modelId,
367
+ });
368
+ }
369
+
370
+ /** @deprecated */
371
+ onObjectStepStart(event: ObjectOnStepStartEvent): void {
372
+ const state = this.getCallState(event.callId);
373
+ if (!state?.rootSpan || !state.rootContext) return;
374
+
375
+ const { telemetry } = state;
376
+ const providerName = mapProviderName(event.provider);
377
+
378
+ const attributes = selectAttributes(telemetry, {
379
+ 'gen_ai.operation.name': 'chat',
380
+ 'gen_ai.provider.name': providerName,
381
+ 'gen_ai.request.model': event.modelId,
382
+ 'gen_ai.output.type': 'json',
383
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
384
+ | number
385
+ | undefined,
386
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
387
+ | number
388
+ | undefined,
389
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
390
+ | number
391
+ | undefined,
392
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
393
+ | number
394
+ | undefined,
395
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
396
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
397
+ 'gen_ai.input.messages': {
398
+ input: () =>
399
+ event.promptMessages
400
+ ? JSON.stringify(formatInputMessages(event.promptMessages))
401
+ : undefined,
402
+ },
403
+ });
404
+
405
+ const spanName = `chat ${event.modelId}`;
406
+ state.stepSpan = this.tracer.startSpan(
407
+ spanName,
408
+ { attributes, kind: SpanKind.CLIENT },
409
+ state.rootContext,
410
+ );
411
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
412
+ }
413
+
414
+ /** @deprecated */
415
+ onObjectStepFinish(event: ObjectOnStepFinishEvent): void {
416
+ const state = this.getCallState(event.callId);
417
+ if (!state?.stepSpan) return;
418
+
419
+ const { telemetry } = state;
420
+
421
+ state.stepSpan.setAttributes(
422
+ selectAttributes(telemetry, {
423
+ 'gen_ai.response.finish_reasons': [event.finishReason],
424
+ 'gen_ai.response.id': event.response.id,
425
+ 'gen_ai.response.model': event.response.modelId,
426
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
427
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
428
+ 'gen_ai.usage.cache_read.input_tokens': event.usage.cachedInputTokens,
429
+ 'gen_ai.output.messages': {
430
+ output: () => {
431
+ try {
432
+ return JSON.stringify(
433
+ formatObjectOutputMessages({
434
+ objectText: event.objectText,
435
+ finishReason: event.finishReason,
436
+ }),
437
+ );
438
+ } catch {
439
+ return event.objectText;
440
+ }
441
+ },
442
+ },
443
+ }),
444
+ );
445
+
446
+ state.stepSpan.end();
447
+ state.stepSpan = undefined;
448
+ state.stepContext = undefined;
449
+ }
450
+
451
+ private onEmbedOperationStart(event: EmbedOnStartEvent): void {
452
+ const telemetry: TelemetrySettings = {
453
+ isEnabled: event.isEnabled,
454
+ recordInputs: event.recordInputs,
455
+ recordOutputs: event.recordOutputs,
456
+ functionId: event.functionId,
457
+ };
458
+
459
+ const settings: Record<string, unknown> = {
460
+ maxRetries: event.maxRetries,
461
+ };
462
+
463
+ const providerName = mapProviderName(event.provider);
464
+
465
+ const attributes = selectAttributes(telemetry, {
466
+ 'gen_ai.operation.name': 'embeddings',
467
+ 'gen_ai.provider.name': providerName,
468
+ 'gen_ai.request.model': event.modelId,
469
+ });
470
+
471
+ const spanName = `embeddings ${event.modelId}`;
472
+ const rootSpan = this.tracer.startSpan(spanName, {
473
+ attributes,
474
+ kind: SpanKind.CLIENT,
475
+ });
476
+ const rootContext = trace.setSpan(context.active(), rootSpan);
477
+
478
+ this.callStates.set(event.callId, {
479
+ operationId: event.operationId,
480
+ telemetry,
481
+ rootSpan,
482
+ rootContext,
483
+ stepSpan: undefined,
484
+ stepContext: undefined,
485
+ embedSpans: new Map(),
486
+ rerankSpan: undefined,
487
+ toolSpans: new Map(),
488
+ settings,
489
+ provider: event.provider,
490
+ modelId: event.modelId,
491
+ });
492
+ }
493
+
494
+ onStepStart(event: OtelStepStartEvent): void {
495
+ const state = this.getCallState(event.callId);
496
+ if (!state?.rootSpan || !state.rootContext) return;
497
+
498
+ const { telemetry } = state;
499
+ const providerName = mapProviderName(event.provider);
500
+
501
+ const attributes = selectAttributes(telemetry, {
502
+ 'gen_ai.operation.name': 'chat',
503
+ 'gen_ai.provider.name': providerName,
504
+ 'gen_ai.request.model': event.modelId,
505
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
506
+ | number
507
+ | undefined,
508
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
509
+ | number
510
+ | undefined,
511
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
512
+ | number
513
+ | undefined,
514
+ 'gen_ai.request.stop_sequences': state.settings.stopSequences as
515
+ | string[]
516
+ | undefined,
517
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
518
+ | number
519
+ | undefined,
520
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
521
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
522
+ 'gen_ai.input.messages': {
523
+ input: () =>
524
+ event.promptMessages
525
+ ? JSON.stringify(formatInputMessages(event.promptMessages))
526
+ : undefined,
527
+ },
528
+ 'gen_ai.tool.definitions': {
529
+ input: () =>
530
+ event.stepTools ? JSON.stringify(event.stepTools) : undefined,
531
+ },
532
+ });
533
+
534
+ const spanName = `chat ${event.modelId}`;
535
+ state.stepSpan = this.tracer.startSpan(
536
+ spanName,
537
+ { attributes, kind: SpanKind.CLIENT },
538
+ state.rootContext,
539
+ );
540
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
541
+ }
542
+
543
+ onToolCallStart(event: OnToolCallStartEvent<ToolSet>): void {
544
+ const state = this.getCallState(event.callId);
545
+ if (!state?.stepContext) return;
546
+
547
+ const { telemetry } = state;
548
+ const { toolCall } = event;
549
+
550
+ const attributes = selectAttributes(telemetry, {
551
+ 'gen_ai.operation.name': 'execute_tool',
552
+ 'gen_ai.tool.name': toolCall.toolName,
553
+ 'gen_ai.tool.call.id': toolCall.toolCallId,
554
+ 'gen_ai.tool.type': 'function',
555
+ 'gen_ai.tool.call.arguments': {
556
+ input: () => JSON.stringify(toolCall.input),
557
+ },
558
+ });
559
+
560
+ const spanName = `execute_tool ${toolCall.toolName}`;
561
+ const toolSpan = this.tracer.startSpan(
562
+ spanName,
563
+ { attributes, kind: SpanKind.INTERNAL },
564
+ state.stepContext,
565
+ );
566
+ const toolContext = trace.setSpan(state.stepContext, toolSpan);
567
+
568
+ state.toolSpans.set(toolCall.toolCallId, {
569
+ span: toolSpan,
570
+ context: toolContext,
571
+ });
572
+ }
573
+
574
+ onToolCallFinish(event: OnToolCallFinishEvent<ToolSet>): void {
575
+ const state = this.getCallState(event.callId);
576
+ if (!state) return;
577
+
578
+ const toolSpanEntry = state.toolSpans.get(event.toolCall.toolCallId);
579
+ if (!toolSpanEntry) return;
580
+
581
+ const { span } = toolSpanEntry;
582
+ const { telemetry } = state;
583
+
584
+ if (event.success) {
585
+ try {
586
+ span.setAttributes(
587
+ selectAttributes(telemetry, {
588
+ 'gen_ai.tool.call.result': {
589
+ output: () => JSON.stringify(event.output),
590
+ },
591
+ }),
592
+ );
593
+ } catch {
594
+ // JSON.stringify might fail for non-serializable results
595
+ }
596
+ } else {
597
+ recordSpanError(span, event.error);
598
+ }
599
+
600
+ span.end();
601
+ state.toolSpans.delete(event.toolCall.toolCallId);
602
+ }
603
+
604
+ onStepFinish(event: OnStepFinishEvent<ToolSet>): void {
605
+ const state = this.getCallState(event.callId);
606
+ if (!state?.stepSpan) return;
607
+
608
+ const { telemetry } = state;
609
+
610
+ state.stepSpan.setAttributes(
611
+ selectAttributes(telemetry, {
612
+ 'gen_ai.response.finish_reasons': [event.finishReason],
613
+ 'gen_ai.response.id': event.response.id,
614
+ 'gen_ai.response.model': event.response.modelId,
615
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
616
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
617
+ 'gen_ai.usage.cache_read.input_tokens':
618
+ event.usage.inputTokenDetails?.cacheReadTokens ??
619
+ event.usage.cachedInputTokens,
620
+ 'gen_ai.usage.cache_creation.input_tokens':
621
+ event.usage.inputTokenDetails?.cacheWriteTokens,
622
+ 'gen_ai.output.messages': {
623
+ output: () =>
624
+ JSON.stringify(
625
+ formatOutputMessages({
626
+ text: event.text ?? undefined,
627
+ reasoning: event.reasoning as ReadonlyArray<{ text?: string }>,
628
+ toolCalls: event.toolCalls,
629
+ files: event.files,
630
+ finishReason: event.finishReason,
631
+ }),
632
+ ),
633
+ },
634
+ }),
635
+ );
636
+
637
+ state.stepSpan.end();
638
+ state.stepSpan = undefined;
639
+ state.stepContext = undefined;
640
+ }
641
+
642
+ onFinish(
643
+ event:
644
+ | OnFinishEvent<ToolSet>
645
+ | ObjectOnFinishEvent<unknown>
646
+ | EmbedOnFinishEvent
647
+ | RerankOnFinishEvent,
648
+ ): void {
649
+ const state = this.getCallState(event.callId);
650
+ if (!state?.rootSpan) return;
651
+
652
+ if (
653
+ state.operationId === 'ai.embed' ||
654
+ state.operationId === 'ai.embedMany'
655
+ ) {
656
+ this.onEmbedOperationFinish(event as EmbedOnFinishEvent);
657
+ return;
658
+ }
659
+
660
+ if (state.operationId === 'ai.rerank') {
661
+ this.onRerankOperationFinish(event as RerankOnFinishEvent);
662
+ return;
663
+ }
664
+
665
+ if (
666
+ state.operationId === 'ai.generateObject' ||
667
+ state.operationId === 'ai.streamObject'
668
+ ) {
669
+ this.onObjectOperationFinish(event as ObjectOnFinishEvent<unknown>);
670
+ return;
671
+ }
672
+
673
+ this.onGenerateFinish(event as OnFinishEvent<ToolSet>);
674
+ }
675
+
676
+ private onGenerateFinish(event: OnFinishEvent<ToolSet>): void {
677
+ const state = this.getCallState(event.callId);
678
+ if (!state?.rootSpan) return;
679
+
680
+ const { telemetry } = state;
681
+
682
+ state.rootSpan.setAttributes(
683
+ selectAttributes(telemetry, {
684
+ 'gen_ai.response.finish_reasons': [event.finishReason],
685
+ 'gen_ai.usage.input_tokens': event.totalUsage.inputTokens,
686
+ 'gen_ai.usage.output_tokens': event.totalUsage.outputTokens,
687
+ 'gen_ai.usage.cache_read.input_tokens':
688
+ event.totalUsage.inputTokenDetails?.cacheReadTokens ??
689
+ event.totalUsage.cachedInputTokens,
690
+ 'gen_ai.usage.cache_creation.input_tokens':
691
+ event.totalUsage.inputTokenDetails?.cacheWriteTokens,
692
+ 'gen_ai.output.messages': {
693
+ output: () =>
694
+ JSON.stringify(
695
+ formatOutputMessages({
696
+ text: event.text ?? undefined,
697
+ reasoning: event.reasoning as ReadonlyArray<{ text?: string }>,
698
+ toolCalls: event.toolCalls,
699
+ files: event.files,
700
+ finishReason: event.finishReason,
701
+ }),
702
+ ),
703
+ },
704
+ }),
705
+ );
706
+
707
+ state.rootSpan.end();
708
+ this.cleanupCallState(event.callId);
709
+ }
710
+
711
+ private onObjectOperationFinish(event: ObjectOnFinishEvent<unknown>): void {
712
+ const state = this.getCallState(event.callId);
713
+ if (!state?.rootSpan) return;
714
+
715
+ const { telemetry } = state;
716
+
717
+ state.rootSpan.setAttributes(
718
+ selectAttributes(telemetry, {
719
+ 'gen_ai.response.finish_reasons': [event.finishReason],
720
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
721
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
722
+ 'gen_ai.usage.cache_read.input_tokens': event.usage.cachedInputTokens,
723
+ 'gen_ai.output.messages': {
724
+ output: () =>
725
+ event.object != null
726
+ ? JSON.stringify(
727
+ formatObjectOutputMessages({
728
+ objectText: JSON.stringify(event.object),
729
+ finishReason: event.finishReason,
730
+ }),
731
+ )
732
+ : undefined,
733
+ },
734
+ }),
735
+ );
736
+
737
+ state.rootSpan.end();
738
+ this.cleanupCallState(event.callId);
739
+ }
740
+
741
+ private onEmbedOperationFinish(event: EmbedOnFinishEvent): void {
742
+ const state = this.getCallState(event.callId);
743
+ if (!state?.rootSpan) return;
744
+
745
+ const { telemetry } = state;
746
+
747
+ state.rootSpan.setAttributes(
748
+ selectAttributes(telemetry, {
749
+ 'gen_ai.usage.input_tokens': event.usage.tokens,
750
+ }),
751
+ );
752
+
753
+ state.rootSpan.end();
754
+ this.cleanupCallState(event.callId);
755
+ }
756
+
757
+ onEmbedStart(event: EmbedStartEvent): void {
758
+ const state = this.getCallState(event.callId);
759
+ if (!state?.rootSpan || !state.rootContext) return;
760
+
761
+ const { telemetry } = state;
762
+ const providerName = mapProviderName(state.provider);
763
+
764
+ const attributes = selectAttributes(telemetry, {
765
+ 'gen_ai.operation.name': 'embeddings',
766
+ 'gen_ai.provider.name': providerName,
767
+ 'gen_ai.request.model': state.modelId,
768
+ });
769
+
770
+ const spanName = `embeddings ${state.modelId}`;
771
+ const embedSpan = this.tracer.startSpan(
772
+ spanName,
773
+ { attributes, kind: SpanKind.CLIENT },
774
+ state.rootContext,
775
+ );
776
+ const embedContext = trace.setSpan(state.rootContext, embedSpan);
777
+
778
+ state.embedSpans.set(event.embedCallId, {
779
+ span: embedSpan,
780
+ context: embedContext,
781
+ });
782
+ }
783
+
784
+ onEmbedFinish(event: EmbedFinishEvent): void {
785
+ const state = this.getCallState(event.callId);
786
+ if (!state) return;
787
+
788
+ const embedSpanEntry = state.embedSpans.get(event.embedCallId);
789
+ if (!embedSpanEntry) return;
790
+
791
+ const { span } = embedSpanEntry;
792
+ const { telemetry } = state;
793
+
794
+ span.setAttributes(
795
+ selectAttributes(telemetry, {
796
+ 'gen_ai.usage.input_tokens': event.usage.tokens,
797
+ }),
798
+ );
799
+
800
+ span.end();
801
+ state.embedSpans.delete(event.embedCallId);
802
+ }
803
+
804
+ private onRerankOperationStart(event: RerankOnStartEvent): void {
805
+ const telemetry: TelemetrySettings = {
806
+ isEnabled: event.isEnabled,
807
+ recordInputs: event.recordInputs,
808
+ recordOutputs: event.recordOutputs,
809
+ functionId: event.functionId,
810
+ };
811
+
812
+ const settings: Record<string, unknown> = {
813
+ maxRetries: event.maxRetries,
814
+ };
815
+
816
+ const providerName = mapProviderName(event.provider);
817
+
818
+ const attributes = selectAttributes(telemetry, {
819
+ 'gen_ai.operation.name': 'rerank',
820
+ 'gen_ai.provider.name': providerName,
821
+ 'gen_ai.request.model': event.modelId,
822
+ });
823
+
824
+ const spanName = `rerank ${event.modelId}`;
825
+ const rootSpan = this.tracer.startSpan(spanName, {
826
+ attributes,
827
+ kind: SpanKind.CLIENT,
828
+ });
829
+ const rootContext = trace.setSpan(context.active(), rootSpan);
830
+
831
+ this.callStates.set(event.callId, {
832
+ operationId: event.operationId,
833
+ telemetry,
834
+ rootSpan,
835
+ rootContext,
836
+ stepSpan: undefined,
837
+ stepContext: undefined,
838
+ embedSpans: new Map(),
839
+ rerankSpan: undefined,
840
+ toolSpans: new Map(),
841
+ settings,
842
+ provider: event.provider,
843
+ modelId: event.modelId,
844
+ });
845
+ }
846
+
847
+ private onRerankOperationFinish(event: RerankOnFinishEvent): void {
848
+ const state = this.getCallState(event.callId);
849
+ if (!state?.rootSpan) return;
850
+
851
+ state.rootSpan.end();
852
+ this.cleanupCallState(event.callId);
853
+ }
854
+
855
+ onRerankStart(event: RerankStartEvent): void {
856
+ const state = this.getCallState(event.callId);
857
+ if (!state?.rootSpan || !state.rootContext) return;
858
+
859
+ const { telemetry } = state;
860
+ const providerName = mapProviderName(state.provider);
861
+
862
+ const attributes = selectAttributes(telemetry, {
863
+ 'gen_ai.operation.name': 'rerank',
864
+ 'gen_ai.provider.name': providerName,
865
+ 'gen_ai.request.model': state.modelId,
866
+ });
867
+
868
+ const spanName = `rerank ${state.modelId}`;
869
+ const rerankSpan = this.tracer.startSpan(
870
+ spanName,
871
+ { attributes, kind: SpanKind.CLIENT },
872
+ state.rootContext,
873
+ );
874
+ const rerankContext = trace.setSpan(state.rootContext, rerankSpan);
875
+
876
+ state.rerankSpan = { span: rerankSpan, context: rerankContext };
877
+ }
878
+
879
+ onRerankFinish(event: RerankFinishEvent): void {
880
+ const state = this.getCallState(event.callId);
881
+ if (!state?.rerankSpan) return;
882
+
883
+ const { span } = state.rerankSpan;
884
+
885
+ span.end();
886
+ state.rerankSpan = undefined;
887
+ }
888
+
889
+ onChunk(_event: OnChunkEvent<ToolSet>): void {
890
+ // No-op: streaming chunk events are not part of the GenAI SemConv.
891
+ }
892
+
893
+ onError(error: unknown): void {
894
+ const event = error as { callId?: string; error?: unknown };
895
+ if (!event?.callId) return;
896
+
897
+ const state = this.getCallState(event.callId);
898
+ if (!state?.rootSpan) return;
899
+
900
+ const actualError = event.error ?? error;
901
+
902
+ if (state.stepSpan) {
903
+ recordSpanError(state.stepSpan, actualError);
904
+ state.stepSpan.end();
905
+ }
906
+
907
+ for (const { span: embedSpan } of state.embedSpans.values()) {
908
+ recordSpanError(embedSpan, actualError);
909
+ embedSpan.end();
910
+ }
911
+ state.embedSpans.clear();
912
+
913
+ if (state.rerankSpan) {
914
+ recordSpanError(state.rerankSpan.span, actualError);
915
+ state.rerankSpan.span.end();
916
+ state.rerankSpan = undefined;
917
+ }
918
+
919
+ recordSpanError(state.rootSpan, actualError);
920
+
921
+ state.rootSpan.end();
922
+ this.cleanupCallState(event.callId);
923
+ }
924
+ }