1- import * as lodash from "lodash" ;
21import type * as llamaindex from "llamaindex" ;
32
43import {
@@ -13,15 +12,29 @@ import {
1312} from "@opentelemetry/api" ;
1413import { safeExecuteInTheMiddle } from "@opentelemetry/instrumentation" ;
1514
16- import { SpanAttributes } from "@traceloop/ai-semantic-conventions" ;
1715import {
18- ATTR_GEN_AI_COMPLETION ,
19- ATTR_GEN_AI_PROMPT ,
16+ SpanAttributes ,
17+ FinishReasons ,
18+ } from "@traceloop/ai-semantic-conventions" ;
19+ import {
20+ ATTR_GEN_AI_INPUT_MESSAGES ,
21+ ATTR_GEN_AI_OPERATION_NAME ,
22+ ATTR_GEN_AI_OUTPUT_MESSAGES ,
23+ ATTR_GEN_AI_PROVIDER_NAME ,
2024 ATTR_GEN_AI_REQUEST_MODEL ,
2125 ATTR_GEN_AI_REQUEST_TOP_P ,
26+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS ,
2227 ATTR_GEN_AI_RESPONSE_MODEL ,
23- ATTR_GEN_AI_SYSTEM ,
28+ ATTR_GEN_AI_USAGE_INPUT_TOKENS ,
29+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS ,
30+ GEN_AI_OPERATION_NAME_VALUE_CHAT ,
31+ GEN_AI_PROVIDER_NAME_VALUE_OPENAI ,
2432} from "@opentelemetry/semantic-conventions/incubating" ;
33+ import {
34+ formatInputMessages ,
35+ formatOutputMessage ,
36+ mapOpenAIContentBlock ,
37+ } from "@traceloop/instrumentation-utils" ;
2538
2639import { LlamaIndexInstrumentationConfig } from "./types" ;
2740import { shouldSendPrompts , llmGeneratorWrapper } from "./utils" ;
@@ -33,9 +46,21 @@ type AsyncResponseType =
3346 | AsyncIterable < llamaindex . ChatResponseChunk >
3447 | AsyncIterable < llamaindex . CompletionResponse > ;
3548
49+ const classNameToProviderName : Record < string , string > = {
50+ OpenAI : GEN_AI_PROVIDER_NAME_VALUE_OPENAI ,
51+ } ;
52+
53+ export const openAIFinishReasonMap : Record < string , string > = {
54+ stop : FinishReasons . STOP ,
55+ length : FinishReasons . LENGTH ,
56+ tool_calls : FinishReasons . TOOL_CALL ,
57+ content_filter : FinishReasons . CONTENT_FILTER ,
58+ function_call : FinishReasons . TOOL_CALL ,
59+ } ;
60+
3661export class CustomLLMInstrumentation {
3762 constructor (
38- private config : LlamaIndexInstrumentationConfig ,
63+ private config : ( ) => LlamaIndexInstrumentationConfig ,
3964 private diag : DiagLogger ,
4065 private tracer : ( ) => Tracer ,
4166 ) { }
@@ -50,44 +75,30 @@ export class CustomLLMInstrumentation {
5075 const messages = params ?. messages ;
5176 const streaming = params ?. stream ;
5277
53- const span = plugin
54- . tracer ( )
55- . startSpan ( `llamaindex.${ lodash . snakeCase ( className ) } .chat` , {
56- kind : SpanKind . CLIENT ,
57- } ) ;
78+ const span = plugin . tracer ( ) . startSpan ( `chat ${ this . metadata . model } ` , {
79+ kind : SpanKind . CLIENT ,
80+ } ) ;
5881
5982 try {
60- span . setAttribute ( ATTR_GEN_AI_SYSTEM , className ) ;
83+ span . setAttribute (
84+ ATTR_GEN_AI_PROVIDER_NAME ,
85+ classNameToProviderName [ className ] ?? className . toLowerCase ( ) ,
86+ ) ;
6187 span . setAttribute ( ATTR_GEN_AI_REQUEST_MODEL , this . metadata . model ) ;
62- span . setAttribute ( SpanAttributes . LLM_REQUEST_TYPE , "chat" ) ;
88+ span . setAttribute (
89+ ATTR_GEN_AI_OPERATION_NAME ,
90+ GEN_AI_OPERATION_NAME_VALUE_CHAT ,
91+ ) ;
6392 span . setAttribute ( ATTR_GEN_AI_REQUEST_TOP_P , this . metadata . topP ) ;
64- if ( shouldSendPrompts ( plugin . config ) ) {
65- for ( const messageIdx in messages ) {
66- const content = messages [ messageIdx ] . content ;
67- if ( typeof content === "string" ) {
68- span . setAttribute (
69- `${ ATTR_GEN_AI_PROMPT } .${ messageIdx } .content` ,
70- content as string ,
71- ) ;
72- } else if (
73- ( content as llamaindex . MessageContentDetail [ ] ) [ 0 ] . type ===
74- "text"
75- ) {
76- span . setAttribute (
77- `${ ATTR_GEN_AI_PROMPT } .${ messageIdx } .content` ,
78- ( content as llamaindex . MessageContentTextDetail [ ] ) [ 0 ] . text ,
79- ) ;
80- }
81-
82- span . setAttribute (
83- `${ ATTR_GEN_AI_PROMPT } .${ messageIdx } .role` ,
84- messages [ messageIdx ] . role ,
85- ) ;
86- }
93+ if ( shouldSendPrompts ( plugin . config ( ) ) && messages ) {
94+ span . setAttribute (
95+ ATTR_GEN_AI_INPUT_MESSAGES ,
96+ formatInputMessages ( messages , mapOpenAIContentBlock ) ,
97+ ) ;
8798 }
8899 } catch ( e ) {
89100 plugin . diag . warn ( e ) ;
90- plugin . config . exceptionLogger ?.( e ) ;
101+ plugin . config ( ) . exceptionLogger ?.( e ) ;
91102 }
92103
93104 const execContext = trace . setSpan ( context . active ( ) , span ) ;
@@ -138,36 +149,59 @@ export class CustomLLMInstrumentation {
138149 ) : T {
139150 span . setAttribute ( ATTR_GEN_AI_RESPONSE_MODEL , metadata . model ) ;
140151
141- if ( ! shouldSendPrompts ( this . config ) ) {
142- span . setStatus ( { code : SpanStatusCode . OK } ) ;
143- span . end ( ) ;
144- return result ;
145- }
146-
147152 try {
148- if ( ( result as llamaindex . ChatResponse ) . message ) {
153+ const raw = ( result as any ) . raw ;
154+ const finishReason : string | null =
155+ raw ?. choices ?. [ 0 ] ?. finish_reason ?? null ;
156+
157+ // finish_reasons: metadata, not content — always set outside shouldSendPrompts
158+ if ( finishReason != null ) {
159+ span . setAttribute ( ATTR_GEN_AI_RESPONSE_FINISH_REASONS , [
160+ openAIFinishReasonMap [ finishReason ] ?? finishReason ,
161+ ] ) ;
162+ }
163+
164+ // Token usage: always set when available
165+ const usage = raw ?. usage ;
166+ if ( usage ) {
167+ span . setAttribute ( ATTR_GEN_AI_USAGE_INPUT_TOKENS , usage . prompt_tokens ) ;
168+ span . setAttribute (
169+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS ,
170+ usage . completion_tokens ,
171+ ) ;
149172 span . setAttribute (
150- ` ${ ATTR_GEN_AI_COMPLETION } .0.role` ,
151- ( result as llamaindex . ChatResponse ) . message . role ,
173+ SpanAttributes . GEN_AI_USAGE_TOTAL_TOKENS ,
174+ usage . total_tokens ,
152175 ) ;
176+ }
177+
178+ // output messages: content — always set inside shouldSendPrompts
179+ if (
180+ shouldSendPrompts ( this . config ( ) ) &&
181+ ( result as llamaindex . ChatResponse ) . message
182+ ) {
153183 const content = ( result as llamaindex . ChatResponse ) . message . content ;
154- if ( typeof content === "string" ) {
155- span . setAttribute ( `${ ATTR_GEN_AI_COMPLETION } .0.content` , content ) ;
156- } else if ( content [ 0 ] . type === "text" ) {
157- span . setAttribute (
158- `${ ATTR_GEN_AI_COMPLETION } .0.content` ,
159- content [ 0 ] . text ,
160- ) ;
161- }
162- span . setStatus ( { code : SpanStatusCode . OK } ) ;
184+ // Normalize to array so mapOpenAIContentBlock handles both string and block array
185+ const contentArray = typeof content === "string" ? [ content ] : content ;
186+ span . setAttribute (
187+ ATTR_GEN_AI_OUTPUT_MESSAGES ,
188+ formatOutputMessage (
189+ contentArray ,
190+ finishReason ,
191+ openAIFinishReasonMap ,
192+ GEN_AI_OPERATION_NAME_VALUE_CHAT ,
193+ mapOpenAIContentBlock ,
194+ ) ,
195+ ) ;
163196 }
197+
198+ span . setStatus ( { code : SpanStatusCode . OK } ) ;
164199 } catch ( e ) {
165200 this . diag . warn ( e ) ;
166- this . config . exceptionLogger ?.( e ) ;
201+ this . config ( ) . exceptionLogger ?.( e ) ;
167202 }
168203
169204 span . end ( ) ;
170-
171205 return result ;
172206 }
173207
@@ -178,14 +212,54 @@ export class CustomLLMInstrumentation {
178212 metadata : llamaindex . LLMMetadata ,
179213 ) : T {
180214 span . setAttribute ( ATTR_GEN_AI_RESPONSE_MODEL , metadata . model ) ;
181- if ( ! shouldSendPrompts ( this . config ) ) {
182- span . setStatus ( { code : SpanStatusCode . OK } ) ;
183- span . end ( ) ;
184- return result ;
185- }
186215
187- return llmGeneratorWrapper ( result , execContext , ( message ) => {
188- span . setAttribute ( `${ ATTR_GEN_AI_COMPLETION } .0.content` , message ) ;
216+ return llmGeneratorWrapper ( result , execContext , ( message , lastChunk ) => {
217+ try {
218+ // Extract finish_reason and usage from the last chunk's raw OpenAI
219+ // response — available when stream_options: { include_usage: true }
220+ // is set on the LLM (OpenAI sends usage in the final streaming chunk).
221+ const lastRaw = lastChunk ?. raw as any ;
222+ const finishReason : string | null =
223+ lastRaw ?. choices ?. [ 0 ] ?. finish_reason ?? null ;
224+ const usage = lastRaw ?. usage ?? null ;
225+
226+ if ( finishReason != null ) {
227+ span . setAttribute ( ATTR_GEN_AI_RESPONSE_FINISH_REASONS , [
228+ openAIFinishReasonMap [ finishReason ] ?? finishReason ,
229+ ] ) ;
230+ }
231+
232+ if ( usage ) {
233+ span . setAttribute (
234+ ATTR_GEN_AI_USAGE_INPUT_TOKENS ,
235+ usage . prompt_tokens ,
236+ ) ;
237+ span . setAttribute (
238+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS ,
239+ usage . completion_tokens ,
240+ ) ;
241+ span . setAttribute (
242+ SpanAttributes . GEN_AI_USAGE_TOTAL_TOKENS ,
243+ usage . total_tokens ,
244+ ) ;
245+ }
246+
247+ if ( shouldSendPrompts ( this . config ( ) ) ) {
248+ span . setAttribute (
249+ ATTR_GEN_AI_OUTPUT_MESSAGES ,
250+ formatOutputMessage (
251+ [ message ] ,
252+ finishReason ,
253+ openAIFinishReasonMap ,
254+ GEN_AI_OPERATION_NAME_VALUE_CHAT ,
255+ mapOpenAIContentBlock ,
256+ ) ,
257+ ) ;
258+ }
259+ } catch ( e ) {
260+ this . diag . warn ( e ) ;
261+ this . config ( ) . exceptionLogger ?.( e ) ;
262+ }
189263 span . setStatus ( { code : SpanStatusCode . OK } ) ;
190264 span . end ( ) ;
191265 } ) as any ;
0 commit comments