AWS Bedrock
Use the fromBedrock adapter with AWS Bedrock Converse API
Uses Bedrock's Converse API, which provides a unified request/response interface across supported model families (Anthropic, Titan, Llama, Mistral, and others).
JSON handling depends on the requested format:
json_objectis not supported as a real constraint. Converse has no field that mechanically guarantees valid JSON output for this mode. Whether an unsetjson_objectrequest throws or is silently downgraded depends on what else the call asks for. A plain call (llm.call({ userContent }), noschema, no explicitjsonMode) is silently treated as plain text, keeping the common case working unchanged. An explicitjsonMode: truethrowsLLMError('invalid_params').schemawithoutjsonSchema, whether or notjsonModeis set explicitly, also throwsLLMError('invalid_params'), sinceschemaneeds JSON output to validate against and shouldn't be silently downgraded into skipping validation. UsejsonSchemainstead in every one of those throwing cases.json_schemauses Bedrock Converse tool use. The adapter forwards the schema name, description, and strict setting into the tool spec. Strict enforcement depends on the selected Bedrock model's tool-use capabilities.
reasoning_effort is not forwarded because Bedrock does not expose an equivalent option through Converse.
BedrockConverseClient is the public client type exported by VernLLM and accepted by fromBedrock. It allows TypeScript users to type their Bedrock Converse client without depending on adapter internals.
The AWS SDK retries on its own too
AWS SDK v3's standard retry mode retries transient failures, including ThrottlingException,
internally by default (maxAttempts: 3), before VernLLM's own retry loop ever sees them. Pass
maxAttempts: 1 to BedrockRuntimeClient if you want VernLLM to be the sole retry authority. See
SDK-internal retries.
Note: Tool-based structured output requires a Bedrock model that supports tool use. fromBedrock doesn't try to detect this from a failed call, AWS's error text for an unsupported model isn't a documented, stable contract, so guessing at it would be unreliable. By default, a jsonSchema call to a model without tool support surfaces Bedrock's raw error unchanged. If you want a clearer, earlier failure, pass toolUseSupportedModels (see below).
Setup
Option 1: raw AWS SDK client (recommended)
If @aws-sdk/client-bedrock-runtime is already installed, pass a BedrockRuntimeClient directly to fromBedrock. No wrapper needed. fromBedrock detects the raw AWS client and drives it internally via client.send(new ConverseCommand(...)) and client.send(new ConverseStreamCommand(...)):
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime';
import { VernLLM, fromBedrock } from 'vern-llm';
const client = new BedrockRuntimeClient({ region: 'us-east-1' });
const llm = new VernLLM({
client: fromBedrock(client),
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
});vern-llm has zero runtime dependencies, including for this path.
@aws-sdk/client-bedrock-runtime is not a dependency or a peer dependency. Passing a raw AWS
client pulls in ConverseCommand/ConverseStreamCommand with a dynamic import() on the first
real request, not when fromBedrock is called. Nothing is added to package.json just by
importing vern-llm, and bundlers only reach the AWS SDK for code paths that actually pass a raw
client. If the package isn't installed, the first request throws a clear LLMError naming what's
missing.
Two structural gaps between AWS's generated types and BedrockConverseClient are handled for you
on this path. A ConverseStreamCommand response missing stream (AWS types it as optional)
throws a clear LLMError('api') instead of crashing the internal for await loop. Any streaming
event VernLLM doesn't model, including AWS's generated $unknown member, is dropped before
reaching your code instead of being forwarded unnarrowed.
Option 2: hand-written wrapper (zero-dependency)
Skip @aws-sdk/client-bedrock-runtime entirely, or use a different AWS SDK generation or a hand-rolled HTTP client. Wrap it yourself to match BedrockConverseClient:
import {
BedrockRuntimeClient,
ConverseCommand,
ConverseStreamCommand,
} from '@aws-sdk/client-bedrock-runtime';
import { VernLLM, fromBedrock } from 'vern-llm';
const client = new BedrockRuntimeClient({ region: 'us-east-1' });
const converseClient = {
converse: (params, options) =>
client.send(new ConverseCommand(params), { abortSignal: options.signal }),
converseStream: (params, options) =>
client.send(new ConverseStreamCommand(params), { abortSignal: options.signal }),
};
const llm = new VernLLM({
client: fromBedrock(converseClient),
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
});converseStream is optional. Add it only if you plan to use stream: true. Omitting it makes
fromBedrock's createStream throw a clear LLMError('invalid_params') (code
unsupported_capability) instead of failing silently.
fromBedrock doesn't depend on @aws-sdk/client-bedrock-runtime's types on this path. It accepts
anything matching BedrockConverseClient's minimal .converse() shape. Use
BedrockConverseClient from VernLLM to type your wrapper without pulling in the full AWS SDK
types.
Structured output via tool use
When jsonSchema is set, fromBedrock builds a single Converse tool from the schema, forwards its name, description, and strict setting, and forces the model to call it via toolChoice, rather than instructing for JSON via prompt text. This is the default behavior for every model; see Combining tools with jsonSchema below for the native alternative available on models covered by nativeStructuredOutputModels, which uses outputConfig.textFormat instead and composes with real tools.
import { z } from 'zod';
const CandidateSchema = z.object({
name: z.string(),
skills: z.array(z.string()),
});
const result = await llm.call({
systemPrompt: 'Extract the candidate name and skills.',
userContent: resumeText,
jsonSchema: {
name: 'Candidate',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
skills: { type: 'array', items: { type: 'string' } },
},
required: ['name', 'skills'],
},
},
schema: CandidateSchema,
});Without jsonSchema, behavior depends on whether the call is also asking for validated output. A plain call with no schema and no explicit jsonMode is silently treated as plain text: Converse can't enforce json_object, so it's never sent, and fromBedrock's response is returned unparsed. Setting jsonMode: true explicitly, or passing schema on its own (with or without an explicit jsonMode), throws LLMError('invalid_params') instead: the former is a caller deliberately asking for a JSON guarantee this adapter can't provide, and the latter needs JSON output to validate schema against, so it isn't silently downgraded into skipping validation either. Use jsonSchema for anything that needs guaranteed-structured output.
Preflighting tool-use support
toolUseSupportedModels, passed as the second argument to fromBedrock, lets you reject jsonSchema calls to models you know don't support Converse tool use, before the request is ever sent, with a clear LLMError('validation') instead of Bedrock's raw error:
import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';
import { VernLLM, fromBedrock, type BedrockConverseClient } from 'vern-llm';
const client = new BedrockRuntimeClient({ region: 'us-east-1' });
const converseClient: BedrockConverseClient = {
converse: (params, options) =>
client.send(new ConverseCommand(params), { abortSignal: options.signal }),
};
const llm = new VernLLM({
client: fromBedrock(converseClient, {
// A static allowlist...
toolUseSupportedModels: [
'anthropic.claude-3-5-sonnet-20241022-v2:0',
'anthropic.claude-3-haiku-20240307-v1:0',
],
// ...or a predicate, e.g. by model family:
// toolUseSupportedModels: (modelId) => modelId.startsWith('anthropic.'),
}),
model: 'anthropic.claude-3-5-sonnet-20241022-v2:0',
});This is opt-in and only affects calls that set jsonSchema; plain text calls are never preflight-checked, and by default (no toolUseSupportedModels configured) no preflight check runs at all.
The same preflight also runs when a jsonSchema call ends up sending toolConfig for any reason, including the native-structured-output path below when real tools are sent alongside outputConfig, since that still requires Converse tool-use support even though outputConfig itself doesn't.
Combining tools with jsonSchema
By default, jsonSchema and tools cannot be used in the same call: jsonSchema is emulated as
a forced single tool call via toolConfig, which occupies the same field real tools need.
On unsupported models, this throws LLMError('invalid_params') with code: 'unsupported_capability'
and issues.capability: 'tools_with_json_schema', so the default fallback policy can try the next
target.
Bedrock Converse also supports native, schema-constrained output, outputConfig.textFormat, a
request field independent of toolConfig, so it composes with real tools on models that support
it. This is opt-in, pass nativeStructuredOutputModels as part of the second argument to
fromBedrock:
const llm = new VernLLM({
client: fromBedrock(converseClient, {
nativeStructuredOutputModels: ['anthropic.claude-native-model-id'],
// ...or a predicate:
// nativeStructuredOutputModels: (modelId) => modelId.startsWith('anthropic.claude-4'),
}),
model: 'anthropic.claude-native-model-id',
});There is no built-in default list, see Structured Output → Combining with tools for why, and for a link to Anthropic's current compatibility list. On Bedrock specifically, Anthropic's compatibility page documents native structured output as available for a narrower model subset than the Claude API's own list, so check the linked page rather than assuming parity with fromAnthropic. On a covered model, jsonSchema is sent as outputConfig.textFormat instead of a forced
tool call, and the schema-conforming JSON arrives as ordinary text content, no unwrapping needed,
while any real tools you pass are built and sent exactly as they would be without jsonSchema
present. On a model not covered by nativeStructuredOutputModels, combining tools with
jsonSchema throws LLMError('invalid_params') with code: 'unsupported_capability' and
issues.capability: 'tools_with_json_schema', naming the model and pointing at this option. The
default fallback policy can then try the next target.
outputConfig.textFormat's shape is not flat, and not the parsed-object schema every other part
of this adapter builds. The real Bedrock Converse API nests the schema one level deeper, under
structure.jsonSchema, and requires schema there as a JSON-encoded string:
{
"outputConfig": {
"textFormat": {
"type": "json_schema",
"structure": {
"jsonSchema": {
"schema": "{\"type\":\"object\",\"properties\":{...}}",
"name": "WeatherReport",
"description": "optional"
}
}
}
}
}name and description are accepted here, unlike Anthropic's native mechanism, but there is no
strict field, unlike Bedrock's own tool-based schema (toolSpec.inputSchema.json, used on the
legacy path), which takes a parsed object and does have strict.
fromBedrock's second argument also accepts reasoningEffortTokens, an override for the token
counts reasoningEffort tiers map onto for Claude models on Bedrock. See the
Call Params reference for the full table and
how it works across every adapter.
Streaming
fromBedrock implements createStream on top of ConverseStreamCommand. contentBlockDelta
events map onto text-delta chunks, and toolUse deltas map onto tool_call_delta chunks, no
extra setup is needed beyond the client already passed to fromBedrock:
const { chunks, finalResult } = await llm.call({
userContent: 'Write a short poem about the ocean.',
stream: true,
});
for await (const chunk of chunks) {
if (chunk.type === 'text-delta') process.stdout.write(chunk.delta);
}Just like the non-streaming path, when jsonSchema forces a single tool call, fromBedrock
streams the tool's JSON payload as text-delta chunks and suppresses genuine preamble text.
A mid-stream AWS exception event, throttlingException, validationException,
internalServerException, serviceUnavailableException, or modelStreamErrorException, is
surfaced as a correctly-typed LLMError (with a real HTTP status where AWS provides one) rather
than being silently dropped. See Error Handling for the LLMError
shape.
See Streaming for the full stream: true contract.
Tool calling
On Claude models, Bedrock doesn't support tool calling and thinking at the same time when
toolChoice forces tool use, the same restriction as the underlying Claude API itself (see
Anthropic → Tool calling). Setting
budgetTokens/reasoningEffort alongside a forced toolChoice (a specific tool, or
'required') throws LLMError('invalid_params') locally before the request is sent, Bedrock
rejects the combination outright otherwise. toolChoice: 'auto' (the default when tools is set)
works fine alongside thinking. Pass budgetTokens: null/reasoningEffort: null to opt one call
out of an instance-level reasoning default when it needs a forced choice. This only applies to
Claude models, non-Claude models on Bedrock don't support budgetTokens/reasoningEffort at all
(see the reasoning note above). See Call Params →
Reasoning for the full detail.
tools and tool_calls map onto Converse's native toolUse / toolResult content blocks, the
same mechanism used internally for jsonSchema. Consecutive tool result messages from a
multi-tool turn are merged back into a single user turn, matching what Converse expects. Converse
has no toolChoice equivalent to 'none' while tools are still offered, so fromBedrock throws
LLMError('validation') for that case rather than silently falling back to 'auto'. See Tool Calling for the full reference.
Multimodal input
fromBedrock supports VernLLM's provider-independent ContentBlock[] format for image input.
Image blocks are translated into Bedrock Converse's native image content blocks automatically. The
selected Bedrock model must support image input for multimodal requests to succeed.
const result = await llm.call({
userContent: [
{
type: 'text',
text: 'Describe this image.',
},
{
type: 'image',
mimeType: 'image/png',
data: imageBase64,
},
],
});Image data must be base64-encoded bytes without a data: URL prefix. Supported image types are
image/png, image/jpeg, image/gif, and image/webp.
Plain string userContent remains unchanged for text-only requests.