@ai-sdk/otel 1.0.0-beta.3 → 1.0.0-beta.30

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,928 @@
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
+ metadata: event.metadata,
222
+ };
223
+
224
+ const settings: Record<string, unknown> = {
225
+ maxOutputTokens: event.maxOutputTokens,
226
+ temperature: event.temperature,
227
+ topP: event.topP,
228
+ topK: event.topK,
229
+ presencePenalty: event.presencePenalty,
230
+ frequencyPenalty: event.frequencyPenalty,
231
+ stopSequences: event.stopSequences,
232
+ seed: event.seed,
233
+ maxRetries: event.maxRetries,
234
+ };
235
+
236
+ const providerName = mapProviderName(event.provider);
237
+ const operationName = mapOperationName(event.operationId);
238
+
239
+ const attributes = selectAttributes(telemetry, {
240
+ 'gen_ai.operation.name': operationName,
241
+ 'gen_ai.provider.name': providerName,
242
+ 'gen_ai.request.model': event.modelId,
243
+ 'gen_ai.agent.name': telemetry.functionId,
244
+ 'gen_ai.request.frequency_penalty': event.frequencyPenalty,
245
+ 'gen_ai.request.max_tokens': event.maxOutputTokens,
246
+ 'gen_ai.request.presence_penalty': event.presencePenalty,
247
+ 'gen_ai.request.temperature': (event.temperature ?? undefined) as
248
+ | number
249
+ | undefined,
250
+ 'gen_ai.request.top_k': event.topK,
251
+ 'gen_ai.request.top_p': event.topP,
252
+ 'gen_ai.request.stop_sequences': event.stopSequences,
253
+ 'gen_ai.request.seed': event.seed,
254
+ 'gen_ai.system_instructions': event.system
255
+ ? {
256
+ input: () =>
257
+ JSON.stringify(formatSystemInstructions(event.system!)),
258
+ }
259
+ : undefined,
260
+ 'gen_ai.input.messages': {
261
+ input: () =>
262
+ JSON.stringify(
263
+ formatModelMessages({
264
+ prompt: event.prompt,
265
+ messages: event.messages,
266
+ }),
267
+ ),
268
+ },
269
+ });
270
+
271
+ const spanName = `${operationName} ${event.modelId}`;
272
+ const rootSpan = this.tracer.startSpan(spanName, {
273
+ attributes,
274
+ kind: SpanKind.INTERNAL,
275
+ });
276
+ const rootContext = trace.setSpan(context.active(), rootSpan);
277
+
278
+ this.callStates.set(event.callId, {
279
+ operationId: event.operationId,
280
+ telemetry,
281
+ rootSpan,
282
+ rootContext,
283
+ stepSpan: undefined,
284
+ stepContext: undefined,
285
+ embedSpans: new Map(),
286
+ rerankSpan: undefined,
287
+ toolSpans: new Map(),
288
+ settings,
289
+ provider: event.provider,
290
+ modelId: event.modelId,
291
+ });
292
+ }
293
+
294
+ private onObjectOperationStart(event: ObjectOnStartEvent): void {
295
+ const telemetry: TelemetrySettings = {
296
+ isEnabled: event.isEnabled,
297
+ recordInputs: event.recordInputs,
298
+ recordOutputs: event.recordOutputs,
299
+ functionId: event.functionId,
300
+ metadata: event.metadata,
301
+ };
302
+
303
+ const settings: Record<string, unknown> = {
304
+ maxOutputTokens: event.maxOutputTokens,
305
+ temperature: event.temperature,
306
+ topP: event.topP,
307
+ topK: event.topK,
308
+ presencePenalty: event.presencePenalty,
309
+ frequencyPenalty: event.frequencyPenalty,
310
+ seed: event.seed,
311
+ maxRetries: event.maxRetries,
312
+ };
313
+
314
+ const providerName = mapProviderName(event.provider);
315
+ const operationName = mapOperationName(event.operationId);
316
+
317
+ const attributes = selectAttributes(telemetry, {
318
+ 'gen_ai.operation.name': operationName,
319
+ 'gen_ai.provider.name': providerName,
320
+ 'gen_ai.request.model': event.modelId,
321
+ 'gen_ai.agent.name': telemetry.functionId,
322
+ 'gen_ai.output.type': 'json',
323
+ 'gen_ai.request.frequency_penalty': event.frequencyPenalty,
324
+ 'gen_ai.request.max_tokens': event.maxOutputTokens,
325
+ 'gen_ai.request.presence_penalty': event.presencePenalty,
326
+ 'gen_ai.request.temperature': (event.temperature ?? undefined) as
327
+ | number
328
+ | undefined,
329
+ 'gen_ai.request.top_k': event.topK,
330
+ 'gen_ai.request.top_p': event.topP,
331
+ 'gen_ai.request.seed': event.seed,
332
+ 'gen_ai.system_instructions': event.system
333
+ ? {
334
+ input: () =>
335
+ JSON.stringify(formatSystemInstructions(event.system!)),
336
+ }
337
+ : undefined,
338
+ 'gen_ai.input.messages': {
339
+ input: () =>
340
+ JSON.stringify(
341
+ formatModelMessages({
342
+ prompt: event.prompt,
343
+ messages: event.messages,
344
+ }),
345
+ ),
346
+ },
347
+ });
348
+
349
+ const spanName = `${operationName} ${event.modelId}`;
350
+ const rootSpan = this.tracer.startSpan(spanName, {
351
+ attributes,
352
+ kind: SpanKind.INTERNAL,
353
+ });
354
+ const rootContext = trace.setSpan(context.active(), rootSpan);
355
+
356
+ this.callStates.set(event.callId, {
357
+ operationId: event.operationId,
358
+ telemetry,
359
+ rootSpan,
360
+ rootContext,
361
+ stepSpan: undefined,
362
+ stepContext: undefined,
363
+ embedSpans: new Map(),
364
+ rerankSpan: undefined,
365
+ toolSpans: new Map(),
366
+ settings,
367
+ provider: event.provider,
368
+ modelId: event.modelId,
369
+ });
370
+ }
371
+
372
+ /** @deprecated */
373
+ onObjectStepStart(event: ObjectOnStepStartEvent): void {
374
+ const state = this.getCallState(event.callId);
375
+ if (!state?.rootSpan || !state.rootContext) return;
376
+
377
+ const { telemetry } = state;
378
+ const providerName = mapProviderName(event.provider);
379
+
380
+ const attributes = selectAttributes(telemetry, {
381
+ 'gen_ai.operation.name': 'chat',
382
+ 'gen_ai.provider.name': providerName,
383
+ 'gen_ai.request.model': event.modelId,
384
+ 'gen_ai.output.type': 'json',
385
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
386
+ | number
387
+ | undefined,
388
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
389
+ | number
390
+ | undefined,
391
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
392
+ | number
393
+ | undefined,
394
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
395
+ | number
396
+ | undefined,
397
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
398
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
399
+ 'gen_ai.input.messages': {
400
+ input: () =>
401
+ event.promptMessages
402
+ ? JSON.stringify(formatInputMessages(event.promptMessages))
403
+ : undefined,
404
+ },
405
+ });
406
+
407
+ const spanName = `chat ${event.modelId}`;
408
+ state.stepSpan = this.tracer.startSpan(
409
+ spanName,
410
+ { attributes, kind: SpanKind.CLIENT },
411
+ state.rootContext,
412
+ );
413
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
414
+ }
415
+
416
+ /** @deprecated */
417
+ onObjectStepFinish(event: ObjectOnStepFinishEvent): void {
418
+ const state = this.getCallState(event.callId);
419
+ if (!state?.stepSpan) return;
420
+
421
+ const { telemetry } = state;
422
+
423
+ state.stepSpan.setAttributes(
424
+ selectAttributes(telemetry, {
425
+ 'gen_ai.response.finish_reasons': [event.finishReason],
426
+ 'gen_ai.response.id': event.response.id,
427
+ 'gen_ai.response.model': event.response.modelId,
428
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
429
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
430
+ 'gen_ai.usage.cache_read.input_tokens': event.usage.cachedInputTokens,
431
+ 'gen_ai.output.messages': {
432
+ output: () => {
433
+ try {
434
+ return JSON.stringify(
435
+ formatObjectOutputMessages({
436
+ objectText: event.objectText,
437
+ finishReason: event.finishReason,
438
+ }),
439
+ );
440
+ } catch {
441
+ return event.objectText;
442
+ }
443
+ },
444
+ },
445
+ }),
446
+ );
447
+
448
+ state.stepSpan.end();
449
+ state.stepSpan = undefined;
450
+ state.stepContext = undefined;
451
+ }
452
+
453
+ private onEmbedOperationStart(event: EmbedOnStartEvent): void {
454
+ const telemetry: TelemetrySettings = {
455
+ isEnabled: event.isEnabled,
456
+ recordInputs: event.recordInputs,
457
+ recordOutputs: event.recordOutputs,
458
+ functionId: event.functionId,
459
+ metadata: event.metadata,
460
+ };
461
+
462
+ const settings: Record<string, unknown> = {
463
+ maxRetries: event.maxRetries,
464
+ };
465
+
466
+ const providerName = mapProviderName(event.provider);
467
+
468
+ const attributes = selectAttributes(telemetry, {
469
+ 'gen_ai.operation.name': 'embeddings',
470
+ 'gen_ai.provider.name': providerName,
471
+ 'gen_ai.request.model': event.modelId,
472
+ });
473
+
474
+ const spanName = `embeddings ${event.modelId}`;
475
+ const rootSpan = this.tracer.startSpan(spanName, {
476
+ attributes,
477
+ kind: SpanKind.CLIENT,
478
+ });
479
+ const rootContext = trace.setSpan(context.active(), rootSpan);
480
+
481
+ this.callStates.set(event.callId, {
482
+ operationId: event.operationId,
483
+ telemetry,
484
+ rootSpan,
485
+ rootContext,
486
+ stepSpan: undefined,
487
+ stepContext: undefined,
488
+ embedSpans: new Map(),
489
+ rerankSpan: undefined,
490
+ toolSpans: new Map(),
491
+ settings,
492
+ provider: event.provider,
493
+ modelId: event.modelId,
494
+ });
495
+ }
496
+
497
+ onStepStart(event: OtelStepStartEvent): void {
498
+ const state = this.getCallState(event.callId);
499
+ if (!state?.rootSpan || !state.rootContext) return;
500
+
501
+ const { telemetry } = state;
502
+ const providerName = mapProviderName(event.provider);
503
+
504
+ const attributes = selectAttributes(telemetry, {
505
+ 'gen_ai.operation.name': 'chat',
506
+ 'gen_ai.provider.name': providerName,
507
+ 'gen_ai.request.model': event.modelId,
508
+ 'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
509
+ | number
510
+ | undefined,
511
+ 'gen_ai.request.max_tokens': state.settings.maxOutputTokens as
512
+ | number
513
+ | undefined,
514
+ 'gen_ai.request.presence_penalty': state.settings.presencePenalty as
515
+ | number
516
+ | undefined,
517
+ 'gen_ai.request.stop_sequences': state.settings.stopSequences as
518
+ | string[]
519
+ | undefined,
520
+ 'gen_ai.request.temperature': (state.settings.temperature ?? undefined) as
521
+ | number
522
+ | undefined,
523
+ 'gen_ai.request.top_k': state.settings.topK as number | undefined,
524
+ 'gen_ai.request.top_p': state.settings.topP as number | undefined,
525
+ 'gen_ai.input.messages': {
526
+ input: () =>
527
+ event.promptMessages
528
+ ? JSON.stringify(formatInputMessages(event.promptMessages))
529
+ : undefined,
530
+ },
531
+ 'gen_ai.tool.definitions': {
532
+ input: () =>
533
+ event.stepTools ? JSON.stringify(event.stepTools) : undefined,
534
+ },
535
+ });
536
+
537
+ const spanName = `chat ${event.modelId}`;
538
+ state.stepSpan = this.tracer.startSpan(
539
+ spanName,
540
+ { attributes, kind: SpanKind.CLIENT },
541
+ state.rootContext,
542
+ );
543
+ state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
544
+ }
545
+
546
+ onToolCallStart(event: OnToolCallStartEvent<ToolSet>): void {
547
+ const state = this.getCallState(event.callId);
548
+ if (!state?.stepContext) return;
549
+
550
+ const { telemetry } = state;
551
+ const { toolCall } = event;
552
+
553
+ const attributes = selectAttributes(telemetry, {
554
+ 'gen_ai.operation.name': 'execute_tool',
555
+ 'gen_ai.tool.name': toolCall.toolName,
556
+ 'gen_ai.tool.call.id': toolCall.toolCallId,
557
+ 'gen_ai.tool.type': 'function',
558
+ 'gen_ai.tool.call.arguments': {
559
+ input: () => JSON.stringify(toolCall.input),
560
+ },
561
+ });
562
+
563
+ const spanName = `execute_tool ${toolCall.toolName}`;
564
+ const toolSpan = this.tracer.startSpan(
565
+ spanName,
566
+ { attributes, kind: SpanKind.INTERNAL },
567
+ state.stepContext,
568
+ );
569
+ const toolContext = trace.setSpan(state.stepContext, toolSpan);
570
+
571
+ state.toolSpans.set(toolCall.toolCallId, {
572
+ span: toolSpan,
573
+ context: toolContext,
574
+ });
575
+ }
576
+
577
+ onToolCallFinish(event: OnToolCallFinishEvent<ToolSet>): void {
578
+ const state = this.getCallState(event.callId);
579
+ if (!state) return;
580
+
581
+ const toolSpanEntry = state.toolSpans.get(event.toolCall.toolCallId);
582
+ if (!toolSpanEntry) return;
583
+
584
+ const { span } = toolSpanEntry;
585
+ const { telemetry } = state;
586
+
587
+ if (event.success) {
588
+ try {
589
+ span.setAttributes(
590
+ selectAttributes(telemetry, {
591
+ 'gen_ai.tool.call.result': {
592
+ output: () => JSON.stringify(event.output),
593
+ },
594
+ }),
595
+ );
596
+ } catch {
597
+ // JSON.stringify might fail for non-serializable results
598
+ }
599
+ } else {
600
+ recordSpanError(span, event.error);
601
+ }
602
+
603
+ span.end();
604
+ state.toolSpans.delete(event.toolCall.toolCallId);
605
+ }
606
+
607
+ onStepFinish(event: OnStepFinishEvent<ToolSet>): void {
608
+ const state = this.getCallState(event.callId);
609
+ if (!state?.stepSpan) return;
610
+
611
+ const { telemetry } = state;
612
+
613
+ state.stepSpan.setAttributes(
614
+ selectAttributes(telemetry, {
615
+ 'gen_ai.response.finish_reasons': [event.finishReason],
616
+ 'gen_ai.response.id': event.response.id,
617
+ 'gen_ai.response.model': event.response.modelId,
618
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
619
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
620
+ 'gen_ai.usage.cache_read.input_tokens':
621
+ event.usage.inputTokenDetails?.cacheReadTokens ??
622
+ event.usage.cachedInputTokens,
623
+ 'gen_ai.usage.cache_creation.input_tokens':
624
+ event.usage.inputTokenDetails?.cacheWriteTokens,
625
+ 'gen_ai.output.messages': {
626
+ output: () =>
627
+ JSON.stringify(
628
+ formatOutputMessages({
629
+ text: event.text ?? undefined,
630
+ reasoning: event.reasoning as ReadonlyArray<{ text?: string }>,
631
+ toolCalls: event.toolCalls,
632
+ files: event.files,
633
+ finishReason: event.finishReason,
634
+ }),
635
+ ),
636
+ },
637
+ }),
638
+ );
639
+
640
+ state.stepSpan.end();
641
+ state.stepSpan = undefined;
642
+ state.stepContext = undefined;
643
+ }
644
+
645
+ onFinish(
646
+ event:
647
+ | OnFinishEvent<ToolSet>
648
+ | ObjectOnFinishEvent<unknown>
649
+ | EmbedOnFinishEvent
650
+ | RerankOnFinishEvent,
651
+ ): void {
652
+ const state = this.getCallState(event.callId);
653
+ if (!state?.rootSpan) return;
654
+
655
+ if (
656
+ state.operationId === 'ai.embed' ||
657
+ state.operationId === 'ai.embedMany'
658
+ ) {
659
+ this.onEmbedOperationFinish(event as EmbedOnFinishEvent);
660
+ return;
661
+ }
662
+
663
+ if (state.operationId === 'ai.rerank') {
664
+ this.onRerankOperationFinish(event as RerankOnFinishEvent);
665
+ return;
666
+ }
667
+
668
+ if (
669
+ state.operationId === 'ai.generateObject' ||
670
+ state.operationId === 'ai.streamObject'
671
+ ) {
672
+ this.onObjectOperationFinish(event as ObjectOnFinishEvent<unknown>);
673
+ return;
674
+ }
675
+
676
+ this.onGenerateFinish(event as OnFinishEvent<ToolSet>);
677
+ }
678
+
679
+ private onGenerateFinish(event: OnFinishEvent<ToolSet>): void {
680
+ const state = this.getCallState(event.callId);
681
+ if (!state?.rootSpan) return;
682
+
683
+ const { telemetry } = state;
684
+
685
+ state.rootSpan.setAttributes(
686
+ selectAttributes(telemetry, {
687
+ 'gen_ai.response.finish_reasons': [event.finishReason],
688
+ 'gen_ai.usage.input_tokens': event.totalUsage.inputTokens,
689
+ 'gen_ai.usage.output_tokens': event.totalUsage.outputTokens,
690
+ 'gen_ai.usage.cache_read.input_tokens':
691
+ event.totalUsage.inputTokenDetails?.cacheReadTokens ??
692
+ event.totalUsage.cachedInputTokens,
693
+ 'gen_ai.usage.cache_creation.input_tokens':
694
+ event.totalUsage.inputTokenDetails?.cacheWriteTokens,
695
+ 'gen_ai.output.messages': {
696
+ output: () =>
697
+ JSON.stringify(
698
+ formatOutputMessages({
699
+ text: event.text ?? undefined,
700
+ reasoning: event.reasoning as ReadonlyArray<{ text?: string }>,
701
+ toolCalls: event.toolCalls,
702
+ files: event.files,
703
+ finishReason: event.finishReason,
704
+ }),
705
+ ),
706
+ },
707
+ }),
708
+ );
709
+
710
+ state.rootSpan.end();
711
+ this.cleanupCallState(event.callId);
712
+ }
713
+
714
+ private onObjectOperationFinish(event: ObjectOnFinishEvent<unknown>): void {
715
+ const state = this.getCallState(event.callId);
716
+ if (!state?.rootSpan) return;
717
+
718
+ const { telemetry } = state;
719
+
720
+ state.rootSpan.setAttributes(
721
+ selectAttributes(telemetry, {
722
+ 'gen_ai.response.finish_reasons': [event.finishReason],
723
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
724
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
725
+ 'gen_ai.usage.cache_read.input_tokens': event.usage.cachedInputTokens,
726
+ 'gen_ai.output.messages': {
727
+ output: () =>
728
+ event.object != null
729
+ ? JSON.stringify(
730
+ formatObjectOutputMessages({
731
+ objectText: JSON.stringify(event.object),
732
+ finishReason: event.finishReason,
733
+ }),
734
+ )
735
+ : undefined,
736
+ },
737
+ }),
738
+ );
739
+
740
+ state.rootSpan.end();
741
+ this.cleanupCallState(event.callId);
742
+ }
743
+
744
+ private onEmbedOperationFinish(event: EmbedOnFinishEvent): void {
745
+ const state = this.getCallState(event.callId);
746
+ if (!state?.rootSpan) return;
747
+
748
+ const { telemetry } = state;
749
+
750
+ state.rootSpan.setAttributes(
751
+ selectAttributes(telemetry, {
752
+ 'gen_ai.usage.input_tokens': event.usage.tokens,
753
+ }),
754
+ );
755
+
756
+ state.rootSpan.end();
757
+ this.cleanupCallState(event.callId);
758
+ }
759
+
760
+ onEmbedStart(event: EmbedStartEvent): void {
761
+ const state = this.getCallState(event.callId);
762
+ if (!state?.rootSpan || !state.rootContext) return;
763
+
764
+ const { telemetry } = state;
765
+ const providerName = mapProviderName(state.provider);
766
+
767
+ const attributes = selectAttributes(telemetry, {
768
+ 'gen_ai.operation.name': 'embeddings',
769
+ 'gen_ai.provider.name': providerName,
770
+ 'gen_ai.request.model': state.modelId,
771
+ });
772
+
773
+ const spanName = `embeddings ${state.modelId}`;
774
+ const embedSpan = this.tracer.startSpan(
775
+ spanName,
776
+ { attributes, kind: SpanKind.CLIENT },
777
+ state.rootContext,
778
+ );
779
+ const embedContext = trace.setSpan(state.rootContext, embedSpan);
780
+
781
+ state.embedSpans.set(event.embedCallId, {
782
+ span: embedSpan,
783
+ context: embedContext,
784
+ });
785
+ }
786
+
787
+ onEmbedFinish(event: EmbedFinishEvent): void {
788
+ const state = this.getCallState(event.callId);
789
+ if (!state) return;
790
+
791
+ const embedSpanEntry = state.embedSpans.get(event.embedCallId);
792
+ if (!embedSpanEntry) return;
793
+
794
+ const { span } = embedSpanEntry;
795
+ const { telemetry } = state;
796
+
797
+ span.setAttributes(
798
+ selectAttributes(telemetry, {
799
+ 'gen_ai.usage.input_tokens': event.usage.tokens,
800
+ }),
801
+ );
802
+
803
+ span.end();
804
+ state.embedSpans.delete(event.embedCallId);
805
+ }
806
+
807
+ private onRerankOperationStart(event: RerankOnStartEvent): void {
808
+ const telemetry: TelemetrySettings = {
809
+ isEnabled: event.isEnabled,
810
+ recordInputs: event.recordInputs,
811
+ recordOutputs: event.recordOutputs,
812
+ functionId: event.functionId,
813
+ metadata: event.metadata,
814
+ };
815
+
816
+ const settings: Record<string, unknown> = {
817
+ maxRetries: event.maxRetries,
818
+ };
819
+
820
+ const providerName = mapProviderName(event.provider);
821
+
822
+ const attributes = selectAttributes(telemetry, {
823
+ 'gen_ai.operation.name': 'rerank',
824
+ 'gen_ai.provider.name': providerName,
825
+ 'gen_ai.request.model': event.modelId,
826
+ });
827
+
828
+ const spanName = `rerank ${event.modelId}`;
829
+ const rootSpan = this.tracer.startSpan(spanName, {
830
+ attributes,
831
+ kind: SpanKind.CLIENT,
832
+ });
833
+ const rootContext = trace.setSpan(context.active(), rootSpan);
834
+
835
+ this.callStates.set(event.callId, {
836
+ operationId: event.operationId,
837
+ telemetry,
838
+ rootSpan,
839
+ rootContext,
840
+ stepSpan: undefined,
841
+ stepContext: undefined,
842
+ embedSpans: new Map(),
843
+ rerankSpan: undefined,
844
+ toolSpans: new Map(),
845
+ settings,
846
+ provider: event.provider,
847
+ modelId: event.modelId,
848
+ });
849
+ }
850
+
851
+ private onRerankOperationFinish(event: RerankOnFinishEvent): void {
852
+ const state = this.getCallState(event.callId);
853
+ if (!state?.rootSpan) return;
854
+
855
+ state.rootSpan.end();
856
+ this.cleanupCallState(event.callId);
857
+ }
858
+
859
+ onRerankStart(event: RerankStartEvent): void {
860
+ const state = this.getCallState(event.callId);
861
+ if (!state?.rootSpan || !state.rootContext) return;
862
+
863
+ const { telemetry } = state;
864
+ const providerName = mapProviderName(state.provider);
865
+
866
+ const attributes = selectAttributes(telemetry, {
867
+ 'gen_ai.operation.name': 'rerank',
868
+ 'gen_ai.provider.name': providerName,
869
+ 'gen_ai.request.model': state.modelId,
870
+ });
871
+
872
+ const spanName = `rerank ${state.modelId}`;
873
+ const rerankSpan = this.tracer.startSpan(
874
+ spanName,
875
+ { attributes, kind: SpanKind.CLIENT },
876
+ state.rootContext,
877
+ );
878
+ const rerankContext = trace.setSpan(state.rootContext, rerankSpan);
879
+
880
+ state.rerankSpan = { span: rerankSpan, context: rerankContext };
881
+ }
882
+
883
+ onRerankFinish(event: RerankFinishEvent): void {
884
+ const state = this.getCallState(event.callId);
885
+ if (!state?.rerankSpan) return;
886
+
887
+ const { span } = state.rerankSpan;
888
+
889
+ span.end();
890
+ state.rerankSpan = undefined;
891
+ }
892
+
893
+ onChunk(_event: OnChunkEvent<ToolSet>): void {
894
+ // No-op: streaming chunk events are not part of the GenAI SemConv.
895
+ }
896
+
897
+ onError(error: unknown): void {
898
+ const event = error as { callId?: string; error?: unknown };
899
+ if (!event?.callId) return;
900
+
901
+ const state = this.getCallState(event.callId);
902
+ if (!state?.rootSpan) return;
903
+
904
+ const actualError = event.error ?? error;
905
+
906
+ if (state.stepSpan) {
907
+ recordSpanError(state.stepSpan, actualError);
908
+ state.stepSpan.end();
909
+ }
910
+
911
+ for (const { span: embedSpan } of state.embedSpans.values()) {
912
+ recordSpanError(embedSpan, actualError);
913
+ embedSpan.end();
914
+ }
915
+ state.embedSpans.clear();
916
+
917
+ if (state.rerankSpan) {
918
+ recordSpanError(state.rerankSpan.span, actualError);
919
+ state.rerankSpan.span.end();
920
+ state.rerankSpan = undefined;
921
+ }
922
+
923
+ recordSpanError(state.rootSpan, actualError);
924
+
925
+ state.rootSpan.end();
926
+ this.cleanupCallState(event.callId);
927
+ }
928
+ }