VernLLMVernLLM
Adapters

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_object is not supported as a real constraint. Converse has no field that mechanically guarantees valid JSON output for this mode. Whether an unset json_object request throws or is silently downgraded depends on what else the call asks for. A plain call (llm.call({ userContent }), no schema, no explicit jsonMode) is silently treated as plain text, keeping the common case working unchanged. An explicit jsonMode: true throws LLMError('invalid_params'). schema without jsonSchema, whether or not jsonMode is set explicitly, also throws LLMError('invalid_params'), since schema needs JSON output to validate against and shouldn't be silently downgraded into skipping validation. Use jsonSchema instead in every one of those throwing cases.
  • json_schema uses 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

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(...)):

bedrock-client-setup.ts
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:

bedrock-basic-setup.ts
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.

bedrock-structured-output.ts
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:

bedrock-preflight.ts
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:

bedrock-native-structured-output.ts
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-shape.json
{
  "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:

bedrock-streaming.ts
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.

bedrock-multimodal-input.ts
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.

On this page