@@ -44,34 +44,23 @@ import {
4444
4545/**
4646 * Extract request attributes from method arguments
47- * Following Sentry's AI Agents conventions
4847 */
4948function extractRequestAttributes ( args : unknown [ ] , methodPath : string ) : Record < string , unknown > {
5049 const attributes : Record < string , unknown > = {
5150 [ GEN_AI_SYSTEM_ATTRIBUTE ] : 'openai' ,
5251 [ GEN_AI_OPERATION_NAME_ATTRIBUTE ] : getOperationName ( methodPath ) ,
5352 } ;
5453
55- if ( args . length > 0 && args [ 0 ] && typeof args [ 0 ] === 'object' ) {
54+ if ( args . length > 0 && typeof args [ 0 ] === 'object' && args [ 0 ] !== null ) {
5655 const params = args [ 0 ] as Record < string , unknown > ;
5756
58- attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = params . model || 'unknown' ;
59-
60- if ( params . temperature !== undefined ) {
61- attributes [ GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE ] = params . temperature ;
62- }
63- if ( params . top_p !== undefined ) {
64- attributes [ GEN_AI_REQUEST_TOP_P_ATTRIBUTE ] = params . top_p ;
65- }
66-
67- if ( params . frequency_penalty !== undefined ) {
57+ attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = params . model ?? 'unknown' ;
58+ if ( 'temperature' in params ) attributes [ GEN_AI_REQUEST_TEMPERATURE_ATTRIBUTE ] = params . temperature ;
59+ if ( 'top_p' in params ) attributes [ GEN_AI_REQUEST_TOP_P_ATTRIBUTE ] = params . top_p ;
60+ if ( 'frequency_penalty' in params )
6861 attributes [ GEN_AI_REQUEST_FREQUENCY_PENALTY_ATTRIBUTE ] = params . frequency_penalty ;
69- }
70- if ( params . presence_penalty !== undefined ) {
71- attributes [ GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE ] = params . presence_penalty ;
72- }
62+ if ( 'presence_penalty' in params ) attributes [ GEN_AI_REQUEST_PRESENCE_PENALTY_ATTRIBUTE ] = params . presence_penalty ;
7363 } else {
74- // REQUIRED: Ensure model is always set even when no params provided
7564 attributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] = 'unknown' ;
7665 }
7766
@@ -134,7 +123,6 @@ function setCommonResponseAttributes(span: Span, id?: string, model?: string, ti
134123 */
135124function addChatCompletionAttributes ( span : Span , response : OpenAiChatCompletionObject ) : void {
136125 setCommonResponseAttributes ( span , response . id , response . model , response . created ) ;
137-
138126 if ( response . usage ) {
139127 setTokenUsageAttributes (
140128 span ,
@@ -143,11 +131,10 @@ function addChatCompletionAttributes(span: Span, response: OpenAiChatCompletionO
143131 response . usage . total_tokens ,
144132 ) ;
145133 }
146-
147- // Finish reasons - must be stringified array
148- if ( response . choices && Array . isArray ( response . choices ) ) {
149- const finishReasons = response . choices . map ( choice => choice . finish_reason ) . filter ( reason => reason !== null ) ;
150-
134+ if ( Array . isArray ( response . choices ) ) {
135+ const finishReasons = response . choices
136+ . map ( choice => choice . finish_reason )
137+ . filter ( ( reason ) : reason is string => reason !== null ) ;
151138 if ( finishReasons . length > 0 ) {
152139 span . setAttributes ( {
153140 [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( finishReasons ) ,
@@ -161,14 +148,11 @@ function addChatCompletionAttributes(span: Span, response: OpenAiChatCompletionO
161148 */
162149function addResponsesApiAttributes ( span : Span , response : OpenAIResponseObject ) : void {
163150 setCommonResponseAttributes ( span , response . id , response . model , response . created_at ) ;
164-
165151 if ( response . status ) {
166152 span . setAttributes ( {
167153 [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( [ response . status ] ) ,
168154 } ) ;
169155 }
170-
171- // Token usage for responses API
172156 if ( response . usage ) {
173157 setTokenUsageAttributes (
174158 span ,
@@ -190,27 +174,28 @@ function addResponseAttributes(span: Span, result: unknown, recordOutputs?: bool
190174
191175 if ( isChatCompletionResponse ( response ) ) {
192176 addChatCompletionAttributes ( span , response ) ;
193-
194- if ( recordOutputs && response . choices && response . choices . length > 0 ) {
177+ if ( recordOutputs && response . choices ?. length ) {
195178 const responseTexts = response . choices . map ( choice => choice . message ?. content || '' ) ;
196- span . setAttributes ( {
197- [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : JSON . stringify ( responseTexts ) ,
198- } ) ;
179+ span . setAttributes ( { [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : JSON . stringify ( responseTexts ) } ) ;
199180 }
200181 } else if ( isResponsesApiResponse ( response ) ) {
201182 addResponsesApiAttributes ( span , response ) ;
202-
203183 if ( recordOutputs && response . output_text ) {
204- span . setAttributes ( {
205- [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : response . output_text ,
206- } ) ;
184+ span . setAttributes ( { [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] : response . output_text } ) ;
207185 }
208186 }
209187}
210188
211- /**
212- * Get options from integration configuration
213- */
189+ // Extract and record AI request inputs, if present. This is intentionally separate from response attributes.
190+ function addRequestAttributes ( span : Span , params : Record < string , unknown > ) : void {
191+ if ( 'messages' in params ) {
192+ span . setAttributes ( { [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . messages ) } ) ;
193+ }
194+ if ( 'input' in params ) {
195+ span . setAttributes ( { [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . input ) } ) ;
196+ }
197+ }
198+
214199function getOptionsFromIntegration ( ) : OpenAiOptions {
215200 const scope = getCurrentScope ( ) ;
216201 const client = scope . getClient ( ) ;
@@ -237,33 +222,24 @@ function instrumentMethod<T extends unknown[], R>(
237222 return async function instrumentedMethod ( ...args : T ) : Promise < R > {
238223 const finalOptions = options || getOptionsFromIntegration ( ) ;
239224 const requestAttributes = extractRequestAttributes ( args , methodPath ) ;
240- const model = requestAttributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] || 'unknown' ;
225+ const model = ( requestAttributes [ GEN_AI_REQUEST_MODEL_ATTRIBUTE ] as string ) || 'unknown' ;
241226 const operationName = getOperationName ( methodPath ) ;
242227
243228 return startSpan (
244229 {
245- // Span name follows Sentry convention: "{operation_name} {model}"
246- // e.g., "chat gpt-4", "chat o3-mini", "embeddings text-embedding-3-small"
247230 name : `${ operationName } ${ model } ` ,
248231 op : getSpanOperation ( methodPath ) ,
249232 attributes : requestAttributes as Record < string , SpanAttributeValue > ,
250233 } ,
251234 async ( span : Span ) => {
252235 try {
253- // Record inputs if enabled - must be stringified JSON
254236 if ( finalOptions . recordInputs && args [ 0 ] && typeof args [ 0 ] === 'object' ) {
255- const params = args [ 0 ] as Record < string , unknown > ;
256- if ( params . messages ) {
257- span . setAttributes ( {
258- [ GEN_AI_REQUEST_MESSAGES_ATTRIBUTE ] : JSON . stringify ( params . messages ) ,
259- } ) ;
260- }
237+ addRequestAttributes ( span , args [ 0 ] as Record < string , unknown > ) ;
261238 }
262239
263240 const result = await originalMethod . apply ( context , args ) ;
264241 // TODO: Add streaming support
265242 addResponseAttributes ( span , result , finalOptions . recordOutputs ) ;
266-
267243 return result ;
268244 } catch ( error ) {
269245 captureException ( error ) ;
@@ -279,20 +255,16 @@ function instrumentMethod<T extends unknown[], R>(
279255 */
280256function createDeepProxy ( target : object , currentPath = '' , options ?: OpenAiOptions ) : OpenAiClient {
281257 return new Proxy ( target , {
282- get ( obj : Record < string | symbol , unknown > , prop : string | symbol ) : unknown {
283- if ( typeof prop === 'symbol' ) {
284- return obj [ prop ] ;
285- }
286-
287- const value = obj [ prop ] ;
288- const methodPath = buildMethodPath ( currentPath , prop ) ;
258+ get ( obj : object , prop : string ) : unknown {
259+ const value = ( obj as Record < string , unknown > ) [ prop ] ;
260+ const methodPath = buildMethodPath ( currentPath , String ( prop ) ) ;
289261
290262 if ( typeof value === 'function' && shouldInstrument ( methodPath ) ) {
291263 return instrumentMethod ( value as ( ...args : unknown [ ] ) => Promise < unknown > , methodPath , obj , options ) ;
292264 }
293265
294- if ( typeof value === 'object' && value !== null ) {
295- return createDeepProxy ( value , methodPath , options ) ;
266+ if ( value && typeof value === 'object' ) {
267+ return createDeepProxy ( value as object , methodPath , options ) ;
296268 }
297269
298270 return value ;
0 commit comments