newrelic 13.2.0 → 13.3.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.
- package/NEWS.md +44 -0
- package/THIRD_PARTY_NOTICES.md +219 -8
- package/api.js +1 -1
- package/esm-loader.mjs +6 -1
- package/lib/instrumentation/@azure/functions.js +8 -46
- package/lib/instrumentation/core/http-outbound.js +26 -10
- package/lib/instrumentation/core/http.js +20 -118
- package/lib/instrumentations.js +0 -14
- package/lib/otel/logs/index.js +80 -0
- package/lib/otel/metrics/index.js +119 -0
- package/lib/otel/setup-signal.js +37 -0
- package/lib/otel/setup.js +15 -25
- package/lib/otel/{attr-reconciler.js → traces/attr-reconciler.js} +2 -2
- package/lib/otel/{exception-mapping.js → traces/exception-mapping.js} +1 -1
- package/lib/otel/traces/index.js +40 -0
- package/lib/otel/{rules.js → traces/rules.js} +1 -1
- package/lib/otel/{segment-synthesis.js → traces/segment-synthesis.js} +5 -4
- package/lib/otel/{segments → traces/segments}/consumer.js +4 -4
- package/lib/otel/{segments → traces/segments}/database.js +6 -6
- package/lib/otel/{segments → traces/segments}/http-external.js +11 -8
- package/lib/otel/traces/segments/index.js +22 -0
- package/lib/otel/{segments → traces/segments}/internal.js +1 -1
- package/lib/otel/{segments → traces/segments}/producer.js +2 -2
- package/lib/otel/{segments → traces/segments}/server.js +4 -4
- package/lib/otel/{segments → traces/segments}/utils.js +1 -1
- package/lib/otel/{span-processor.js → traces/span-processor.js} +13 -19
- package/lib/otel/{transformation-rules.json → traces/transformation-rules.json} +3 -2
- package/lib/otel/{utils.js → traces/utils.js} +2 -1
- package/lib/serverless/aws-lambda.js +7 -68
- package/lib/shim/webframework-shim/middleware.js +1 -1
- package/lib/shimmer.js +2 -2
- package/lib/subscriber-configs.js +2 -1
- package/lib/subscribers/base.js +30 -0
- package/lib/subscribers/elasticsearch/config.js +1 -0
- package/lib/subscribers/mcp-sdk/client-request.js +66 -0
- package/lib/subscribers/mcp-sdk/config.js +13 -75
- package/lib/subscribers/openai/base.js +21 -0
- package/lib/subscribers/openai/chat-responses.js +15 -0
- package/lib/subscribers/openai/chat.js +85 -0
- package/lib/subscribers/openai/client.js +31 -0
- package/lib/subscribers/openai/config.js +85 -0
- package/lib/subscribers/openai/embeddings.js +54 -0
- package/lib/subscribers/openai/utils.js +349 -0
- package/lib/transaction/index.js +157 -56
- package/lib/transaction/trace/segment.js +5 -2
- package/lib/util/urltils.js +158 -192
- package/lib/utilization/common.js +0 -6
- package/package.json +3 -4
- package/lib/instrumentation/openai.js +0 -482
- package/lib/otel/logs/bootstrap-logs.js +0 -84
- package/lib/otel/metrics/bootstrap-metrics.js +0 -117
- package/lib/otel/segments/index.js +0 -22
- package/lib/patch-module.js +0 -70
- package/lib/subscribers/mcp-sdk/client-prompt.js +0 -23
- package/lib/subscribers/mcp-sdk/client-resource.js +0 -24
- package/lib/subscribers/mcp-sdk/client-tool.js +0 -23
- package/lib/subscribers/mcp-sdk/client.js +0 -29
- /package/lib/otel/{constants.js → traces/constants.js} +0 -0
- /package/lib/otel/{sampler.js → traces/sampler.js} +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
/*
|
|
8
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
9
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const OpenAISubscriber = require('./base')
|
|
13
|
+
const { AI } = require('../../metrics/names')
|
|
14
|
+
const { OPENAI } = AI
|
|
15
|
+
const {
|
|
16
|
+
addLlmMeta,
|
|
17
|
+
recordChatCompletionMessages,
|
|
18
|
+
instrumentStream,
|
|
19
|
+
} = require('./utils')
|
|
20
|
+
const semver = require('semver')
|
|
21
|
+
const MIN_STREAM_VERSION = '4.12.2'
|
|
22
|
+
|
|
23
|
+
class OpenAIChatCompletions extends OpenAISubscriber {
|
|
24
|
+
constructor({ agent, logger, channelName = 'nr_completionsCreate' }) {
|
|
25
|
+
super({ agent, logger, channelName })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handler(data, ctx) {
|
|
29
|
+
const { arguments: args, moduleVersion } = data
|
|
30
|
+
const [request] = args
|
|
31
|
+
if (request.stream && semver.lt(moduleVersion, MIN_STREAM_VERSION)) {
|
|
32
|
+
this.logger.warn(`Instrumenting chat completion streams is only supported with openai version ${MIN_STREAM_VERSION}+.`)
|
|
33
|
+
return ctx
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const segment = this.agent.tracer.createSegment({
|
|
37
|
+
name: OPENAI.COMPLETION,
|
|
38
|
+
parent: ctx.segment,
|
|
39
|
+
transaction: ctx.transaction
|
|
40
|
+
})
|
|
41
|
+
const newCtx = ctx.enterSegment({ segment })
|
|
42
|
+
return newCtx
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
asyncEnd(data) {
|
|
46
|
+
const ctx = this.agent.tracer.getContext()
|
|
47
|
+
if (!ctx?.segment || !ctx?.transaction) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
const { result: response, arguments: args, error: err } = data
|
|
51
|
+
const [request] = args
|
|
52
|
+
|
|
53
|
+
if (request.stream) {
|
|
54
|
+
instrumentStream({
|
|
55
|
+
agent: this.agent,
|
|
56
|
+
logger: this.logger,
|
|
57
|
+
request,
|
|
58
|
+
headers: ctx.extras?.headers,
|
|
59
|
+
response,
|
|
60
|
+
segment: ctx.segment,
|
|
61
|
+
transaction: ctx.transaction,
|
|
62
|
+
err
|
|
63
|
+
})
|
|
64
|
+
} else {
|
|
65
|
+
recordChatCompletionMessages({
|
|
66
|
+
agent: this.agent,
|
|
67
|
+
logger: this.logger,
|
|
68
|
+
segment: ctx.segment,
|
|
69
|
+
transaction: ctx.transaction,
|
|
70
|
+
request,
|
|
71
|
+
response,
|
|
72
|
+
headers: ctx.extras?.headers,
|
|
73
|
+
err
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
addLlmMeta({
|
|
78
|
+
agent: this.agent,
|
|
79
|
+
transaction: ctx.transaction,
|
|
80
|
+
version: data.moduleVersion,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = OpenAIChatCompletions
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const OpenAISubscriber = require('./base')
|
|
8
|
+
class OpenAIClientSubscriber extends OpenAISubscriber {
|
|
9
|
+
constructor({ agent, logger }) {
|
|
10
|
+
super({ agent, logger, channelName: 'nr_makeRequest' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
handler(data, ctx) {
|
|
14
|
+
const { self } = data
|
|
15
|
+
ctx.extras = { apiKey: self.apiKey }
|
|
16
|
+
return ctx
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
asyncEnd(data) {
|
|
20
|
+
const { result } = data
|
|
21
|
+
const ctx = this.agent.tracer.getContext()
|
|
22
|
+
if (ctx?.segment) {
|
|
23
|
+
const headers = result?.response?.headers
|
|
24
|
+
? Object.fromEntries(result.response.headers)
|
|
25
|
+
: { ...result?.headers }
|
|
26
|
+
ctx.extras = { headers }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = OpenAIClientSubscriber
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
openai: [
|
|
10
|
+
{
|
|
11
|
+
path: './openai/chat.js',
|
|
12
|
+
instrumentations: [
|
|
13
|
+
{
|
|
14
|
+
channelName: 'nr_completionsCreate',
|
|
15
|
+
module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/chat/completions.js' },
|
|
16
|
+
functionQuery: {
|
|
17
|
+
className: 'Completions',
|
|
18
|
+
methodName: 'create',
|
|
19
|
+
kind: 'Async'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
channelName: 'nr_completionsCreate',
|
|
24
|
+
module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/chat/completions/completions.js' },
|
|
25
|
+
functionQuery: {
|
|
26
|
+
className: 'Completions',
|
|
27
|
+
methodName: 'create',
|
|
28
|
+
kind: 'Async'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
path: './openai/chat-responses.js',
|
|
35
|
+
instrumentations: [
|
|
36
|
+
{
|
|
37
|
+
channelName: 'nr_responses',
|
|
38
|
+
module: { name: 'openai', versionRange: '>=4.87.0', filePath: 'resources/responses/responses.js' },
|
|
39
|
+
functionQuery: {
|
|
40
|
+
className: 'Responses',
|
|
41
|
+
methodName: 'create',
|
|
42
|
+
kind: 'Async'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: './openai/client.js',
|
|
49
|
+
instrumentations: [
|
|
50
|
+
{
|
|
51
|
+
channelName: 'nr_makeRequest',
|
|
52
|
+
module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'core.js' },
|
|
53
|
+
functionQuery: {
|
|
54
|
+
className: 'APIClient',
|
|
55
|
+
methodName: 'makeRequest',
|
|
56
|
+
kind: 'Async'
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
channelName: 'nr_makeRequest',
|
|
61
|
+
module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'client.js' },
|
|
62
|
+
functionQuery: {
|
|
63
|
+
className: 'OpenAI',
|
|
64
|
+
methodName: 'makeRequest',
|
|
65
|
+
kind: 'Async'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: './openai/embeddings.js',
|
|
72
|
+
instrumentations: [
|
|
73
|
+
{
|
|
74
|
+
channelName: 'nr_embeddingsCreate',
|
|
75
|
+
module: { name: 'openai', versionRange: '>=4.0.0', filePath: 'resources/embeddings.js' },
|
|
76
|
+
functionQuery: {
|
|
77
|
+
className: 'Embeddings',
|
|
78
|
+
methodName: 'create',
|
|
79
|
+
kind: 'Async'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const OpenAISubscriber = require('./base')
|
|
8
|
+
const { AI } = require('../../metrics/names')
|
|
9
|
+
const { OPENAI } = AI
|
|
10
|
+
const {
|
|
11
|
+
addLlmMeta,
|
|
12
|
+
recordEmbeddingMessage
|
|
13
|
+
} = require('./utils')
|
|
14
|
+
|
|
15
|
+
class OpenAIEmbeddings extends OpenAISubscriber {
|
|
16
|
+
constructor({ agent, logger }) {
|
|
17
|
+
super({ agent, logger, channelName: 'nr_embeddingsCreate' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
handler(data, ctx) {
|
|
21
|
+
const segment = this.agent.tracer.createSegment({
|
|
22
|
+
name: OPENAI.EMBEDDING,
|
|
23
|
+
parent: ctx.segment,
|
|
24
|
+
transaction: ctx.transaction
|
|
25
|
+
})
|
|
26
|
+
const newCtx = ctx.enterSegment({ segment })
|
|
27
|
+
return newCtx
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
asyncEnd(data) {
|
|
31
|
+
const ctx = this.agent.tracer.getContext()
|
|
32
|
+
if (!ctx?.segment || !ctx?.transaction) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
const { arguments: args, error: err } = data
|
|
36
|
+
const { result: response } = data
|
|
37
|
+
const [request] = args
|
|
38
|
+
const agent = this.agent
|
|
39
|
+
const { segment, transaction } = ctx
|
|
40
|
+
recordEmbeddingMessage({
|
|
41
|
+
agent,
|
|
42
|
+
logger: this.logger,
|
|
43
|
+
segment,
|
|
44
|
+
transaction,
|
|
45
|
+
request,
|
|
46
|
+
response,
|
|
47
|
+
headers: ctx.extras?.headers,
|
|
48
|
+
err
|
|
49
|
+
})
|
|
50
|
+
addLlmMeta({ agent, transaction, version: data.moduleVersion })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = OpenAIEmbeddings
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict'
|
|
7
|
+
const {
|
|
8
|
+
LlmChatCompletionMessage,
|
|
9
|
+
LlmChatCompletionSummary,
|
|
10
|
+
LlmEmbedding,
|
|
11
|
+
LlmErrorMessage
|
|
12
|
+
} = require('../../llm-events/openai')
|
|
13
|
+
const { extractLlmContext } = require('../../util/llm-utils')
|
|
14
|
+
|
|
15
|
+
const { AI } = require('../../metrics/names')
|
|
16
|
+
const { OPENAI } = AI
|
|
17
|
+
const { DESTINATIONS } = require('../../config/attribute-filter')
|
|
18
|
+
let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses the response from OpenAI and extracts the message content and role.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} response The OpenAI SDK response object
|
|
24
|
+
* @returns {{ content: string, role: string }} the message object with fields `content` and `role`
|
|
25
|
+
*/
|
|
26
|
+
function getMessageFromResponse(response) {
|
|
27
|
+
let content
|
|
28
|
+
let role
|
|
29
|
+
if (response?.object === 'response') {
|
|
30
|
+
content = response?.output?.[0]?.content?.[0]?.text
|
|
31
|
+
role = response?.output?.[0]?.role
|
|
32
|
+
} else {
|
|
33
|
+
content = response?.choices?.[0]?.message?.content
|
|
34
|
+
role = response?.choices?.[0]?.message?.role
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { content, role }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parses all messages from the OpenAI request object.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} request The OpenAI SDK request object
|
|
44
|
+
* @param logger
|
|
45
|
+
* @returns {Array<object>} an array of message objects with fields `content` and `role`
|
|
46
|
+
*/
|
|
47
|
+
function getMessagesFromRequest(request, logger) {
|
|
48
|
+
// There are a few different ways to pass messages to OpenAI SDK.
|
|
49
|
+
//
|
|
50
|
+
// For langchain and `chat.completions.create`, messages are passed
|
|
51
|
+
// as an array of objects with `content` and `role` properties
|
|
52
|
+
// to the `messages` field of the request.
|
|
53
|
+
//
|
|
54
|
+
// For `responses.create`, messages are passed as an array of objects
|
|
55
|
+
// with `content` and `role` properties OR as a single string (implied
|
|
56
|
+
// to be a user message) to the `input` field of the request.
|
|
57
|
+
let messages = []
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(request?.input)) {
|
|
60
|
+
// Handle array of input messages
|
|
61
|
+
messages = request.input.filter((msg) => msg?.content && msg?.role)
|
|
62
|
+
} else if (typeof request?.input === 'string') {
|
|
63
|
+
// Handle single string input as a user message
|
|
64
|
+
messages = [{ content: request.input, role: 'user' }]
|
|
65
|
+
} else if (Array.isArray(request?.messages)) {
|
|
66
|
+
// Handle array of messages
|
|
67
|
+
messages = request.messages.filter((msg) => msg?.content && msg?.role)
|
|
68
|
+
} else {
|
|
69
|
+
logger.warn('No valid messages found in OpenAI request object.')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return messages
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enqueues a LLM event to the custom event aggregator
|
|
77
|
+
*
|
|
78
|
+
* @param {object} params input params
|
|
79
|
+
* @param {Agent} params.agent NR agent instance
|
|
80
|
+
* @param {string} params.type LLM event type
|
|
81
|
+
* @param {object} params.msg LLM event
|
|
82
|
+
*/
|
|
83
|
+
function recordEvent({ agent, type, msg }) {
|
|
84
|
+
const llmContext = extractLlmContext(agent)
|
|
85
|
+
|
|
86
|
+
agent.customEventAggregator.add([
|
|
87
|
+
{ type, timestamp: Date.now() },
|
|
88
|
+
Object.assign({}, msg, llmContext)
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Increments the tracking metric and sets the llm attribute on transactions
|
|
94
|
+
*
|
|
95
|
+
* @param {object} params input params
|
|
96
|
+
* @param {Agent} params.agent NR agent instance
|
|
97
|
+
* @param {Transaction} params.transaction active transaction
|
|
98
|
+
* @param params.version
|
|
99
|
+
*/
|
|
100
|
+
function addLlmMeta({ agent, transaction, version }) {
|
|
101
|
+
TRACKING_METRIC = `${TRACKING_METRIC}/${version}`
|
|
102
|
+
agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
|
|
103
|
+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates LlmChatCompletionSummary for a chat completion creation.
|
|
108
|
+
* Also iterates over both input messages and the first response message
|
|
109
|
+
* and creates LlmChatCompletionMessage.
|
|
110
|
+
*
|
|
111
|
+
* Also assigns relevant ids by response id for LlmFeedbackEvent creation
|
|
112
|
+
*
|
|
113
|
+
* @param {object} params input params
|
|
114
|
+
* @param {Agent} params.agent NR agent instance
|
|
115
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
116
|
+
* @param {object} params.request chat completion params
|
|
117
|
+
* @param {object} params.response chat completion response
|
|
118
|
+
* @param {boolean} [params.err] err if it exists
|
|
119
|
+
* @param {Transaction} params.transaction active transaction
|
|
120
|
+
* @param params.headers
|
|
121
|
+
* @param params.logger
|
|
122
|
+
*/
|
|
123
|
+
function recordChatCompletionMessages({
|
|
124
|
+
agent,
|
|
125
|
+
headers,
|
|
126
|
+
logger,
|
|
127
|
+
segment,
|
|
128
|
+
request,
|
|
129
|
+
response,
|
|
130
|
+
err,
|
|
131
|
+
transaction
|
|
132
|
+
}) {
|
|
133
|
+
if (shouldSkipInstrumentation(agent.config, logger) === true) {
|
|
134
|
+
logger.debug('skipping sending of ai data')
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!response) {
|
|
139
|
+
// If we get an error, it is possible that `response = null`.
|
|
140
|
+
// In that case, we define it to be an empty object.
|
|
141
|
+
response = {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
response.headers = headers
|
|
145
|
+
// explicitly end segment to consistent duration
|
|
146
|
+
// for both LLM events and the segment
|
|
147
|
+
segment.end()
|
|
148
|
+
const completionSummary = new LlmChatCompletionSummary({
|
|
149
|
+
agent,
|
|
150
|
+
segment,
|
|
151
|
+
transaction,
|
|
152
|
+
request,
|
|
153
|
+
response,
|
|
154
|
+
withError: err != null
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Note: langchain still expects a message event even
|
|
158
|
+
// when the response is empty, so we will not filter
|
|
159
|
+
// the response message.
|
|
160
|
+
const messages = [
|
|
161
|
+
...getMessagesFromRequest(request, logger),
|
|
162
|
+
getMessageFromResponse(response)
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
messages.forEach((message, index) => {
|
|
166
|
+
const completionMsg = new LlmChatCompletionMessage({
|
|
167
|
+
agent,
|
|
168
|
+
segment,
|
|
169
|
+
transaction,
|
|
170
|
+
request,
|
|
171
|
+
response,
|
|
172
|
+
index,
|
|
173
|
+
completionId: completionSummary.id,
|
|
174
|
+
message
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
|
|
181
|
+
|
|
182
|
+
if (err) {
|
|
183
|
+
const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
|
|
184
|
+
agent.errors.add(transaction, err, llmError)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
delete response.headers
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Checks if we should skip instrumentation.
|
|
192
|
+
* Currently, it checks if `ai_monitoring.enabled` is true
|
|
193
|
+
* and the package version >= 4.0.0
|
|
194
|
+
*
|
|
195
|
+
* @param {object} config agent config
|
|
196
|
+
* @param logger
|
|
197
|
+
* @returns {boolean} flag if instrumentation should be skipped
|
|
198
|
+
*/
|
|
199
|
+
function shouldSkipInstrumentation(config, logger) {
|
|
200
|
+
if (config?.ai_monitoring?.enabled !== true) {
|
|
201
|
+
logger.debug('config.ai_monitoring.enabled is set to false.')
|
|
202
|
+
return true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `chat.completions.create` can return a stream once promise resolves.
|
|
208
|
+
* This wraps the iterator which is a generator function.
|
|
209
|
+
* We will call the original iterator, intercept chunks and yield
|
|
210
|
+
* to the original. On complete, we will construct the new message object
|
|
211
|
+
* with what we have seen in the stream and create the chat completion
|
|
212
|
+
* messages.
|
|
213
|
+
* @param {object} params input params
|
|
214
|
+
* @param {Agent} params.agent NR agent instance
|
|
215
|
+
* @param {Shim} params.shim the current shim instance
|
|
216
|
+
* @param {object} params.request chat completion params
|
|
217
|
+
* @param {object} params.response chat completion response
|
|
218
|
+
* @param {TraceSegment} params.segment active segment from chat completion
|
|
219
|
+
* @param {Transaction} params.transaction active transaction
|
|
220
|
+
* @param {Error} params.err error if it exists
|
|
221
|
+
* @param params.headers
|
|
222
|
+
* @param params.logger
|
|
223
|
+
*/
|
|
224
|
+
function instrumentStream({ agent, headers, logger, request, response, segment, transaction, err = null }) {
|
|
225
|
+
if (!agent.config.ai_monitoring.streaming.enabled) {
|
|
226
|
+
logger.warn(
|
|
227
|
+
'`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
|
|
228
|
+
)
|
|
229
|
+
agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (err) {
|
|
234
|
+
// If there is an error already e.g. APIConnectionError,
|
|
235
|
+
// the iterator will not be called, so we have to
|
|
236
|
+
// record the chat completion messages with the error now.
|
|
237
|
+
segment.touch()
|
|
238
|
+
recordChatCompletionMessages({
|
|
239
|
+
agent,
|
|
240
|
+
headers,
|
|
241
|
+
logger,
|
|
242
|
+
segment,
|
|
243
|
+
transaction,
|
|
244
|
+
request,
|
|
245
|
+
response,
|
|
246
|
+
err
|
|
247
|
+
})
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const orig = response.iterator
|
|
252
|
+
response.iterator = async function * wrappedIterator() {
|
|
253
|
+
let content = ''
|
|
254
|
+
let role = ''
|
|
255
|
+
let chunk
|
|
256
|
+
try {
|
|
257
|
+
const iterator = orig.apply(this, arguments)
|
|
258
|
+
|
|
259
|
+
for await (chunk of iterator) {
|
|
260
|
+
if (chunk.choices?.[0]?.delta?.role) {
|
|
261
|
+
role = chunk.choices[0].delta.role
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
content += chunk.choices?.[0]?.delta?.content ?? ''
|
|
265
|
+
yield chunk
|
|
266
|
+
}
|
|
267
|
+
} catch (streamErr) {
|
|
268
|
+
err = streamErr
|
|
269
|
+
throw err
|
|
270
|
+
} finally {
|
|
271
|
+
if (chunk?.choices) {
|
|
272
|
+
chunk.choices[0].message = { role, content }
|
|
273
|
+
} else if (chunk?.response) {
|
|
274
|
+
chunk = chunk.response
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// update segment duration since we want to extend the time it took to
|
|
278
|
+
// handle the stream
|
|
279
|
+
segment.touch()
|
|
280
|
+
|
|
281
|
+
recordChatCompletionMessages({
|
|
282
|
+
agent,
|
|
283
|
+
headers,
|
|
284
|
+
logger,
|
|
285
|
+
segment,
|
|
286
|
+
transaction,
|
|
287
|
+
request,
|
|
288
|
+
response: chunk,
|
|
289
|
+
err
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function recordEmbeddingMessage({
|
|
296
|
+
agent,
|
|
297
|
+
logger,
|
|
298
|
+
request,
|
|
299
|
+
headers,
|
|
300
|
+
response,
|
|
301
|
+
segment,
|
|
302
|
+
transaction,
|
|
303
|
+
err
|
|
304
|
+
}) {
|
|
305
|
+
if (!response) {
|
|
306
|
+
// If we get an error, it is possible that `response = null`.
|
|
307
|
+
// In that case, we define it to be an empty object.
|
|
308
|
+
response = {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
segment.end()
|
|
312
|
+
if (shouldSkipInstrumentation(agent.config, logger) === true) {
|
|
313
|
+
// We need this check inside the wrapper because it is possible for monitoring
|
|
314
|
+
// to be disabled at the account level. In such a case, the value is set
|
|
315
|
+
// after the instrumentation has been initialized.
|
|
316
|
+
logger.debug('skipping sending of ai data')
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
response.headers = headers
|
|
321
|
+
// explicitly end segment to get consistent duration
|
|
322
|
+
// for both LLM events and the segment
|
|
323
|
+
|
|
324
|
+
const embedding = new LlmEmbedding({
|
|
325
|
+
agent,
|
|
326
|
+
segment,
|
|
327
|
+
transaction,
|
|
328
|
+
request,
|
|
329
|
+
response,
|
|
330
|
+
withError: err != null
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
|
|
334
|
+
|
|
335
|
+
if (err) {
|
|
336
|
+
const llmError = new LlmErrorMessage({ cause: err, embedding, response })
|
|
337
|
+
agent.errors.add(transaction, err, llmError)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// cleanup keys on response before returning to user code
|
|
341
|
+
delete response.headers
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
addLlmMeta,
|
|
346
|
+
instrumentStream,
|
|
347
|
+
recordChatCompletionMessages,
|
|
348
|
+
recordEmbeddingMessage
|
|
349
|
+
}
|