@purista/harness-bedrock 1.2.6 → 1.5.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/dist/index.js +94 -50
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { BaseModelProvider,
|
|
2
|
+
import { BaseModelProvider, parseProviderJson, safePartialJson, toTokenUsage, withoutObjectTool } from '@purista/harness';
|
|
3
3
|
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from '@aws-sdk/client-bedrock-runtime';
|
|
4
4
|
/**
|
|
5
5
|
* Creates an Amazon Bedrock-backed harness `ModelProvider`.
|
|
@@ -30,22 +30,26 @@ class BedrockModelProvider extends BaseModelProvider {
|
|
|
30
30
|
}
|
|
31
31
|
async doText(req) {
|
|
32
32
|
req.signal.throwIfAborted();
|
|
33
|
-
const
|
|
33
|
+
const { input, requestOptions } = toConverseRequest(req, false);
|
|
34
|
+
const response = await this.client.send(new ConverseCommand(input), { ...requestOptions, abortSignal: req.signal });
|
|
34
35
|
const toolCalls = extractToolCalls(response, req, 'text');
|
|
35
36
|
return {
|
|
36
37
|
content: outputText(response),
|
|
37
38
|
...(toolCalls ? { toolCalls } : {}),
|
|
38
|
-
usage:
|
|
39
|
+
usage: toTokenUsage(response.usage?.inputTokens, response.usage?.outputTokens),
|
|
39
40
|
finishReason: toFinishReason(response.stopReason),
|
|
41
|
+
outcome: toOutcome(response.stopReason),
|
|
40
42
|
raw: response
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
async *doTextStream(req) {
|
|
44
46
|
req.signal.throwIfAborted();
|
|
45
|
-
const
|
|
47
|
+
const { input, requestOptions } = toConverseRequest(req, false);
|
|
48
|
+
const response = await this.client.send(new ConverseStreamCommand(input), { ...requestOptions, abortSignal: req.signal });
|
|
46
49
|
const toolState = new Map();
|
|
47
50
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
48
51
|
let finishReason = 'stop';
|
|
52
|
+
let providerFinishReason;
|
|
49
53
|
for await (const event of response.stream ?? []) {
|
|
50
54
|
req.signal.throwIfAborted();
|
|
51
55
|
if (event.contentBlockDelta?.delta?.text) {
|
|
@@ -71,70 +75,114 @@ class BedrockModelProvider extends BaseModelProvider {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
if (event.metadata?.usage) {
|
|
74
|
-
usage =
|
|
78
|
+
usage = toTokenUsage(event.metadata.usage.inputTokens, event.metadata.usage.outputTokens);
|
|
75
79
|
}
|
|
76
80
|
if (event.messageStop?.stopReason) {
|
|
77
|
-
|
|
81
|
+
providerFinishReason = event.messageStop.stopReason;
|
|
82
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
|
-
yield { kind: 'finish', usage, finishReason };
|
|
85
|
+
yield { kind: 'finish', usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
81
86
|
}
|
|
82
87
|
async doObject(req) {
|
|
83
88
|
req.signal.throwIfAborted();
|
|
84
|
-
const
|
|
89
|
+
const { input, requestOptions } = toConverseRequest(req, true);
|
|
90
|
+
const response = await this.client.send(new ConverseCommand(input), { ...requestOptions, abortSignal: req.signal });
|
|
85
91
|
const toolUse = response.output?.message?.content?.find((block) => block.toolUse?.name === 'harness_response')?.toolUse;
|
|
86
92
|
const toolCalls = withoutObjectTool(extractToolCalls(response, req, 'object'));
|
|
87
93
|
const object = (toolUse?.input ?? parseJson(outputText(response) || '{}', req, 'object'));
|
|
88
94
|
return {
|
|
89
95
|
object,
|
|
90
96
|
...(toolCalls ? { toolCalls } : {}),
|
|
91
|
-
usage:
|
|
97
|
+
usage: toTokenUsage(response.usage?.inputTokens, response.usage?.outputTokens),
|
|
92
98
|
finishReason: toFinishReason(response.stopReason),
|
|
99
|
+
outcome: toOutcome(response.stopReason),
|
|
93
100
|
raw: response
|
|
94
101
|
};
|
|
95
102
|
}
|
|
96
103
|
async *doObjectStream(req) {
|
|
97
104
|
req.signal.throwIfAborted();
|
|
98
|
-
const
|
|
105
|
+
const { input, requestOptions } = toConverseRequest(req, true);
|
|
106
|
+
const response = await this.client.send(new ConverseStreamCommand(input), { ...requestOptions, abortSignal: req.signal });
|
|
99
107
|
let text = '';
|
|
100
108
|
let objectInput = '';
|
|
109
|
+
let objectBlockIndex;
|
|
110
|
+
const toolState = new Map();
|
|
101
111
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
102
112
|
let finishReason = 'stop';
|
|
113
|
+
let providerFinishReason;
|
|
103
114
|
for await (const event of response.stream ?? []) {
|
|
104
115
|
req.signal.throwIfAborted();
|
|
105
116
|
if (event.contentBlockDelta?.delta?.text) {
|
|
106
117
|
text += event.contentBlockDelta.delta.text;
|
|
107
118
|
yield { kind: 'partial', partial: safePartialJson(text) };
|
|
108
119
|
}
|
|
120
|
+
if (event.contentBlockStart?.start?.toolUse) {
|
|
121
|
+
// Only the synthetic `harness_response` block carries the structured
|
|
122
|
+
// object; other tool blocks are real tool calls and must not bleed
|
|
123
|
+
// into the object JSON (parity with the OpenAI/Azure adapters).
|
|
124
|
+
const blockIndex = event.contentBlockStart.contentBlockIndex ?? 0;
|
|
125
|
+
if (event.contentBlockStart.start.toolUse.name === 'harness_response' && objectBlockIndex === undefined) {
|
|
126
|
+
objectBlockIndex = blockIndex;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
toolState.set(blockIndex, {
|
|
130
|
+
id: String(event.contentBlockStart.start.toolUse.toolUseId),
|
|
131
|
+
name: String(event.contentBlockStart.start.toolUse.name),
|
|
132
|
+
input: ''
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
109
136
|
if (event.contentBlockDelta?.delta?.toolUse?.input) {
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
const blockIndex = event.contentBlockDelta.contentBlockIndex ?? 0;
|
|
138
|
+
const state = toolState.get(blockIndex);
|
|
139
|
+
if (state) {
|
|
140
|
+
state.input += event.contentBlockDelta.delta.toolUse.input;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
objectInput += event.contentBlockDelta.delta.toolUse.input;
|
|
144
|
+
yield { kind: 'partial', partial: safePartialJson(objectInput) };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (event.contentBlockStop) {
|
|
148
|
+
const state = toolState.get(event.contentBlockStop.contentBlockIndex ?? 0);
|
|
149
|
+
if (state) {
|
|
150
|
+
yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(state.input || '{}', req, 'objectStream') } };
|
|
151
|
+
toolState.delete(event.contentBlockStop.contentBlockIndex ?? 0);
|
|
152
|
+
}
|
|
112
153
|
}
|
|
113
154
|
if (event.metadata?.usage) {
|
|
114
|
-
usage =
|
|
155
|
+
usage = toTokenUsage(event.metadata.usage.inputTokens, event.metadata.usage.outputTokens);
|
|
115
156
|
}
|
|
116
157
|
if (event.messageStop?.stopReason) {
|
|
117
|
-
|
|
158
|
+
providerFinishReason = event.messageStop.stopReason;
|
|
159
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
const object = parseJson(objectInput || text || '{}', req, 'objectStream');
|
|
121
|
-
yield { kind: 'finish', object, usage, finishReason };
|
|
163
|
+
yield { kind: 'finish', object, usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
122
164
|
}
|
|
123
165
|
}
|
|
124
166
|
function toClientOptions(options) {
|
|
125
167
|
const { client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
126
|
-
return clientOptions;
|
|
168
|
+
return { maxAttempts: 1, ...clientOptions };
|
|
127
169
|
}
|
|
128
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Builds the Converse request body and extracts `requestOptions`, which are
|
|
172
|
+
* per-request SDK transport options (mirroring the other adapters) and must
|
|
173
|
+
* not leak into the Converse request body.
|
|
174
|
+
*/
|
|
175
|
+
function toConverseRequest(req, forceObject) {
|
|
129
176
|
const providerOptions = {
|
|
130
177
|
...(req.defaults?.providerOptions ?? {}),
|
|
131
178
|
...(req.call?.providerOptions ?? {})
|
|
132
179
|
};
|
|
180
|
+
const { requestOptions, ...bodyOptions } = providerOptions;
|
|
133
181
|
const { system, messages } = toBedrockMessages(req.messages);
|
|
134
182
|
const modelTools = toTools(req.tools) ?? [];
|
|
135
183
|
const tools = forceObject ? [...modelTools, toObjectTool(req)] : modelTools;
|
|
136
184
|
const forceObjectTool = forceObject && modelTools.length === 0;
|
|
137
|
-
|
|
185
|
+
const input = {
|
|
138
186
|
modelId: req.model,
|
|
139
187
|
messages,
|
|
140
188
|
...(system.length > 0 ? { system } : {}),
|
|
@@ -145,8 +193,9 @@ function toConverseInput(req, forceObject) {
|
|
|
145
193
|
...((req.call?.topP ?? req.defaults?.topP) !== undefined ? { topP: req.call?.topP ?? req.defaults?.topP } : {}),
|
|
146
194
|
...((req.call?.stopSequences ?? req.defaults?.stopSequences) !== undefined ? { stopSequences: req.call?.stopSequences ?? req.defaults?.stopSequences } : {})
|
|
147
195
|
},
|
|
148
|
-
...
|
|
196
|
+
...bodyOptions
|
|
149
197
|
};
|
|
198
|
+
return { input, ...(requestOptions ? { requestOptions } : {}) };
|
|
150
199
|
}
|
|
151
200
|
function toBedrockMessages(messages) {
|
|
152
201
|
const system = messages.filter((message) => message.role === 'system').map((message) => ({ text: message.content }));
|
|
@@ -215,39 +264,12 @@ function extractToolCalls(response, req, method) {
|
|
|
215
264
|
arguments: typeof call.input === 'string' ? parseJson(call.input, req, method) : call.input ?? {}
|
|
216
265
|
}));
|
|
217
266
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return
|
|
267
|
+
const MALFORMED_JSON_MESSAGE = 'Amazon Bedrock returned malformed structured JSON.';
|
|
268
|
+
function callContext(req, method) {
|
|
269
|
+
return { provider: 'bedrock', model: req.model, method };
|
|
221
270
|
}
|
|
222
271
|
function parseJson(content, req, method) {
|
|
223
|
-
|
|
224
|
-
return JSON.parse(content);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
throw malformedResponseError(req, method, 'Amazon Bedrock returned malformed structured JSON.', content, error);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function malformedResponseError(req, method, message, body, cause) {
|
|
231
|
-
return new ModelError(message, {
|
|
232
|
-
provider: 'bedrock',
|
|
233
|
-
model: req.model,
|
|
234
|
-
method,
|
|
235
|
-
reason: 'malformed_response',
|
|
236
|
-
providerBody: body
|
|
237
|
-
}, cause);
|
|
238
|
-
}
|
|
239
|
-
function safePartialJson(content) {
|
|
240
|
-
try {
|
|
241
|
-
return JSON.parse(content);
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return { _partial: content };
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function toUsage(inputTokens, outputTokens) {
|
|
248
|
-
const input = inputTokens ?? 0;
|
|
249
|
-
const output = outputTokens ?? 0;
|
|
250
|
-
return { inputTokens: input, outputTokens: output, totalTokens: input + output };
|
|
272
|
+
return parseProviderJson(content, callContext(req, method), MALFORMED_JSON_MESSAGE);
|
|
251
273
|
}
|
|
252
274
|
function toFinishReason(value) {
|
|
253
275
|
switch (value) {
|
|
@@ -261,7 +283,29 @@ function toFinishReason(value) {
|
|
|
261
283
|
case 'content_filtered':
|
|
262
284
|
case 'guardrail_intervened':
|
|
263
285
|
return 'content_filter';
|
|
286
|
+
case 'malformed_model_output':
|
|
287
|
+
case 'malformed_tool_use':
|
|
288
|
+
return 'malformed';
|
|
289
|
+
case 'model_context_window_exceeded':
|
|
290
|
+
return 'context_limit';
|
|
264
291
|
default:
|
|
265
292
|
return 'error';
|
|
266
293
|
}
|
|
267
294
|
}
|
|
295
|
+
function toOutcome(value) {
|
|
296
|
+
const finishReason = toFinishReason(value);
|
|
297
|
+
return {
|
|
298
|
+
finishReason,
|
|
299
|
+
...(typeof value === 'string' ? { providerFinishReason: value } : {})
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Stream outcome built from the tracked finish reason; `providerFinishReason`
|
|
304
|
+
* is omitted entirely when the provider never sent a stop reason.
|
|
305
|
+
*/
|
|
306
|
+
function streamOutcome(finishReason, providerFinishReason) {
|
|
307
|
+
return {
|
|
308
|
+
finishReason,
|
|
309
|
+
...(typeof providerFinishReason === 'string' ? { providerFinishReason } : {})
|
|
310
|
+
};
|
|
311
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purista/harness-bedrock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Amazon Bedrock model provider adapter for @purista/harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"vitest": "^4.1.8"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@purista/harness": "
|
|
51
|
+
"@purista/harness": "^1.5.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.15.0"
|