@purista/harness-bedrock 1.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/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @purista/harness-bedrock
2
+
3
+ Amazon Bedrock model provider adapter for `@purista/harness`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @purista/harness @purista/harness-bedrock
9
+ ```
10
+
11
+ Configure AWS credentials with the standard AWS SDK credential chain and pass
12
+ the target region in application code.
13
+
14
+ ```ts
15
+ import { bedrock } from '@purista/harness-bedrock'
16
+
17
+ bedrock({ region: process.env.AWS_REGION ?? 'us-east-1' })
18
+ ```
19
+
20
+ ## Package Format
21
+
22
+ This package is ESM-only and ships compiled JavaScript plus TypeScript
23
+ declarations from `dist/`. Source files, tests, source maps, and local configs
24
+ are not included in the published package.
@@ -0,0 +1,28 @@
1
+ import type { BaseModelProviderOptions, ModelProvider } from '@purista/harness';
2
+ import { type BedrockRuntimeClientConfig } from '@aws-sdk/client-bedrock-runtime';
3
+ export interface BedrockFactoryOptions extends BedrockRuntimeClientConfig {
4
+ /** Optional injected client for tests or custom transport behavior. */
5
+ client?: BedrockClient;
6
+ /** Optional adapter-level logger override. Defaults to the harness logger when registered. */
7
+ harnessLogger?: BaseModelProviderOptions['logger'];
8
+ /** Optional adapter-level telemetry override. Defaults to the harness telemetry shim when registered. */
9
+ telemetry?: BaseModelProviderOptions['telemetry'];
10
+ /** Optional adapter-level timeout override. Defaults to the harness model timeout when registered. */
11
+ harnessTimeoutMs?: number;
12
+ }
13
+ /**
14
+ * Creates an Amazon Bedrock-backed harness `ModelProvider`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { bedrock } from '@purista/harness-bedrock'
19
+ *
20
+ * const provider = bedrock({ region: 'us-east-1' })
21
+ * ```
22
+ */
23
+ export declare function bedrock(options?: BedrockFactoryOptions): ModelProvider;
24
+ export type BedrockClient = {
25
+ send(command: unknown, options?: {
26
+ abortSignal?: AbortSignal;
27
+ }): Promise<any>;
28
+ };
package/dist/index.js ADDED
@@ -0,0 +1,259 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { BaseModelProvider, ModelError } from '@purista/harness';
3
+ import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from '@aws-sdk/client-bedrock-runtime';
4
+ /**
5
+ * Creates an Amazon Bedrock-backed harness `ModelProvider`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { bedrock } from '@purista/harness-bedrock'
10
+ *
11
+ * const provider = bedrock({ region: 'us-east-1' })
12
+ * ```
13
+ */
14
+ export function bedrock(options = {}) {
15
+ return new BedrockModelProvider(options);
16
+ }
17
+ class BedrockModelProvider extends BaseModelProvider {
18
+ options;
19
+ client;
20
+ constructor(options) {
21
+ super({
22
+ id: 'bedrock',
23
+ genAiSystem: 'aws.bedrock',
24
+ ...(options.harnessLogger ? { logger: options.harnessLogger } : {}),
25
+ ...(options.telemetry ? { telemetry: options.telemetry } : {}),
26
+ ...(options.harnessTimeoutMs !== undefined ? { timeoutMs: options.harnessTimeoutMs } : {})
27
+ });
28
+ this.options = options;
29
+ this.client = options.client ?? new BedrockRuntimeClient(toClientOptions(options));
30
+ }
31
+ async doText(req) {
32
+ req.signal.throwIfAborted();
33
+ const response = await this.client.send(new ConverseCommand(toConverseInput(req, false)), { abortSignal: req.signal });
34
+ const toolCalls = extractToolCalls(response, req, 'text');
35
+ return {
36
+ content: outputText(response),
37
+ ...(toolCalls ? { toolCalls } : {}),
38
+ usage: toUsage(response.usage?.inputTokens, response.usage?.outputTokens),
39
+ finishReason: toFinishReason(response.stopReason),
40
+ raw: response
41
+ };
42
+ }
43
+ async *doTextStream(req) {
44
+ req.signal.throwIfAborted();
45
+ const response = await this.client.send(new ConverseStreamCommand(toConverseInput(req, false)), { abortSignal: req.signal });
46
+ const toolState = new Map();
47
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
48
+ let finishReason = 'stop';
49
+ for await (const event of response.stream ?? []) {
50
+ req.signal.throwIfAborted();
51
+ if (event.contentBlockDelta?.delta?.text) {
52
+ yield { kind: 'delta', text: event.contentBlockDelta.delta.text };
53
+ }
54
+ if (event.contentBlockStart?.start?.toolUse) {
55
+ toolState.set(event.contentBlockStart.contentBlockIndex ?? 0, {
56
+ id: String(event.contentBlockStart.start.toolUse.toolUseId),
57
+ name: String(event.contentBlockStart.start.toolUse.name),
58
+ input: ''
59
+ });
60
+ }
61
+ if (event.contentBlockDelta?.delta?.toolUse?.input) {
62
+ const state = toolState.get(event.contentBlockDelta.contentBlockIndex ?? 0);
63
+ if (state)
64
+ state.input += event.contentBlockDelta.delta.toolUse.input;
65
+ }
66
+ if (event.contentBlockStop) {
67
+ const state = toolState.get(event.contentBlockStop.contentBlockIndex ?? 0);
68
+ if (state) {
69
+ yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(state.input || '{}', req, 'textStream') } };
70
+ toolState.delete(event.contentBlockStop.contentBlockIndex ?? 0);
71
+ }
72
+ }
73
+ if (event.metadata?.usage) {
74
+ usage = toUsage(event.metadata.usage.inputTokens, event.metadata.usage.outputTokens);
75
+ }
76
+ if (event.messageStop?.stopReason) {
77
+ finishReason = toFinishReason(event.messageStop.stopReason);
78
+ }
79
+ }
80
+ yield { kind: 'finish', usage, finishReason };
81
+ }
82
+ async doObject(req) {
83
+ req.signal.throwIfAborted();
84
+ const response = await this.client.send(new ConverseCommand(toConverseInput(req, true)), { abortSignal: req.signal });
85
+ const toolUse = response.output?.message?.content?.find((block) => block.toolUse?.name === 'harness_response')?.toolUse;
86
+ const object = (toolUse?.input ?? parseJson(outputText(response) || '{}', req, 'object'));
87
+ return {
88
+ object,
89
+ usage: toUsage(response.usage?.inputTokens, response.usage?.outputTokens),
90
+ finishReason: toFinishReason(response.stopReason),
91
+ raw: response
92
+ };
93
+ }
94
+ async *doObjectStream(req) {
95
+ req.signal.throwIfAborted();
96
+ const response = await this.client.send(new ConverseStreamCommand(toConverseInput(req, true)), { abortSignal: req.signal });
97
+ let text = '';
98
+ let objectInput = '';
99
+ let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
100
+ let finishReason = 'stop';
101
+ for await (const event of response.stream ?? []) {
102
+ req.signal.throwIfAborted();
103
+ if (event.contentBlockDelta?.delta?.text) {
104
+ text += event.contentBlockDelta.delta.text;
105
+ yield { kind: 'partial', partial: safePartialJson(text) };
106
+ }
107
+ if (event.contentBlockDelta?.delta?.toolUse?.input) {
108
+ objectInput += event.contentBlockDelta.delta.toolUse.input;
109
+ yield { kind: 'partial', partial: safePartialJson(objectInput) };
110
+ }
111
+ if (event.metadata?.usage) {
112
+ usage = toUsage(event.metadata.usage.inputTokens, event.metadata.usage.outputTokens);
113
+ }
114
+ if (event.messageStop?.stopReason) {
115
+ finishReason = toFinishReason(event.messageStop.stopReason);
116
+ }
117
+ }
118
+ const object = parseJson(objectInput || text || '{}', req, 'objectStream');
119
+ yield { kind: 'finish', object, usage, finishReason };
120
+ }
121
+ }
122
+ function toClientOptions(options) {
123
+ const { client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
124
+ return clientOptions;
125
+ }
126
+ function toConverseInput(req, forceObject) {
127
+ const providerOptions = {
128
+ ...(req.defaults?.providerOptions ?? {}),
129
+ ...(req.call?.providerOptions ?? {})
130
+ };
131
+ const { system, messages } = toBedrockMessages(req.messages);
132
+ const tools = forceObject ? [toObjectTool(req)] : toTools(req.tools);
133
+ return {
134
+ modelId: req.model,
135
+ messages,
136
+ ...(system.length > 0 ? { system } : {}),
137
+ ...(tools ? { toolConfig: { tools, ...(forceObject ? { toolChoice: { tool: { name: 'harness_response' } } } : {}) } } : {}),
138
+ inferenceConfig: {
139
+ ...(req.call?.maxTokens ?? req.defaults?.maxTokens !== undefined ? { maxTokens: req.call?.maxTokens ?? req.defaults?.maxTokens } : {}),
140
+ ...(req.call?.temperature ?? req.defaults?.temperature !== undefined ? { temperature: req.call?.temperature ?? req.defaults?.temperature } : {}),
141
+ ...(req.call?.topP ?? req.defaults?.topP !== undefined ? { topP: req.call?.topP ?? req.defaults?.topP } : {}),
142
+ ...(req.call?.stopSequences ?? req.defaults?.stopSequences ? { stopSequences: req.call?.stopSequences ?? req.defaults?.stopSequences } : {})
143
+ },
144
+ ...providerOptions
145
+ };
146
+ }
147
+ function toBedrockMessages(messages) {
148
+ const system = messages.filter((message) => message.role === 'system').map((message) => ({ text: message.content }));
149
+ const converted = messages.filter((message) => message.role !== 'system').map((message) => {
150
+ if (message.role === 'tool') {
151
+ return {
152
+ role: 'user',
153
+ content: [{ toolResult: { toolUseId: message.toolCallId, content: [{ text: message.content }] } }]
154
+ };
155
+ }
156
+ if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
157
+ return {
158
+ role: 'assistant',
159
+ content: [
160
+ ...(typeof message.content === 'string' && message.content ? [{ text: message.content }] : []),
161
+ ...message.toolCalls.map((call) => ({ toolUse: { toolUseId: call.id, name: call.name, input: call.arguments } }))
162
+ ]
163
+ };
164
+ }
165
+ return {
166
+ role: message.role,
167
+ content: typeof message.content === 'string' ? [{ text: message.content }] : message.content.map(toContentBlock)
168
+ };
169
+ });
170
+ return { system, messages: converted };
171
+ }
172
+ function toContentBlock(part) {
173
+ if (part.kind === 'text')
174
+ return { text: part.text };
175
+ if (part.kind === 'image') {
176
+ const format = part.mimeType.split('/')[1] ?? 'png';
177
+ return { image: { format, source: { bytes: Buffer.from(part.dataBase64, 'base64') } } };
178
+ }
179
+ return { text: `[unsupported ${part.kind} content omitted]` };
180
+ }
181
+ function toTools(tools) {
182
+ if (!tools || tools.length === 0)
183
+ return undefined;
184
+ return tools.map((tool) => ({
185
+ toolSpec: {
186
+ name: tool.name,
187
+ description: tool.description,
188
+ inputSchema: { json: tool.parameters }
189
+ }
190
+ }));
191
+ }
192
+ function toObjectTool(req) {
193
+ return {
194
+ toolSpec: {
195
+ name: 'harness_response',
196
+ description: 'Return the structured response object.',
197
+ inputSchema: { json: req.schema }
198
+ }
199
+ };
200
+ }
201
+ function outputText(response) {
202
+ return response.output?.message?.content?.filter((block) => typeof block.text === 'string').map((block) => block.text).join('') ?? '';
203
+ }
204
+ function extractToolCalls(response, req, method) {
205
+ const calls = response.output?.message?.content?.map((block) => block.toolUse).filter(Boolean);
206
+ if (!calls || calls.length === 0)
207
+ return undefined;
208
+ return calls.map((call) => ({
209
+ id: String(call.toolUseId),
210
+ name: String(call.name),
211
+ arguments: typeof call.input === 'string' ? parseJson(call.input, req, method) : call.input ?? {}
212
+ }));
213
+ }
214
+ function parseJson(content, req, method) {
215
+ try {
216
+ return JSON.parse(content);
217
+ }
218
+ catch (error) {
219
+ throw malformedResponseError(req, method, 'Amazon Bedrock returned malformed structured JSON.', content, error);
220
+ }
221
+ }
222
+ function malformedResponseError(req, method, message, body, cause) {
223
+ return new ModelError(message, {
224
+ provider: 'bedrock',
225
+ model: req.model,
226
+ method,
227
+ reason: 'malformed_response',
228
+ providerBody: body
229
+ }, cause);
230
+ }
231
+ function safePartialJson(content) {
232
+ try {
233
+ return JSON.parse(content);
234
+ }
235
+ catch {
236
+ return { _partial: content };
237
+ }
238
+ }
239
+ function toUsage(inputTokens, outputTokens) {
240
+ const input = inputTokens ?? 0;
241
+ const output = outputTokens ?? 0;
242
+ return { inputTokens: input, outputTokens: output, totalTokens: input + output };
243
+ }
244
+ function toFinishReason(value) {
245
+ switch (value) {
246
+ case 'end_turn':
247
+ case 'stop_sequence':
248
+ return 'stop';
249
+ case 'max_tokens':
250
+ return 'length';
251
+ case 'tool_use':
252
+ return 'tool_calls';
253
+ case 'content_filtered':
254
+ case 'guardrail_intervened':
255
+ return 'content_filter';
256
+ default:
257
+ return 'error';
258
+ }
259
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@purista/harness-bedrock",
3
+ "version": "1.0.0",
4
+ "description": "Amazon Bedrock model provider adapter for @purista/harness.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/**/*.js",
16
+ "dist/**/*.d.ts",
17
+ "package.json",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "license": "Apache-2.0",
22
+ "sideEffects": false,
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/puristajs/harness",
26
+ "directory": "packages/harness-bedrock"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/puristajs/harness/issues"
30
+ },
31
+ "homepage": "https://github.com/puristajs/harness#readme",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "scripts": {
36
+ "clean": "rm -rf dist",
37
+ "build": "npm run clean && tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.typecheck.json",
39
+ "test": "vitest run",
40
+ "lint": "echo 'no lint configured for @purista/harness-bedrock'"
41
+ },
42
+ "dependencies": {
43
+ "@aws-sdk/client-bedrock-runtime": "^3.1045.0"
44
+ },
45
+ "devDependencies": {
46
+ "@vitest/coverage-v8": "^4.1.5",
47
+ "typescript": "^6.0.3",
48
+ "vitest": "^4.1.5"
49
+ },
50
+ "peerDependencies": {
51
+ "@purista/harness": "*"
52
+ },
53
+ "engines": {
54
+ "node": ">=24.15.0"
55
+ }
56
+ }