Skip to content

Bedrock provider

Standalone — usable in any Node project, no @warlock.js/core required.

@warlock.js/ai-bedrock is the AWS Bedrock provider adapter for @warlock.js/ai. It speaks Bedrock’s Converse API — the model-agnostic surface — so one wire mapping covers every Bedrock-hosted family (Anthropic Claude, Amazon Nova, Meta Llama, Mistral, Cohere) and hands it all back as a vendor-neutral ModelContract. Auth uses your AWS credentials, exactly like any other AWS SDK call, or an Amazon Bedrock API key.

Terminal window
npm install @warlock.js/ai @warlock.js/ai-bedrock

One BedrockSDK holds one live BedrockRuntimeClient. Build it once per account/region and reuse it everywhere — every model and embedder it produces shares the same client, credential refresh, and retry config.

import { BedrockSDK } from "@warlock.js/ai-bedrock";
const bedrock = new BedrockSDK({
region: "us-east-1",
// Standard AWS SDK credential resolution — env vars, ~/.aws/credentials,
// IAM role on EC2/ECS, SSO. Pass explicit credentials only if needed.
});

region is the one required field. The whole config object is forwarded straight to BedrockRuntimeClient, so any client option works — explicit credentials, a custom endpoint, a requestHandler, retry settings. provider relabels the upstream (defaults to "bedrock").

const bedrock = new BedrockSDK({
region: "us-east-1",
credentials: { accessKeyId: "AKIA...", secretAccessKey: "..." },
});

Bedrock also accepts Amazon Bedrock API keys, which are bearer tokens rather than AWS credentials. Pass one as apiKey:

const bedrock = new BedrockSDK({
apiKey: process.env.BEDROCK_API_KEY!,
region: "us-east-1",
});

Every request then carries Authorization: Bearer <apiKey> instead of a SigV4 signature, and the AWS credential chain is never consulted. The adapter sets the AWS SDK’s token and authSchemePreference: ["httpBearerAuth"] for you.

  • apiKey cannot be combined with credentials or token — the constructor throws InvalidRequestError rather than guess which one should sign. An empty key throws too.
  • Without apiKey or credentials, the AWS SDK itself still picks up a key from the AWS_BEARER_TOKEN_BEDROCK environment variable.
  • API keys cover the Bedrock and Bedrock Runtime APIs this adapter calls (Converse, ConverseStream, InvokeModel for embeddings, CountTokens).

Build a model, hand it to an agent, run it. execute() never throws — failures land in error as a typed AIError.

import { ai } from "@warlock.js/ai";
import { BedrockSDK } from "@warlock.js/ai-bedrock";
const bedrock = new BedrockSDK({ region: "us-east-1" });
const assistant = ai.agent({
model: bedrock.model({ name: "anthropic.claude-sonnet-4-5-20250929-v1:0" }),
systemPrompt: "You are a concise senior TypeScript engineer.",
});
const { text, usage, error } = await assistant.execute("Why use generics?");
if (error) {
console.warn(error.code, error.category);
} else {
console.log(text, usage.total);
}

name is the Bedrock model id or inference-profile id — Claude, Nova, Llama, and friends all go through the same call.

bedrock.model({ name: "anthropic.claude-sonnet-4-5-20250929-v1:0" });
bedrock.model({ name: "us.amazon.nova-pro-v1:0" });
bedrock.model({ name: "meta.llama3-1-8b-instruct-v1:0" });

The embedder targets the Amazon Titan Text Embeddings family (amazon.titan-embed-text-v2:0 and v1) via Bedrock’s InvokeModel.

const embedder = bedrock.embedder({ name: "amazon.titan-embed-text-v2:0" });
const { vector } = await embedder.embed("Hello world");
const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);

Titan v2 supports output truncation — pass dimensions (256 / 512 / 1024) and the adapter forwards it:

bedrock.embedder({ name: "amazon.titan-embed-text-v2:0", dimensions: 256 });

