newrelic 11.4.0 → 11.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/NEWS.md +80 -20
  2. package/README.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +115 -115
  4. package/api.js +108 -3
  5. package/lib/adaptive-sampler.js +1 -1
  6. package/lib/agent.js +2 -0
  7. package/lib/collector/remote-method.js +1 -1
  8. package/lib/config/default.js +14 -0
  9. package/lib/config/hsm.js +3 -0
  10. package/lib/instrumentation/@elastic/elasticsearch.js +2 -2
  11. package/lib/instrumentation/@nestjs/core.js +1 -1
  12. package/lib/instrumentation/@prisma/client.js +1 -1
  13. package/lib/instrumentation/cassandra-driver.js +1 -1
  14. package/lib/instrumentation/fastify.js +1 -1
  15. package/lib/instrumentation/grpc-js/grpc.js +1 -1
  16. package/lib/instrumentation/mongodb.js +1 -1
  17. package/lib/instrumentation/openai.js +329 -0
  18. package/lib/instrumentation/pino/pino.js +1 -1
  19. package/lib/instrumentations.js +1 -0
  20. package/lib/llm-events/openai/chat-completion-message.js +20 -0
  21. package/lib/llm-events/openai/chat-completion-summary.js +20 -0
  22. package/lib/llm-events/openai/embedding.js +15 -0
  23. package/lib/llm-events/openai/error-message.js +30 -0
  24. package/lib/llm-events/openai/event.js +56 -0
  25. package/lib/llm-events/openai/feedback-message.js +20 -0
  26. package/lib/llm-events/openai/index.js +20 -0
  27. package/lib/llm-events/tracked-ids.js +27 -0
  28. package/lib/logger.js +1 -1
  29. package/lib/metrics/names.js +6 -1
  30. package/lib/metrics/normalizer/rule.js +1 -1
  31. package/lib/metrics/normalizer.js +2 -2
  32. package/lib/shim/conglomerate-shim.js +3 -2
  33. package/lib/shim/datastore-shim.js +3 -2
  34. package/lib/shim/index.js +11 -8
  35. package/lib/shim/message-shim.js +3 -2
  36. package/lib/shim/promise-shim.js +4 -3
  37. package/lib/shim/shim.js +24 -22
  38. package/lib/shim/transaction-shim.js +7 -5
  39. package/lib/shim/webframework-shim.js +9 -5
  40. package/lib/shimmer.js +19 -13
  41. package/lib/spans/streaming-span-event-aggregator.js +1 -1
  42. package/lib/symbols.js +2 -0
  43. package/lib/timer.js +3 -3
  44. package/lib/transaction/index.js +5 -0
  45. package/lib/util/flatten.js +1 -1
  46. package/lib/util/objects.js +1 -1
  47. package/lib/util/process-version.js +2 -2
  48. package/lib/util/properties.js +2 -2
  49. package/lib/util/urltils.js +4 -4
  50. package/package.json +3 -3
package/lib/agent.js CHANGED
@@ -197,6 +197,8 @@ function Agent(config) {
197
197
  // Used by shutdown code as well as entity tracking stats
198
198
  this.activeTransactions = 0
199
199
 
200
+ this.llm = {}
201
+
200
202
  // Finally, add listeners for the agent's own events.
201
203
  this.on('transactionFinished', this._transactionFinished.bind(this))
202
204
  }
@@ -360,7 +360,7 @@ RemoteMethod.prototype._path = function _path() {
360
360
  * @param {object} options
361
361
  * @param {number} options.body - Data to be sent.
362
362
  * @param {object} options.nrHeaders - NR request headers from the connect response.
363
- * @param {bool} options.compressed - The compression method used, if any.
363
+ * @param {boolean} options.compressed - The compression method used, if any.
364
364
  */
365
365
  RemoteMethod.prototype._headers = function _headers(options) {
366
366
  const agent = this._userAgent()
@@ -1345,6 +1345,20 @@ defaultConfig.definition = () => ({
1345
1345
  formatter: boolean,
1346
1346
  default: false
1347
1347
  }
1348
+ },
1349
+
1350
+ /**
1351
+ * When enabled, instrumentation of supported AI libraries will be in
1352
+ * effect.
1353
+ */
1354
+ ai_monitoring: {
1355
+ /**
1356
+ * Toggles the generation of AI monitoring events by the agent.
1357
+ */
1358
+ enabled: {
1359
+ formatter: boolean,
1360
+ default: false
1361
+ }
1348
1362
  }
1349
1363
  })
