@ai-sdk/otel 1.0.0-beta.13 → 1.0.0-beta.130

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