Titan has no batch endpoint. Its InvokeModel body accepts one inputText per call, so embedMany issues one request per input sequentially and aggregates token usage. For high-throughput batch embedding, reach for the OpenAI or Google adapter, which batch natively.

  • Tool calling — vendor-neutral ToolConfigs map to Converse toolUse blocks; results round-trip back as toolResult blocks on a user turn.
  • Streaming — stream() runs ConverseStream and yields text deltas, a tool-call once a toolUse block’s input JSON is complete, and a terminal done with the final finish reason and usage.
  • Structured output — on by default. A root-object JSON Schema is forwarded to Converse’s native outputConfig.textFormat (Bedrock wants it as a stringified schema; the adapter handles that). Otherwise it degrades to the agent’s soft system-prompt hint plus client-side validation.
  • Vision — auto-detected from the model id substring. Multimodal families covered: Anthropic Claude 3 / 3.5 / 3.7 / 4, Amazon Nova Lite/Pro/Premier, Meta Llama 3.2 (11B/90B) and Llama 4. Text-only families (Llama 3/3.1, Titan Text, Mistral 7B, Cohere Command) stay off. Override either way with the vision flag.
  • PDF / document input — auto-detected from the model id, mirroring vision minus the image-only Llama families. Inferred on for Anthropic Claude 3 / 3.5 / 3.7 / 4 and Amazon Nova Lite/Pro/Premier (they accept Converse document content blocks); off for text-only and image-only families. When off, a PDF attachment raises a typed capability error up front. Override with the pdf flag.
  • Audio input — off by default and never auto-inferred: Bedrock Converse takes no audio content blocks for the families this adapter targets, so every model reports audio: false unless you set audio: true for a model id you’ve confirmed accepts audio.
  • Embeddings — Amazon Titan Text Embeddings, single-input (see above).

Images must be base64 bytes. Bedrock Converse has no remote-URL image source — supply inlined base64 (image/jpeg, image/png, image/gif, image/webp). Passing a { url } image raises a typed InvalidRequestError upfront rather than a downstream Bedrock fault.

For a caching-capable model (Claude 3.5+ / 4 / 5 and the Nova family), a per-call cacheControl: { breakpoints: 1 } places Converse cachePoint blocks on each stable part of the prefix, in the order Bedrock processes them: after the tools (Claude only — Nova accepts cache points in system and messages only), after the system prompt, and on the last message.

ttl: "1h" — from the per-call cacheControl.ttl or the model’s promptCacheTtl — is sent only to models that accept it: Claude Haiku 4.5, Claude Sonnet / Opus 4.5 and later, and every Claude 5 family. Other models keep the 5-minute default, since AWS rejects the field there. Placement and TTL rules follow Bedrock’s prompt-caching guide.

import { BedrockSDK } from "@warlock.js/ai-bedrock";
const bedrock = new BedrockSDK({ region: "us-east-1" });
const model = bedrock.model({
name: "global.anthropic.claude-sonnet-4-6",
promptCacheTtl: "1h", // default lifetime for this model's cache points
});
await model.complete(
[
{ role: "system", content: "Long, stable instructions…" },
{ role: "user", content: "First question" },
],
{ cacheControl: { breakpoints: 1 } },
);

Every response reports token usage (input, output, total), on model.complete() and the streaming done chunk as well as on agent reports. With caching, Bedrock’s inputTokens covers only the non-cached input — cache reads (cachedTokens) and writes (cacheWriteTokens, with the 1-hour share from cacheDetails in cacheWrite1hTokens) come on top of it — so the usage is marked inputIncludesCache: false and cost bills each channel exactly once.

Built-in Claude prices. Claude models invoked through a global. or us. inference profile are priced out of the box from BEDROCK_GLOBAL_PRICING / BEDROCK_US_PRICING (lookup: bedrockPricingFor(id)), taken from Amazon Bedrock pricing (US East, N. Virginia), fetched 2026-10-17: input, output, cache read, 5-minute write (1.25× input) and 1-hour write (2× input). In-Region ids with no profile prefix, other geographies (eu., apac., …), Claude Haiku 5.5 and non-Claude models are not built in — their rates vary by Region or were not verified — so pass pricing for them. Any Claude entry you supply without write rates gets the 1.25× / 2× rates filled in.

Resolution: per-model pricing > SDK pricing registry (exact id) > built-in profile price > unpriced (Usage.cost stays undefined).

