newrelic 12.24.0 → 13.0.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 CHANGED
@@ -1,3 +1,51 @@
1
+ ### v13.0.0 (2025-07-23)
2
+ #### ⚠ BREAKING CHANGES
3
+
4
+ This version of the Node.js agent is a SemVer MAJOR update and contains the following breaking changes. MAJOR versions may drop support for language runtimes that have reached End-of-Life according to the maintainer. Additionally, MAJOR versions may drop support for and remove certain instrumentation. For more details on these changes please see the [migration guide](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/installation-configuration/update-nodejs-agent/).
5
+
6
+ * Dropped support for Node.js 18
7
+ * Updated min supported version for `fastify` to 3.0.0, `pino` to 8.0.0, and `koa-router` to 12.0.0
8
+
9
+ #### Features
10
+
11
+ * Dropped support for Node.js 18 ([#3097](https://github.com/newrelic/node-newrelic/pull/3097)) ([cb7c080](https://github.com/newrelic/node-newrelic/commit/cb7c080aadb0afbf6a894c1abbd1c77bd6b11e31))
12
+ * Updated min supported version for `fastify`, `pino`, and `koa-router` ([#3108](https://github.com/newrelic/node-newrelic/pull/3108)) ([ae9e21d](https://github.com/newrelic/node-newrelic/commit/ae9e21d94896e5c540927477b91113b5a38dc31c))
13
+
14
+ #### Documentation
15
+
16
+ * Updated AI Monitoring compatibility docs with new AWS Bedrock APIs ([#3230](https://github.com/newrelic/node-newrelic/pull/3230)) ([e1ff0d3](https://github.com/newrelic/node-newrelic/commit/e1ff0d3e2b1d3481f0c85e5c0b7d32bacae332ab))
17
+ * Updated compatibility report ([#3237](https://github.com/newrelic/node-newrelic/pull/3237)) ([dc85e4e](https://github.com/newrelic/node-newrelic/commit/dc85e4e196f67038492deb5c13ecf76cd8161025))
18
+
19
+
20
+ #### Tests
21
+
22
+ * Fixed `assertSegments` to insert all expected segments ([#3239](https://github.com/newrelic/node-newrelic/pull/3239)) ([de183fb](https://github.com/newrelic/node-newrelic/commit/de183fbe608fbf6fb97ad3508f506957b784d868))
23
+ * Updated tests to mock outgoing calls to avoid issues in CI ([#3234](https://github.com/newrelic/node-newrelic/pull/3234)) ([66694c6](https://github.com/newrelic/node-newrelic/commit/66694c6885cd384967d0402bf6a2cfe6910f3d95))
24
+
25
+ #### Continuous integration
26
+
27
+ * Updated prepare release to exclude semver major copy for repos other than `node-newrelic` ([#3242](https://github.com/newrelic/node-newrelic/pull/3242)) ([82d9689](https://github.com/newrelic/node-newrelic/commit/82d968921b5fa6e4e7b3961285237e60a69b4dee))
28
+ * Updated release prep to add copy around breaking changes ([#3199](https://github.com/newrelic/node-newrelic/pull/3199)) ([ca1131c](https://github.com/newrelic/node-newrelic/commit/ca1131cb3b2cea27b91a7b76ddbe39484bf9e032))
29
+
30
+ ### v12.25.0 (2025-07-09)
31
+
32
+ #### Features
33
+
34
+ * Instrument AWS Bedrock Converse API ([#3191](https://github.com/newrelic/node-newrelic/pull/3191)) ([16ac684](https://github.com/newrelic/node-newrelic/commit/16ac6845a82506bbdd0e51f3f9a6d05ac8016246))
35
+
36
+ #### Bug fixes
37
+
38
+ * `eventSegment.addAttribute` will not crash if null ([#3215](https://github.com/newrelic/node-newrelic/pull/3215)) ([8955f37](https://github.com/newrelic/node-newrelic/commit/8955f3737e686b4bf6beab2d4e53a1f5eb221166))
39
+ * `openai` Responses API support changed to >=v4.87.0 ([#3219](https://github.com/newrelic/node-newrelic/pull/3219)) ([19c0c70](https://github.com/newrelic/node-newrelic/commit/19c0c7011c8c877098c5a57063354f01f5f83295))
40
+
41
+ #### Documentation
42
+
43
+ * Updated compatibility report ([#3220](https://github.com/newrelic/node-newrelic/pull/3220)) ([b4350c7](https://github.com/newrelic/node-newrelic/commit/b4350c7e3a718e47d066c5c2c1f4f5f20f43e52c))
44
+
45
+ #### Miscellaneous chores
46
+
47
+ * Converted .proto to .json for bundling ([#3212](https://github.com/newrelic/node-newrelic/pull/3212)) ([1e208d4](https://github.com/newrelic/node-newrelic/commit/1e208d43acc1811674126b6aa987c895178f4fc3))
48
+
1
49
  ### v12.24.0 (2025-07-07)
2
50
 
3
51
  #### Features
@@ -82,7 +82,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
82
82
 
83
83
  **[optionalDependencies](#optionalDependencies)**
84
84
 
85
- * [@contrast/fn-inspect](#contrastfn-inspect)
85
+ * [@newrelic/fn-inspect](#newrelicfn-inspect)
86
86
  * [@newrelic/native-metrics](#newrelicnative-metrics)
87
87
  * [@prisma/prisma-fmt-wasm](#prismaprisma-fmt-wasm)
88
88
 
@@ -95,7 +95,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic
95
95
 
96
96
  ### @grpc/grpc-js
97
97
 
98
- This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.13.4](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.4)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.4/LICENSE):
98
+ This product includes source derived from [@grpc/grpc-js](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js) ([v1.13.3](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/tree/v1.13.3)), distributed under the [Apache-2.0 License](https://github.com/grpc/grpc-node/tree/master/packages/grpc-js/blob/v1.13.3/LICENSE):
99
99
 
100
100
  ```
101
101
  Apache License
@@ -1902,7 +1902,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1902
1902
 
1903
1903
  ### import-in-the-middle
1904
1904
 
1905
- This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.14.0](https://github.com/nodejs/import-in-the-middle/tree/v1.14.0)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.14.0/LICENSE):
1905
+ This product includes source derived from [import-in-the-middle](https://github.com/nodejs/import-in-the-middle) ([v1.13.2](https://github.com/nodejs/import-in-the-middle/tree/v1.13.2)), distributed under the [Apache-2.0 License](https://github.com/nodejs/import-in-the-middle/blob/v1.13.2/LICENSE):
1906
1906
 
1907
1907
  ```
1908
1908
  Apache License
@@ -2333,7 +2333,7 @@ SOFTWARE.
2333
2333
 
2334
2334
  ### @aws-sdk/client-s3
2335
2335
 
2336
- This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.821.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.821.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.821.0/LICENSE):
2336
+ This product includes source derived from [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3) ([v3.808.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE):
2337
2337
 
2338
2338
  ```
2339
2339
  Apache License
@@ -2542,7 +2542,7 @@ This product includes source derived from [@aws-sdk/client-s3](https://github.co
2542
2542
 
2543
2543
  ### @aws-sdk/s3-request-presigner
2544
2544
 
2545
- This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.821.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.821.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.821.0/LICENSE):
2545
+ This product includes source derived from [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3) ([v3.808.0](https://github.com/aws/aws-sdk-js-v3/tree/v3.808.0)), distributed under the [Apache-2.0 License](https://github.com/aws/aws-sdk-js-v3/blob/v3.808.0/LICENSE):
2546
2546
 
2547
2547
  ```
2548
2548
  Apache License
@@ -4370,7 +4370,7 @@ SOFTWARE.
4370
4370
 
4371
4371
  ### eslint-plugin-jsdoc
4372
4372
 
4373
- This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.7.1](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.7.1)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.7.1/LICENSE):
4373
+ This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v50.6.14](https://github.com/gajus/eslint-plugin-jsdoc/tree/v50.6.14)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v50.6.14/LICENSE):
4374
4374
 
4375
4375
  ```
4376
4376
  Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
@@ -4402,7 +4402,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4402
4402
 
4403
4403
  ### eslint
4404
4404
 
4405
- This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.28.0](https://github.com/eslint/eslint/tree/v9.28.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.28.0/LICENSE):
4405
+ This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v9.26.0](https://github.com/eslint/eslint/tree/v9.26.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v9.26.0/LICENSE):
4406
4406
 
4407
4407
  ```
4408
4408
  Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
@@ -5235,9 +5235,9 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5235
5235
 
5236
5236
  ## optionalDependencies
5237
5237
 
5238
- ### @contrast/fn-inspect
5238
+ ### @newrelic/fn-inspect
5239
5239
 
5240
- This product includes source derived from [@contrast/fn-inspect](https://github.com/Contrast-Security-Inc/node-fn-inspect) ([v4.3.0](https://github.com/Contrast-Security-Inc/node-fn-inspect/tree/v4.3.0)), distributed under the [MIT License](https://github.com/Contrast-Security-Inc/node-fn-inspect/blob/v4.3.0/LICENSE):
5240
+ This product includes source derived from [@newrelic/fn-inspect](https://github.com/newrelic-forks/node-fn-inspect) ([v4.4.0](https://github.com/newrelic-forks/node-fn-inspect/tree/v4.4.0)), distributed under the [MIT License](https://github.com/newrelic-forks/node-fn-inspect/blob/v4.4.0/LICENSE):
5241
5241
 
5242
5242
  ```
5243
5243
  Copyright 2022 Contrast Security, Inc
@@ -12,7 +12,6 @@ const logger = require('./logger').child({ component: 'environment' })
12
12
  const stringify = require('json-stringify-safe')
13
13
  const asyncEachLimit = require('./util/async-each-limit')
14
14
  const DISPATCHER_VERSION = 'Dispatcher Version'
15
- const semver = require('semver')
16
15
 
17
16
  // As of 1.7.0 you can no longer dynamically link v8
18
17
  // https://github.com/nodejs/io.js/commit/d726a177ed
@@ -293,7 +292,7 @@ function remapConfigSettings() {
293
292
  }
294
293
  })
295
294
 
296
- maybeAddMissingProcessVars()
295
+ addMissingProcessVars()
297
296
  }
298
297
  }
299
298
 
@@ -301,11 +300,9 @@ function remapConfigSettings() {
301
300
  * As of Node 19 DTrace and ETW are no longer bundled
302
301
  * see: https://nodejs.org/en/blog/announcements/v19-release-announce#dtrace/systemtap/etw-support
303
302
  */
304
- function maybeAddMissingProcessVars() {
305
- if (semver.gte(process.version, '19.0.0')) {
306
- addSetting(remapping.node_use_dtrace, 'no')
307
- addSetting(remapping.node_use_etw, 'no')
308
- }
303
+ function addMissingProcessVars() {
304
+ addSetting(remapping.node_use_dtrace, 'no')
305
+ addSetting(remapping.node_use_etw, 'no')
309
306
  }
310
307
 
311
308
  async function getOtherPackages() {
@@ -22,7 +22,7 @@ const connectionStates = require('./connection/states')
22
22
 
23
23
  const PROTO_OPTIONS = { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
24
24
 
25
- const PROTO_DEFINITION_PATH = __dirname + '../../../lib/grpc/endpoints/infinite-tracing/v1.proto'
25
+ const PROTO_DEFINITION = require('./endpoints/infinite-tracing/v1.json')
26
26
 
27
27
  const DEFAULT_RECONNECT_DELAY_MS = 15 * 1000
28
28
 
@@ -369,7 +369,7 @@ class GrpcConnection extends EventEmitter {
369
369
  const endpoint = this._endpoint
370
370
  logger.trace('Creating gRPC client for: ', endpoint)
371
371
 
372
- const packageDefinition = protoLoader.loadSync(PROTO_DEFINITION_PATH, PROTO_OPTIONS)
372
+ const packageDefinition = protoLoader.fromJSON(PROTO_DEFINITION, PROTO_OPTIONS)
373
373
 
374
374
  const protoDescriptor = grpc.loadPackageDefinition(packageDefinition)
375
375
 
@@ -0,0 +1,10 @@
1
+ The `.proto` files in this directory are provided by New Relic internal
2
+ specifications.
3
+
4
+ The `.json` files in this directory are JSON descriptor files derived from
5
+ the equivalent `.proto` files. This is accomplished by utilizing the CLI
6
+ tool provided by `protobufjs`. As an example:
7
+
8
+ ```sh
9
+ npx --package=protobufjs-cli -c 'pbjs --keep-case v1.proto' > v1.json
10
+ ```
@@ -0,0 +1,106 @@
1
+ {
2
+ "nested": {
3
+ "com": {
4
+ "nested": {
5
+ "newrelic": {
6
+ "nested": {
7
+ "trace": {
8
+ "nested": {
9
+ "v1": {
10
+ "nested": {
11
+ "IngestService": {
12
+ "methods": {
13
+ "RecordSpan": {
14
+ "requestType": "Span",
15
+ "requestStream": true,
16
+ "responseType": "RecordStatus",
17
+ "responseStream": true
18
+ },
19
+ "RecordSpanBatch": {
20
+ "requestType": "SpanBatch",
21
+ "requestStream": true,
22
+ "responseType": "RecordStatus",
23
+ "responseStream": true
24
+ }
25
+ }
26
+ },
27
+ "SpanBatch": {
28
+ "fields": {
29
+ "spans": {
30
+ "rule": "repeated",
31
+ "type": "Span",
32
+ "id": 1
33
+ }
34
+ }
35
+ },
36
+ "Span": {
37
+ "fields": {
38
+ "trace_id": {
39
+ "type": "string",
40
+ "id": 1
41
+ },
42
+ "intrinsics": {
43
+ "keyType": "string",
44
+ "type": "AttributeValue",
45
+ "id": 2
46
+ },
47
+ "user_attributes": {
48
+ "keyType": "string",
49
+ "type": "AttributeValue",
50
+ "id": 3
51
+ },
52
+ "agent_attributes": {
53
+ "keyType": "string",
54
+ "type": "AttributeValue",
55
+ "id": 4
56
+ }
57
+ }
58
+ },
59
+ "AttributeValue": {
60
+ "oneofs": {
61
+ "value": {
62
+ "oneof": [
63
+ "string_value",
64
+ "bool_value",
65
+ "int_value",
66
+ "double_value"
67
+ ]
68
+ }
69
+ },
70
+ "fields": {
71
+ "string_value": {
72
+ "type": "string",
73
+ "id": 1
74
+ },
75
+ "bool_value": {
76
+ "type": "bool",
77
+ "id": 2
78
+ },
79
+ "int_value": {
80
+ "type": "int64",
81
+ "id": 3
82
+ },
83
+ "double_value": {
84
+ "type": "double",
85
+ "id": 4
86
+ }
87
+ }
88
+ },
89
+ "RecordStatus": {
90
+ "fields": {
91
+ "messages_seen": {
92
+ "type": "uint64",
93
+ "id": 1
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
@@ -19,9 +19,27 @@ const { AI } = require('../../../metrics/names')
19
19
  const { RecorderSpec } = require('../../../shim/specs')
20
20
  const InstrumentationDescriptor = require('../../../instrumentation-descriptor')
21
21
  const { extractLlmContext } = require('../../../util/llm-utils')
22
+ const ConverseStreamHandler = require('../../../llm-events/aws-bedrock/converse-stream-handler')
22
23
 
23
24
  let TRACKING_METRIC
24
25
 
26
+ const INSTRUMENTED_COMMANDS = new Set([
27
+ 'InvokeModelCommand',
28
+ 'InvokeModelWithResponseStreamCommand',
29
+ 'ConverseCommand',
30
+ 'ConverseStreamCommand'
31
+ ])
32
+
33
+ const STREAMING_COMMANDS = new Set([
34
+ 'InvokeModelWithResponseStreamCommand',
35
+ 'ConverseStreamCommand'
36
+ ])
37
+
38
+ const CONVERSE_COMMANDS = new Set([
39
+ 'ConverseCommand',
40
+ 'ConverseStreamCommand'
41
+ ])
42
+
25
43
  /**
26
44
  * Helper to determine if we should instrument the bedrock middleware call
27
45
  *
@@ -42,7 +60,7 @@ function shouldSkipInstrumentation(config) {
42
60
  */
43
61
  function isStreamingEnabled({ commandName, config }) {
44
62
  return (
45
- commandName === 'InvokeModelWithResponseStreamCommand' &&
63
+ STREAMING_COMMANDS.has(commandName) &&
46
64
  config.ai_monitoring?.streaming?.enabled
47
65
  )
48
66
  }
@@ -242,6 +260,7 @@ function createBedrockResponse({ bedrockCommand, response, err }) {
242
260
  function getBedrockSpec({ commandName }, shim, _original, _name, args) {
243
261
  const { agent } = shim
244
262
  const { input } = args[0]
263
+ const isConverse = CONVERSE_COMMANDS.has(commandName)
245
264
  const bedrockCommand = new BedrockCommand(input)
246
265
  const { modelType } = bedrockCommand
247
266
 
@@ -261,17 +280,33 @@ function getBedrockSpec({ commandName }, shim, _original, _name, args) {
261
280
 
262
281
  if (err && !response) {
263
282
  handleResponse(passThroughParams)
264
- } else if (response.output.body instanceof Uint8Array) {
283
+ } else if (response.output.body instanceof Uint8Array || 'output' in response.output) {
265
284
  // non-streamed response
266
285
  handleResponse(passThroughParams)
267
286
  } else if (isStreamingEnabled({ commandName, config: agent.config })) {
268
287
  // stream response
269
- const handler = new StreamHandler({
270
- stream: response.output.body,
271
- onComplete: handleResponse,
272
- passThroughParams
273
- })
274
- response.output.body = handler.generator(handleResponse)
288
+ if (isConverse) {
289
+ const handler = new ConverseStreamHandler({
290
+ stream: response.output.stream,
291
+ onComplete: handleResponse,
292
+ passThroughParams
293
+ })
294
+ response.output = {
295
+ ...response.output ?? {},
296
+ output: {
297
+ message: {
298
+ content: handler.generator(handleResponse)
299
+ }
300
+ }
301
+ }
302
+ } else {
303
+ const handler = new StreamHandler({
304
+ stream: response.output.body,
305
+ onComplete: handleResponse,
306
+ passThroughParams
307
+ })
308
+ response.output.body = handler.generator(handleResponse)
309
+ }
275
310
  } else if (!isStreamingEnabled({ commandName, config: agent.config })) {
276
311
  shim.logger.warn(
277
312
  'ai_monitoring.streaming.enabled is set to `false`, stream will not be instrumented.'
@@ -285,6 +320,11 @@ function getBedrockSpec({ commandName }, shim, _original, _name, args) {
285
320
 
286
321
  function handleResponse({ shim, err, response, segment, transaction, bedrockCommand, modelType }) {
287
322
  const { agent } = shim
323
+ // For the ConverseStreamHandler only, response.output does not
324
+ // contain stopReason yet, so we set it here.
325
+ if (response?.output && this?.stopReason) {
326
+ response.output.stopReason = this.stopReason
327
+ }
288
328
  const bedrockResponse = createBedrockResponse({ bedrockCommand, response, err })
289
329
 
290
330
  addLlmMeta({ agent, segment, transaction })
@@ -319,13 +359,12 @@ function handleResponse({ shim, err, response, segment, transaction, bedrockComm
319
359
  * @param {object} config AWS configuration object
320
360
  * @param {function} next the next middleware function in stack
321
361
  * @param {object} context AWS client context info
362
+ *
363
+ * @returns {function} the next middleware function in stack
322
364
  */
323
365
  function bedrockMiddleware(shim, config, next, context) {
324
366
  const { commandName } = context
325
- if (
326
- commandName === 'InvokeModelCommand' ||
327
- commandName === 'InvokeModelWithResponseStreamCommand'
328
- ) {
367
+ if (INSTRUMENTED_COMMANDS.has(commandName)) {
329
368
  return shim.record(next, getBedrockSpec.bind(null, { commandName }))
330
369
  }
331
370
 
@@ -394,10 +394,10 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
394
394
  }
395
395
  )
396
396
 
397
- // New API introduced in openai@5.0.0
397
+ // New API introduced in openai@4.87.0
398
398
  // Also instruments chat completion creation
399
399
  // and creates the LLM events
400
- if (semver.gte(shim.pkgVersion, '5.0.0')) {
400
+ if (semver.gte(shim.pkgVersion, '4.87.0')) {
401
401
  shim.record(openaiClient.Responses.prototype, 'create', function wrapCreate(shim, create, name, args) {
402
402
  const [request] = args
403
403
 
@@ -16,10 +16,10 @@ const synthetics = require('../synthetics')
16
16
  const urltils = require('../util/urltils')
17
17
 
18
18
  const channels = [
19
- { channel: diagnosticsChannel.channel('undici:request:create'), hook: requestCreateHook },
20
- { channel: diagnosticsChannel.channel('undici:request:headers'), hook: requestHeadersHook },
21
- { channel: diagnosticsChannel.channel('undici:request:trailers'), hook: endAndRestoreSegment },
22
- { channel: diagnosticsChannel.channel('undici:request:error'), hook: endAndRestoreSegment }
19
+ { channel: 'undici:request:create', hook: requestCreateHook },
20
+ { channel: 'undici:request:headers', hook: requestHeadersHook },
21
+ { channel: 'undici:request:trailers', hook: endAndRestoreSegment },
22
+ { channel: 'undici:request:error', hook: endAndRestoreSegment }
23
23
  ]
24
24
 
25
25
  /**
@@ -32,18 +32,17 @@ const channels = [
32
32
  * @param shim
33
33
  */
34
34
  module.exports = function addUndiciChannels(agent, _undici, _modName, shim) {
35
- channels.forEach(({ channel, hook }) => {
36
- if (!channel.hasSubscribers) {
37
- channel.subscribe(hook.bind(null, shim))
38
- }
35
+ channels.forEach(({ channel, hook }, index) => {
36
+ const boundHook = hook.bind(null, shim)
37
+ diagnosticsChannel.subscribe(channel, boundHook)
38
+ // store the bound hook for unsubscription later
39
+ channels[index].boundHook = boundHook
39
40
  })
40
41
  }
41
42
 
42
43
  module.exports.unsubscribe = function unsubscribe() {
43
- channels.forEach(({ channel, hook }) => {
44
- if (channel.hasSubscribers) {
45
- channel.unsubscribe(hook)
46
- }
44
+ channels.forEach(({ channel, boundHook }) => {
45
+ diagnosticsChannel.unsubscribe(channel, boundHook)
47
46
  })
48
47
  }
49
48
 
@@ -5,25 +5,43 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const { stringifyClaudeChunkedMessage } = require('./utils')
8
+ const { stringifyClaudeChunkedMessage, stringifyConverseChunkedMessage } = require('./utils')
9
9
 
10
10
  /**
11
- * Parses an AWS invoke command instance into a re-usable entity.
11
+ * Parses an AWS Bedrock command instance into a re-usable entity,
12
+ * unifying logic for both InvokeModel and Converse commands.
12
13
  */
13
14
  class BedrockCommand {
14
15
  #input
15
- #body
16
16
  #modelId
17
+ #body
18
+ #messages
19
+ #isConverseCommand
17
20
 
18
21
  /**
19
- * @param {object} input The `input` property from an InvokeModelCommand or
20
- * InvokeModelWithResponseStreamCommand instance that is used for the
21
- * conversation.
22
+ * @param {object} input The `input` property from an InvokeModelCommand,
23
+ * InvokeModelWithResponseStreamCommand, ConverseCommand, or
24
+ * ConverseStreamCommand instance.
22
25
  */
23
26
  constructor(input) {
24
27
  this.#input = input
25
- this.#body = JSON.parse(this.#input.body)
26
28
  this.#modelId = this.#input.modelId?.toLowerCase() ?? ''
29
+
30
+ if (Object.hasOwn(input, 'body') === true) {
31
+ this.#body = JSON.parse(this.#input.body)
32
+ this.#isConverseCommand = false
33
+ } else if (Object.hasOwn(input, 'messages') === true) {
34
+ this.#messages = input.messages
35
+ this.#isConverseCommand = true
36
+ }
37
+ }
38
+
39
+ /**
40
+ *
41
+ * @returns {boolean} True if the command is from the Converse API.
42
+ */
43
+ get isConverse() {
44
+ return this.#isConverseCommand
27
45
  }
28
46
 
29
47
  /**
@@ -32,17 +50,24 @@ class BedrockCommand {
32
50
  * @returns {number|undefined}
33
51
  */
34
52
  get maxTokens() {
35
- let result
36
- if (this.isClaude() === true) {
37
- result = this.#body.max_tokens_to_sample
38
- } else if (this.isClaude3() === true || this.isCohere() === true) {
39
- result = this.#body.max_tokens
40
- } else if (this.isLlama() === true) {
41
- result = this.#body.max_gen_length
42
- } else if (this.isTitan() === true) {
43
- result = this.#body.textGenerationConfig?.maxTokenCount
53
+ if (this.#isConverseCommand) {
54
+ // Logic for Converse
55
+ return this.#input?.inferenceConfig?.maxTokens
56
+ } else {
57
+ // Logic for InvokeModel
58
+ if (this.isClaude() === true) {
59
+ return this.#body.max_tokens_to_sample
60
+ }
61
+ if (this.isClaude3() === true || this.isCohere() === true) {
62
+ return this.#body.max_tokens
63
+ }
64
+ if (this.isLlama() === true) {
65
+ return this.#body.max_gen_length
66
+ }
67
+ if (this.isTitan() === true) {
68
+ return this.#body.textGenerationConfig?.maxTokenCount
69
+ }
44
70
  }
45
- return result
46
71
  }
47
72
 
48
73
  /**
@@ -60,40 +85,53 @@ class BedrockCommand {
60
85
  * @returns {string} One of `embedding` or `completion`.
61
86
  */
62
87
  get modelType() {
63
- // At the moment, this is a simple check. If Amazon ever introduces a
64
- // complex identifier, we can implement a more complicated check.
88
+ // This logic is common to both command types
65
89
  return this.#modelId.toLowerCase().includes('embed') ? 'embedding' : 'completion'
66
90
  }
67
91
 
68
92
  /**
69
93
  * The question posed to the LLM.
70
94
  *
71
- * @returns {object[]} The array of context messages passed to the LLM (or a single user prompt for legacy "non-chat" models)
95
+ * @returns {string|string[]|object[]}
72
96
  */
73
97
  get prompt() {
74
- if (this.isTitan() === true || this.isTitanEmbed() === true) {
75
- return [
76
- {
77
- role: 'user',
78
- content: this.#body.inputText
98
+ if (this.#isConverseCommand) {
99
+ // Logic for Converse
100
+ const result = []
101
+ for (const message of this.#messages) {
102
+ // The `message.content` field is an array of ContentBlock objects.
103
+ // For text messages, the structure is: content: [{ text: '...' }]
104
+ if (Array.isArray(message?.content) === true) {
105
+ result.push({
106
+ role: message.role,
107
+ content: stringifyConverseChunkedMessage(message.content)
108
+ })
79
109
  }
80
- ]
81
- } else if (this.isCohereEmbed() === true) {
82
- return [
83
- {
84
- role: 'user',
85
- content: this.#body.texts.join(' ')
86
- }
87
- ]
88
- } else if (
89
- this.isClaudeTextCompletionApi() === true ||
90
- this.isCohere() === true ||
91
- this.isLlama() === true
110
+ }
111
+ return result
112
+ }
113
+
114
+ // Logic for InvokeModel
115
+ if (this.isTitan() || this.isTitanEmbed()) {
116
+ return [{ role: 'user', content: this.#body.inputText }]
117
+ }
118
+
119
+ if (this.isCohereEmbed()) {
120
+ return [{ role: 'user', content: this.#body.texts.join(' ') }]
121
+ }
122
+
123
+ if (
124
+ this.isClaudeTextCompletionApi(this.#body) ||
125
+ this.isCohere() ||
126
+ this.isLlama()
92
127
  ) {
93
128
  return [{ role: 'user', content: this.#body.prompt }]
94
- } else if (this.isClaudeMessagesApi() === true) {
129
+ }
130
+
131
+ if (this.isClaudeMessagesApi(this.#body)) {
95
132
  return normalizeClaude3Messages(this.#body?.messages)
96
133
  }
134
+
97
135
  return []
98
136
  }
99
137
 
@@ -101,20 +139,25 @@ class BedrockCommand {
101
139
  * @returns {number|undefined}
102
140
  */
103
141
  get temperature() {
104
- let result
142
+ if (this.#isConverseCommand) {
143
+ // Logic for Converse
144
+ return this.#input?.inferenceConfig?.temperature
145
+ }
146
+ // Logic for InvokeModel
105
147
  if (this.isTitan() === true) {
106
- result = this.#body.textGenerationConfig?.temperature
107
- } else if (
148
+ return this.#body.textGenerationConfig?.temperature
149
+ }
150
+ if (
108
151
  this.isClaude() === true ||
109
- this.isClaude3() === true ||
110
- this.isCohere() === true ||
111
- this.isLlama() === true
152
+ this.isClaude3() === true ||
153
+ this.isCohere() === true ||
154
+ this.isLlama() === true
112
155
  ) {
113
- result = this.#body.temperature
156
+ return this.#body.temperature
114
157
  }
115
- return result
116
158
  }
117
159
 
160
+ // Helper methods that depend on modelId (common to both types)
118
161
  isClaude() {
119
162
  return this.#modelId.split('.').slice(-2).join('.').startsWith('anthropic.claude-v')
120
163
  }
@@ -143,21 +186,22 @@ class BedrockCommand {
143
186
  return this.#modelId.startsWith('amazon.titan-embed')
144
187
  }
145
188
 
146
- isClaudeMessagesApi() {
147
- return (this.isClaude3() === true || this.isClaude() === true) && 'messages' in this.#body
189
+ isClaudeMessagesApi(body) {
190
+ return (this.isClaude3() === true || this.isClaude() === true) && 'messages' in body
148
191
  }
149
192
 
150
- isClaudeTextCompletionApi() {
151
- return this.isClaude() === true && 'prompt' in this.#body
193
+ isClaudeTextCompletionApi(body) {
194
+ return this.isClaude() === true && 'prompt' in body
152
195
  }
153
196
  }
154
197
 
155
198
  /**
156
- * Claude v3 requests in Bedrock can have two different "chat" flavors. This function normalizes them into a consistent
199
+ * Claude v3 requests in Bedrock can have two different "chat" flavors.
200
+ * This function normalizes them into a consistent
157
201
  * format per the AIM agent spec
158
202
  *
159
- * @param messages - The raw array of messages passed to the invoke API
160
- * @returns {number|undefined} - The normalized messages
203
+ * @param {Array<object>} messages - The raw array of messages passed to the invoke API
204
+ * @returns {Array<object>} - The normalized messages
161
205
  */
162
206
  function normalizeClaude3Messages(messages) {
163
207
  const result = []
@@ -5,7 +5,7 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const { stringifyClaudeChunkedMessage } = require('./utils')
8
+ const { stringifyClaudeChunkedMessage, stringifyConverseChunkedMessage } = require('./utils')
9
9
 
10
10
  /**
11
11
  * @typedef {object} AwsBedrockMiddlewareResponse
@@ -18,60 +18,49 @@ const { stringifyClaudeChunkedMessage } = require('./utils')
18
18
  */
19
19
 
20
20
  /**
21
- * Represents a response from the Bedrock API. Given that the API response
22
- * has as many different shapes as the number of models it supports, and the
23
- * fact that responses require byte array processing, this object provides
24
- * an abstraction that normalizes responses into a known interface and
25
- * simplifies accessing desired fields.
21
+ * Represents a response from the Bedrock API, handling both standard and Converse API responses.
22
+ * This object provides an abstraction that normalizes responses into a known interface and
23
+ * simplifies accessing desired fields, accommodating the varied shapes of Bedrock API responses.
26
24
  */
27
25
  class BedrockResponse {
26
+ #originalResponse
28
27
  #innerResponse
29
28
  #innerOutput
30
29
  #parsedBody
30
+ #output
31
31
  #command
32
32
  #completions = []
33
33
  #id
34
+ #isConverse
34
35
 
35
36
  /**
36
37
  * @param {object} params
37
38
  * @param {AwsBedrockMiddlewareResponse} params.response
38
39
  * @param {BedrockCommand} params.bedrockCommand
39
- * @param params.isError
40
+ * @param {boolean} params.isError
40
41
  */
41
42
  constructor({ response, bedrockCommand, isError = false }) {
43
+ this.#originalResponse = response
42
44
  this.#innerResponse = isError ? response.$response : response.response
43
45
  this.#command = bedrockCommand
44
46
  this.isError = isError
47
+ this.#isConverse = this.#command.isConverse ?? false
45
48
 
46
49
  if (this.isError) {
47
50
  return
48
51
  }
49
52
 
50
- this.#innerOutput = response.output
51
-
52
- const json = new TextDecoder().decode(this.#innerOutput.body)
53
- this.#parsedBody = JSON.parse(json)
54
-
55
- const cmd = this.#command
56
- const body = this.#parsedBody
57
- if (cmd.isClaude() === true) {
58
- // TODO: can we make this thing give more than one completion?
59
- body.completion && this.#completions.push(body.completion)
60
- } else if (cmd.isClaude3() === true) {
61
- if (body?.type === 'message_stop') {
62
- // Streamed response
63
- this.#completions = body.completions
64
- } else {
65
- this.#completions = [stringifyClaudeChunkedMessage(body?.content)]
66
- }
67
- this.#id = body.id
68
- } else if (cmd.isCohere() === true) {
69
- this.#completions = body.generations?.map((g) => g.text) ?? []
70
- this.#id = body.id
71
- } else if (cmd.isLlama() === true) {
72
- body.generation && this.#completions.push(body.generation)
73
- } else if (cmd.isTitan() === true) {
74
- this.#completions = body.results?.map((r) => r.outputText) ?? []
53
+ if (this.#isConverse) {
54
+ this.#innerOutput = response.output.output
55
+ this.#output = this.#innerOutput.message
56
+ } else {
57
+ this.#innerOutput = response.output
58
+ const json = new TextDecoder().decode(this.#innerOutput.body)
59
+ this.#parsedBody = JSON.parse(json)
60
+
61
+ const cmd = this.#command
62
+ const body = this.#parsedBody
63
+ this.#extractCompletionsAndId(cmd, body)
75
64
  }
76
65
  }
77
66
 
@@ -82,32 +71,40 @@ class BedrockResponse {
82
71
  * prompt.
83
72
  */
84
73
  get completions() {
85
- return this.#completions
74
+ if (this.#isConverse === false) {
75
+ return this.#completions
76
+ }
77
+ const content = this.#output?.content
78
+ if (!content) return []
79
+ return [typeof content === 'string' ? content : stringifyConverseChunkedMessage(content)]
86
80
  }
87
81
 
88
82
  /**
89
83
  * The reason the model has given for finishing the response.
90
84
  *
91
- * @returns {string|*}
85
+ * @returns {string|undefined}
92
86
  */
93
87
  get finishReason() {
94
- let result
95
-
96
88
  if (this.isError) {
97
- return result
89
+ return undefined
90
+ }
91
+ if (this.#isConverse) {
92
+ return this.#originalResponse.output.stopReason
98
93
  }
99
-
100
94
  const cmd = this.#command
101
95
  if (cmd.isClaude() === true || cmd.isClaude3() === true) {
102
- result = this.#parsedBody.stop_reason
103
- } else if (cmd.isCohere() === true) {
104
- result = this.#parsedBody.generations?.find((r) => r.finish_reason !== null)?.finish_reason
105
- } else if (cmd.isLlama() === true) {
106
- result = this.#parsedBody.stop_reason
107
- } else if (cmd.isTitan() === true) {
108
- result = this.#parsedBody.results?.find((r) => r.completionReason !== null)?.completionReason
96
+ return this.#parsedBody.stop_reason
97
+ }
98
+ if (cmd.isCohere() === true) {
99
+ return this.#parsedBody.generations?.find((r) => r.finish_reason !== null)?.finish_reason
100
+ }
101
+ if (cmd.isLlama() === true) {
102
+ return this.#parsedBody.stop_reason
109
103
  }
110
- return result
104
+ if (cmd.isTitan() === true) {
105
+ return this.#parsedBody.results?.find((r) => r.completionReason !== null)?.completionReason
106
+ }
107
+ return undefined
111
108
  }
112
109
 
113
110
  /**
@@ -125,7 +122,8 @@ class BedrockResponse {
125
122
  * @returns {string|undefined}
126
123
  */
127
124
  get id() {
128
- return this.#id
125
+ // For Converse API, the requestId serves as the id.
126
+ return this.#isConverse ? this.requestId : this.#id
129
127
  }
130
128
 
131
129
  /**
@@ -146,12 +144,30 @@ class BedrockResponse {
146
144
  return this.#innerResponse.statusCode
147
145
  }
148
146
 
149
- #tokenCount(headerName) {
150
- const headerVal = this.headers?.[headerName]
151
- if (headerVal != null) {
152
- return parseInt(headerVal, 10)
147
+ /**
148
+ * Extracts and sets #completions and #id from the InvokeModel response body.
149
+ * @param {BedrockCommand} cmd
150
+ * @param {*} body
151
+ */
152
+ #extractCompletionsAndId(cmd, body) {
153
+ if (cmd.isClaude() === true) {
154
+ body.completion && this.#completions.push(body.completion)
155
+ } else if (cmd.isClaude3() === true) {
156
+ if (body?.type === 'message_stop') {
157
+ // Streamed response
158
+ this.#completions = body.completions
159
+ } else {
160
+ this.#completions = [stringifyClaudeChunkedMessage(body?.content)]
161
+ }
162
+ this.#id = body.id
163
+ } else if (cmd.isCohere() === true) {
164
+ this.#completions = body.generations?.map((g) => g.text) ?? []
165
+ this.#id = body.id
166
+ } else if (cmd.isLlama() === true) {
167
+ body.generation && this.#completions.push(body.generation)
168
+ } else if (cmd.isTitan() === true) {
169
+ this.#completions = body.results?.map((r) => r.outputText) ?? []
153
170
  }
154
- return undefined
155
171
  }
156
172
  }
157
173
 
@@ -57,7 +57,7 @@ class LlmChatCompletionMessage extends LlmEvent {
57
57
 
58
58
  #setId(index) {
59
59
  const cmd = this.bedrockCommand
60
- if (cmd.isTitan() === true || cmd.isClaude() === true) {
60
+ if (cmd.isConverse || cmd.isTitan() === true || cmd.isClaude() === true) {
61
61
  this.id = `${this.id}-${index}`
62
62
  } else if (cmd.isCohere() === true) {
63
63
  this.id = `${this.bedrockResponse.id || this.id}-${index}`
@@ -27,16 +27,14 @@ class LlmChatCompletionSummary extends LlmEvent {
27
27
  const { segment, isError } = params
28
28
  this.error = isError
29
29
  this.duration = segment.getDurationInMillis()
30
- this['request.max_tokens'] = this.bedrockCommand.maxTokens
31
-
32
- const nm = 'response.number_of_messages'
33
- const cfr = 'response.choices.finish_reason'
34
- const rt = 'request.temperature'
35
30
 
36
31
  const cmd = this.bedrockCommand
37
- this[cfr] = this.bedrockResponse.finishReason
38
- this[rt] = cmd.temperature
39
- this[nm] = (this.bedrockCommand.prompt.length) + this.bedrockResponse.completions.length
32
+ const res = this.bedrockResponse
33
+
34
+ this['request.max_tokens'] = cmd.maxTokens
35
+ this['response.choices.finish_reason'] = res.finishReason
36
+ this['request.temperature'] = cmd.temperature
37
+ this['response.number_of_messages'] = (cmd.prompt.length ?? 0) + (res.completions.length ?? 0)
40
38
  }
41
39
  }
42
40
 
@@ -0,0 +1,155 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ /**
9
+ * A stream handler processes a streamed response from the Bedrock API by
10
+ * intercepting each stream event, passing that event unmodified up to the
11
+ * consuming client, and then collecting the information into an "API response"
12
+ * object that satisfies the requirements of {@link BedrockResponse}. Once the
13
+ * stream has reached its end, the handler applies some finalizations to the
14
+ * compiled response object, updates the pass through parameters with the new
15
+ * object, and then invokes the final response handler passed in through
16
+ * `onComplete`.
17
+ */
18
+ class ConverseStreamHandler {
19
+ // We have to make most of the properties of this object public so that
20
+ // the at-construction-time attached generator function can access them
21
+ // through the `this` reference. See https://jrfom.com/posts/2023/10/31/js-classes/
22
+ // for details on why this is necessary.
23
+
24
+ /**
25
+ * The parameters to pass through to the `onComplete` function once the
26
+ * stream has been processed. The `response` property on this object will
27
+ * be overwritten with the response object that has been compiled by the
28
+ * stream handler.
29
+ */
30
+ passThroughParams
31
+
32
+ /**
33
+ * The original async iterable returned from the AWS SDK.
34
+ */
35
+ stream
36
+
37
+ /**
38
+ * The New Relic agent's internal trace segment. The `.touch` method will be
39
+ * used to mark the end time of the stream processing.
40
+ */
41
+ segment
42
+
43
+ /**
44
+ * For streaming messages, we want to capture the effective message chunks like we would see in the non-streaming API
45
+ */
46
+ observedChunks = []
47
+
48
+ stopReason
49
+
50
+ /**
51
+ * Represents an API response object as {@link BedrockResponse} expects.
52
+ * It will be updated by the stream handler with information received
53
+ * during processing of the stream. Upon stream completion, this object
54
+ * will replace the `response` property of `passThroughParams`.
55
+ *
56
+ * @type {object}
57
+ */
58
+ response = {
59
+ response: {
60
+ headers: {},
61
+ statusCode: 200
62
+ },
63
+ output: {
64
+ output: {
65
+ message: {
66
+
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * The function that will be invoked once the stream processing has finished.
74
+ * It will receive `this.passThroughParams` as the only parameter.
75
+ */
76
+ onComplete
77
+
78
+ constructor({ stream, passThroughParams, onComplete }) {
79
+ this.passThroughParams = passThroughParams
80
+ this.stream = stream
81
+ this.onComplete = onComplete
82
+ this.segment = passThroughParams.segment
83
+ this.generator = handleConverse
84
+ }
85
+
86
+ /**
87
+ * Encodes the output body into a Uint8Array, updates the pass through
88
+ * parameters with the compiled response object, and invokes the response
89
+ * handler. The trace segment is also updated to mark the end of the stream
90
+ * processing and account for the time it took to process the stream within
91
+ * the trace.
92
+ */
93
+ finish() {
94
+ this.passThroughParams.response = this.response
95
+ this.onComplete(this.passThroughParams)
96
+
97
+ this.segment.touch()
98
+ }
99
+
100
+ /**
101
+ * If the given event is the last event in the stream, update the response
102
+ * headers with the metrics data from the event.
103
+ *
104
+ * @param {object} parsedEvent
105
+ */
106
+ updateHeaders(parsedEvent) {
107
+ this.response.response.headers = {
108
+ 'x-amzn-requestid': this.passThroughParams.response.response.headers['x-amzn-requestid']
109
+ }
110
+ delete parsedEvent['amazon-bedrock-invocationMetrics']
111
+ }
112
+ }
113
+
114
+ // eslint-disable-next-line sonarjs/cognitive-complexity
115
+ async function * handleConverse() {
116
+ let activeChunk = null
117
+
118
+ try {
119
+ for await (const event of this.stream) {
120
+ yield event
121
+ this.updateHeaders(event)
122
+ if (event.messageStart?.role) {
123
+ this.role = 'assistant'
124
+ } else if (event.contentBlockStart?.start) {
125
+ // Handles a Content block start event. Tool use only.
126
+ const blockStartData = event.contentBlockStart.start
127
+ if (blockStartData.toolUse) {
128
+ activeChunk = { toolUse: { name: blockStartData.toolUse.name } }
129
+ }
130
+ } else if (event.contentBlockStop) {
131
+ if (activeChunk !== null) {
132
+ this.observedChunks.push(activeChunk)
133
+ activeChunk = null
134
+ }
135
+ } else if (event.contentBlockDelta?.delta) {
136
+ if (event.contentBlockDelta.delta.text) {
137
+ // It seems like the first streamed chunk does not always start with a contentBlockStart message
138
+ // If the stream starts with a delta, assume the current chunk is text
139
+ if (activeChunk === null) {
140
+ activeChunk = { text: '' }
141
+ }
142
+ activeChunk.text += event.contentBlockDelta.delta.text
143
+ }
144
+ // There are also deltas for tool use (stringified inputs) but we don't currently record them so we just ignore for now
145
+ } else if (event.messageStop) {
146
+ this.stopReason = event.messageStop?.stopReason
147
+ }
148
+ }
149
+ } finally {
150
+ this.response.output.output.message.content = this.observedChunks
151
+ this.finish()
152
+ }
153
+ }
154
+
155
+ module.exports = ConverseStreamHandler
@@ -13,5 +13,6 @@ module.exports = {
13
13
  LlmEmbedding: require('./embedding'),
14
14
  LlmEvent: require('./event'),
15
15
  LlmError: require('./error'),
16
- StreamHandler: require('./stream-handler')
16
+ StreamHandler: require('./stream-handler'),
17
+ ConverseStreamHandler: require('./converse-stream-handler')
17
18
  }
@@ -31,6 +31,43 @@ function stringifyClaudeChunkedMessage(chunks) {
31
31
  return stringifiedChunks.join('\n\n')
32
32
  }
33
33
 
34
+ /**
35
+ *
36
+ * @param {object[]} chunks - The "chunks" that make up a single conceptual message. In a multi-modal scenario, a single message
37
+ * might have a number of different-typed chunks interspersed
38
+ * @returns {string} - A stringified version of the message. We make a best-effort effort attempt to represent non-text chunks. In the future
39
+ * we may want to extend the agent to support these non-text chunks in a richer way. Placeholders are represented in an XML-like format but
40
+ * are NOT intended to be parsed as valid XML
41
+ */
42
+ function stringifyConverseChunkedMessage(chunks) {
43
+ const stringifiedChunks = chunks.map((chunk) => {
44
+ if ('text' in chunk) {
45
+ return chunk.text
46
+ } else if ('image' in chunk) {
47
+ return '<image>'
48
+ } else if ('document' in chunk) {
49
+ return `<document>${chunk.document.name ?? ''}</document>`
50
+ } else if ('toolUse' in chunk) {
51
+ return `<tool_use>${chunk.toolUse?.name ?? ''}</tool_use>`
52
+ } else if ('json' in chunk) {
53
+ return `<json>${JSON.stringify(chunk.json)}</json>`
54
+ } else if ('toolResult' in chunk) {
55
+ // Tool results can have their own chunks. For now we intentionally don't recurse in for non-text tool results
56
+ return `<tool_result>${
57
+ stringifyConverseChunkedMessage(
58
+ (chunk.toolResult.content ?? [])
59
+ .filter(subChunk => !('toolUse' in subChunk || 'toolResult' in subChunk))
60
+ )
61
+ }</tool_result>`
62
+ } else if ('guardContent' in chunk) {
63
+ return `<guard_content>${chunk.guardContent?.text ?? ''}</guard_content>`
64
+ }
65
+ return '<unknown_chunk>'
66
+ })
67
+ return stringifiedChunks.join('\n\n')
68
+ }
69
+
34
70
  module.exports = {
35
- stringifyClaudeChunkedMessage
71
+ stringifyClaudeChunkedMessage,
72
+ stringifyConverseChunkedMessage
36
73
  }
package/lib/shim/shim.js CHANGED
@@ -2030,7 +2030,7 @@ function wrapStreamEmit({ stream, shim, segment, specEvent, shouldCreateSegment,
2030
2030
  const newContext = context.enterSegment({ segment: eventSegment })
2031
2031
  eventBoundEmit = shim.bindContext({ nodule: emit, full: true, context: newContext })
2032
2032
  }
2033
- eventSegment.addAttribute('count', ++emitCount)
2033
+ if (eventSegment) eventSegment.addAttribute('count', ++emitCount)
2034
2034
  emitToCall = eventBoundEmit
2035
2035
  }
2036
2036
  if (evnt === 'end' || evnt === 'error') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "12.24.0",
3
+ "version": "13.0.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -151,7 +151,7 @@
151
151
  ],
152
152
  "homepage": "https://github.com/newrelic/node-newrelic",
153
153
  "engines": {
154
- "node": ">=18",
154
+ "node": ">=20",
155
155
  "npm": ">=6.0.0"
156
156
  },
157
157
  "directories": {
@@ -224,7 +224,7 @@
224
224
  },
225
225
  "optionalDependencies": {
226
226
  "@newrelic/fn-inspect": "^4.4.0",
227
- "@newrelic/native-metrics": "^11.1.0",
227
+ "@newrelic/native-metrics": "^12.0.0",
228
228
  "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085"
229
229
  },
230
230
  "devDependencies": {
@@ -234,7 +234,7 @@
234
234
  "@matteo.collina/tspl": "^0.1.1",
235
235
  "@newrelic/eslint-config": "^0.5.0",
236
236
  "@newrelic/newrelic-oss-cli": "^0.1.2",
237
- "@newrelic/test-utilities": "^9.1.0",
237
+ "@newrelic/test-utilities": "^10.0.0",
238
238
  "@octokit/rest": "^18.0.15",
239
239
  "@slack/bolt": "^3.7.0",
240
240
  "@smithy/eventstream-codec": "^2.2.0",