1- import { ReadableSpan } from "@opentelemetry/sdk-trace-node" ;
1+ import { ReadableSpan , Span } from "@opentelemetry/sdk-trace-node" ;
22import { SpanAttributes } from "@traceloop/ai-semantic-conventions" ;
33
4+ const AI_GENERATE_TEXT = "ai.generateText" ;
45const AI_GENERATE_TEXT_DO_GENERATE = "ai.generateText.doGenerate" ;
56const AI_GENERATE_OBJECT_DO_GENERATE = "ai.generateObject.doGenerate" ;
67const AI_STREAM_TEXT_DO_STREAM = "ai.streamText.doStream" ;
78const HANDLED_SPAN_NAMES : Record < string , string > = {
8- [ AI_GENERATE_TEXT_DO_GENERATE ] : "ai.generateText.generate" ,
9- [ AI_GENERATE_OBJECT_DO_GENERATE ] : "ai.generateObject.generate" ,
10- [ AI_STREAM_TEXT_DO_STREAM ] : "ai.streamText.stream" ,
9+ [ AI_GENERATE_TEXT ] : "run.ai" ,
10+ [ AI_GENERATE_TEXT_DO_GENERATE ] : "text.generate" ,
11+ [ AI_GENERATE_OBJECT_DO_GENERATE ] : "object.generate" ,
12+ [ AI_STREAM_TEXT_DO_STREAM ] : "text.stream" ,
1113} ;
1214
15+ const TOOL_SPAN_NAME = "ai.toolCall" ;
16+
1317const AI_RESPONSE_TEXT = "ai.response.text" ;
1418const AI_RESPONSE_OBJECT = "ai.response.object" ;
1519const AI_RESPONSE_TOOL_CALLS = "ai.response.toolCalls" ;
@@ -19,6 +23,7 @@ const AI_USAGE_PROMPT_TOKENS = "ai.usage.promptTokens";
1923const AI_USAGE_COMPLETION_TOKENS = "ai.usage.completionTokens" ;
2024const AI_MODEL_PROVIDER = "ai.model.provider" ;
2125const AI_PROMPT_TOOLS = "ai.prompt.tools" ;
26+ const AI_TELEMETRY_METADATA_PREFIX = "ai.telemetry.metadata." ;
2227const TYPE_TEXT = "text" ;
2328const TYPE_TOOL_CALL = "tool_call" ;
2429const ROLE_ASSISTANT = "assistant" ;
@@ -47,14 +52,6 @@ const VENDOR_MAPPING: Record<string, string> = {
4752 openrouter : "OpenRouter" ,
4853} ;
4954
50- export const transformAiSdkSpanName = ( span : ReadableSpan ) : void => {
51- // Unfortunately, the span name is not writable as this is not the intended behavior
52- // but it is a workaround to set the correct span name
53- if ( span . name in HANDLED_SPAN_NAMES ) {
54- ( span as any ) . name = HANDLED_SPAN_NAMES [ span . name ] ;
55- }
56- } ;
57-
5855const transformResponseText = ( attributes : Record < string , any > ) : void => {
5956 if ( AI_RESPONSE_TEXT in attributes ) {
6057 attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.content` ] =
@@ -367,9 +364,41 @@ const transformVendor = (attributes: Record<string, any>): void => {
367364 }
368365} ;
369366
370- export const transformAiSdkAttributes = (
371- attributes : Record < string , any > ,
372- ) : void => {
367+ const transformTelemetryMetadata = ( attributes : Record < string , any > ) : void => {
368+ const metadataAttributes : Record < string , string > = { } ;
369+ const keysToDelete : string [ ] = [ ] ;
370+
371+ // Find all ai.telemetry.metadata.* attributes
372+ for ( const [ key , value ] of Object . entries ( attributes ) ) {
373+ if ( key . startsWith ( AI_TELEMETRY_METADATA_PREFIX ) ) {
374+ const metadataKey = key . substring ( AI_TELEMETRY_METADATA_PREFIX . length ) ;
375+
376+ // Always mark for deletion since it's a telemetry metadata attribute
377+ keysToDelete . push ( key ) ;
378+
379+ if ( metadataKey && value != null ) {
380+ // Convert value to string for association properties
381+ const stringValue = typeof value === "string" ? value : String ( value ) ;
382+ metadataAttributes [ metadataKey ] = stringValue ;
383+
384+ // Also set as traceloop association property attribute
385+ attributes [
386+ `${ SpanAttributes . TRACELOOP_ASSOCIATION_PROPERTIES } .${ metadataKey } `
387+ ] = stringValue ;
388+ }
389+ }
390+ }
391+
392+ // Remove original ai.telemetry.metadata.* attributes
393+ keysToDelete . forEach ( ( key ) => {
394+ delete attributes [ key ] ;
395+ } ) ;
396+
397+ // Note: Context setting for child span inheritance should be done before span creation,
398+ // not during transformation. Use `withTelemetryMetadataContext` function for context propagation.
399+ } ;
400+
401+ export const transformLLMSpans = ( attributes : Record < string , any > ) : void => {
373402 transformResponseText ( attributes ) ;
374403 transformResponseObject ( attributes ) ;
375404 transformResponseToolCalls ( attributes ) ;
@@ -379,16 +408,40 @@ export const transformAiSdkAttributes = (
379408 transformCompletionTokens ( attributes ) ;
380409 calculateTotalTokens ( attributes ) ;
381410 transformVendor ( attributes ) ;
411+ transformTelemetryMetadata ( attributes ) ;
412+ } ;
413+
414+ const transformToolCalls = ( span : ReadableSpan ) : void => {
415+ if (
416+ span . attributes [ "ai.toolCall.args" ] &&
417+ span . attributes [ "ai.toolCall.result" ]
418+ ) {
419+ span . attributes [ SpanAttributes . TRACELOOP_ENTITY_INPUT ] =
420+ span . attributes [ "ai.toolCall.args" ] ;
421+ delete span . attributes [ "ai.toolCall.args" ] ;
422+ span . attributes [ SpanAttributes . TRACELOOP_ENTITY_OUTPUT ] =
423+ span . attributes [ "ai.toolCall.result" ] ;
424+ delete span . attributes [ "ai.toolCall.result" ] ;
425+ }
382426} ;
383427
384428const shouldHandleSpan = ( span : ReadableSpan ) : boolean => {
385- return span . name in HANDLED_SPAN_NAMES ;
429+ return span . instrumentationScope ?. name === "ai" ;
430+ } ;
431+
432+ export const transformAiSdkSpanNames = ( span : Span ) : void => {
433+ if ( span . name === TOOL_SPAN_NAME ) {
434+ span . updateName ( `${ span . attributes [ "ai.toolCall.name" ] as string } .tool` ) ;
435+ }
436+ if ( span . name in HANDLED_SPAN_NAMES ) {
437+ span . updateName ( HANDLED_SPAN_NAMES [ span . name ] ) ;
438+ }
386439} ;
387440
388- export const transformAiSdkSpan = ( span : ReadableSpan ) : void => {
441+ export const transformAiSdkSpanAttributes = ( span : ReadableSpan ) : void => {
389442 if ( ! shouldHandleSpan ( span ) ) {
390443 return ;
391444 }
392- transformAiSdkSpanName ( span ) ;
393- transformAiSdkAttributes ( span . attributes ) ;
445+ transformLLMSpans ( span . attributes ) ;
446+ transformToolCalls ( span ) ;
394447} ;
0 commit comments