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