11import type { Span } from '../../types-hoist/span' ;
2+ import type { SpanAttributeValue } from '../../types-hoist/span' ;
23import {
34 GEN_AI_CONVERSATION_ID_ATTRIBUTE ,
45 GEN_AI_REQUEST_DIMENSIONS_ATTRIBUTE ,
@@ -12,71 +13,13 @@ import {
1213 GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ,
1314 GEN_AI_RESPONSE_ID_ATTRIBUTE ,
1415 GEN_AI_RESPONSE_MODEL_ATTRIBUTE ,
16+ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ,
1517 GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ,
1618 GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ,
1719 GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ,
1820 GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ,
1921} from '../ai/gen-ai-attributes' ;
20- import type {
21- ChatCompletionChunk ,
22- OpenAiChatCompletionObject ,
23- OpenAIConversationObject ,
24- OpenAICreateEmbeddingsObject ,
25- OpenAIResponseObject ,
26- ResponseStreamingEvent ,
27- } from './types' ;
28-
29- /**
30- * Check if response is a Chat Completion object
31- */
32- export function isChatCompletionResponse ( response : unknown ) : response is OpenAiChatCompletionObject {
33- return (
34- response !== null &&
35- typeof response === 'object' &&
36- 'object' in response &&
37- ( response as Record < string , unknown > ) . object === 'chat.completion'
38- ) ;
39- }
40-
41- /**
42- * Check if response is a Responses API object
43- */
44- export function isResponsesApiResponse ( response : unknown ) : response is OpenAIResponseObject {
45- return (
46- response !== null &&
47- typeof response === 'object' &&
48- 'object' in response &&
49- ( response as Record < string , unknown > ) . object === 'response'
50- ) ;
51- }
52-
53- /**
54- * Check if response is an Embeddings API object
55- */
56- export function isEmbeddingsResponse ( response : unknown ) : response is OpenAICreateEmbeddingsObject {
57- if ( response === null || typeof response !== 'object' || ! ( 'object' in response ) ) {
58- return false ;
59- }
60- const responseObject = response as Record < string , unknown > ;
61- return (
62- responseObject . object === 'list' &&
63- typeof responseObject . model === 'string' &&
64- responseObject . model . toLowerCase ( ) . includes ( 'embedding' )
65- ) ;
66- }
67-
68- /**
69- * Check if response is a Conversations API object
70- * @see https://platform.openai.com/docs/api-reference/conversations
71- */
72- export function isConversationResponse ( response : unknown ) : response is OpenAIConversationObject {
73- return (
74- response !== null &&
75- typeof response === 'object' &&
76- 'object' in response &&
77- ( response as Record < string , unknown > ) . object === 'conversation'
78- ) ;
79- }
22+ import type { ChatCompletionChunk , ResponseStreamingEvent } from './types' ;
8023
8124/**
8225 * Check if streaming event is from the Responses API
@@ -104,157 +47,108 @@ export function isChatCompletionChunk(event: unknown): event is ChatCompletionCh
10447}
10548
10649/**
107- * Add attributes for Chat Completion responses
50+ * Add response attributes to a span using duck-typing.
51+ * Works for Chat Completions, Responses API, Embeddings, and Conversations API responses.
10852 */
109- export function addChatCompletionAttributes (
110- span : Span ,
111- response : OpenAiChatCompletionObject ,
112- recordOutputs ?: boolean ,
113- ) : void {
114- setCommonResponseAttributes ( span , response . id , response . model ) ;
115- if ( response . usage ) {
116- setTokenUsageAttributes (
117- span ,
118- response . usage . prompt_tokens ,
119- response . usage . completion_tokens ,
120- response . usage . total_tokens ,
121- ) ;
53+ export function addResponseAttributes ( span : Span , result : unknown , recordOutputs ?: boolean ) : void {
54+ if ( ! result || typeof result !== 'object' ) return ;
55+
56+ const response = result as Record < string , unknown > ;
57+ const attrs : Record < string , SpanAttributeValue > = { } ;
58+
59+ // Response ID
60+ if ( typeof response . id === 'string' ) {
61+ attrs [ GEN_AI_RESPONSE_ID_ATTRIBUTE ] = response . id ;
12262 }
63+
64+ // Response model
65+ if ( typeof response . model === 'string' ) {
66+ attrs [ GEN_AI_RESPONSE_MODEL_ATTRIBUTE ] = response . model ;
67+ }
68+
69+ // Conversation ID (conversation objects use id as conversation link)
70+ if ( response . object === 'conversation' && typeof response . id === 'string' ) {
71+ attrs [ GEN_AI_CONVERSATION_ID_ATTRIBUTE ] = response . id ;
72+ }
73+
74+ // Token usage — supports both naming conventions (chat: prompt_tokens/completion_tokens, responses: input_tokens/output_tokens)
75+ if ( response . usage && typeof response . usage === 'object' ) {
76+ const usage = response . usage as Record < string , unknown > ;
77+
78+ const inputTokens = usage . prompt_tokens ?? usage . input_tokens ;
79+ if ( typeof inputTokens === 'number' ) {
80+ attrs [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] = inputTokens ;
81+ }
82+
83+ const outputTokens = usage . completion_tokens ?? usage . output_tokens ;
84+ if ( typeof outputTokens === 'number' ) {
85+ attrs [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] = outputTokens ;
86+ }
87+
88+ if ( typeof usage . total_tokens === 'number' ) {
89+ attrs [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] = usage . total_tokens ;
90+ }
91+ }
92+
93+ // Finish reasons from choices (chat completions)
12394 if ( Array . isArray ( response . choices ) ) {
124- const finishReasons = response . choices
95+ const choices = response . choices as Array < Record < string , unknown > > ;
96+ const finishReasons = choices
12597 . map ( choice => choice . finish_reason )
126- . filter ( ( reason ) : reason is string => reason !== null ) ;
98+ . filter ( ( reason ) : reason is string => typeof reason === 'string' ) ;
12799 if ( finishReasons . length > 0 ) {
128- span . setAttributes ( {
129- [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( finishReasons ) ,
130- } ) ;
100+ attrs [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] = JSON . stringify ( finishReasons ) ;
131101 }
132102
133- // Extract tool calls from all choices (only if recordOutputs is true)
134103 if ( recordOutputs ) {
135- const toolCalls = response . choices
136- . map ( choice => choice . message ?. tool_calls )
104+ // Response text from choices
105+ const responseTexts = choices . map ( choice => {
106+ const message = choice . message as Record < string , unknown > | undefined ;
107+ return ( message ?. content as string ) || '' ;
108+ } ) ;
109+ attrs [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] = JSON . stringify ( responseTexts ) ;
110+
111+ // Tool calls from choices
112+ const toolCalls = choices
113+ . map ( choice => {
114+ const message = choice . message as Record < string , unknown > | undefined ;
115+ return message ?. tool_calls ;
116+ } )
137117 . filter ( calls => Array . isArray ( calls ) && calls . length > 0 )
138118 . flat ( ) ;
139119
140120 if ( toolCalls . length > 0 ) {
141- span . setAttributes ( {
142- [ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ] : JSON . stringify ( toolCalls ) ,
143- } ) ;
121+ attrs [ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ] = JSON . stringify ( toolCalls ) ;
144122 }
145123 }
146124 }
147- }
148125
149- /**
150- * Add attributes for Responses API responses
151- */
152- export function addResponsesApiAttributes ( span : Span , response : OpenAIResponseObject , recordOutputs ?: boolean ) : void {
153- setCommonResponseAttributes ( span , response . id , response . model ) ;
154- if ( response . status ) {
155- span . setAttributes ( {
156- [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] : JSON . stringify ( [ response . status ] ) ,
157- } ) ;
158- }
159- if ( response . usage ) {
160- setTokenUsageAttributes (
161- span ,
162- response . usage . input_tokens ,
163- response . usage . output_tokens ,
164- response . usage . total_tokens ,
165- ) ;
126+ // Finish reason from status (responses API)
127+ if ( typeof response . status === 'string' ) {
128+ // Only set if not already set from choices
129+ if ( ! attrs [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] ) {
130+ attrs [ GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE ] = JSON . stringify ( [ response . status ] ) ;
131+ }
166132 }
167133
168- // Extract function calls from output (only if recordOutputs is true)
169134 if ( recordOutputs ) {
170- const responseWithOutput = response as OpenAIResponseObject & { output ?: unknown [ ] } ;
171- if ( Array . isArray ( responseWithOutput . output ) && responseWithOutput . output . length > 0 ) {
172- // Filter for function_call type objects in the output array
173- const functionCalls = responseWithOutput . output . filter (
174- ( item ) : unknown =>
175- // oxlint-disable-next-line typescript/prefer-optional-chain
176- typeof item === 'object' && item !== null && ( item as Record < string , unknown > ) . type === 'function_call' ,
177- ) ;
135+ // Response text from output_text (responses API)
136+ if ( typeof response . output_text === 'string' && ! attrs [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] ) {
137+ attrs [ GEN_AI_RESPONSE_TEXT_ATTRIBUTE ] = response . output_text ;
138+ }
178139
140+ // Tool calls from output array (responses API)
141+ if ( Array . isArray ( response . output ) && response . output . length > 0 && ! attrs [ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ] ) {
142+ const functionCalls = ( response . output as Array < Record < string , unknown > > ) . filter (
143+ item => item ?. type === 'function_call' ,
144+ ) ;
179145 if ( functionCalls . length > 0 ) {
180- span . setAttributes ( {
181- [ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ] : JSON . stringify ( functionCalls ) ,
182- } ) ;
146+ attrs [ GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE ] = JSON . stringify ( functionCalls ) ;
183147 }
184148 }
185149 }
186- }
187-
188- /**
189- * Add attributes for Embeddings API responses
190- */
191- export function addEmbeddingsAttributes ( span : Span , response : OpenAICreateEmbeddingsObject ) : void {
192- span . setAttributes ( {
193- [ GEN_AI_RESPONSE_MODEL_ATTRIBUTE ] : response . model ,
194- } ) ;
195150
196- if ( response . usage ) {
197- setTokenUsageAttributes ( span , response . usage . prompt_tokens , undefined , response . usage . total_tokens ) ;
198- }
199- }
200-
201- /**
202- * Add attributes for Conversations API responses
203- * @see https://platform.openai.com/docs/api-reference/conversations
204- */
205- export function addConversationAttributes ( span : Span , response : OpenAIConversationObject ) : void {
206- const { id } = response ;
207-
208- span . setAttributes ( {
209- [ GEN_AI_RESPONSE_ID_ATTRIBUTE ] : id ,
210- // The conversation id is used to link messages across API calls
211- [ GEN_AI_CONVERSATION_ID_ATTRIBUTE ] : id ,
212- } ) ;
213- }
214-
215- /**
216- * Set token usage attributes
217- * @param span - The span to add attributes to
218- * @param promptTokens - The number of prompt tokens
219- * @param completionTokens - The number of completion tokens
220- * @param totalTokens - The number of total tokens
221- */
222- export function setTokenUsageAttributes (
223- span : Span ,
224- promptTokens ?: number ,
225- completionTokens ?: number ,
226- totalTokens ?: number ,
227- ) : void {
228- if ( promptTokens !== undefined ) {
229- span . setAttributes ( {
230- [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] : promptTokens ,
231- } ) ;
232- }
233- if ( completionTokens !== undefined ) {
234- span . setAttributes ( {
235- [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] : completionTokens ,
236- } ) ;
237- }
238- if ( totalTokens !== undefined ) {
239- span . setAttributes ( {
240- [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] : totalTokens ,
241- } ) ;
242- }
243- }
244-
245- /**
246- * Set common response attributes
247- * @param span - The span to add attributes to
248- * @param id - The response id
249- * @param model - The response model
250- */
251- export function setCommonResponseAttributes ( span : Span , id : string , model : string ) : void {
252- span . setAttributes ( {
253- [ GEN_AI_RESPONSE_ID_ATTRIBUTE ] : id ,
254- } ) ;
255- span . setAttributes ( {
256- [ GEN_AI_RESPONSE_MODEL_ATTRIBUTE ] : model ,
257- } ) ;
151+ span . setAttributes ( attrs ) ;
258152}
259153
260154/**
0 commit comments