Skip to content

Commit fe978d9

Browse files
committed
WIP
1 parent 36dd933 commit fe978d9

1 file changed

Lines changed: 136 additions & 35 deletions

File tree

lib/active_agent/providers/concerns/instrumentation.rb

Lines changed: 136 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)