@ai-sdk/otel 1.0.0-beta.57 → 1.0.0-beta.59

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.
@@ -6,45 +6,39 @@ import {
6
6
  context,
7
7
  Context as OpenTelemetryContext,
8
8
  Span,
9
- SpanKind,
10
9
  SpanStatusCode,
11
10
  trace,
12
11
  Tracer,
13
12
  } from '@opentelemetry/api';
14
13
  import type {
15
- EmbedFinishEvent,
16
- EmbedOnFinishEvent,
17
- EmbedOnStartEvent,
14
+ EmbeddingModelCallEndEvent,
15
+ EmbedEndEvent,
18
16
  EmbedStartEvent,
19
- ObjectOnFinishEvent,
20
- ObjectOnStartEvent,
21
- ObjectOnStepFinishEvent,
22
- ObjectOnStepStartEvent,
23
- OnChunkEvent,
24
- OnFinishEvent,
25
- OnStartEvent,
26
- OnStepFinishEvent,
27
- OnStepStartEvent,
17
+ EmbeddingModelCallStartEvent,
18
+ GenerateObjectEndEvent,
19
+ GenerateObjectStartEvent,
20
+ GenerateObjectStepEndEvent,
21
+ GenerateObjectStepStartEvent,
22
+ StreamTextChunkEvent,
23
+ GenerateTextEndEvent,
24
+ GenerateTextStartEvent,
25
+ GenerateTextStepEndEvent,
26
+ GenerateTextStepStartEvent,
28
27
  ToolExecutionEndEvent,
29
28
  ToolExecutionStartEvent,
30
29
  OutputInterface as Output,
31
- RerankFinishEvent,
32
- RerankOnFinishEvent,
33
- RerankOnStartEvent,
30
+ RerankingModelCallEndEvent,
31
+ RerankEndEvent,
34
32
  RerankStartEvent,
33
+ RerankingModelCallStartEvent,
34
+ InferTelemetryEvent,
35
35
  Telemetry,
36
36
  TelemetryOptions,
37
37
  ToolSet,
38
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';
39
+ import { assembleOperationName } from './assemble-operation-name';
40
+ import { getBaseTelemetryAttributes } from './get-base-telemetry-attributes';
41
+ import { stringifyForTelemetry } from './stringify-for-telemetry';
48
42
 
49
43
  function recordSpanError(span: Span, error: unknown): void {
50
44
  if (error instanceof Error) {
@@ -119,7 +113,7 @@ interface OtelStepStartEvent<
119
113
  TOOLS extends ToolSet = ToolSet,
120
114
  RUNTIME_CONTEXT extends AISDKContext = AISDKContext,
121
115
  OUTPUT extends Output = Output,
122
- > extends OnStepStartEvent<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
116
+ > extends GenerateTextStepStartEvent<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
123
117
  readonly promptMessages?: LanguageModelV4Prompt;
124
118
  readonly stepTools?: ReadonlyArray<Record<string, unknown>>;
125
119
  readonly stepToolChoice?: unknown;
@@ -135,14 +129,16 @@ interface CallState {
135
129
  embedSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
136
130
  rerankSpan: { span: Span; context: OpenTelemetryContext } | undefined;
137
131
  toolSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
132
+ baseTelemetryAttributes: Attributes;
138
133
  settings: Record<string, unknown>;
139
- provider: string;
140
- modelId: string;
141
134
  }
142
135
 
143
- export class GenAIOpenTelemetry implements Telemetry {
136
+ export class LegacyOpenTelemetry implements Telemetry {
144
137
  private readonly callStates = new Map<string, CallState>();
145
138
 
139
+ /**
140
+ * The tracer to use for the telemetry data.
141
+ */
146
142
  private readonly tracer: Tracer;
147
143
 
148
144
  constructor(
@@ -150,7 +146,7 @@ export class GenAIOpenTelemetry implements Telemetry {
150
146
  tracer?: Tracer;
151
147
  } = {},
152
148
  ) {
153
- this.tracer = options.tracer ?? trace.getTracer('gen_ai');
149
+ this.tracer = options.tracer ?? trace.getTracer('ai');
154
150
  }
155
151
 
156
152
  private getCallState(callId: string): CallState | undefined {
@@ -181,23 +177,23 @@ export class GenAIOpenTelemetry implements Telemetry {
181
177
 
182
178
  onStart(
183
179
  event:
184
- | OnStartEvent
185
- | ObjectOnStartEvent
186
- | EmbedOnStartEvent
187
- | RerankOnStartEvent,
180
+ | InferTelemetryEvent<GenerateTextStartEvent>
181
+ | InferTelemetryEvent<GenerateObjectStartEvent>
182
+ | InferTelemetryEvent<EmbedStartEvent>
183
+ | InferTelemetryEvent<RerankStartEvent>,
188
184
  ): void {
189
- if (event.isEnabled === false) return;
190
-
191
185
  if (
192
186
  event.operationId === 'ai.embed' ||
193
187
  event.operationId === 'ai.embedMany'
194
188
  ) {
195
- this.onEmbedOperationStart(event as EmbedOnStartEvent);
189
+ this.onEmbedOperationStart(event as InferTelemetryEvent<EmbedStartEvent>);
196
190
  return;
197
191
  }
198
192
 
199
193
  if (event.operationId === 'ai.rerank') {
200
- this.onRerankOperationStart(event as RerankOnStartEvent);
194
+ this.onRerankOperationStart(
195
+ event as InferTelemetryEvent<RerankStartEvent>,
196
+ );
201
197
  return;
202
198
  }
203
199
 
@@ -205,16 +201,19 @@ export class GenAIOpenTelemetry implements Telemetry {
205
201
  event.operationId === 'ai.generateObject' ||
206
202
  event.operationId === 'ai.streamObject'
207
203
  ) {
208
- this.onObjectOperationStart(event as ObjectOnStartEvent);
204
+ this.onObjectOperationStart(
205
+ event as InferTelemetryEvent<GenerateObjectStartEvent>,
206
+ );
209
207
  return;
210
208
  }
211
209
 
212
- this.onGenerateStart(event as OnStartEvent);
210
+ this.onGenerateStart(event as InferTelemetryEvent<GenerateTextStartEvent>);
213
211
  }
214
212
 
215
- private onGenerateStart(event: OnStartEvent): void {
213
+ private onGenerateStart(
214
+ event: InferTelemetryEvent<GenerateTextStartEvent>,
215
+ ): void {
216
216
  const telemetry: TelemetryOptions = {
217
- isEnabled: event.isEnabled,
218
217
  recordInputs: event.recordInputs,
219
218
  recordOutputs: event.recordOutputs,
220
219
  functionId: event.functionId,
@@ -232,46 +231,31 @@ export class GenAIOpenTelemetry implements Telemetry {
232
231
  maxRetries: event.maxRetries,
233
232
  };
234
233
 
235
- const providerName = mapProviderName(event.provider);
236
- const operationName = mapOperationName(event.operationId);
234
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
235
+ model: { provider: event.provider, modelId: event.modelId },
236
+ headers: event.headers,
237
+ settings,
238
+ context: event.runtimeContext as Record<string, unknown> | undefined,
239
+ });
237
240
 
238
241
  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': {
242
+ ...assembleOperationName({
243
+ operationId: event.operationId,
244
+ telemetry,
245
+ }),
246
+ ...baseTelemetryAttributes,
247
+ 'ai.model.provider': event.provider,
248
+ 'ai.model.id': event.modelId,
249
+ 'ai.prompt': {
260
250
  input: () =>
261
- JSON.stringify(
262
- formatModelMessages({
263
- prompt: event.prompt,
264
- messages: event.messages,
265
- }),
266
- ),
251
+ JSON.stringify({
252
+ system: event.system,
253
+ messages: event.messages,
254
+ }),
267
255
  },
268
256
  });
269
257
 
270
- const spanName = `${operationName} ${event.modelId}`;
271
- const rootSpan = this.tracer.startSpan(spanName, {
272
- attributes,
273
- kind: SpanKind.INTERNAL,
274
- });
258
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
275
259
  const rootContext = trace.setSpan(context.active(), rootSpan);
276
260
 
277
261
  this.callStates.set(event.callId, {
@@ -284,15 +268,15 @@ export class GenAIOpenTelemetry implements Telemetry {
284
268
  embedSpans: new Map(),
285
269
  rerankSpan: undefined,
286
270
  toolSpans: new Map(),
271
+ baseTelemetryAttributes,
287
272
  settings,
288
- provider: event.provider,
289
- modelId: event.modelId,
290
273
  });
291
274
  }
292
275
 
293
- private onObjectOperationStart(event: ObjectOnStartEvent): void {
276
+ private onObjectOperationStart(
277
+ event: InferTelemetryEvent<GenerateObjectStartEvent>,
278
+ ): void {
294
279
  const telemetry: TelemetryOptions = {
295
- isEnabled: event.isEnabled,
296
280
  recordInputs: event.recordInputs,
297
281
  recordOutputs: event.recordOutputs,
298
282
  functionId: event.functionId,
@@ -309,46 +293,36 @@ export class GenAIOpenTelemetry implements Telemetry {
309
293
  maxRetries: event.maxRetries,
310
294
  };
311
295
 
312
- const providerName = mapProviderName(event.provider);
313
- const operationName = mapOperationName(event.operationId);
296
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
297
+ model: { provider: event.provider, modelId: event.modelId },
298
+ headers: event.headers,
299
+ settings,
300
+ context: undefined,
301
+ });
314
302
 
315
303
  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': {
304
+ ...assembleOperationName({
305
+ operationId: event.operationId,
306
+ telemetry,
307
+ }),
308
+ ...baseTelemetryAttributes,
309
+ 'ai.prompt': {
337
310
  input: () =>
338
- JSON.stringify(
339
- formatModelMessages({
340
- prompt: event.prompt,
341
- messages: event.messages,
342
- }),
343
- ),
311
+ JSON.stringify({
312
+ system: event.system,
313
+ prompt: event.prompt,
314
+ messages: event.messages,
315
+ }),
344
316
  },
317
+ 'ai.schema': event.schema
318
+ ? { input: () => JSON.stringify(event.schema) }
319
+ : undefined,
320
+ 'ai.schema.name': event.schemaName,
321
+ 'ai.schema.description': event.schemaDescription,
322
+ 'ai.settings.output': event.output,
345
323
  });
346
324
 
347
- const spanName = `${operationName} ${event.modelId}`;
348
- const rootSpan = this.tracer.startSpan(spanName, {
349
- attributes,
350
- kind: SpanKind.INTERNAL,
351
- });
325
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
352
326
  const rootContext = trace.setSpan(context.active(), rootSpan);
353
327
 
354
328
  this.callStates.set(event.callId, {
@@ -361,25 +335,38 @@ export class GenAIOpenTelemetry implements Telemetry {
361
335
  embedSpans: new Map(),
362
336
  rerankSpan: undefined,
363
337
  toolSpans: new Map(),
338
+ baseTelemetryAttributes,
364
339
  settings,
365
- provider: event.provider,
366
- modelId: event.modelId,
367
340
  });
368
341
  }
369
342
 
370
343
  /** @deprecated */
371
- onObjectStepStart(event: ObjectOnStepStartEvent): void {
344
+ onObjectStepStart(event: GenerateObjectStepStartEvent): void {
372
345
  const state = this.getCallState(event.callId);
373
346
  if (!state?.rootSpan || !state.rootContext) return;
374
347
 
375
348
  const { telemetry } = state;
376
- const providerName = mapProviderName(event.provider);
349
+
350
+ const stepOperationId =
351
+ state.operationId === 'ai.streamObject'
352
+ ? 'ai.streamObject.doStream'
353
+ : 'ai.generateObject.doGenerate';
377
354
 
378
355
  const attributes = selectAttributes(telemetry, {
379
- 'gen_ai.operation.name': 'chat',
380
- 'gen_ai.provider.name': providerName,
356
+ ...assembleOperationName({
357
+ operationId: stepOperationId,
358
+ telemetry,
359
+ }),
360
+ ...state.baseTelemetryAttributes,
361
+ 'ai.prompt.messages': {
362
+ input: () =>
363
+ event.promptMessages
364
+ ? stringifyForTelemetry(event.promptMessages)
365
+ : undefined,
366
+ },
367
+
368
+ 'gen_ai.system': event.provider,
381
369
  'gen_ai.request.model': event.modelId,
382
- 'gen_ai.output.type': 'json',
383
370
  'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
384
371
  | number
385
372
  | undefined,
@@ -394,25 +381,18 @@ export class GenAIOpenTelemetry implements Telemetry {
394
381
  | undefined,
395
382
  'gen_ai.request.top_k': state.settings.topK as number | undefined,
396
383
  '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
384
  });
404
385
 
405
- const spanName = `chat ${event.modelId}`;
406
386
  state.stepSpan = this.tracer.startSpan(
407
- spanName,
408
- { attributes, kind: SpanKind.CLIENT },
387
+ stepOperationId,
388
+ { attributes },
409
389
  state.rootContext,
410
390
  );
411
391
  state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
412
392
  }
413
393
 
414
394
  /** @deprecated */
415
- onObjectStepFinish(event: ObjectOnStepFinishEvent): void {
395
+ onObjectStepFinish(event: GenerateObjectStepEndEvent): void {
416
396
  const state = this.getCallState(event.callId);
417
397
  if (!state?.stepSpan) return;
418
398
 
@@ -420,37 +400,55 @@ export class GenAIOpenTelemetry implements Telemetry {
420
400
 
421
401
  state.stepSpan.setAttributes(
422
402
  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': {
403
+ 'ai.response.finishReason': event.finishReason,
404
+ 'ai.response.object': {
430
405
  output: () => {
431
406
  try {
432
- return JSON.stringify(
433
- formatObjectOutputMessages({
434
- objectText: event.objectText,
435
- finishReason: event.finishReason,
436
- }),
437
- );
407
+ return JSON.stringify(JSON.parse(event.objectText));
438
408
  } catch {
439
409
  return event.objectText;
440
410
  }
441
411
  },
442
412
  },
413
+ 'ai.response.id': event.response.id,
414
+ 'ai.response.model': event.response.modelId,
415
+ 'ai.response.timestamp': event.response.timestamp.toISOString(),
416
+ 'ai.response.providerMetadata': event.providerMetadata
417
+ ? JSON.stringify(event.providerMetadata)
418
+ : undefined,
419
+
420
+ 'ai.usage.inputTokens': event.usage.inputTokens,
421
+ 'ai.usage.outputTokens': event.usage.outputTokens,
422
+ 'ai.usage.totalTokens': event.usage.totalTokens,
423
+ 'ai.usage.reasoningTokens': event.usage.reasoningTokens,
424
+ 'ai.usage.cachedInputTokens': event.usage.cachedInputTokens,
425
+
426
+ 'gen_ai.response.finish_reasons': [event.finishReason],
427
+ 'gen_ai.response.id': event.response.id,
428
+ 'gen_ai.response.model': event.response.modelId,
429
+ 'gen_ai.usage.input_tokens': event.usage.inputTokens,
430
+ 'gen_ai.usage.output_tokens': event.usage.outputTokens,
443
431
  }),
444
432
  );
445
433
 
434
+ if (event.msToFirstChunk != null) {
435
+ state.stepSpan.addEvent('ai.stream.firstChunk', {
436
+ 'ai.stream.msToFirstChunk': event.msToFirstChunk,
437
+ });
438
+ state.stepSpan.setAttributes({
439
+ 'ai.stream.msToFirstChunk': event.msToFirstChunk,
440
+ });
441
+ }
442
+
446
443
  state.stepSpan.end();
447
444
  state.stepSpan = undefined;
448
445
  state.stepContext = undefined;
449
446
  }
450
447
 
451
- private onEmbedOperationStart(event: EmbedOnStartEvent): void {
448
+ private onEmbedOperationStart(
449
+ event: InferTelemetryEvent<EmbedStartEvent>,
450
+ ): void {
452
451
  const telemetry: TelemetryOptions = {
453
- isEnabled: event.isEnabled,
454
452
  recordInputs: event.recordInputs,
455
453
  recordOutputs: event.recordOutputs,
456
454
  functionId: event.functionId,
@@ -460,19 +458,36 @@ export class GenAIOpenTelemetry implements Telemetry {
460
458
  maxRetries: event.maxRetries,
461
459
  };
462
460
 
463
- const providerName = mapProviderName(event.provider);
461
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
462
+ model: { provider: event.provider, modelId: event.modelId },
463
+ headers: event.headers,
464
+ settings,
465
+ context: undefined,
466
+ });
467
+
468
+ const value = event.value;
469
+ const isMany = event.operationId === 'ai.embedMany';
464
470
 
465
471
  const attributes = selectAttributes(telemetry, {
466
- 'gen_ai.operation.name': 'embeddings',
467
- 'gen_ai.provider.name': providerName,
468
- 'gen_ai.request.model': event.modelId,
472
+ ...assembleOperationName({
473
+ operationId: event.operationId,
474
+ telemetry,
475
+ }),
476
+ ...baseTelemetryAttributes,
477
+ ...(isMany
478
+ ? {
479
+ 'ai.values': {
480
+ input: () => (value as string[]).map(v => JSON.stringify(v)),
481
+ },
482
+ }
483
+ : {
484
+ 'ai.value': {
485
+ input: () => JSON.stringify(value),
486
+ },
487
+ }),
469
488
  });
470
489
 
471
- const spanName = `embeddings ${event.modelId}`;
472
- const rootSpan = this.tracer.startSpan(spanName, {
473
- attributes,
474
- kind: SpanKind.CLIENT,
475
- });
490
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
476
491
  const rootContext = trace.setSpan(context.active(), rootSpan);
477
492
 
478
493
  this.callStates.set(event.callId, {
@@ -485,9 +500,8 @@ export class GenAIOpenTelemetry implements Telemetry {
485
500
  embedSpans: new Map(),
486
501
  rerankSpan: undefined,
487
502
  toolSpans: new Map(),
503
+ baseTelemetryAttributes,
488
504
  settings,
489
- provider: event.provider,
490
- modelId: event.modelId,
491
505
  });
492
506
  }
493
507
 
@@ -496,11 +510,38 @@ export class GenAIOpenTelemetry implements Telemetry {
496
510
  if (!state?.rootSpan || !state.rootContext) return;
497
511
 
498
512
  const { telemetry } = state;
499
- const providerName = mapProviderName(event.provider);
513
+
514
+ const stepOperationId =
515
+ state.operationId === 'ai.streamText'
516
+ ? 'ai.streamText.doStream'
517
+ : 'ai.generateText.doGenerate';
500
518
 
501
519
  const attributes = selectAttributes(telemetry, {
502
- 'gen_ai.operation.name': 'chat',
503
- 'gen_ai.provider.name': providerName,
520
+ ...assembleOperationName({
521
+ operationId: stepOperationId,
522
+ telemetry,
523
+ }),
524
+ ...state.baseTelemetryAttributes,
525
+ 'ai.model.provider': event.provider,
526
+ 'ai.model.id': event.modelId,
527
+
528
+ 'ai.prompt.messages': {
529
+ input: () =>
530
+ event.promptMessages
531
+ ? stringifyForTelemetry(event.promptMessages)
532
+ : undefined,
533
+ },
534
+ 'ai.prompt.tools': {
535
+ input: () => event.stepTools?.map(tool => JSON.stringify(tool)),
536
+ },
537
+ 'ai.prompt.toolChoice': {
538
+ input: () =>
539
+ event.stepToolChoice != null
540
+ ? JSON.stringify(event.stepToolChoice)
541
+ : undefined,
542
+ },
543
+
544
+ 'gen_ai.system': event.provider,
504
545
  'gen_ai.request.model': event.modelId,
505
546
  'gen_ai.request.frequency_penalty': state.settings.frequencyPenalty as
506
547
  | number
@@ -519,22 +560,11 @@ export class GenAIOpenTelemetry implements Telemetry {
519
560
  | undefined,
520
561
  'gen_ai.request.top_k': state.settings.topK as number | undefined,
521
562
  '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
563
  });
533
564
 
534
- const spanName = `chat ${event.modelId}`;
535
565
  state.stepSpan = this.tracer.startSpan(
536
- spanName,
537
- { attributes, kind: SpanKind.CLIENT },
566
+ stepOperationId,
567
+ { attributes },
538
568
  state.rootContext,
539
569
  );
540
570
  state.stepContext = trace.setSpan(state.rootContext, state.stepSpan);
@@ -548,19 +578,20 @@ export class GenAIOpenTelemetry implements Telemetry {
548
578
  const { toolCall } = event;
549
579
 
550
580
  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),
581
+ ...assembleOperationName({
582
+ operationId: 'ai.toolCall',
583
+ telemetry,
584
+ }),
585
+ 'ai.toolCall.name': toolCall.toolName,
586
+ 'ai.toolCall.id': toolCall.toolCallId,
587
+ 'ai.toolCall.args': {
588
+ output: () => JSON.stringify(toolCall.input),
557
589
  },
558
590
  });
559
591
 
560
- const spanName = `execute_tool ${toolCall.toolName}`;
561
592
  const toolSpan = this.tracer.startSpan(
562
- spanName,
563
- { attributes, kind: SpanKind.INTERNAL },
593
+ 'ai.toolCall',
594
+ { attributes },
564
595
  state.stepContext,
565
596
  );
566
597
  const toolContext = trace.setSpan(state.stepContext, toolSpan);
@@ -581,12 +612,13 @@ export class GenAIOpenTelemetry implements Telemetry {
581
612
  const { span } = toolSpanEntry;
582
613
  const { telemetry } = state;
583
614
 
584
- if (event.success) {
615
+ const { toolOutput } = event;
616
+ if (toolOutput.type === 'tool-result') {
585
617
  try {
586
618
  span.setAttributes(
587
619
  selectAttributes(telemetry, {
588
- 'gen_ai.tool.call.result': {
589
- output: () => JSON.stringify(event.output),
620
+ 'ai.toolCall.result': {
621
+ output: () => JSON.stringify(toolOutput.output),
590
622
  },
591
623
  }),
592
624
  );
@@ -594,14 +626,14 @@ export class GenAIOpenTelemetry implements Telemetry {
594
626
  // JSON.stringify might fail for non-serializable results
595
627
  }
596
628
  } else {
597
- recordSpanError(span, event.error);
629
+ recordSpanError(span, toolOutput.error);
598
630
  }
599
631
 
600
632
  span.end();
601
633
  state.toolSpans.delete(event.toolCall.toolCallId);
602
634
  }
603
635
 
604
- onStepFinish(event: OnStepFinishEvent<ToolSet>): void {
636
+ onStepFinish(event: GenerateTextStepEndEvent<ToolSet>): void {
605
637
  const state = this.getCallState(event.callId);
606
638
  if (!state?.stepSpan) return;
607
639
 
@@ -609,28 +641,71 @@ export class GenAIOpenTelemetry implements Telemetry {
609
641
 
610
642
  state.stepSpan.setAttributes(
611
643
  selectAttributes(telemetry, {
644
+ 'ai.response.finishReason': event.finishReason,
645
+ 'ai.response.text': {
646
+ output: () => event.text ?? undefined,
647
+ },
648
+ 'ai.response.reasoning': {
649
+ output: () =>
650
+ event.reasoning.length > 0
651
+ ? event.reasoning
652
+ .filter(part => 'text' in part)
653
+ .map(part => part.text)
654
+ .join('\n')
655
+ : undefined,
656
+ },
657
+ 'ai.response.toolCalls': {
658
+ output: () =>
659
+ event.toolCalls.length > 0
660
+ ? JSON.stringify(
661
+ event.toolCalls.map(toolCall => ({
662
+ toolCallId: toolCall.toolCallId,
663
+ toolName: toolCall.toolName,
664
+ input: toolCall.input,
665
+ })),
666
+ )
667
+ : undefined,
668
+ },
669
+ 'ai.response.files': {
670
+ output: () =>
671
+ event.files.length > 0
672
+ ? JSON.stringify(
673
+ event.files.map(file => ({
674
+ type: 'file',
675
+ mediaType: file.mediaType,
676
+ data: file.base64,
677
+ })),
678
+ )
679
+ : undefined,
680
+ },
681
+ 'ai.response.id': event.response.id,
682
+ 'ai.response.model': event.response.modelId,
683
+ 'ai.response.timestamp': event.response.timestamp.toISOString(),
684
+ 'ai.response.providerMetadata': event.providerMetadata
685
+ ? JSON.stringify(event.providerMetadata)
686
+ : undefined,
687
+
688
+ 'ai.usage.inputTokens': event.usage.inputTokens,
689
+ 'ai.usage.outputTokens': event.usage.outputTokens,
690
+ 'ai.usage.totalTokens': event.usage.totalTokens,
691
+ 'ai.usage.reasoningTokens': event.usage.reasoningTokens,
692
+ 'ai.usage.cachedInputTokens': event.usage.cachedInputTokens,
693
+ 'ai.usage.inputTokenDetails.noCacheTokens':
694
+ event.usage.inputTokenDetails?.noCacheTokens,
695
+ 'ai.usage.inputTokenDetails.cacheReadTokens':
696
+ event.usage.inputTokenDetails?.cacheReadTokens,
697
+ 'ai.usage.inputTokenDetails.cacheWriteTokens':
698
+ event.usage.inputTokenDetails?.cacheWriteTokens,
699
+ 'ai.usage.outputTokenDetails.textTokens':
700
+ event.usage.outputTokenDetails?.textTokens,
701
+ 'ai.usage.outputTokenDetails.reasoningTokens':
702
+ event.usage.outputTokenDetails?.reasoningTokens,
703
+
612
704
  'gen_ai.response.finish_reasons': [event.finishReason],
613
705
  'gen_ai.response.id': event.response.id,
614
706
  'gen_ai.response.model': event.response.modelId,
615
707
  'gen_ai.usage.input_tokens': event.usage.inputTokens,
616
708
  '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
709
  }),
635
710
  );
636
711
 
@@ -641,10 +716,10 @@ export class GenAIOpenTelemetry implements Telemetry {
641
716
 
642
717
  onFinish(
643
718
  event:
644
- | OnFinishEvent<ToolSet>
645
- | ObjectOnFinishEvent<unknown>
646
- | EmbedOnFinishEvent
647
- | RerankOnFinishEvent,
719
+ | GenerateTextEndEvent<ToolSet>
720
+ | GenerateObjectEndEvent<unknown>
721
+ | EmbedEndEvent
722
+ | RerankEndEvent,
648
723
  ): void {
649
724
  const state = this.getCallState(event.callId);
650
725
  if (!state?.rootSpan) return;
@@ -653,12 +728,12 @@ export class GenAIOpenTelemetry implements Telemetry {
653
728
  state.operationId === 'ai.embed' ||
654
729
  state.operationId === 'ai.embedMany'
655
730
  ) {
656
- this.onEmbedOperationFinish(event as EmbedOnFinishEvent);
731
+ this.onEmbedOperationFinish(event as EmbedEndEvent);
657
732
  return;
658
733
  }
659
734
 
660
735
  if (state.operationId === 'ai.rerank') {
661
- this.onRerankOperationFinish(event as RerankOnFinishEvent);
736
+ this.onRerankOperationFinish(event as RerankEndEvent);
662
737
  return;
663
738
  }
664
739
 
@@ -666,14 +741,14 @@ export class GenAIOpenTelemetry implements Telemetry {
666
741
  state.operationId === 'ai.generateObject' ||
667
742
  state.operationId === 'ai.streamObject'
668
743
  ) {
669
- this.onObjectOperationFinish(event as ObjectOnFinishEvent<unknown>);
744
+ this.onObjectOperationFinish(event as GenerateObjectEndEvent<unknown>);
670
745
  return;
671
746
  }
672
747
 
673
- this.onGenerateFinish(event as OnFinishEvent<ToolSet>);
748
+ this.onGenerateFinish(event as GenerateTextEndEvent<ToolSet>);
674
749
  }
675
750
 
676
- private onGenerateFinish(event: OnFinishEvent<ToolSet>): void {
751
+ private onGenerateFinish(event: GenerateTextEndEvent<ToolSet>): void {
677
752
  const state = this.getCallState(event.callId);
678
753
  if (!state?.rootSpan) return;
679
754
 
@@ -681,26 +756,62 @@ export class GenAIOpenTelemetry implements Telemetry {
681
756
 
682
757
  state.rootSpan.setAttributes(
683
758
  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': {
759
+ 'ai.response.finishReason': event.finishReason,
760
+ 'ai.response.text': {
761
+ output: () => event.text ?? undefined,
762
+ },
763
+ 'ai.response.reasoning': {
764
+ output: () =>
765
+ event.reasoning.length > 0
766
+ ? event.reasoning
767
+ .filter(part => 'text' in part)
768
+ .map(part => part.text)
769
+ .join('\n')
770
+ : undefined,
771
+ },
772
+ 'ai.response.toolCalls': {
773
+ output: () =>
774
+ event.toolCalls.length > 0
775
+ ? JSON.stringify(
776
+ event.toolCalls.map(toolCall => ({
777
+ toolCallId: toolCall.toolCallId,
778
+ toolName: toolCall.toolName,
779
+ input: toolCall.input,
780
+ })),
781
+ )
782
+ : undefined,
783
+ },
784
+ 'ai.response.files': {
693
785
  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
- ),
786
+ event.files.length > 0
787
+ ? JSON.stringify(
788
+ event.files.map(file => ({
789
+ type: 'file',
790
+ mediaType: file.mediaType,
791
+ data: file.base64,
792
+ })),
793
+ )
794
+ : undefined,
703
795
  },
796
+ 'ai.response.providerMetadata': event.providerMetadata
797
+ ? JSON.stringify(event.providerMetadata)
798
+ : undefined,
799
+
800
+ 'ai.usage.inputTokens': event.totalUsage.inputTokens,
801
+ 'ai.usage.outputTokens': event.totalUsage.outputTokens,
802
+ 'ai.usage.totalTokens': event.totalUsage.totalTokens,
803
+ 'ai.usage.reasoningTokens': event.totalUsage.reasoningTokens,
804
+ 'ai.usage.cachedInputTokens': event.totalUsage.cachedInputTokens,
805
+ 'ai.usage.inputTokenDetails.noCacheTokens':
806
+ event.totalUsage.inputTokenDetails?.noCacheTokens,
807
+ 'ai.usage.inputTokenDetails.cacheReadTokens':
808
+ event.totalUsage.inputTokenDetails?.cacheReadTokens,
809
+ 'ai.usage.inputTokenDetails.cacheWriteTokens':
810
+ event.totalUsage.inputTokenDetails?.cacheWriteTokens,
811
+ 'ai.usage.outputTokenDetails.textTokens':
812
+ event.totalUsage.outputTokenDetails?.textTokens,
813
+ 'ai.usage.outputTokenDetails.reasoningTokens':
814
+ event.totalUsage.outputTokenDetails?.reasoningTokens,
704
815
  }),
705
816
  );
706
817
 
@@ -708,7 +819,9 @@ export class GenAIOpenTelemetry implements Telemetry {
708
819
  this.cleanupCallState(event.callId);
709
820
  }
710
821
 
711
- private onObjectOperationFinish(event: ObjectOnFinishEvent<unknown>): void {
822
+ private onObjectOperationFinish(
823
+ event: GenerateObjectEndEvent<unknown>,
824
+ ): void {
712
825
  const state = this.getCallState(event.callId);
713
826
  if (!state?.rootSpan) return;
714
827
 
@@ -716,21 +829,20 @@ export class GenAIOpenTelemetry implements Telemetry {
716
829
 
717
830
  state.rootSpan.setAttributes(
718
831
  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': {
832
+ 'ai.response.finishReason': event.finishReason,
833
+ 'ai.response.object': {
724
834
  output: () =>
725
- event.object != null
726
- ? JSON.stringify(
727
- formatObjectOutputMessages({
728
- objectText: JSON.stringify(event.object),
729
- finishReason: event.finishReason,
730
- }),
731
- )
732
- : undefined,
835
+ event.object != null ? JSON.stringify(event.object) : undefined,
733
836
  },
837
+ 'ai.response.providerMetadata': event.providerMetadata
838
+ ? JSON.stringify(event.providerMetadata)
839
+ : undefined,
840
+
841
+ 'ai.usage.inputTokens': event.usage.inputTokens,
842
+ 'ai.usage.outputTokens': event.usage.outputTokens,
843
+ 'ai.usage.totalTokens': event.usage.totalTokens,
844
+ 'ai.usage.reasoningTokens': event.usage.reasoningTokens,
845
+ 'ai.usage.cachedInputTokens': event.usage.cachedInputTokens,
734
846
  }),
735
847
  );
736
848
 
@@ -738,15 +850,28 @@ export class GenAIOpenTelemetry implements Telemetry {
738
850
  this.cleanupCallState(event.callId);
739
851
  }
740
852
 
741
- private onEmbedOperationFinish(event: EmbedOnFinishEvent): void {
853
+ private onEmbedOperationFinish(event: EmbedEndEvent): void {
742
854
  const state = this.getCallState(event.callId);
743
855
  if (!state?.rootSpan) return;
744
856
 
745
857
  const { telemetry } = state;
858
+ const isMany = state.operationId === 'ai.embedMany';
746
859
 
747
860
  state.rootSpan.setAttributes(
748
861
  selectAttributes(telemetry, {
749
- 'gen_ai.usage.input_tokens': event.usage.tokens,
862
+ ...(isMany
863
+ ? {
864
+ 'ai.embeddings': {
865
+ output: () =>
866
+ (event.embedding as number[][]).map(e => JSON.stringify(e)),
867
+ },
868
+ }
869
+ : {
870
+ 'ai.embedding': {
871
+ output: () => JSON.stringify(event.embedding),
872
+ },
873
+ }),
874
+ 'ai.usage.tokens': event.usage.tokens,
750
875
  }),
751
876
  );
752
877
 
@@ -754,23 +879,26 @@ export class GenAIOpenTelemetry implements Telemetry {
754
879
  this.cleanupCallState(event.callId);
755
880
  }
756
881
 
757
- onEmbedStart(event: EmbedStartEvent): void {
882
+ onEmbedStart(event: EmbeddingModelCallStartEvent): void {
758
883
  const state = this.getCallState(event.callId);
759
884
  if (!state?.rootSpan || !state.rootContext) return;
760
885
 
761
886
  const { telemetry } = state;
762
- const providerName = mapProviderName(state.provider);
763
887
 
764
888
  const attributes = selectAttributes(telemetry, {
765
- 'gen_ai.operation.name': 'embeddings',
766
- 'gen_ai.provider.name': providerName,
767
- 'gen_ai.request.model': state.modelId,
889
+ ...assembleOperationName({
890
+ operationId: event.operationId,
891
+ telemetry,
892
+ }),
893
+ ...state.baseTelemetryAttributes,
894
+ 'ai.values': {
895
+ input: () => event.values.map(v => JSON.stringify(v)),
896
+ },
768
897
  });
769
898
 
770
- const spanName = `embeddings ${state.modelId}`;
771
899
  const embedSpan = this.tracer.startSpan(
772
- spanName,
773
- { attributes, kind: SpanKind.CLIENT },
900
+ event.operationId,
901
+ { attributes },
774
902
  state.rootContext,
775
903
  );
776
904
  const embedContext = trace.setSpan(state.rootContext, embedSpan);
@@ -781,7 +909,7 @@ export class GenAIOpenTelemetry implements Telemetry {
781
909
  });
782
910
  }
783
911
 
784
- onEmbedFinish(event: EmbedFinishEvent): void {
912
+ onEmbedFinish(event: EmbeddingModelCallEndEvent): void {
785
913
  const state = this.getCallState(event.callId);
786
914
  if (!state) return;
787
915
 
@@ -793,7 +921,11 @@ export class GenAIOpenTelemetry implements Telemetry {
793
921
 
794
922
  span.setAttributes(
795
923
  selectAttributes(telemetry, {
796
- 'gen_ai.usage.input_tokens': event.usage.tokens,
924
+ 'ai.embeddings': {
925
+ output: () =>
926
+ event.embeddings.map(embedding => JSON.stringify(embedding)),
927
+ },
928
+ 'ai.usage.tokens': event.usage.tokens,
797
929
  }),
798
930
  );
799
931
 
@@ -801,9 +933,10 @@ export class GenAIOpenTelemetry implements Telemetry {
801
933
  state.embedSpans.delete(event.embedCallId);
802
934
  }
803
935
 
804
- private onRerankOperationStart(event: RerankOnStartEvent): void {
936
+ private onRerankOperationStart(
937
+ event: InferTelemetryEvent<RerankStartEvent>,
938
+ ): void {
805
939
  const telemetry: TelemetryOptions = {
806
- isEnabled: event.isEnabled,
807
940
  recordInputs: event.recordInputs,
808
941
  recordOutputs: event.recordOutputs,
809
942
  functionId: event.functionId,
@@ -813,19 +946,25 @@ export class GenAIOpenTelemetry implements Telemetry {
813
946
  maxRetries: event.maxRetries,
814
947
  };
815
948
 
816
- const providerName = mapProviderName(event.provider);
949
+ const baseTelemetryAttributes = getBaseTelemetryAttributes({
950
+ model: { provider: event.provider, modelId: event.modelId },
951
+ headers: event.headers,
952
+ settings,
953
+ context: undefined,
954
+ });
817
955
 
818
956
  const attributes = selectAttributes(telemetry, {
819
- 'gen_ai.operation.name': 'rerank',
820
- 'gen_ai.provider.name': providerName,
821
- 'gen_ai.request.model': event.modelId,
957
+ ...assembleOperationName({
958
+ operationId: event.operationId,
959
+ telemetry,
960
+ }),
961
+ ...baseTelemetryAttributes,
962
+ 'ai.documents': {
963
+ input: () => event.documents.map(d => JSON.stringify(d)),
964
+ },
822
965
  });
823
966
 
824
- const spanName = `rerank ${event.modelId}`;
825
- const rootSpan = this.tracer.startSpan(spanName, {
826
- attributes,
827
- kind: SpanKind.CLIENT,
828
- });
967
+ const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
829
968
  const rootContext = trace.setSpan(context.active(), rootSpan);
830
969
 
831
970
  this.callStates.set(event.callId, {
@@ -838,13 +977,12 @@ export class GenAIOpenTelemetry implements Telemetry {
838
977
  embedSpans: new Map(),
839
978
  rerankSpan: undefined,
840
979
  toolSpans: new Map(),
980
+ baseTelemetryAttributes,
841
981
  settings,
842
- provider: event.provider,
843
- modelId: event.modelId,
844
982
  });
845
983
  }
846
984
 
847
- private onRerankOperationFinish(event: RerankOnFinishEvent): void {
985
+ private onRerankOperationFinish(event: RerankEndEvent): void {
848
986
  const state = this.getCallState(event.callId);
849
987
  if (!state?.rootSpan) return;
850
988
 
@@ -852,23 +990,26 @@ export class GenAIOpenTelemetry implements Telemetry {
852
990
  this.cleanupCallState(event.callId);
853
991
  }
854
992
 
855
- onRerankStart(event: RerankStartEvent): void {
993
+ onRerankStart(event: RerankingModelCallStartEvent): void {
856
994
  const state = this.getCallState(event.callId);
857
995
  if (!state?.rootSpan || !state.rootContext) return;
858
996
 
859
997
  const { telemetry } = state;
860
- const providerName = mapProviderName(state.provider);
861
998
 
862
999
  const attributes = selectAttributes(telemetry, {
863
- 'gen_ai.operation.name': 'rerank',
864
- 'gen_ai.provider.name': providerName,
865
- 'gen_ai.request.model': state.modelId,
1000
+ ...assembleOperationName({
1001
+ operationId: event.operationId,
1002
+ telemetry,
1003
+ }),
1004
+ ...state.baseTelemetryAttributes,
1005
+ 'ai.documents': {
1006
+ input: () => event.documents.map(d => JSON.stringify(d)),
1007
+ },
866
1008
  });
867
1009
 
868
- const spanName = `rerank ${state.modelId}`;
869
1010
  const rerankSpan = this.tracer.startSpan(
870
- spanName,
871
- { attributes, kind: SpanKind.CLIENT },
1011
+ event.operationId,
1012
+ { attributes },
872
1013
  state.rootContext,
873
1014
  );
874
1015
  const rerankContext = trace.setSpan(state.rootContext, rerankSpan);
@@ -876,18 +1017,57 @@ export class GenAIOpenTelemetry implements Telemetry {
876
1017
  state.rerankSpan = { span: rerankSpan, context: rerankContext };
877
1018
  }
878
1019
 
879
- onRerankFinish(event: RerankFinishEvent): void {
1020
+ onRerankFinish(event: RerankingModelCallEndEvent): void {
880
1021
  const state = this.getCallState(event.callId);
881
1022
  if (!state?.rerankSpan) return;
882
1023
 
883
1024
  const { span } = state.rerankSpan;
1025
+ const { telemetry } = state;
1026
+
1027
+ span.setAttributes(
1028
+ selectAttributes(telemetry, {
1029
+ 'ai.ranking.type': event.documentsType,
1030
+ 'ai.ranking': {
1031
+ output: () => event.ranking.map(r => JSON.stringify(r)),
1032
+ },
1033
+ }),
1034
+ );
884
1035
 
885
1036
  span.end();
886
1037
  state.rerankSpan = undefined;
887
1038
  }
888
1039
 
889
- onChunk(_event: OnChunkEvent<ToolSet>): void {
890
- // No-op: streaming chunk events are not part of the GenAI SemConv.
1040
+ onChunk(event: StreamTextChunkEvent<ToolSet>): void {
1041
+ const chunk = event.chunk as {
1042
+ type: string;
1043
+ callId?: unknown;
1044
+ attributes?: unknown;
1045
+ };
1046
+
1047
+ if (typeof chunk.callId !== 'string') {
1048
+ return;
1049
+ }
1050
+
1051
+ if (
1052
+ chunk.type !== 'ai.stream.firstChunk' &&
1053
+ chunk.type !== 'ai.stream.finish'
1054
+ ) {
1055
+ return;
1056
+ }
1057
+
1058
+ const state = this.getCallState(chunk.callId);
1059
+ if (!state?.stepSpan) return;
1060
+
1061
+ const attributes = Object.fromEntries(
1062
+ Object.entries(
1063
+ (chunk.attributes as Record<string, unknown>) ?? {},
1064
+ ).filter(([, value]) => value != null),
1065
+ ) as Attributes;
1066
+
1067
+ state.stepSpan.addEvent(chunk.type, attributes);
1068
+ if (Object.keys(attributes).length > 0) {
1069
+ state.stepSpan.setAttributes(attributes);
1070
+ }
891
1071
  }
892
1072
 
893
1073
  onError(error: unknown): void {