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 +48 -0
- package/THIRD_PARTY_NOTICES.md +9 -9
- package/lib/environment.js +4 -7
- package/lib/grpc/connection.js +2 -2
- package/lib/grpc/endpoints/infinite-tracing/Readme.md +10 -0
- package/lib/grpc/endpoints/infinite-tracing/v1.json +106 -0
- package/lib/instrumentation/aws-sdk/v3/bedrock.js +51 -12
- package/lib/instrumentation/openai.js +2 -2
- package/lib/instrumentation/undici.js +11 -12
- package/lib/llm-events/aws-bedrock/bedrock-command.js +97 -53
- package/lib/llm-events/aws-bedrock/bedrock-response.js +68 -52
- package/lib/llm-events/aws-bedrock/chat-completion-message.js +1 -1
- package/lib/llm-events/aws-bedrock/chat-completion-summary.js +6 -8
- package/lib/llm-events/aws-bedrock/converse-stream-handler.js +155 -0
- package/lib/llm-events/aws-bedrock/index.js +2 -1
- package/lib/llm-events/aws-bedrock/utils.js +38 -1
- package/lib/shim/shim.js +1 -1
- package/package.json +4 -4
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
-
* [@
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
### @
|
|
5238
|
+
### @newrelic/fn-inspect
|
|
5239
5239
|
|
|
5240
|
-
This product includes source derived from [@
|
|
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
|
package/lib/environment.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
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() {
|
package/lib/grpc/connection.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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@
|
|
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, '
|
|
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:
|
|
20
|
-
{ channel:
|
|
21
|
-
{ channel:
|
|
22
|
-
{ channel:
|
|
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
|
-
|
|
37
|
-
|
|
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,
|
|
44
|
-
|
|
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
|
|
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
|
|
20
|
-
* InvokeModelWithResponseStreamCommand
|
|
21
|
-
*
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
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[]}
|
|
95
|
+
* @returns {string|string[]|object[]}
|
|
72
96
|
*/
|
|
73
97
|
get prompt() {
|
|
74
|
-
if (this
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.
|
|
91
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
}
|
|
148
|
+
return this.#body.textGenerationConfig?.temperature
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
108
151
|
this.isClaude() === true ||
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
this.isClaude3() === true ||
|
|
153
|
+
this.isCohere() === true ||
|
|
154
|
+
this.isLlama() === true
|
|
112
155
|
) {
|
|
113
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
body
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
this[
|
|
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": "
|
|
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": ">=
|
|
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": "^
|
|
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": "^
|
|
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",
|