@warlock.js/ai-bedrock 4.1.1

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/cjs/index.cjs ADDED
@@ -0,0 +1,808 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _aws_sdk_client_bedrock_runtime = require("@aws-sdk/client-bedrock-runtime");
3
+ let _warlock_js_ai = require("@warlock.js/ai");
4
+ let _warlock_js_logger = require("@warlock.js/logger");
5
+
6
+ //#region ../../@warlock.js/ai-bedrock/src/utils/map-stop-reason.ts
7
+ const stopReasonMap = {
8
+ end_turn: "stop",
9
+ stop_sequence: "stop",
10
+ max_tokens: "length",
11
+ tool_use: "tool_calls"
12
+ };
13
+ /**
14
+ * Map Bedrock Converse's `stopReason` to the normalized `FinishReason`
15
+ * union.
16
+ *
17
+ * `end_turn` / `stop_sequence` are natural stops. `max_tokens` maps to
18
+ * `length`. `tool_use` maps to `tool_calls`. Everything else —
19
+ * `content_filtered`, `guardrail_intervened`, `malformed_tool_use`,
20
+ * `malformed_model_output`, `model_context_window_exceeded`, `null`,
21
+ * or any future value — falls through to `"error"`: none produced a
22
+ * clean terminal answer, so the agent must not treat them as success.
23
+ *
24
+ * @example
25
+ * mapStopReason("end_turn"); // "stop"
26
+ * mapStopReason("tool_use"); // "tool_calls"
27
+ * mapStopReason("guardrail_intervened"); // "error"
28
+ * mapStopReason(undefined); // "error"
29
+ */
30
+ function mapStopReason(raw) {
31
+ return stopReasonMap[raw ?? ""] ?? "error";
32
+ }
33
+
34
+ //#endregion
35
+ //#region ../../@warlock.js/ai-bedrock/src/utils/to-bedrock-messages.ts
36
+ const MEDIA_TYPE_TO_FORMAT = {
37
+ "image/jpeg": "jpeg",
38
+ "image/png": "png",
39
+ "image/gif": "gif",
40
+ "image/webp": "webp"
41
+ };
42
+ /**
43
+ * Convert vendor-neutral `Message[]` into Bedrock Converse's request
44
+ * shape.
45
+ *
46
+ * Converse differs from the OpenAI Chat protocol in three ways this
47
+ * function absorbs:
48
+ *
49
+ * 1. **No `system` role.** System messages become a separate
50
+ * `SystemContentBlock[]` (one `{ text }` block each).
51
+ * 2. **Tool results are `user` turns.** A neutral `tool` message
52
+ * becomes a `user` message carrying a single `toolResult` block.
53
+ * 3. **Tool calls are `toolUse` content blocks.** An assistant message
54
+ * with `toolCalls` becomes an `assistant` message: an optional
55
+ * leading `text` block followed by one `toolUse` block per call.
56
+ *
57
+ * @example
58
+ * const { system, messages } = toBedrockMessages([
59
+ * { role: "system", content: "Be concise." },
60
+ * { role: "user", content: "Hi" },
61
+ * ]);
62
+ */
63
+ function toBedrockMessages(messages) {
64
+ const system = [];
65
+ const mapped = [];
66
+ for (const message of messages) {
67
+ if (message.role === "system") {
68
+ system.push({ text: stringifyContent(message.content) });
69
+ continue;
70
+ }
71
+ if (message.role === "tool") {
72
+ mapped.push({
73
+ role: "user",
74
+ content: [{ toolResult: {
75
+ toolUseId: message.toolCallId ?? "",
76
+ content: [{ text: stringifyContent(message.content) }]
77
+ } }]
78
+ });
79
+ continue;
80
+ }
81
+ if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
82
+ const blocks = [];
83
+ const text = stringifyContent(message.content);
84
+ if (text) blocks.push({ text });
85
+ for (const toolCall of message.toolCalls) blocks.push({ toolUse: {
86
+ toolUseId: toolCall.id,
87
+ name: toolCall.name,
88
+ input: toolCall.input ?? {}
89
+ } });
90
+ mapped.push({
91
+ role: "assistant",
92
+ content: blocks
93
+ });
94
+ continue;
95
+ }
96
+ if (message.role === "user" && Array.isArray(message.content)) {
97
+ mapped.push({
98
+ role: "user",
99
+ content: message.content.map(toBedrockContentBlock)
100
+ });
101
+ continue;
102
+ }
103
+ mapped.push({
104
+ role: message.role === "assistant" ? "assistant" : "user",
105
+ content: [{ text: stringifyContent(message.content) }]
106
+ });
107
+ }
108
+ return {
109
+ system: system.length > 0 ? system : void 0,
110
+ messages: mapped
111
+ };
112
+ }
113
+ /**
114
+ * Multipart content is only meaningful on user messages — for any other
115
+ * role collapse a `ContentPart[]` to its concatenated text. Plain
116
+ * strings pass through unchanged.
117
+ */
118
+ function stringifyContent(content) {
119
+ if (typeof content === "string") return content;
120
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("");
121
+ }
122
+ /**
123
+ * Map a resolved `ContentPart` to a Bedrock `ContentBlock`. Bedrock's
124
+ * `ImageSource` only accepts raw bytes or an S3 location — there is no
125
+ * remote-URL source. A neutral `{ url }` image therefore cannot be
126
+ * sent and surfaces a typed `InvalidRequestError` upfront rather than
127
+ * a downstream Bedrock validation fault. The agent has already
128
+ * resolved attachments, so this never fetches or reads anything.
129
+ */
130
+ function toBedrockContentBlock(part) {
131
+ if (part.type === "text") return { text: part.text };
132
+ if ("url" in part.source) throw new _warlock_js_ai.InvalidRequestError("Bedrock Converse does not support remote-URL image sources; supply base64 image bytes instead.");
133
+ const format = MEDIA_TYPE_TO_FORMAT[part.source.mediaType];
134
+ if (!format) throw new _warlock_js_ai.InvalidRequestError(`Unsupported image media type for Bedrock: "${part.source.mediaType}" (expected image/jpeg, image/png, image/gif, or image/webp).`);
135
+ return { image: {
136
+ format,
137
+ source: { bytes: Buffer.from(part.source.base64, "base64") }
138
+ } };
139
+ }
140
+
141
+ //#endregion
142
+ //#region ../../@warlock.js/ai-bedrock/src/utils/to-bedrock-tools.ts
143
+ /**
144
+ * Convert vendor-neutral `ToolConfig[]` into Bedrock Converse's
145
+ * `ToolConfiguration`. Each tool becomes a `toolSpec` with a JSON
146
+ * `inputSchema`. Bedrock requires the schema root to be an object —
147
+ * a non-object extraction degrades to a parameterless object schema
148
+ * so registration never fails.
149
+ *
150
+ * Returns `undefined` when there are no tools so the caller can omit
151
+ * `toolConfig` from the request entirely (Bedrock rejects an empty
152
+ * `tools` array).
153
+ *
154
+ * @example
155
+ * const toolConfig = toBedrockToolConfig([weatherTool]);
156
+ * await client.send(new ConverseCommand({ modelId, messages, toolConfig }));
157
+ */
158
+ function toBedrockToolConfig(tools) {
159
+ if (!tools || tools.length === 0) return;
160
+ return { tools: tools.map((tool) => ({ toolSpec: {
161
+ name: tool.name,
162
+ description: tool.description,
163
+ inputSchema: { json: toJsonSchema(tool.input) }
164
+ } })) };
165
+ }
166
+ /**
167
+ * Resolve a tool's input schema to a JSON-Schema object. Bedrock's
168
+ * `ToolInputSchema.json` requires an object root; anything else (or a
169
+ * failed extraction) degrades to a parameterless object so the tool
170
+ * still registers.
171
+ */
172
+ function toJsonSchema(input) {
173
+ const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
174
+ if (schema && schema.type === "object") return schema;
175
+ return { type: "object" };
176
+ }
177
+
178
+ //#endregion
179
+ //#region ../../@warlock.js/ai-bedrock/src/utils/wrap-bedrock-error.ts
180
+ const TIMEOUT_NAMES = new Set([
181
+ "ModelTimeoutException",
182
+ "TimeoutError",
183
+ "RequestTimeout",
184
+ "RequestTimeoutException"
185
+ ]);
186
+ /**
187
+ * Wrap any thrown value caught inside the Bedrock adapter into the
188
+ * appropriate `@warlock.js/ai` `AIError` subclass.
189
+ *
190
+ * **Dispatch strategy.** AWS errors carry no provider machine `code`;
191
+ * the stable identifier is the Smithy exception `name`. Dispatch keys
192
+ * on `name`, falls back to `$metadata.httpStatusCode` when the name is
193
+ * missing (flattened/proxied errors). `ValidationException` is split:
194
+ * the "input is too long / exceeds context window" phrasing maps to
195
+ * `ContextLengthExceededError`, everything else to
196
+ * `InvalidRequestError`.
197
+ *
198
+ * `AIError` instances pass through unchanged so `catch/throw wrap(e)`
199
+ * pipelines never double-wrap.
200
+ *
201
+ * @example
202
+ * try {
203
+ * return await this.client.send(new ConverseCommand(...));
204
+ * } catch (thrown) {
205
+ * throw wrapBedrockError(thrown);
206
+ * }
207
+ */
208
+ function wrapBedrockError(thrown) {
209
+ if (thrown instanceof _warlock_js_ai.AIError) return thrown;
210
+ const shape = toShape(thrown);
211
+ const context = buildContext(shape);
212
+ const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
213
+ if (isTimeout(shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
214
+ cause: thrown,
215
+ context
216
+ });
217
+ if (shape.name === "AccessDeniedException" || shape.httpStatusCode === 403) return new _warlock_js_ai.ProviderAuthError(message, {
218
+ cause: thrown,
219
+ context
220
+ });
221
+ if (shape.httpStatusCode === 401) return new _warlock_js_ai.ProviderAuthError(message, {
222
+ cause: thrown,
223
+ context
224
+ });
225
+ if (shape.name === "ServiceQuotaExceededException") return new _warlock_js_ai.QuotaExceededError(message, {
226
+ cause: thrown,
227
+ context
228
+ });
229
+ if (shape.name === "ThrottlingException" || shape.httpStatusCode === 429) return new _warlock_js_ai.ProviderRateLimitError(message, {
230
+ cause: thrown,
231
+ context
232
+ });
233
+ if (shape.name === "ValidationException") {
234
+ if (/too long|context window|maximum context|exceeds the maximum/i.test(message)) return new _warlock_js_ai.ContextLengthExceededError(message, {
235
+ cause: thrown,
236
+ context
237
+ });
238
+ return new _warlock_js_ai.InvalidRequestError(message, {
239
+ cause: thrown,
240
+ context
241
+ });
242
+ }
243
+ if (shape.name === "ResourceNotFoundException" || shape.name === "ConflictException" || isClientStatus(shape.httpStatusCode)) return new _warlock_js_ai.InvalidRequestError(message, {
244
+ cause: thrown,
245
+ context
246
+ });
247
+ return new _warlock_js_ai.ProviderError(message, {
248
+ cause: thrown,
249
+ context
250
+ });
251
+ }
252
+ /**
253
+ * Read the raw error shape without depending on `instanceof`. AWS
254
+ * exceptions expose `$metadata`; plain/proxied errors may carry
255
+ * `status` / `code` instead.
256
+ */
257
+ function toShape(thrown) {
258
+ if (typeof thrown !== "object" || thrown === null) return {};
259
+ const raw = thrown;
260
+ const metadata = raw.$metadata;
261
+ return {
262
+ name: typeof raw.name === "string" ? raw.name : void 0,
263
+ message: typeof raw.message === "string" ? raw.message : void 0,
264
+ httpStatusCode: metadata && typeof metadata.httpStatusCode === "number" ? metadata.httpStatusCode : typeof raw.status === "number" ? raw.status : void 0,
265
+ requestId: metadata && typeof metadata.requestId === "string" ? metadata.requestId : void 0,
266
+ code: typeof raw.code === "string" ? raw.code : void 0
267
+ };
268
+ }
269
+ /**
270
+ * Decide whether the error is a timeout. Bedrock surfaces
271
+ * `ModelTimeoutException`; the AWS transport layer surfaces
272
+ * `TimeoutError` / `ETIMEDOUT` / `ECONNABORTED`.
273
+ */
274
+ function isTimeout(shape) {
275
+ if (shape.name && TIMEOUT_NAMES.has(shape.name)) return true;
276
+ return shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED";
277
+ }
278
+ /** True for HTTP 4xx — a client-side request problem, not a server fault. */
279
+ function isClientStatus(status) {
280
+ return typeof status === "number" && status >= 400 && status < 500;
281
+ }
282
+ /**
283
+ * Attach the raw diagnostic fields to `error.context`. The Smithy
284
+ * exception `name` is the closest thing Bedrock has to a stable error
285
+ * code, so it lands on `context.code`.
286
+ */
287
+ function buildContext(shape) {
288
+ const context = {};
289
+ if (shape.httpStatusCode !== void 0) context.status = shape.httpStatusCode;
290
+ if (shape.name) context.code = shape.name;
291
+ if (shape.requestId) context.requestId = shape.requestId;
292
+ return context;
293
+ }
294
+
295
+ //#endregion
296
+ //#region ../../@warlock.js/ai-bedrock/src/embedder.ts
297
+ const LOG_MODULE$1 = "ai.bedrock";
298
+ /**
299
+ * Bedrock-backed implementation of `EmbedderContract`, targeting the
300
+ * Amazon Titan Text Embeddings family
301
+ * (`amazon.titan-embed-text-v2:0` / v1) via `InvokeModel`.
302
+ *
303
+ * **Role.** Converts text into floating-point vectors. Standalone
304
+ * primitive — unrelated to Converse / tools / the agent loop.
305
+ *
306
+ * **Single-input only upstream.** Titan's `InvokeModel` body accepts
307
+ * one `inputText` per call — there is no batch endpoint. `embedMany`
308
+ * therefore issues one request per input sequentially and aggregates
309
+ * token usage. This is a deliberate, documented trade-off: a real
310
+ * batch API does not exist for Titan on Bedrock, so the alternative
311
+ * (failing `embedMany`) would be worse. Cohere embeddings on Bedrock
312
+ * *do* batch but use an incompatible body shape — out of scope; use
313
+ * the OpenAI adapter or a future Cohere adapter when batch throughput
314
+ * matters.
315
+ *
316
+ * **Dimensions.** When no `dimensions` override is given,
317
+ * `this.dimensions` starts at `0` and is populated from the first
318
+ * response's vector length, then cached. Passing `dimensions` forwards
319
+ * Titan v2's truncation hint (256 / 512 / 1024) and sets the initial
320
+ * value immediately.
321
+ *
322
+ * @example
323
+ * const embedder = new BedrockEmbedder(client, { name: "amazon.titan-embed-text-v2:0" });
324
+ * const { vector } = await embedder.embed("Hello world");
325
+ * const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);
326
+ */
327
+ var BedrockEmbedder = class {
328
+ constructor(client, config, provider = "bedrock") {
329
+ this.logger = _warlock_js_logger.log;
330
+ this.client = client;
331
+ this.name = config.name;
332
+ this.provider = provider;
333
+ this.configuredDimensions = config.dimensions;
334
+ this.dimensions = config.dimensions ?? 0;
335
+ }
336
+ async embed(input) {
337
+ const { vector, tokens } = await this.invoke(input);
338
+ return {
339
+ vector,
340
+ dimensions: this.dimensions,
341
+ usage: {
342
+ promptTokens: tokens,
343
+ totalTokens: tokens
344
+ }
345
+ };
346
+ }
347
+ async embedMany(inputs) {
348
+ const vectors = [];
349
+ let tokens = 0;
350
+ for (const input of inputs) {
351
+ const result = await this.invoke(input);
352
+ vectors.push(result.vector);
353
+ tokens += result.tokens;
354
+ }
355
+ const usage = {
356
+ promptTokens: tokens,
357
+ totalTokens: tokens
358
+ };
359
+ return {
360
+ vectors,
361
+ dimensions: this.dimensions,
362
+ usage
363
+ };
364
+ }
365
+ /**
366
+ * Issue a single Titan `InvokeModel` embedding request: encode the
367
+ * JSON body, send, wrap provider errors, decode the response, and
368
+ * cache `dimensions` on the first successful call.
369
+ */
370
+ async invoke(input) {
371
+ this.logger.debug(LOG_MODULE$1, "embedder.request", "InvokeModel embeddings", { model: this.name });
372
+ const body = JSON.stringify({
373
+ inputText: input,
374
+ ...this.configuredDimensions !== void 0 ? { dimensions: this.configuredDimensions } : {}
375
+ });
376
+ let raw;
377
+ try {
378
+ raw = await this.client.send(new _aws_sdk_client_bedrock_runtime.InvokeModelCommand({
379
+ modelId: this.name,
380
+ contentType: "application/json",
381
+ accept: "application/json",
382
+ body: new TextEncoder().encode(body)
383
+ }));
384
+ } catch (thrown) {
385
+ const wrapped = wrapBedrockError(thrown);
386
+ this.logger.error(LOG_MODULE$1, "embedder.error", wrapped.message, {
387
+ code: wrapped.code,
388
+ context: wrapped.context
389
+ });
390
+ throw wrapped;
391
+ }
392
+ const decoded = JSON.parse(new TextDecoder().decode(raw.body));
393
+ if (this.dimensions === 0) this.dimensions = decoded.embedding.length;
394
+ this.logger.debug(LOG_MODULE$1, "embedder.response", "InvokeModel embeddings returned", {
395
+ dimensions: decoded.embedding.length,
396
+ tokens: decoded.inputTextTokenCount
397
+ });
398
+ return {
399
+ vector: decoded.embedding,
400
+ tokens: decoded.inputTextTokenCount
401
+ };
402
+ }
403
+ };
404
+
405
+ //#endregion
406
+ //#region ../../@warlock.js/ai-bedrock/src/known-vision-models.ts
407
+ /**
408
+ * Substrings that identify Bedrock model ids whose family accepts image
409
+ * input on the Converse API.
410
+ *
411
+ * Bedrock model ids are provider-prefixed and version-suffixed
412
+ * (`anthropic.claude-3-5-sonnet-20240620-v1:0`, `us.amazon.nova-pro-v1:0`,
413
+ * `meta.llama3-2-90b-instruct-v1:0`), so a substring match is the only
414
+ * robust check across the cross-region inference-profile prefixes
415
+ * (`us.`, `eu.`, `apac.`) and date/version tags.
416
+ *
417
+ * Multimodal families covered: Anthropic Claude 3 / 3.5 / 3.7 / 4,
418
+ * Amazon Nova Lite/Pro/Premier, Meta Llama 3.2 (11B/90B) and Llama 4.
419
+ * Text-only families (Llama 3/3.1, Titan Text, Mistral 7B, Cohere
420
+ * Command) are intentionally absent. Override per-model via
421
+ * `bedrock.model({ name, vision: true | false })`.
422
+ */
423
+ const VISION_CAPABLE_SUBSTRINGS = [
424
+ "claude-3",
425
+ "claude-sonnet-4",
426
+ "claude-opus-4",
427
+ "claude-haiku-4",
428
+ "nova-lite",
429
+ "nova-pro",
430
+ "nova-premier",
431
+ "llama3-2-11b",
432
+ "llama3-2-90b",
433
+ "llama4"
434
+ ];
435
+ /**
436
+ * Infer whether a Bedrock model id supports vision based on the known
437
+ * multimodal-family substrings. Unknown ids default to `false` so that
438
+ * passing an image attachment to an unsupported model surfaces a clear,
439
+ * agent-side capability error instead of an opaque Bedrock validation
440
+ * fault.
441
+ *
442
+ * @example
443
+ * inferVisionCapability("anthropic.claude-3-5-sonnet-20240620-v1:0"); // → true
444
+ * inferVisionCapability("us.amazon.nova-pro-v1:0"); // → true
445
+ * inferVisionCapability("meta.llama3-1-8b-instruct-v1:0"); // → false
446
+ * inferVisionCapability("amazon.titan-text-express-v1"); // → false
447
+ */
448
+ function inferVisionCapability(modelId) {
449
+ const normalized = modelId.toLowerCase();
450
+ return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));
451
+ }
452
+
453
+ //#endregion
454
+ //#region ../../@warlock.js/ai-bedrock/src/model.ts
455
+ const LOG_MODULE = "ai.bedrock";
456
+ /**
457
+ * Bedrock-backed implementation of `ModelContract`.
458
+ *
459
+ * **Role.** The provider-facing bridge between the vendor-neutral
460
+ * `@warlock.js/ai` agent runtime and AWS Bedrock's Converse /
461
+ * ConverseStream API. Converse is the model-agnostic surface — one
462
+ * wire mapping covers every Bedrock-hosted family (Anthropic Claude,
463
+ * Amazon Nova, Meta Llama, Mistral, Cohere) instead of per-family
464
+ * `InvokeModel` body shapes.
465
+ *
466
+ * **Responsibility.**
467
+ * - Owns: a long-lived `BedrockRuntimeClient` + frozen `ModelConfig`
468
+ * (modelId, temperature, maxTokens) used as per-call defaults.
469
+ * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into
470
+ * Converse shapes (system hoisting, `toolUse` / `toolResult` blocks,
471
+ * image bytes) on the way out, and Converse's content-block response
472
+ * (text, tool calls, stop reason, token usage) back into the neutral
473
+ * shapes on the way in.
474
+ * - Does NOT own: dispatching tools, looping, history, retries — those
475
+ * are agent concerns. The model is a per-call protocol adapter.
476
+ *
477
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
478
+ * across calls"): the AWS client is heavy to construct and reused for
479
+ * the SDK's lifetime.
480
+ *
481
+ * @example
482
+ * import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
483
+ * const client = new BedrockRuntimeClient({ region: "us-east-1" });
484
+ * const model = new BedrockModel(client, {
485
+ * name: "anthropic.claude-sonnet-4-5-20250929-v1:0",
486
+ * });
487
+ *
488
+ * const myAgent = agent({ model, tools: [searchTool] });
489
+ * const result = await myAgent.execute("Summarize today's news.");
490
+ */
491
+ var BedrockModel = class {
492
+ constructor(client, config, provider = "bedrock") {
493
+ this.logger = _warlock_js_logger.log;
494
+ this.client = client;
495
+ this.config = config;
496
+ this.name = config.name;
497
+ this.provider = provider;
498
+ this.pricing = config.pricing;
499
+ this.capabilities = {
500
+ structuredOutput: config.structuredOutput ?? true,
501
+ vision: config.vision ?? inferVisionCapability(config.name)
502
+ };
503
+ }
504
+ /**
505
+ * Single-shot completion via the Converse API. Sends the full
506
+ * message list, waits for the terminal response, and reshapes it
507
+ * into a vendor-neutral `ModelResponse`. Per-call `options` override
508
+ * the instance defaults for this call only.
509
+ */
510
+ async complete(messages, options) {
511
+ this.logger.debug(LOG_MODULE, "request", "Starting Converse call", {
512
+ model: this.name,
513
+ messageCount: messages.length,
514
+ streaming: false,
515
+ toolCount: options?.tools?.length ?? 0
516
+ });
517
+ let response;
518
+ try {
519
+ response = await this.client.send(new _aws_sdk_client_bedrock_runtime.ConverseCommand(this.buildRequest(messages, options)), options?.signal ? { abortSignal: options.signal } : void 0);
520
+ } catch (thrown) {
521
+ throw this.logAndWrap(thrown);
522
+ }
523
+ const blocks = response.output?.message?.content ?? [];
524
+ const finishReason = mapStopReason(response.stopReason);
525
+ const usage = this.extractUsage(response.usage);
526
+ const toolCalls = this.extractToolCalls(blocks);
527
+ this.logger.debug(LOG_MODULE, "response", "Converse call succeeded", {
528
+ finishReason,
529
+ usage
530
+ });
531
+ return {
532
+ content: this.extractText(blocks),
533
+ finishReason,
534
+ usage,
535
+ toolCalls
536
+ };
537
+ }
538
+ /**
539
+ * Incremental streaming completion via ConverseStream. Yields neutral
540
+ * `ModelStreamChunk`s — `delta` for text, `tool-call` once a
541
+ * `toolUse` block's accumulated input JSON is complete, and a
542
+ * terminal `done` with the final finish reason + usage totals.
543
+ */
544
+ async *stream(messages, options) {
545
+ this.logger.debug(LOG_MODULE, "request", "Starting ConverseStream call", {
546
+ model: this.name,
547
+ messageCount: messages.length,
548
+ streaming: true,
549
+ toolCount: options?.tools?.length ?? 0
550
+ });
551
+ let response;
552
+ try {
553
+ response = await this.client.send(new _aws_sdk_client_bedrock_runtime.ConverseStreamCommand(this.buildRequest(messages, options)), options?.signal ? { abortSignal: options.signal } : void 0);
554
+ } catch (thrown) {
555
+ throw this.logAndWrap(thrown);
556
+ }
557
+ let rawStopReason;
558
+ const usage = {
559
+ input: 0,
560
+ output: 0,
561
+ total: 0
562
+ };
563
+ const toolBlocks = /* @__PURE__ */ new Map();
564
+ try {
565
+ for await (const event of response.stream ?? []) {
566
+ if (event.contentBlockStart?.start?.toolUse) {
567
+ const start = event.contentBlockStart.start.toolUse;
568
+ toolBlocks.set(event.contentBlockStart.contentBlockIndex ?? 0, {
569
+ id: start.toolUseId ?? "",
570
+ name: start.name ?? "",
571
+ json: ""
572
+ });
573
+ continue;
574
+ }
575
+ if (event.contentBlockDelta?.delta) {
576
+ const delta = event.contentBlockDelta.delta;
577
+ if (delta.text) yield {
578
+ type: "delta",
579
+ content: delta.text
580
+ };
581
+ else if (delta.toolUse) {
582
+ const accumulator = toolBlocks.get(event.contentBlockDelta.contentBlockIndex ?? 0);
583
+ if (accumulator) accumulator.json += delta.toolUse.input ?? "";
584
+ }
585
+ continue;
586
+ }
587
+ if (event.contentBlockStop) {
588
+ const accumulator = toolBlocks.get(event.contentBlockStop.contentBlockIndex ?? 0);
589
+ if (accumulator) {
590
+ yield {
591
+ type: "tool-call",
592
+ id: accumulator.id,
593
+ name: accumulator.name,
594
+ input: (0, _warlock_js_ai.safeJsonParse)(accumulator.json, {})
595
+ };
596
+ toolBlocks.delete(event.contentBlockStop.contentBlockIndex ?? 0);
597
+ }
598
+ continue;
599
+ }
600
+ if (event.messageStop) rawStopReason = event.messageStop.stopReason;
601
+ if (event.metadata?.usage) {
602
+ const raw = event.metadata.usage;
603
+ usage.input = raw.inputTokens ?? 0;
604
+ usage.output = raw.outputTokens ?? 0;
605
+ usage.total = raw.totalTokens ?? usage.input + usage.output;
606
+ if (raw.cacheReadInputTokens && raw.cacheReadInputTokens > 0) usage.cachedTokens = raw.cacheReadInputTokens;
607
+ }
608
+ }
609
+ } catch (thrown) {
610
+ throw this.logAndWrap(thrown);
611
+ }
612
+ const finishReason = mapStopReason(rawStopReason);
613
+ this.logger.debug(LOG_MODULE, "response", "ConverseStream call succeeded", {
614
+ finishReason,
615
+ usage
616
+ });
617
+ yield {
618
+ type: "done",
619
+ finishReason,
620
+ usage
621
+ };
622
+ }
623
+ /**
624
+ * Assemble the Converse request shared by `complete()` and
625
+ * `stream()` (both command shapes take the same input). Hoists the
626
+ * system prompt, maps inference params, and conditionally attaches
627
+ * tools and native structured output.
628
+ */
629
+ buildRequest(messages, options) {
630
+ const { system, messages: bedrockMessages } = toBedrockMessages(messages);
631
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens;
632
+ const temperature = options?.temperature ?? this.config.temperature;
633
+ return {
634
+ modelId: this.name,
635
+ messages: bedrockMessages,
636
+ ...system ? { system } : {},
637
+ inferenceConfig: {
638
+ ...maxTokens !== void 0 ? { maxTokens } : {},
639
+ ...temperature !== void 0 ? { temperature } : {}
640
+ },
641
+ ...this.buildToolConfig(options?.tools),
642
+ ...this.buildOutputConfig(options?.responseSchema)
643
+ };
644
+ }
645
+ /**
646
+ * Spread-friendly tool fragment. Returns an empty object when no
647
+ * tools were supplied (Bedrock rejects an empty `tools` array).
648
+ */
649
+ buildToolConfig(tools) {
650
+ const toolConfig = toBedrockToolConfig(tools);
651
+ return toolConfig ? { toolConfig } : {};
652
+ }
653
+ /**
654
+ * Translate the neutral `responseSchema` into Converse's native
655
+ * `outputConfig.textFormat` (JSON-schema structured output). Bedrock
656
+ * requires the schema as a stringified JSON document and only
657
+ * accepts an object root. Emitted only when the model is
658
+ * `structuredOutput`-capable and the schema is an object — otherwise
659
+ * the agent's soft system-prompt hint + client-side `validate()`
660
+ * carry shape (same degradation philosophy as the OpenAI adapter).
661
+ */
662
+ buildOutputConfig(responseSchema) {
663
+ if (!responseSchema || !this.capabilities.structuredOutput) return {};
664
+ if (responseSchema.type !== "object" || typeof responseSchema.properties !== "object") return {};
665
+ return { outputConfig: { textFormat: {
666
+ type: "json_schema",
667
+ structure: { jsonSchema: {
668
+ name: "response",
669
+ schema: JSON.stringify(responseSchema)
670
+ } }
671
+ } } };
672
+ }
673
+ /**
674
+ * Concatenate every `text` content block into the single neutral
675
+ * `content` string. `toolUse` and other block types are surfaced
676
+ * separately via `extractToolCalls`.
677
+ */
678
+ extractText(blocks) {
679
+ return blocks.map((block) => "text" in block && typeof block.text === "string" ? block.text : "").join("");
680
+ }
681
+ /**
682
+ * Reshape Converse `toolUse` content blocks into the neutral
683
+ * `ModelToolCallRequest[]`. Returns `undefined` when no tools were
684
+ * requested so callers can branch on presence.
685
+ */
686
+ extractToolCalls(blocks) {
687
+ const toolCalls = [];
688
+ for (const block of blocks) if ("toolUse" in block && block.toolUse) toolCalls.push({
689
+ id: block.toolUse.toolUseId ?? "",
690
+ name: block.toolUse.name ?? "",
691
+ input: block.toolUse.input ?? {}
692
+ });
693
+ return toolCalls.length > 0 ? toolCalls : void 0;
694
+ }
695
+ /**
696
+ * Normalize Converse's `TokenUsage` into the neutral `Usage` shape.
697
+ * Bedrock supplies a pre-summed `totalTokens`; cache-read tokens are
698
+ * surfaced as `cachedTokens` only when non-zero.
699
+ */
700
+ extractUsage(raw) {
701
+ if (!raw) return {
702
+ input: 0,
703
+ output: 0,
704
+ total: 0
705
+ };
706
+ const input = raw.inputTokens ?? 0;
707
+ const output = raw.outputTokens ?? 0;
708
+ const cached = raw.cacheReadInputTokens;
709
+ return {
710
+ input,
711
+ output,
712
+ total: raw.totalTokens ?? input + output,
713
+ ...cached && cached > 0 ? { cachedTokens: cached } : {}
714
+ };
715
+ }
716
+ /**
717
+ * Wrap a thrown provider error into the typed `AIError` hierarchy
718
+ * and emit the standard error log line before it propagates. Shared
719
+ * by every catch site so the log shape stays identical.
720
+ */
721
+ logAndWrap(thrown) {
722
+ const wrapped = wrapBedrockError(thrown);
723
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
724
+ code: wrapped.code,
725
+ context: wrapped.context
726
+ });
727
+ return wrapped;
728
+ }
729
+ };
730
+
731
+ //#endregion
732
+ //#region ../../@warlock.js/ai-bedrock/src/sdk.ts
733
+ /**
734
+ * AWS Bedrock-backed implementation of `SDKAdapterContract`.
735
+ *
736
+ * **Role.** The package entry point for any Bedrock-hosted model via
737
+ * the Converse API. A single `BedrockSDK` holds one live
738
+ * `BedrockRuntimeClient`, shared by every `ModelContract` and
739
+ * `EmbedderContract` it produces. Construct one SDK per AWS
740
+ * account/region and reuse it everywhere.
741
+ *
742
+ * **Responsibility.**
743
+ * - Owns: a long-lived `BedrockRuntimeClient` (region, credential
744
+ * chain) and its lifetime. Factory for `BedrockModel` /
745
+ * `BedrockEmbedder` instances sharing that client.
746
+ * - Does NOT own: anything per-call — those live in `BedrockModel` /
747
+ * `BedrockEmbedder` and the agent runtime.
748
+ *
749
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
750
+ * across many calls"): the AWS client is heavy to construct and
751
+ * designed for reuse; keeping it on `this` aligns with the
752
+ * `new BedrockRuntimeClient(...)` upstream convention.
753
+ *
754
+ * @example
755
+ * const bedrock = new BedrockSDK({ region: "us-east-1" });
756
+ * const model = bedrock.model({ name: "anthropic.claude-sonnet-4-5-20250929-v1:0" });
757
+ * const embedder = bedrock.embedder({ name: "amazon.titan-embed-text-v2:0" });
758
+ */
759
+ var BedrockSDK = class {
760
+ constructor(config) {
761
+ const { provider, pricing, ...clientConfig } = config;
762
+ this.client = new _aws_sdk_client_bedrock_runtime.BedrockRuntimeClient(clientConfig);
763
+ this.provider = provider ?? "bedrock";
764
+ this.pricing = pricing;
765
+ }
766
+ /**
767
+ * Build a `BedrockModel` bound to this SDK's client. Each call
768
+ * returns a fresh instance; all instances share the underlying AWS
769
+ * client so connection pools, credential refresh, and retry config
770
+ * stay unified. The SDK's `provider` label is forwarded.
771
+ *
772
+ * Pricing resolution: per-model `config.pricing` wins; otherwise the
773
+ * SDK-level registry entry keyed by `config.name`; otherwise
774
+ * `undefined` (no cost computed).
775
+ */
776
+ model(config) {
777
+ const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
778
+ const resolvedConfig = resolvedPricing === config.pricing ? config : {
779
+ ...config,
780
+ pricing: resolvedPricing
781
+ };
782
+ return new BedrockModel(this.client, resolvedConfig, this.provider);
783
+ }
784
+ /**
785
+ * Rough token-count estimate. Uses the character-heuristic
786
+ * (`approximateTokenCount`) from the core package — Bedrock has no
787
+ * offline tokenizer and the per-model tokenizers differ; good enough
788
+ * for budgeting and quota guards, not for billing.
789
+ */
790
+ async count(text, _model) {
791
+ return (0, _warlock_js_ai.approximateTokenCount)(text);
792
+ }
793
+ /**
794
+ * Build a `BedrockEmbedder` (Amazon Titan Text Embeddings) bound to
795
+ * this SDK's client.
796
+ *
797
+ * @example
798
+ * const embedder = bedrock.embedder({ name: "amazon.titan-embed-text-v2:0" });
799
+ * const { vector } = await embedder.embed("Hello world");
800
+ */
801
+ embedder(config) {
802
+ return new BedrockEmbedder(this.client, config, this.provider);
803
+ }
804
+ };
805
+
806
+ //#endregion
807
+ exports.BedrockSDK = BedrockSDK;
808
+ //# sourceMappingURL=index.cjs.map