1+ import { ReadableSpan } from "@opentelemetry/sdk-trace-node" ;
2+ import { SpanAttributes } from "@traceloop/ai-semantic-conventions" ;
3+
4+ const AI_GENERATE_TEXT_DO_GENERATE = "ai.generateText.doGenerate" ;
5+ const AI_STREAM_TEXT_DO_STREAM = "ai.streamText.doStream" ;
6+ const AI_RESPONSE_TEXT = "ai.response.text" ;
7+ const AI_PROMPT_MESSAGES = "ai.prompt.messages" ;
8+ const AI_USAGE_PROMPT_TOKENS = "ai.usage.promptTokens" ;
9+ const AI_USAGE_COMPLETION_TOKENS = "ai.usage.completionTokens" ;
10+ const AI_MODEL_PROVIDER = "ai.model.provider" ;
11+
12+ export const transformAiSdkSpanName = ( span : ReadableSpan ) : void => {
13+ const nameMap : Record < string , string > = {
14+ [ AI_GENERATE_TEXT_DO_GENERATE ] : "ai.generateText.generate" ,
15+ [ AI_STREAM_TEXT_DO_STREAM ] : "ai.streamText.stream" ,
16+ } ;
17+
18+ if ( span . name in nameMap ) {
19+ // Unfortunately, the span name is not writable as this is not the intended behavior
20+ // but it is a workaround to set the correct span name
21+ ( span as any ) . name = nameMap [ span . name ] ;
22+ }
23+ } ;
24+
25+ export const transformResponseText = ( attributes : Record < string , any > ) : void => {
26+ if ( AI_RESPONSE_TEXT in attributes ) {
27+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.content` ] =
28+ attributes [ AI_RESPONSE_TEXT ] ;
29+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = "assistant" ;
30+ delete attributes [ AI_RESPONSE_TEXT ] ;
31+ }
32+ } ;
33+
34+ export const transformPromptMessages = ( attributes : Record < string , any > ) : void => {
35+ if ( AI_PROMPT_MESSAGES in attributes ) {
36+ try {
37+ const messages = JSON . parse ( attributes [ AI_PROMPT_MESSAGES ] as string ) ;
38+ messages . forEach (
39+ ( msg : { role : string ; content : any } , index : number ) => {
40+ attributes [ `${ SpanAttributes . LLM_PROMPTS } .${ index } .content` ] =
41+ typeof msg . content === "string"
42+ ? msg . content
43+ : JSON . stringify ( msg . content ) ;
44+ attributes [ `${ SpanAttributes . LLM_PROMPTS } .${ index } .role` ] = msg . role ;
45+ } ,
46+ ) ;
47+ delete attributes [ AI_PROMPT_MESSAGES ] ;
48+ } catch {
49+ // Skip if JSON parsing fails
50+ }
51+ }
52+ } ;
53+
54+ export const transformPromptTokens = ( attributes : Record < string , any > ) : void => {
55+ if ( AI_USAGE_PROMPT_TOKENS in attributes ) {
56+ attributes [ `${ SpanAttributes . LLM_USAGE_PROMPT_TOKENS } ` ] =
57+ attributes [ AI_USAGE_PROMPT_TOKENS ] ;
58+ delete attributes [ AI_USAGE_PROMPT_TOKENS ] ;
59+ }
60+ } ;
61+
62+ export const transformCompletionTokens = ( attributes : Record < string , any > ) : void => {
63+ if ( AI_USAGE_COMPLETION_TOKENS in attributes ) {
64+ attributes [ `${ SpanAttributes . LLM_USAGE_COMPLETION_TOKENS } ` ] =
65+ attributes [ AI_USAGE_COMPLETION_TOKENS ] ;
66+ delete attributes [ AI_USAGE_COMPLETION_TOKENS ] ;
67+ }
68+ } ;
69+
70+ export const calculateTotalTokens = ( attributes : Record < string , any > ) : void => {
71+ const promptTokens = attributes [ `${ SpanAttributes . LLM_USAGE_PROMPT_TOKENS } ` ] ;
72+ const completionTokens = attributes [ `${ SpanAttributes . LLM_USAGE_COMPLETION_TOKENS } ` ] ;
73+
74+ if ( promptTokens && completionTokens ) {
75+ attributes [ `${ SpanAttributes . LLM_USAGE_TOTAL_TOKENS } ` ] =
76+ Number ( promptTokens ) + Number ( completionTokens ) ;
77+ }
78+ } ;
79+
80+ export const transformVendor = ( attributes : Record < string , any > ) : void => {
81+ if ( AI_MODEL_PROVIDER in attributes ) {
82+ const vendor = attributes [ AI_MODEL_PROVIDER ] ;
83+ if ( vendor && vendor . startsWith ( "openai" ) ) {
84+ attributes [ SpanAttributes . LLM_SYSTEM ] = "OpenAI" ;
85+ } else {
86+ attributes [ SpanAttributes . LLM_SYSTEM ] = vendor ;
87+ }
88+ delete attributes [ AI_MODEL_PROVIDER ] ;
89+ }
90+ } ;
91+
92+ export const transformAiSdkAttributes = ( attributes : Record < string , any > ) : void => {
93+ transformResponseText ( attributes ) ;
94+ transformPromptMessages ( attributes ) ;
95+ transformPromptTokens ( attributes ) ;
96+ transformCompletionTokens ( attributes ) ;
97+ calculateTotalTokens ( attributes ) ;
98+ transformVendor ( attributes ) ;
99+ } ;
100+
101+ export const transformAiSdkSpan = ( span : ReadableSpan ) : void => {
102+ transformAiSdkSpanName ( span ) ;
103+ transformAiSdkAttributes ( span . attributes ) ;
104+ } ;
0 commit comments