@@ -44,50 +44,151 @@ module Instrumentation
4444 # @param response [Common::PromptResponse] completed response with normalized data
4545 # @return [void]
4646 def instrumentation_prompt_payload ( payload , request , response )
47- # Add request parameters
48- payload . merge! (
49- trace_id : trace_id ,
50- message_count : request . messages &.size || 0 ,
51- stream : !!request . stream
52- )
53-
54- # Add common parameters if available
55- payload [ :model ] = request . model if request . respond_to? ( :model )
56- payload [ :temperature ] = request . temperature if request . respond_to? ( :temperature )
57- payload [ :max_tokens ] = request . max_tokens if request . respond_to? ( :max_tokens )
58- payload [ :top_p ] = request . top_p if request . respond_to? ( :top_p )
59-
60- # Add tool information
61- if request . respond_to? ( :tools )
62- payload [ :has_tools ] = request . tools . present?
63- payload [ :tool_count ] = request . tools &.size || 0
47+ # message_count: prefer the request/input messages (pre-call), fall back to
48+ # response messages only if the request doesn't expose messages. New Relic
49+ # expects parameters[:messages] to be the request messages and computes
50+ # total message counts by adding response choices to that count.
51+ message_count = safe_access ( request , :messages ) &.size
52+ message_count = safe_access ( response , :messages ) &.size if message_count . nil?
53+
54+ payload . merge! ( trace_id : trace_id , message_count : message_count || 0 , stream : !!safe_access ( request , :stream ) )
55+
56+ # Common parameters: prefer response-normalized values, then request
57+ payload [ :model ] = safe_access ( response , :model ) || safe_access ( request , :model )
58+ payload [ :temperature ] = safe_access ( request , :temperature )
59+ payload [ :max_tokens ] = safe_access ( request , :max_tokens )
60+ payload [ :top_p ] = safe_access ( request , :top_p )
61+
62+ # Tools / instructions
63+ if ( tools_val = safe_access ( request , :tools ) )
64+ payload [ :has_tools ] = tools_val . respond_to? ( :present? ) ? tools_val . present? : !!tools_val
65+ payload [ :tool_count ] = tools_val &.size || 0
6466 end
6567
66- # Add instructions indicator if available
67- if request . respond_to? ( :instructions )
68- payload [ :has_instructions ] = request . instructions . present?
68+ if ( instr_val = safe_access ( request , :instructions ) )
69+ payload [ :has_instructions ] = instr_val . respond_to? ( :present? ) ? instr_val . present? : !!instr_val
6970 end
7071
71- # Add usage data if available (CRITICAL for APM integration)
72- # The Usage object already normalizes token counts across all providers
72+ # Usage (normalized)
7373 if response . usage
74+ usage = response . usage
7475 payload [ :usage ] = {
75- input_tokens : response . usage . input_tokens ,
76- output_tokens : response . usage . output_tokens ,
77- total_tokens : response . usage . total_tokens
76+ input_tokens : usage . input_tokens ,
77+ output_tokens : usage . output_tokens ,
78+ total_tokens : usage . total_tokens
7879 }
79- # Add all available usage tokens (cached, reasoning, audio, etc.)
80- payload [ :usage ] [ :cached_tokens ] = response . usage . cached_tokens if response . usage . cached_tokens
81- payload [ :usage ] [ :cache_creation_tokens ] = response . usage . cache_creation_tokens if response . usage . cache_creation_tokens
82- payload [ :usage ] [ :reasoning_tokens ] = response . usage . reasoning_tokens if response . usage . reasoning_tokens
83- payload [ :usage ] [ :audio_tokens ] = response . usage . audio_tokens if response . usage . audio_tokens
80+
81+ payload [ :usage ] [ :cached_tokens ] = usage . cached_tokens if usage . cached_tokens
82+ payload [ :usage ] [ :cache_creation_tokens ] = usage . cache_creation_tokens if usage . cache_creation_tokens
83+ payload [ :usage ] [ :reasoning_tokens ] = usage . reasoning_tokens if usage . reasoning_tokens
84+ payload [ :usage ] [ :audio_tokens ] = usage . audio_tokens if usage . audio_tokens
8485 end
8586
86- # Add response metadata directly from response object
87- # The response model provides normalized access across all providers
88- payload [ :finish_reason ] = response . finish_reason
89- payload [ :response_model ] = response . model
90- payload [ :response_id ] = response . id
87+ # Response metadata
88+ payload [ :finish_reason ] = safe_access ( response , :finish_reason ) || response . finish_reason
89+ payload [ :response_model ] = safe_access ( response , :model ) || response . model
90+ payload [ :response_id ] = safe_access ( response , :id ) || response . id
91+
92+ # Build messages list: prefer request messages; if unavailable use prior
93+ # response messages (all but the final generated message).
94+ if ( req_msgs = safe_access ( request , :messages ) ) . is_a? ( Array )
95+ payload [ :messages ] = req_msgs . map { |m | extract_message_hash ( m , false ) }
96+ else
97+ prior = safe_access ( response , :messages )
98+ prior = prior [ 0 ...-1 ] if prior . is_a? ( Array ) && prior . size > 1
99+ if prior . is_a? ( Array ) && prior . any?
100+ payload [ :messages ] = prior . map { |m | extract_message_hash ( m , false ) }
101+ end
102+ end
103+
104+ # Build a parameters hash that mirrors what New Relic's OpenAI
105+ # instrumentation expects. This makes it easy for APM adapters to
106+ # map our provider payload to their LLM event constructors.
107+ parameters = { }
108+ parameters [ :model ] = payload [ :model ] if payload [ :model ]
109+ parameters [ :max_tokens ] = payload [ :max_tokens ] if payload [ :max_tokens ]
110+ parameters [ :temperature ] = payload [ :temperature ] if payload [ :temperature ]
111+ parameters [ :top_p ] = payload [ :top_p ] if payload [ :top_p ]
112+ parameters [ :stream ] = payload [ :stream ]
113+ parameters [ :messages ] = payload [ :messages ] if payload [ :messages ]
114+
115+ # Include tools/instructions where available — New Relic ignores unknown keys,
116+ # but having them here makes the parameter shape closer to OpenAI's.
117+ parameters [ :tools ] = begin request . tools rescue nil end if begin request . tools rescue nil end
118+ parameters [ :instructions ] = begin request . instructions rescue nil end if begin request . instructions rescue nil end
119+
120+ payload [ :parameters ] = parameters
121+
122+ # Attach raw response (provider-specific) so downstream APM integrations
123+ # can inspect the provider response if needed. Use the normalized raw_response
124+ # available on the Common::Response when possible.
125+ begin
126+ payload [ :response_raw ] = response . raw_response if response . respond_to? ( :raw_response ) && response . raw_response
127+ rescue StandardError
128+ # ignore
129+ end
130+ end
131+
132+ private
133+
134+ # Safely attempt to call a method or lookup a key on an object. We avoid
135+ # probing with `respond_to?` to prevent ActiveModel attribute casting side
136+ # effects; instead we attempt the call and rescue failures.
137+ def safe_access ( obj , name )
138+ return nil if obj . nil?
139+
140+ begin
141+ return obj . public_send ( name )
142+ rescue StandardError
143+ end
144+
145+ begin
146+ return obj [ name ]
147+ rescue StandardError
148+ end
149+
150+ begin
151+ return obj [ name . to_s ]
152+ rescue StandardError
153+ end
154+
155+ nil
156+ end
157+
158+ # NOTE: message access is handled via `safe_access(obj, :messages)` to
159+ # avoid duplicating guarded lookup logic.
160+
161+ # Extract a simple hash from a provider message object or hash-like value.
162+ def extract_message_hash ( msg , is_response = false )
163+ role = begin
164+ if msg . respond_to? ( :[] )
165+ begin msg [ :role ] rescue ( begin msg [ "role" ] rescue nil end ) end
166+ elsif msg . respond_to? ( :role )
167+ msg . role
168+ elsif msg . respond_to? ( :type )
169+ msg . type
170+ end
171+ rescue StandardError
172+ begin msg . role rescue msg . type rescue nil end
173+ end
174+
175+ content = begin
176+ if msg . respond_to? ( :[] )
177+ begin msg [ :content ] rescue ( begin msg [ "content" ] rescue nil end ) end
178+ elsif msg . respond_to? ( :content )
179+ msg . content
180+ elsif msg . respond_to? ( :text )
181+ msg . text
182+ elsif msg . respond_to? ( :to_h )
183+ begin msg . to_h [ :content ] rescue ( begin msg . to_h [ "content" ] rescue nil end ) end
184+ elsif msg . respond_to? ( :to_s )
185+ msg . to_s
186+ end
187+ rescue StandardError
188+ begin msg . to_s rescue nil end
189+ end
190+
191+ { role : role , content : content , is_response : is_response }
91192 end
92193
93194 # Builds and merges payload data for embed instrumentation events.
0 commit comments