1350
1364
 
package/lib/config/hsm.js CHANGED
@@ -55,6 +55,9 @@ const HIGH_SECURITY_SETTINGS = {
55
55
  forwarding: {
56
56
  enabled: false
57
57
  }
58
+ },
59
+ ai_monitoring: {
60
+ enabled: false
58
61
  }
59
62
  }
60
63
 
@@ -19,7 +19,7 @@ const { isNotEmpty } = require('../../util/objects')
19
19
  * @returns {void}
20
20
  */
21
21
  module.exports = function initialize(_agent, elastic, _moduleName, shim) {
22
- const pkgVersion = shim.require('./package.json').version
22
+ const pkgVersion = shim.pkgVersion
23
23
  if (semver.lt(pkgVersion, '7.13.0')) {
24
24
  shim &&
25
25
  shim.logger.debug(
@@ -67,7 +67,7 @@ function queryParser(params) {
67
67
  // let body or bulkBody override querystring, as some requests have both
68
68
  if (isNotEmpty(params.body)) {
69
69
  queryParam = params.body
70
- } else if (isNotEmpty(params.bulkBody)) {
70
+ } else if (Array.isArray(params.bulkBody) && params.bulkBody.length) {
71
71
  queryParam = params.bulkBody
72
72
  }
73
73
 
@@ -8,7 +8,7 @@ const logger = require('../../logger').child({ component: 'nestjs' })
8
8
  const semver = require('semver')
9
9
 
10
10
  module.exports = function initialize(agent, core, moduleName, shim) {
11
- const nestJsVersion = shim.require('./package.json').version
11
+ const nestJsVersion = shim.pkgVersion
12
12
  shim.setFramework(shim.NEST)
13
13
  // Earliest version that runs in the tests
14
14
  if (semver.lt(nestJsVersion, '8.0.0')) {
@@ -154,7 +154,7 @@ function extractPrismaDatasource(client) {
154
154
  * @param {object} shim New Relic shim
155
155
  */
156
156
  module.exports = async function initialize(_agent, prisma, _moduleName, shim) {
157
- const pkgVersion = shim.require('./package.json').version
157
+ const pkgVersion = shim.pkgVersion
158
158
  if (semver.lt(pkgVersion, '4.0.0')) {
159
159
  logger.warn(
160
160
  'Skipping instrumentation of @prisma/client. Minimum supported version of library is 4.0.0, actual version %s',
@@ -17,7 +17,7 @@ const semver = require('semver')
17
17
  * @param {object} shim - shim for instrumentation
18
18
  */
19
19
  module.exports = function initialize(_agent, cassandra, _moduleName, shim) {
20
- const cassandraVersion = shim.require('./package.json').version
20
+ const cassandraVersion = shim.pkgVersion
21
21
 
22
22
  shim.setDatastore(shim.CASSANDRA)
23
23
 
@@ -86,7 +86,7 @@ const setupRouteHandler = (shim, fastify) => {
86
86
  module.exports = function initialize(agent, fastify, moduleName, shim) {
87
87
  shim.setFramework(shim.FASTIFY)
88
88
 
89
- const fastifyVersion = shim.require('./package.json').version
89
+ const fastifyVersion = shim.pkgVersion
90
90
  const isv3Plus = semver.satisfies(fastifyVersion, '>=3.0.0')
91
91
 
92
92
  /**
@@ -20,7 +20,7 @@ module.exports.wrapStartCall = function wrappedClient(shim, callStream) {
20
20
  }
21
21
 
22
22
  module.exports.wrapServer = function wrapServer(shim, server) {
23
- const grpcVersion = shim.require('./package.json').version
23
+ const grpcVersion = shim.pkgVersion
24
24
  if (semver.lt(grpcVersion, '1.4.0')) {
25
25
  shim.logger.debug('gRPC server-side instrumentation only supported on grpc-js >=1.4.0')
26
26
  return
@@ -36,7 +36,7 @@ function initialize(agent, mongodb, moduleName, shim) {
36
36
 
37
37
  shim.setDatastore(shim.MONGODB)
38
38
 
39
- const mongoVersion = shim.require('./package.json').version
39
+ const mongoVersion = shim.pkgVersion
40
40
  if (semver.satisfies(mongoVersion, '>=4.0.0')) {
41
41
  instrumentV4(shim, mongodb)
42
42
  } else if (semver.satisfies(mongoVersion, '>=3.0.6')) {
@@ -0,0 +1,329 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const { openAiHeaders, openAiApiKey } = require('../../lib/symbols')
8
+ const {
9
+ LlmChatCompletionMessage,
10
+ LlmChatCompletionSummary,
11
+ LlmEmbedding,
12
+ LlmErrorMessage
13
+ } = require('../../lib/llm-events/openai')
14
+ const LlmTrackedIds = require('../../lib/llm-events/tracked-ids')
15
+
16
+ const MIN_VERSION = '4.0.0'
17
+ const MIN_STREAM_VERSION = '4.12.2'
18
+ const { AI } = require('../../lib/metrics/names')
19
+ const semver = require('semver')
20
+
21
+ let TRACKING_METRIC = AI.TRACKING_PREFIX
22
+
23
+ /**
24
+ * Checks if we should skip instrumentation.
25
+ * Currently it checks if `ai_monitoring.enabled` is true
26
+ * and the package version >= 4.0.0
27
+ *
28
+ * @param {object} config agent config
29
+ * @param {Shim} shim instance of shim
30
+ * @returns {boolean} flag if instrumentation should be skipped
31
+ */
32
+ function shouldSkipInstrumentation(config, shim) {
33
+ if (config?.ai_monitoring?.enabled !== true) {
34
+ shim.logger.debug('config.ai_monitoring.enabled is set to false.')
35
+ return true
36
+ }
37
+
38
+ const { pkgVersion } = shim
39
+ return semver.lt(pkgVersion, MIN_VERSION)
40
+ }
41
+
42
+ // eslint-disable-next-line sonarjs/cognitive-complexity
43
+ module.exports = function initialize(agent, openai, moduleName, shim) {
44
+ if (shouldSkipInstrumentation(agent.config, shim)) {
45
+ shim.logger.debug(
46
+ `${moduleName} instrumentation support is for versions >=${MIN_VERSION}. Skipping instrumentation.`
47
+ )
48
+ return
49
+ }
50
+
51
+ // Update the tracking metric name with the version of the library
52
+ // being instrumented. We do not have access to the version when
53
+ // initially declaring the variable.
54
+ TRACKING_METRIC = `${TRACKING_METRIC}OpenAI/${shim.pkgVersion}`
55
+
56
+ /**
57
+ * Adds apiKey and response headers to the active segment
58
+ * on symbols
59
+ *
60
+ * @param {object} result from openai request
61
+ * @param {string} apiKey api key from openai client
62
+ */
63
+ function decorateSegment(result, apiKey) {
64
+ const segment = shim.getActiveSegment()
65
+
66
+ if (segment) {
67
+ segment[openAiApiKey] = apiKey
68
+
69
+ // If the result is an error, which is an OpenAI client error, then
70
+ // the headers are provided via a proxy attached to `result.headers`.
71
+ // Otherwise, result is a typical response-like object.
72
+ const headers = result?.response?.headers
73
+ ? Object.fromEntries(result.response.headers)
74
+ : { ...result?.headers }
75
+ segment[openAiHeaders] = headers
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Enqueues a LLM event to the custom event aggregator
81
+ *
82
+ * @param {string} type of LLM event
83
+ * @param {object} msg LLM event
84
+ */
85
+ function recordEvent(type, msg) {
86
+ agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
87
+ msg = agent?.llm?.metadata ? { ...agent.llm.metadata, ...msg } : msg
88
+ agent.customEventAggregator.add([{ type, timestamp: Date.now() }, msg])
89
+ }
90
+
91
+ /**
92
+ * Instrumentation is only done to get the response headers and attach
93
+ * to the active segment as openai hides the headers from the functions we are
94
+ * trying to instrument
95
+ */
96
+ shim.wrap(openai.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
97
+ return function wrappedRequest() {
98
+ const apiKey = this.apiKey
99
+ const result = makeRequest.apply(this, arguments)
100
+ result.then(
101
+ (data) => {
102
+ // add headers on resolve
103
+ decorateSegment(data, apiKey)
104
+ },
105
+ (data) => {
106
+ // add headers on reject
107
+ decorateSegment(data, apiKey)
108
+ }
109
+ )
110
+ return result
111
+ }
112
+ })
113
+
114
+ /**
115
+ * Assigns requestId, conversationId and messageIds for a given
116
+ * chat completion response on the active transaction.
117
+ * This is used for generating LlmFeedbackEvent via `api.recordLlmFeedbackEvent`
118
+ *
119
+ * @param {object} params input params
120
+ * @param {Transaction} params.tx active transaction
121
+ * @param {LlmChatCompletionMessage} params.completionMsg chat completion message
122
+ * @param {string} params.responseId id of response
123
+ */
124
+ function assignIdsToTx({ tx, completionMsg, responseId }) {
125
+ const tracker = tx.llm.responses
126
+ const trackedIds =
127
+ tracker.get(responseId) ??
128
+ new LlmTrackedIds({
129
+ requestId: completionMsg.request_id,
130
+ conversationId: completionMsg.conversation_id
131
+ })
132
+ trackedIds.message_ids.push(completionMsg.id)
133
+ tracker.set(responseId, trackedIds)
134
+ }
135
+
136
+ /**
137
+ * Generates LlmChatCompletionSummary for a chat completion creation.
138
+ * Also iterates over both input messages and the first response message
139
+ * and creates LlmChatCompletionMessage.
140
+ *
141
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
142
+ *
143
+ * @param {object} params input params
144
+ * @param {TraceSegment} params.segment active segment from chat completion
145
+ * @param {object} params.request chat completion params
146
+ * @param {object} params.response chat completion response
147
+ * @param {boolean} [params.err] err if it exists
148
+ */
149
+ function recordChatCompletionMessages({ segment, request, response, err }) {
150
+ if (!response) {
151
+ // If we get an error, it is possible that `response = null`.
152
+ // In that case, we define it to be an empty object.
153
+ response = {}
154
+ }
155
+
156
+ response.headers = segment[openAiHeaders]
157
+ response.api_key = segment[openAiApiKey]
158
+ const tx = segment.transaction
159
+ // explicitly end segment to consistent duration
160
+ // for both LLM events and the segment
161
+ segment.end()
162
+ const completionSummary = new LlmChatCompletionSummary({
163
+ agent,
164
+ segment,
165
+ request,
166
+ response,
167
+ withError: err != null
168
+ })
169
+
170
+ // Only take the first response message and append to input messages
171
+ const messages = [...request.messages, response?.choices?.[0]?.message]
172
+ messages.forEach((message, index) => {
173
+ const completionMsg = new LlmChatCompletionMessage({
174
+ agent,
175
+ segment,
176
+ request,
177
+ response,
178
+ index,
179
+ completionId: completionSummary.id,
180
+ message
181
+ })
182
+
183
+ assignIdsToTx({ tx, completionMsg, responseId: response.id })
184
+ recordEvent('LlmChatCompletionMessage', completionMsg)
185
+ })
186
+
187
+ recordEvent('LlmChatCompletionSummary', completionSummary)
188
+
189
+ if (err) {
190
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
191
+ shim.agent.errors.add(segment.transaction, err, llmError)
192
+ }
193
+
194
+ delete response.headers
195
+ delete response.api_key
196
+ }
197
+
198
+ /*
199
+ * Chat completions create can return a stream once promise resolves
200
+ * This wraps the iterator which is a generator function
201
+ * We will call the original iterator, intercept chunks and yield
202
+ * to the original. On complete we will construct the new message object
203
+ * with what we have seen in the stream and create the chat completion
204
+ * messages
205
+ *
206
+ */
207
+ function instrumentStream({ request, response, segment }) {
208
+ shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
209
+ return async function* wrappedIterator() {
210
+ let content = ''
211
+ let role = ''
212
+ let chunk
213
+ let err
214
+ try {
215
+ const iterator = orig.apply(this, arguments)
216
+
217
+ for await (chunk of iterator) {
218
+ if (chunk.choices?.[0]?.delta?.role) {
219
+ role = chunk.choices[0].delta.role
220
+ }
221
+
222
+ content += chunk.choices?.[0]?.delta?.content ?? ''
223
+ yield chunk
224
+ }
225
+ } catch (streamErr) {
226
+ err = streamErr
227
+ throw err
228
+ } finally {
229
+ chunk.choices[0].message = { role, content }
230
+ // update segment duration since we want to extend the time it took to
231
+ // handle the stream
232
+ segment.touch()
233
+
234
+ recordChatCompletionMessages({
235
+ segment,
236
+ request,
237
+ response: chunk,
238
+ err
239
+ })
240
+ }
241
+ }
242
+ })
243
+ }
244
+
245
+ /**
246
+ * Instruments chat completion creation
247
+ * and creates the LLM events
248
+ *
249
+ * **Note**: Currently only for promises. streams will come later
250
+ */
251
+ shim.record(
252
+ openai.Chat.Completions.prototype,
253
+ 'create',
254
+ function wrapCreate(shim, create, name, args) {
255
+ const [request] = args
256
+ if (request.stream && semver.lt(shim.pkgVersion, MIN_STREAM_VERSION)) {
257
+ shim.logger.warn(
258
+ `Instrumenting chat completion streams is only supported with openai version ${MIN_STREAM_VERSION}+.`
259
+ )
260
+ return
261
+ }
262
+
263
+ return {
264
+ name: `${AI.OPEN_AI}/Chat/Completions/Create`,
265
+ promise: true,
266
+ // eslint-disable-next-line max-params
267
+ after(_shim, _fn, _name, err, response, segment) {
268
+ if (request.stream) {
269
+ instrumentStream({ request, response, segment })
270
+ } else {
271
+ recordChatCompletionMessages({
272
+ segment,
273
+ request,
274
+ response,
275
+ err
276
+ })
277
+ }
278
+ }
279
+ }
280
+ }
281
+ )
282
+
283
+ /**
284
+ * Instruments embedding creation
285
+ * and creates LlmEmbedding event
286
+ */
287
+ shim.record(
288
+ openai.Embeddings.prototype,
289
+ 'create',
290
+ function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
291
+ const [request] = args
292
+ return {
293
+ name: `${AI.OPEN_AI}/Embeddings/Create`,
294
+ promise: true,
295
+ // eslint-disable-next-line max-params
296
+ after(_shim, _fn, _name, err, response, segment) {
297
+ if (!response) {
298
+ // If we get an error, it is possible that `response = null`.
299
+ // In that case, we define it to be an empty object.
300
+ response = {}
301
+ }
302
+ response.headers = segment[openAiHeaders]
303
+ response.api_key = segment[openAiApiKey]
304
+ // explicitly end segment to get consistent duration
305
+ // for both LLM events and the segment
306
+ segment.end()
307
+ const embedding = new LlmEmbedding({
308
+ agent,
309
+ segment,
310
+ request,
311
+ response,
312
+ withError: err != null
313
+ })
314
+
315
+ recordEvent('LlmEmbedding', embedding)
316
+
317
+ if (err) {
318
+ const llmError = new LlmErrorMessage({ cause: err, embedding, response })
319
+ shim.agent.errors.add(segment.transaction, err, llmError)
320
+ }
321
+
322
+ // cleanup keys on response before returning to user code
323
+ delete response.api_key
324
+ delete response.headers
325
+ }
326
+ }
327
+ }
328
+ )
329
+ }
@@ -16,7 +16,7 @@ const {
16
16
  const semver = require('semver')
17
17
 
18
18
  module.exports = function instrument(shim, tools) {
19
- const pinoVersion = shim.require('./package.json').version
19
+ const pinoVersion = shim.pkgVersion
20
20
 
21
21
  if (semver.lt(pinoVersion, '7.0.0')) {
22
22
  shim.logger.debug('Instrumentation only supported on pino >=7.0.0.')
@@ -28,6 +28,7 @@ module.exports = function instrumentations() {
28
28
  'memcached': { type: MODULE_TYPE.DATASTORE },
29
29
  'mongodb': { type: MODULE_TYPE.DATASTORE },
30
30
  'mysql': { module: './instrumentation/mysql' },
31
+ 'openai': { type: MODULE_TYPE.GENERIC },
31
32
  '@nestjs/core': { type: MODULE_TYPE.WEB_FRAMEWORK },
32
33
  'pino': { module: './instrumentation/pino' },
33
34
  'pg': { type: MODULE_TYPE.DATASTORE },
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const LlmEvent = require('./event')
8
+
9
+ module.exports = class LlmChatCompletionMessage extends LlmEvent {
10
+ constructor({ agent, segment, request = {}, response = {}, index = 0, message, completionId }) {
11
+ super({ agent, segment, request, response })
12
+ this.id = `${response.id}-${index}`
13
+ this.conversation_id = this.conversationId(agent)
14
+ this.content = message?.content
15
+ this.role = message?.role
16
+ this.sequence = index
17
+ this.completion_id = completionId
18
+ this.is_response = response?.choices?.[0]?.message?.content === this.content
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const LlmEvent = require('./event')
8
+
9
+ module.exports = class LlmChatCompletionSummary extends LlmEvent {
10
+ constructor({ agent, segment, request = {}, response = {}, withError = false }) {
11
+ super({ agent, segment, request, response, responseAttrs: true })
12
+ this.error = withError
13
+ this.conversation_id = this.conversationId(agent)
14
+ this['request.max_tokens'] = request.max_tokens
15
+ this['request.temperature'] = request.temperature
16
+ this['response.number_of_messages'] = request?.messages?.length + response?.choices?.length
17
+ this['response.usage.completion_tokens'] = response?.usage?.completion_tokens
18
+ this['response.choices.finish_reason'] = response?.choices?.[0]?.finish_reason
19
+ }
20
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const LlmEvent = require('./event')
8
+
9
+ module.exports = class LlmEmbedding extends LlmEvent {
10
+ constructor({ agent, segment, request = {}, response = {}, withError = false }) {
11
+ super({ agent, segment, request, response, responseAttrs: true })
12
+ this.error = withError
13
+ this.input = request.input?.toString()
14
+ }
15
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ /**
9
+ * Represents an error object, to be tracked via `agent.errors`, that is the
10
+ * result of some error returned from the OpenAI API.
11
+ */
12
+ module.exports = class LlmErrorMessage {
13
+ /**
14
+ * @param {object} params Constructor parameters
15
+ * @param {object} [params.response] Instance of an incoming message.
16
+ * @param {object} [params.cause] An instance of the OpenAI error object.
17
+ * @param {LlmChatCompletionSummary} [params.summary] Details about the
18
+ * conversation if it was a chat completion conversation.
19
+ * @param {LlmEmbedding} [params.embedding] Details about the conversation
20
+ * if it was an embedding conversation.
21
+ */
22
+ constructor({ response, cause, summary, embedding } = {}) {
23
+ this['http.statusCode'] = response?.status ?? cause?.status
24
+ this['error.message'] = cause?.message
25
+ this['error.code'] = response?.code ?? cause?.error?.code
26
+ this['error.param'] = response?.param ?? cause?.error?.param
27
+ this.completion_id = summary?.id
28
+ this.embedding_id = embedding?.id
29
+ }
30
+ }
@@ -0,0 +1,56 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const { makeId } = require('../../util/hashes')
8
+ const { DESTINATIONS } = require('../../../lib/config/attribute-filter')
9
+
10
+ module.exports = class LlmEvent {
11
+ constructor({ agent, segment, request, response, responseAttrs = false }) {
12
+ this.id = makeId(36)
13
+ this.appName = agent.config.applications()[0]
14
+ this.request_id = response?.headers?.['x-request-id']
15
+ this.trace_id = segment?.transaction?.traceId
16
+ this.span_id = segment?.id
17
+ this.transaction_id = segment?.transaction?.id
18
+ this['response.model'] = response.model
19
+ this.vendor = 'openAI'
20
+ this.ingest_source = 'Node'
21
+
22
+ /**
23
+ * Used in embedding, and chat completion summary.
24
+ * The flag will include response attributes but also
25
+ * other attributes from request like model, and api key.
26
+ * Lastly, it includes the active span's duration.
27
+ */
28
+ if (responseAttrs) {
29
+ this['request.model'] = request.model || request.engine
30
+ this.duration = segment?.getDurationInMillis()
31
+ this.api_key_last_four_digits = response?.api_key && `sk-${response.api_key.slice(-4)}`
32
+ this.responseAttrs(response)
33
+ }
34
+ }
35
+
36
+ responseAttrs(response) {
37
+ this['response.organization'] = response?.headers?.['openai-organization']
38
+ this['response.usage.total_tokens'] = response?.usage?.total_tokens
39
+ this['response.usage.prompt_tokens'] = response?.usage?.prompt_tokens
40
+ this['response.headers.llmVersion'] = response?.headers?.['openai-version']
41
+ this['response.headers.ratelimitLimitRequests'] =
42
+ response?.headers?.['x-ratelimit-limit-requests']
43
+ this['response.headers.ratelimitLimitTokens'] = response?.headers?.['x-ratelimit-limit-tokens']
44
+ this['response.headers.ratelimitResetTokens'] = response?.headers?.['x-ratelimit-reset-tokens']
45
+ this['response.headers.ratelimitRemainingTokens'] =
46
+ response?.headers?.['x-ratelimit-remaining-tokens']
47
+ this['response.headers.ratelimitRemainingRequests'] =
48
+ response?.headers?.['x-ratelimit-remaining-requests']
49
+ }
50
+
51
+ conversationId(agent) {
52
+ const transaction = agent.tracer.getTransaction()
53
+ const attrs = transaction?.trace?.custom.get(DESTINATIONS.TRANS_SCOPE)
54
+ return attrs?.conversation_id
55
+ }
56
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const { makeId } = require('../../util/hashes')
8
+
9
+ module.exports = class LlmFeedbackMessage {
10
+ constructor(opts) {
11
+ this.id = makeId(32)
12
+ this.conversation_id = opts.conversationId
13
+ this.request_id = opts.requestId
14
+ this.message_id = opts.messageId
15
+ this.category = opts.category
16
+ this.rating = opts.rating
17
+ this.message = opts.message
18
+ this.ingest_source = 'Node'
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const LlmChatCompletionSummary = require('./chat-completion-summary')
9
+ const LlmChatCompletionMessage = require('./chat-completion-message')
10
+ const LlmEmbedding = require('./embedding')
11
+ const LlmFeedbackMessage = require('./feedback-message')
12
+ const LlmErrorMessage = require('./error-message')
13
+
14
+ module.exports = {
15
+ LlmChatCompletionMessage,
16
+ LlmChatCompletionSummary,
17
+ LlmEmbedding,
18
+ LlmErrorMessage,
19
+ LlmFeedbackMessage
20
+ }