import { totalCost } from "@warlock.js/ai";
import { BedrockSDK } from "@warlock.js/ai-bedrock";
const bedrock = new BedrockSDK({
region: "us-east-1",
pricing: {
"anthropic.claude-sonnet-4-5-20250929-v1:0": { input: 3, output: 15, cachedInput: 0.3 },
"amazon.nova-pro-v1:0": { input: 0.8, output: 3.2 }, // your own rates for unlisted models
},
});
const { usage } = await bedrock
.model({ name: "global.anthropic.claude-sonnet-4-6" }) // built-in global rate
.complete([{ role: "user", content: "hi" }]);
console.log(usage.inputIncludesCache, totalCost(usage.cost));

A ServiceUnavailableException surfaces as ProviderOverloadedError (code: "PROVIDER_OVERLOADED", retryable: true). Every provider error carries status, retryable and billed (false for 401 / 402 / 403, otherwise "unknown").

Need an offline token estimate? bedrock.count(text) returns a fast character-heuristic approximation — Bedrock has no offline tokenizer and per-model tokenizers differ, so this is for budgeting, not billing.

For the agent/workflow surface these models plug into, see @warlock.js/ai. For provider-specific notes and the latest model ids, see the warlock-js-ai-bedrock skill.

Provider body options and input-token counts

Section titled “Provider body options and input-token counts”

Native body fields merge after neutral options. When both values are plain objects, their immediate keys merge and caller-provided keys win; untouched neutral fields remain. Arrays, scalars and null replace the field. This merge is one level deep: a nested object explicitly supplied by the caller replaces that nested value.

providerOptions.bedrock.body adds native Converse request fields after the neutral options, for completion and streaming:

const options = {
reasoning: { effort: "high" as const }, // preserved alongside top_k
providerOptions: { bedrock: { body: { additionalModelRequestFields: { top_k: 4 } } } },
};
await model.complete(messages, options);

The key stays bedrock for custom provider labels. Other provider keys are ignored. BedrockCallOptions is exported. modelId, messages, system and toolConfig are protected; attempting to replace them throws InvalidRequestError. Provider fields are otherwise forwarded without validation.

model.countTokens(messages, options?) calls Bedrock CountTokens using the same Converse messages, system blocks, tool configuration and additionalModelRequestFields used for generation. Cancellation is forwarded. Availability depends on the model and region; provider failures use the adapter’s normal typed errors. sdk.count(text) remains a separate offline estimate.

For budget preflight, countInputTokens(model, messages, { tools }) from @warlock.js/ai returns { tokens, source }. It estimates only when native counting is absent or explicitly throws InvalidRequestError with reason: "count-unsupported"; this adapter preserves that signal. System and tools are supported natively. Ordinary validation, auth, network and rate-limit errors propagate. See the cost/budget guide for reserve mode.

if (model.countTokens) {
const inputTokens = await model.countTokens(messages, options);
}

Converse messages combine adjacent turns of the same role, retaining every content block in order. This keeps parallel tool results in one user turn and allows an automatic compaction summary beside an assistant tool-call turn without creating consecutive assistant turns. Agent compaction frames a summary as user context when it replaces the first non-system turn.

Adjacent-turn normalization preserves empty histories and separate system prompts without inserting synthetic conversation turns.

Bedrock system tools go in tools through ai.providerTool({ provider: "bedrock", definition }); the definition is a whole Converse Tool member, appended to toolConfig.tools after the function tool specs. A server_tool_use block in the response is never dispatched as a tool call. See Provider-defined tools.

// Nova web grounding (US cross-region profiles only).
const grounding = ai.providerTool({
provider: "bedrock",
definition: { systemTool: { name: "nova_grounding" } },
});

With streamChunks (on by default under agent.stream()), stream() also yields reasoning for reasoningContent.text deltas and tool-args for client toolUse.input deltas. See Stream reasoning and tool arguments.

Bedrock Converse’s TokenUsage schema and the installed SDK expose token/cache counts, with no hosted-tool request counter (verified 2026-10-18). usage.serverToolUse therefore remains absent; no count or search fee is inferred from server_tool_use system-tool blocks. Hosted system tools continue to stay outside function dispatch. Direct Anthropic counters are not assumed to exist on Converse. Supply fees only when your own verified metering establishes the billing unit.