Amazon Bedrock Converse API in boto3: Converse vs InvokeModel, Images and PDFs, Tool Use, JSON Output, Streaming and Every Error

Logeshwaran
—

The Amazon Bedrock Converse API is one request format for every chat model on Bedrock: in boto3 you call boto3.client("bedrock-runtime").converse(modelId=..., messages=..., system=..., inferenceConfig=...) and read the reply from response["output"]["message"]["content"], whether the model comes from Anthropic, Amazon (Nova), Meta (Llama), Mistral, OpenAI or Z.ai (GLM). invoke_model() is the older, lower-level call: you send each provider's own JSON body and parse its own response, which means different code per model family. Use Converse for chat, images, PDFs and other documents, tool use, structured JSON output, guardrails and prompt caching; use InvokeModel for embeddings and image generation, and for provider-specific features Converse does not expose. Both cost the same per token and need the same bedrock:InvokeModel permission; their streaming versions are converse_stream() and invoke_model_with_response_stream(). Below: complete boto3 code for each, images and PDFs, tool calls, JSON schemas, streaming, listing models, Bedrock API keys, and every error that stops a first call.

Jake's prototype called an Anthropic model through invoke_model, with an Anthropic-shaped JSON body and an Anthropic-shaped response parser. When the team wanted to compare it with Amazon Nova, he found the body was different, the response was different, and the image format was different too. Ethan rewrote the call with Converse in twelve lines, and switching models became a one-word change to modelId.

⚡ Quick Answer

• First call: bedrock-runtime client, converse(), one model ID. Code.

• Converse or InvokeModel? Converse for chat; InvokeModel for embeddings and images. Decide.

• Images and PDFs: image and document content blocks. Format.

• Error on the first try? Usually the model ID or the body shape. Fixes.

Comparison · InvokeModel code · Tools · Structured output · Streaming · List models · API keys.

New to Bedrock? The cards below cover what Bedrock is, its endpoints, model IDs, streaming and boto3 errors.

🧭 NEW HERE? READ THESE FIRST

New to Amazon Bedrock or boto3? These five pages give you the background this one builds on:

📌 Bookmark this; the comparison table and the code blocks are the parts you will come back for.

What is the Converse API, and how is it different from InvokeModel?

What is the Converse API? It is Bedrock's model-neutral way to have a conversation: a list of messages, each with a role (user or assistant) and a list of content blocks (text, image, document, video, tool use, tool result). Bedrock translates that into whatever the chosen model expects. InvokeModel, by contrast, passes your JSON straight to the model in that provider's own format. Side by side:

ConverseInvokeModel
Request formatOne format for every chat modelEach provider's own JSON body
Response formatoutput.message.content, stopReason, usage for all modelsA streaming body you read and parse per provider
Switch modelsChange modelIdRewrite the body and the parser
Embeddings, image generationNoYes
Tools, documents, structured output, guardrails, cachingBuilt in, same shape for all models that support themOnly in each provider's own syntax
Provider-only optionsThrough additionalModelRequestFieldsDirectly in the body
Streaming callconverse_stream()invoke_model_with_response_stream()
Price and permissionSame token prices; both need bedrock:InvokeModel (streaming also bedrock:InvokeModelWithResponseStream)

Both live on the bedrock-runtime client. The Converse API is only on that endpoint; Bedrock's newer OpenAI-style endpoint is a different thing, covered in our bedrock-mantle vs bedrock-runtime guide.

Jake: "So Converse isn't a different model or a different price. It's just a nicer envelope?"

Ethan: "Same models, same bill. One envelope that every model on Bedrock can open."

Your first Converse call in boto3

import boto3

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")

response = bedrock.converse(
    modelId="us.amazon.nova-pro-v1:0",          # or "us.zai.glm-5.3", "us.openai.gpt-6.1-sol", etc.
    system=[{"text": "You are a friendly support agent for an online shop. Keep answers short."}],
    messages=[{"role": "user", "content": [{"text": "Can I return a jacket I bought in a sale?"}]}],
    inferenceConfig={"maxTokens": 400, "temperature": 0.3},
)

reply = "".join(part.get("text", "") for part in response["output"]["message"]["content"])
print(reply)
print(response["stopReason"], response["usage"], response["metrics"]["latencyMs"], "ms")

What each piece is for:

  • modelId: a model ID or, for most current models, an inference profile ID with a us., eu., apac. or global. prefix. Our Bedrock model ID list has them.
  • system: instructions and persona, kept apart from the conversation.
  • messages: the conversation so far, alternating user and assistant, each with a content list.
  • inferenceConfig: the settings every model understands: maxTokens, temperature, topP, stopSequences.
  • The response: the reply's content blocks (join the text parts, as above), stopReason (end_turn, max_tokens, tool_use and others), token usage for your cost tracking, and latency.

The stopReason tells you what to do next, so check it on every call:

stopReasonWhat happenedWhat your code should do
end_turnThe model finished its answerShow the reply
max_tokensCut off at your maxTokensRaise maxTokens, or ask it to continue
tool_useThe model wants to call one of your toolsRun the tool, send a toolResult, call again
stop_sequenceIt produced one of your stopSequencesExpected; use the text before it
guardrail_intervened, content_filteredA guardrail or content filter blocked or changed the outputShow a safe fallback message; log it
model_context_window_exceededHistory plus documents are too long for the modelTrim or summarize the history
malformed_model_output, malformed_tool_useThe model produced output Bedrock could not parseRetry once; simplify the tool schema if it repeats

A multi-turn chat is just a growing list. Bedrock keeps no memory between calls, so you send the history each time:

history = [{"role": "user", "content": [{"text": "Can I return a jacket I bought in a sale?"}]}]
first = bedrock.converse(modelId="us.amazon.nova-pro-v1:0", messages=history)
history.append(first["output"]["message"])                       # the assistant's turn
history.append({"role": "user", "content": [{"text": "And if I bought it online in Australia?"}]})
second = bedrock.converse(modelId="us.amazon.nova-pro-v1:0", messages=history)

Every turn re-sends the whole history as input tokens, so long chats get more expensive per message. Trim old turns, summarize them, or use prompt caching (below) when the history is long.

invoke_model in boto3: three common bodies

With InvokeModel you write the provider's format yourself and get raw JSON back. Every provider has its own: Anthropic's models take the Anthropic Messages body with an anthropic_version field and return a content list; Amazon Nova takes a schemaVersion messages body; embeddings models take an input string. Two worked examples:

1. Amazon Nova (its own messages format, close to Converse):

import json, boto3

bedrock = boto3.client("bedrock-runtime", region_name="us-east-1")
to_json = json.JSONEncoder().encode

response = bedrock.invoke_model(
    modelId="us.amazon.nova-pro-v1:0",
    contentType="application/json", accept="application/json",
    body=to_json({
        "schemaVersion": "messages-v1",
        "system": [{"text": "You are a friendly support agent for an online shop."}],
        "messages": [{"role": "user", "content": [{"text": "Can I return a jacket I bought in a sale?"}]}],
        "inferenceConfig": {"maxTokens": 400},
    }),
)
result = json.loads(response["body"].read())
print("".join(part.get("text", "") for part in result["output"]["message"]["content"]))

2. Embeddings (Amazon Titan Text Embeddings V2), which only InvokeModel can do:

response = bedrock.invoke_model(
    modelId="amazon.titan-embed-text-v2:0",
    body=to_json({"inputText": "Sale items can be returned within 30 days.", "dimensions": 1024, "normalize": True}),
)
vector = json.loads(response["body"].read())["embedding"]
print(len(vector))   # 1024

Two things trip people up here. The body must be a JSON string or bytes, not a Python dict. And response["body"] is a stream that you read once; read it into a variable, then parse it. Our Titan embeddings guide covers dimensions and cost.

Converse or InvokeModel: which should you use?

  1. Is it an embeddings or image-generation model? InvokeModel; Converse does not cover them.
  2. Is it a chat or text model? Start with Converse. You get one format for every model, and comparing models becomes a config change.
  3. Need a provider-only option (for example a sampling setting outside inferenceConfig)? Stay on Converse and pass it in additionalModelRequestFields; ask for extra response fields with additionalModelResponseFieldPaths.
  4. Is a tool or library already built on one provider's native format (some coding agents use Anthropic's format through InvokeModel)? Keep InvokeModel for that tool.
  5. Porting code from a provider's own SDK? InvokeModel lets you paste the same JSON body; plan to move to Converse once it works.

Jake: "So my Anthropic-shaped code wasn't wrong. It was just locked to one provider."

Ethan: "Right. Converse is what lets the cheaper model win the comparison without a rewrite."

Images, PDFs and video in Converse

Files go in as content blocks next to your text. In boto3 you pass raw bytes; the SDK handles the base64 encoding:

with open("receipt.png", "rb") as f:
    image_bytes = f.read()
with open("returns-policy.pdf", "rb") as f:
    pdf_bytes = f.read()

response = bedrock.converse(
    modelId="us.amazon.nova-pro-v1:0",
    messages=[{"role": "user", "content": [
        {"image": {"format": "png", "source": {"bytes": image_bytes}}},
        {"document": {"format": "pdf", "name": "Returns policy", "source": {"bytes": pdf_bytes}}},
        {"text": "Is this purchase still inside the return window in the policy?"},
    ]}],
)

The rules for this format, which explain most ValidationException errors with files:

  • Images: format is png, jpeg, gif or webp. Each image can be up to 3.75 MB and 8,000 pixels on a side, with up to 20 images in a request; individual models may accept less, so resize large photos before sending. Pass raw bytes in boto3 (the SDK base64-encodes them for you); sending a base64 string instead of bytes is a common cause of "invalid image" errors. Models that support it, such as Amazon Nova, also accept "source": {"s3Location": {"uri": "s3://bucket/key"}} so large files never pass through your code.
  • Documents: format is required (pdf, csv, doc, docx, xls, xlsx, html, txt, md); Bedrock does not guess it from the file name, and leaving it out fails with "Document format is required."
  • Document names: letters, numbers, single spaces, hyphens, parentheses and square brackets only, up to 200 characters. "returns_policy_v2.pdf" fails (underscores and a dot); "Returns policy v2" works. Use a neutral name, since the model reads it.
  • Include a text block with your question in the same message as a document.
  • Video (models such as Amazon Nova): a video block with bytes, or an S3 location for larger files.

Amazon Bedrock does not store the text, images or documents you send; they are used only to produce the response. For questions over hundreds of documents, a knowledge base scales better than attaching files to every call; see our Bedrock knowledge base guide.

Tool use (function calling) with Converse

Tool use lets the model ask your code to do something (look up an order, call an API) and then answer with the result. The loop has three steps:

  1. Describe your tools in toolConfig with a name, a description and a JSON schema for the input.
  2. If stopReason is tool_use, find the toolUse block, run your function with its input.
  3. Send the result back as a toolResult block in a user message, with the same toolUseId, and call Converse again for the final answer.
tools = {"tools": [{"toolSpec": {
    "name": "get_order_status",
    "description": "Look up the delivery status of an order by its ID.",
    "inputSchema": {"json": {"type": "object",
                             "properties": {"order_id": {"type": "string"}},
                             "required": ["order_id"]}},
}}]}

messages = [{"role": "user", "content": [{"text": "Where is order A-1042?"}]}]
response = bedrock.converse(modelId="us.amazon.nova-pro-v1:0", messages=messages, toolConfig=tools)

if response["stopReason"] == "tool_use":
    assistant = response["output"]["message"]
    messages.append(assistant)
    for block in assistant["content"]:
        if "toolUse" in block:
            call = block["toolUse"]
            status = {"order_id": call["input"]["order_id"], "status": "out for delivery"}   # your real lookup here
            messages.append({"role": "user", "content": [{"toolResult": {
                "toolUseId": call["toolUseId"], "content": [{"json": status}], "status": "success"}}]})
    response = bedrock.converse(modelId="us.amazon.nova-pro-v1:0", messages=messages, toolConfig=tools)

print("".join(part.get("text", "") for part in response["output"]["message"]["content"]))

Pass toolConfig on every call in the loop, including the one with the tool result. toolChoice can force a specific tool or any tool when you need the model to use one. Real agents run this loop until stopReason is end_turn.

Structured output: make the model return valid JSON

When your code needs to parse the reply, "please answer in JSON" in the prompt works most of the time, which is exactly the problem: the one reply in fifty that adds a friendly sentence before the brace breaks your parser. Ask instead for JSON that matches a schema with outputConfig. The schema goes in as a string:

schema = json.JSONEncoder().encode({
    "type": "object",
    "properties": {"refund_allowed": {"type": "boolean"}, "reason": {"type": "string"}},
    "required": ["refund_allowed", "reason"],
})
response = bedrock.converse(
    modelId=model_id,                      # a model that supports structured output
    messages=[{"role": "user", "content": [{"text": "Sale item, bought 40 days ago, unworn. Refund?"}]}],
    outputConfig={"textFormat": {"type": "json_schema",
                                 "structure": {"jsonSchema": {"schema": schema, "name": "refund_decision"}}}},
)
decision = json.loads("".join(p.get("text", "") for p in response["output"]["message"]["content"]))

Support depends on the model. Since October 2026 it covers Anthropic's model families on Bedrock (on both bedrock-runtime and bedrock-mantle), alongside models such as GPT-6.1 Sol and GLM 5.3; with InvokeModel, Anthropic models take the same idea in their own body as output_config.format. If your model does not accept outputConfig, a forced tool call is the classic alternative, since tool inputs already follow a schema:

  • Define one tool, for example record_decision, whose inputSchema is your output schema.
  • Set toolConfig={"tools": [...], "toolChoice": {"tool": {"name": "record_decision"}}} so the model must call it.
  • Read the toolUse block's input: it is already a Python dict, no parsing needed.

Either way, still validate the values in your code (a schema guarantees the shape, not that "refund_allowed" is the right decision), and check stopReason: a reply cut off at max_tokens is not valid JSON however good the schema is.

Streaming with converse_stream

For chat windows, stream the answer as it is written. converse_stream() takes the same arguments and returns events:

response = bedrock.converse_stream(
    modelId="us.amazon.nova-pro-v1:0",
    messages=[{"role": "user", "content": [{"text": "Write a two-line thank-you note for a customer."}]}],
)
for event in response["stream"]:
    if "contentBlockDelta" in event:
        print(event["contentBlockDelta"]["delta"].get("text", ""), end="", flush=True)
    elif "messageStop" in event:
        print("\n[stop]", event["messageStop"]["stopReason"])
    elif "metadata" in event:
        print("[usage]", event["metadata"]["usage"])

The events arrive in order: messageStart (the role), then for each content block a contentBlockStart, a run of contentBlockDelta events with text (or, for tool calls, pieces of the tool input as a JSON string you join and parse at the end), and a contentBlockStop; then messageStop with the stop reason, and finally metadata with token usage and latency. Errors during the stream arrive as events too (for example a throttling event), so handle them in the same loop rather than only around the call.

Streaming does not make the full answer faster, but the first words appear in a fraction of the time, which is what makes a chat window feel responsive. It also avoids client read timeouts on long answers. Our Bedrock streaming guide covers web frameworks, timeouts and streaming tool calls in more depth.

Other Converse options worth knowing

  • Prompt caching: add a {"cachePoint": {"type": "default"}} block after a long system prompt or document, and repeated calls read it from cache at a lower price; the usage then shows cacheReadInputTokens. Our prompt caching guide has the details per model.
  • Guardrails: guardrailConfig with a guardrail ID and version; stopReason becomes guardrail_intervened when it blocks.
  • additionalModelRequestFields: provider-only parameters, such as {"top_k": 200} for models that support it.
  • performanceConfig: {"latency": "optimized"} on models that offer latency-optimized inference.
  • serviceTier: picks a processing tier on models that offer more than one.
  • requestMetadata: up to 16 key-value tags (for example customer or feature) that you can filter on in model invocation logs, which makes per-feature cost reports possible.

Multi-turn chat: managing the conversation history

Converse is stateless: Bedrock does not remember earlier turns, so your code sends the whole conversation every time. The pattern is to append each assistant reply to the list before the next user message:

import boto3

client = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL = "amazon.nova-lite-v1:0"
system = [{"text": "You are a helpful support agent for an online shop."}]
messages = []

def ask(text):
    messages.append({"role": "user", "content": [{"text": text}]})
    resp = client.converse(modelId=MODEL, system=system, messages=messages,
                           inferenceConfig={"maxTokens": 512})
    reply = resp["output"]["message"]
    messages.append(reply)                      # keep the assistant turn in the history
    return "".join(block.get("text", "") for block in reply["content"])

print(ask("Do you ship to Canada?"))
print(ask("How long does it take?"))           # the model sees the first question too

Three rules keep long chats working. Messages must alternate user and assistant, starting with a user message; two user messages in a row fail validation, which is the cause of many "ValidationException" errors in chat apps. History grows in cost: every earlier turn is billed as input again on each call, so trim old turns or replace them with a short summary once a conversation gets long. And cache the stable part: a long system prompt followed by a cachePoint block is read at the cached price on every turn instead of the full price.

Jake: "The bot forgot the customer's order number after three messages."

Ethan: "Because only the last message was sent. Converse remembers nothing; the list of messages is the memory."

Prompt caching with Converse: a worked example

When every request starts with the same long instructions or document, mark the end of that stable part with a cache point. Bedrock stores it after the first call and later calls read it at a discounted rate and with lower latency:

policy = open("returns_policy.txt").read()          # a few thousand tokens of rules
system = [
    {"text": "You answer customer questions using only this policy:\n" + policy},
    {"cachePoint": {"type": "default"}},             # everything above is cached
]
resp = client.converse(modelId=MODEL, system=system,
                       messages=[{"role": "user", "content": [{"text": "Can I return sale items?"}]}])
u = resp["usage"]
print(u.get("cacheWriteInputTokens", 0), "written,", u.get("cacheReadInputTokens", 0), "read from cache")

The first call shows tokens written to the cache; calls within the cache lifetime show tokens read from it. Caching needs a minimum prompt length that varies by model, the cached part must be byte-for-byte identical (put anything that changes, such as the date or the user's name, after the cache point), and support varies by model, so check that the read counter actually rises in your tests. Our prompt caching guide lists the models and prices.

Guardrails with Converse

A Bedrock guardrail filters harmful content, blocks denied topics and masks personal data in both the prompt and the reply. Attach it per request:

resp = client.converse(
    modelId=MODEL,
    messages=[{"role": "user", "content": [
        {"text": "Shop policy context that should not be checked."},
        {"guardContent": {"text": {"text": "The customer's actual message goes here."}}},
    ]}],
    guardrailConfig={"guardrailIdentifier": "gr-abc123example", "guardrailVersion": "1", "trace": "enabled"},
)
print(resp["stopReason"])        # guardrail_intervened when it blocked something

The guardContent block tells the guardrail to evaluate only that part of the message, so your own trusted instructions and retrieved documents are not flagged or masked by mistake. Create the guardrail once in the Bedrock console or with the API, publish a numbered version, and reference that version in code so later edits to the draft do not change production behavior without a deploy. With trace enabled, the response explains which policy fired, which is useful while you tune the guardrail and worth turning off in production to keep responses small.

Testing Converse code without calling Bedrock

Unit tests should not spend tokens or need network access. botocore's Stubber replaces the API call with a canned response, so you can test how your code handles normal replies, tool calls and errors:

from botocore.stub import Stubber

client = boto3.client("bedrock-runtime", region_name="us-east-1")
fake = {"output": {"message": {"role": "assistant", "content": [{"text": "Yes, we ship to Canada."}]}},
        "stopReason": "end_turn",
        "usage": {"inputTokens": 12, "outputTokens": 7, "totalTokens": 19},
        "metrics": {"latencyMs": 100}}
with Stubber(client) as stub:
    stub.add_response("converse", fake)
    resp = client.converse(modelId=MODEL, messages=[{"role": "user", "content": [{"text": "Canada?"}]}])
    assert "Canada" in resp["output"]["message"]["content"][-1]["text"]

Use stub.add_client_error("converse", "ThrottlingException") to check your retry and fallback paths. Keep a small set of real integration tests too, run on demand, because a stub cannot tell you whether a model ID or a prompt still works. The stubber also checks that your request parameters are valid for the API, so a typo such as inferenceConfig spelled wrongly fails in the test rather than in production.

Jake: "Our tests were calling Bedrock and costing a few dollars a day."

Ethan: "Stub the unit tests, and keep one real smoke test that runs before each deploy."

Running many Converse calls in parallel

Batch jobs (summarizing a thousand tickets, tagging a product catalog) finish far faster with several requests in flight. boto3 clients are thread-safe, so a thread pool is the simplest approach:

from concurrent.futures import ThreadPoolExecutor

def summarize(ticket):
    resp = client.converse(modelId=MODEL, messages=[{"role": "user", "content": [
        {"text": "Summarize this support ticket in one sentence:\n" + ticket}]}],
        inferenceConfig={"maxTokens": 80})
    return "".join(b.get("text", "") for b in resp["output"]["message"]["content"])

with ThreadPoolExecutor(max_workers=8) as pool:
    summaries = list(pool.map(summarize, tickets))

Each worker waits on the network most of the time, so threads work well here even in Python. Start with a small number of workers and raise it while watching for throttling; the adaptive retry configuration above makes the pool slow down on its own when it hits the quota. For very large jobs that can wait, Bedrock's batch inference processes files of requests at a lower price instead of real-time calls, which is cheaper than any amount of parallel Converse traffic.

Choosing a model for your Converse calls

Because Converse uses the same code for every model, switching models is a one-line change, which makes testing several cheap. A sensible way to choose:

  • Start small: a lightweight model such as Nova Lite or Nova Micro handles classification, extraction and short answers at a fraction of a cent per call.
  • Check features: not every model supports tools, images, documents or system prompts through Converse; the Bedrock documentation's supported-features table lists them per model, and a ValidationException often means a feature the model lacks.
  • Compare on your own prompts: run the same 50 real requests through two or three models, score the answers, and record tokens and latency from the response.
  • Move up only where needed: route the hard cases to a larger model and keep the bulk on the cheap one.

Model access also matters: some models require enabling access in the Bedrock console or accepting a Marketplace offer before the first call, and use a cross-Region inference profile ID rather than the base model ID.

Tracking the cost of every Converse call

Every Converse response includes usage with inputTokens, outputTokens and, when caching is used, cacheReadInputTokens and cacheWriteInputTokens. The Converse API itself costs nothing extra; you pay the model's per-token prices. A small helper turns usage into money, so you can log the cost per request or per customer:

PRICES = {  # USD per 1M tokens: input, output (take them from the Bedrock pricing page for your Region)
    "amazon.nova-lite-v1:0": (0.06, 0.24),
}

def call_cost(model_id, usage):
    price_in, price_out = PRICES[model_id]
    return (usage["inputTokens"] * price_in + usage["outputTokens"] * price_out) / 1_000_000

resp = client.converse(modelId=MODEL, messages=[{"role": "user", "content": [{"text": "Hi"}]}])
print(f"${call_cost(MODEL, resp['usage']):.6f}")

A worked example shows why per-call tracking is worth the ten lines. A support bot that sends a 2,000-token system prompt, 1,000 tokens of history and a short question on every turn uses about 3,100 input tokens per call; on Nova Lite at $0.06 per million input tokens that is about $0.0002 a call, and output adds a little more. Ten thousand conversations of five turns each come to roughly $10 to $15 a month. Cache the system prompt and most of that input is billed at the cached rate instead, which matters more on larger models where the same prompt costs many times as much. Logging the cost per conversation also shows you which customers or features drive spend, before the monthly bill does.

Keep the price table in configuration rather than code, since prices change and differ by Region. For a whole-account view, Cost Explorer filtered to Amazon Bedrock, or application inference profiles with cost allocation tags, show spend per app without any code.

Throttling and retries for Converse

A busy app will meet ThrottlingException sooner or later. boto3 can retry for you with backoff; turn on the adaptive mode, which also slows the client down when it sees throttling:

from botocore.config import Config

client = boto3.client("bedrock-runtime", region_name="us-east-1",
                      config=Config(retries={"max_attempts": 8, "mode": "adaptive"},
                                    read_timeout=120))

The standard retry mode retries with exponential backoff and jitter; adaptive adds client-side rate limiting, so a burst of requests from one process backs off as a group instead of hammering the endpoint. Streaming calls need one more step: an error can arrive in the middle of a stream as an event (a throttling or service exception), after some text has already been shown. Catch those inside your event loop, and decide whether to retry the whole request or show what arrived with a short note.

If throttling continues after retries, the account's tokens-per-minute quota for that model is the limit: use a cross-Region inference profile (model IDs starting with us., eu. or global.) to spread requests over several Regions, request a quota increase in Service Quotas, or smooth traffic through a queue. Raise read_timeout for long outputs, since a long non-streaming response can take longer than the default timeout to arrive.

List models and inference profiles with boto3

Listing is on a different client, plain bedrock, which is the control plane. Calling it on bedrock-runtime fails with an AttributeError, and the reverse mistake (calling converse on bedrock) fails the same way.

control = boto3.client("bedrock", region_name="us-east-1")

for m in control.list_foundation_models(byOutputModality="TEXT")["modelSummaries"]:
    print(m["modelId"], m.get("inferenceTypesSupported"))

for p in control.list_inference_profiles()["inferenceProfileSummaries"]:
    print(p["inferenceProfileId"])

If a model's inferenceTypesSupported does not include ON_DEMAND, call it through one of the inference profiles instead of its bare model ID. Other useful filters and calls:

  • list_foundation_models(byProvider="Amazon") or byInferenceType="ON_DEMAND" to narrow the list.
  • get_foundation_model(modelIdentifier="amazon.nova-pro-v1:0") for one model's input and output modalities and whether it streams.
  • From the CLI: aws bedrock list-foundation-models --region us-east-1 --query "modelSummaries[].modelId" --output text.

The lists are per Region, so a model that is missing in eu-west-2 may simply not be offered there; check another Region or an eu. or global. profile that routes to one where it is.

The Converse API in JavaScript (AWS SDK v3)

Node.js apps use the same request shape through @aws-sdk/client-bedrock-runtime. A complete call and a streaming call:

// npm install @aws-sdk/client-bedrock-runtime
import { BedrockRuntimeClient, ConverseCommand, ConverseStreamCommand } from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({ region: "us-east-1" });
const input = {
  modelId: "amazon.nova-lite-v1:0",
  system: [{ text: "You are a concise assistant." }],
  messages: [{ role: "user", content: [{ text: "Give me three names for a coffee shop." }] }],
  inferenceConfig: { maxTokens: 300, temperature: 0.7 },
};

const resp = await client.send(new ConverseCommand(input));
console.log(resp.output.message.content.map((b) => b.text ?? "").join(""));

const stream = await client.send(new ConverseStreamCommand(input));
for await (const event of stream.stream) {
  const text = event.contentBlockDelta?.delta?.text;
  if (text) process.stdout.write(text);
}

Error handling in JavaScript uses the exception's name: ValidationException for a bad request (wrong model ID, messages not alternating, a missing document format), AccessDeniedException for IAM or model access problems, and ThrottlingException for quota limits. The client retries throttling a few times by default; raise maxAttempts in the client options for busy apps. In AWS Lambda with Node.js, create the client outside the handler so it is reused across invocations, and keep the handler's timeout longer than your slowest expected reply.

The SDK reads credentials the same way boto3 does (environment, shared config, or the IAM role of the Lambda function or container). In a browser, call your own backend rather than Bedrock directly, so no AWS credentials end up in front-end code. For Next.js and similar frameworks, run the call in a server route and stream the text to the page.

The Converse API in Java and Spring AI

In Java, the AWS SDK v2 BedrockRuntimeClient has converse and converseStream methods with builder-style requests:

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
import software.amazon.awssdk.services.bedrockruntime.model.*;

BedrockRuntimeClient client = BedrockRuntimeClient.builder().region(Region.US_EAST_1).build();
Message msg = Message.builder().role(ConversationRole.USER)
        .content(ContentBlock.fromText("Summarize our return policy in one sentence.")).build();
ConverseResponse resp = client.converse(r -> r
        .modelId("amazon.nova-lite-v1:0")
        .messages(msg)
        .inferenceConfig(c -> c.maxTokens(300).temperature(0.5F)));
System.out.println(resp.output().message().content().get(0).text());

For many requests at once, the SDK also has BedrockRuntimeAsyncClient, which returns futures and supports converseStream with a response handler for streaming. Tool use follows the same shape as in Python: define tool specifications in a ToolConfiguration, watch for a tool_use stop reason, and send a ToolResultBlock back in the next user message.

Spring AI wraps the same API in its chat client abstraction: add Spring AI's Bedrock Converse starter to your project, set the AWS Region and the model in your application properties, and inject a ChatClient. Spring AI then handles messages, tools and streaming through its own interfaces, and switching models becomes a configuration change. Check the Spring AI reference for the current starter artifact and property names, since they changed between the milestone releases and 1.0.

Build a chat app with Streamlit and converse_stream

Streamlit is the quickest way to put a chat window in front of Bedrock. The app keeps the history in the session and streams each reply as it is generated:

# pip install streamlit boto3      run: streamlit run app.py
import boto3, streamlit as st

client = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL = "amazon.nova-lite-v1:0"
st.title("Shop assistant")
if "messages" not in st.session_state:
    st.session_state.messages = []

for m in st.session_state.messages:
    st.chat_message(m["role"]).write("".join(b.get("text", "") for b in m["content"]))

if prompt := st.chat_input("Ask a question"):
    st.session_state.messages.append({"role": "user", "content": [{"text": prompt}]})
    st.chat_message("user").write(prompt)

    def stream_reply():
        resp = client.converse_stream(modelId=MODEL, messages=st.session_state.messages)
        for event in resp["stream"]:
            if "contentBlockDelta" in event:
                yield event["contentBlockDelta"]["delta"].get("text", "")

    reply = st.chat_message("assistant").write_stream(stream_reply())
    st.session_state.messages.append({"role": "assistant", "content": [{"text": reply}]})

How it works: Streamlit reruns the script on every interaction, so the history lives in st.session_state, which survives reruns for each browser session. write_stream takes a generator, shows text as it arrives, and returns the full reply so you can append it to the history. To add a system prompt, pass system=[...] to converse_stream; to let users attach a file, add st.file_uploader and include its bytes as an image or document block in the next user message.

Run it locally with your AWS profile, or on an EC2 instance or container with an IAM role. Before sharing it, put it behind sign-in (an Application Load Balancer with Cognito, for example), because anyone who reaches the page spends your Bedrock budget.

Jake: "Thirty lines and the team has a chat window on our own Bedrock account."

Ethan: "Put a login in front of it before you send anyone the link."

Converse in LangChain: ChatBedrockConverse

LangChain's AWS package exposes the Converse API as ChatBedrockConverse, so chains and agents get tools, streaming and structured output with the same model IDs:

# pip install langchain-aws
from langchain_aws import ChatBedrockConverse

llm = ChatBedrockConverse(model="amazon.nova-lite-v1:0", region_name="us-east-1", temperature=0.3)
print(llm.invoke("Write a one-line product description for a canvas tote bag.").content)

The usual LangChain features work through it: llm.stream(...) yields chunks as Converse streams them, llm.bind_tools([...]) attaches tools defined as Python functions or Pydantic models and returns tool calls in a provider-neutral format, and llm.with_structured_output(MySchema) returns parsed objects instead of text. Because the class maps everything onto Converse, the same chain runs on Nova, Llama, Mistral or any other Converse model by changing only the model ID, which makes it easy to compare cost and quality across models on your own prompts.

Prefer it over the older ChatBedrock class for new code, since it uses Converse underneath and supports every Converse-capable model the same way. Our LangChain Bedrock guide covers chains, retrieval and agents.

Credentials, Bedrock API keys and IAM

boto3 finds credentials the usual way: environment variables, your profile, SSO, or the role of the EC2 instance, ECS task or Lambda function. Bedrock also has its own API keys: set the key in the AWS_BEARER_TOKEN_BEDROCK environment variable and recent boto3 versions use it for bedrock-runtime calls, no access keys needed. That is handy for quick tests; for production, an IAM role is still the safer choice, since a leaked long-term key works until someone revokes it.

Permissions are the same for both APIs: bedrock:InvokeModel on the model and on the inference profile (and the foundation models behind a cross-Region profile), plus bedrock:InvokeModelWithResponseStream for streaming. To block a model entirely, deny both actions; denying them also blocks Converse.

Errors on the first call, and fixes

  • ValidationException: The provided model identifier is invalid.: a typo in the model ID, or a model not offered in that Region. Copy the ID from list_foundation_models or our model ID list.
  • "Invocation of model ID ... with on-demand throughput isn't supported": the model needs an inference profile; use the us./eu./global. ID. Our on-demand throughput guide explains why.
  • ValidationException: Malformed input request (InvokeModel): the body does not match that provider's format, for example an Anthropic body sent to Nova, or a missing anthropic_version. Converse avoids this class of error.
  • "Document format is required.": add format to the document block. Also check the document name's allowed characters.
  • AccessDeniedException: missing bedrock:InvokeModel on the model or profile, a service control policy that blocks the Region, or a model that needs a one-time access step for your account.
  • ThrottlingException: you hit your tokens-per-minute or requests-per-minute quota; retry with backoff, spread load with a cross-Region profile, or request more. See our throttling guide.
  • ModelTimeoutException or a boto3 read timeout on long answers: stream the response, or raise read_timeout in a botocore Config.
  • AttributeError: 'Bedrock' object has no attribute 'converse': you created boto3.client("bedrock"); use "bedrock-runtime".
  • The reply stops mid-sentence: stopReason is max_tokens; raise maxTokens. If it is model_context_window_exceeded, your history or documents are too long for the model.

To catch these by name in Python, check error.response["Error"]["Code"]; our ClientError guide shows the pattern.

Jake: "Nova answered, GLM answered, and the only line I changed was modelId."

Ethan: "Now put both in a loop with the same ten questions. That's your model comparison, with real token counts from usage."

Frequently asked questions

What is the Amazon Bedrock Converse API?

A single, model-neutral request and response format for chat models on Bedrock, called with converse() or converse_stream() on the bedrock-runtime client.

What is the difference between Converse and InvokeModel?

Converse uses one format for every chat model; InvokeModel sends each provider's own JSON body. Prices and permissions are the same.

How do I call the Bedrock Converse API with boto3?

boto3.client("bedrock-runtime").converse(modelId=..., messages=[{"role": "user", "content": [{"text": "..."}]}]), then read output.message.content.

How do I call invoke_model in boto3?

Pass modelId and a JSON string body in the provider's format, then json.loads(response["body"].read()) to read the result.

Can the Converse API create embeddings?

No. Use invoke_model with an embeddings model such as amazon.titan-embed-text-v2:0.

How do I send an image with the Converse API?

Add a content block {"image": {"format": "png", "source": {"bytes": image_bytes}}} next to a text block in the user message.

How do I send a PDF to Bedrock with Converse?

Use a document block with format "pdf", a name using only letters, numbers, spaces, hyphens, parentheses or brackets, the bytes, and a text block with your question.

Does Converse support tool use?

Yes, with toolConfig; when stopReason is tool_use, run the tool and send a toolResult block back.

How do I get JSON output from Bedrock?

Use outputConfig with textFormat type json_schema and your schema as a string, on models that support it, or a forced tool call.

How do I stream a Bedrock response in Python?

Call converse_stream and print each contentBlockDelta event's delta text.

How do I add a system prompt in Converse?

Pass system=[{"text": "your instructions"}] alongside messages.

How do I use the Converse API in JavaScript?

Install @aws-sdk/client-bedrock-runtime and send a ConverseCommand (or ConverseStreamCommand) with modelId, messages and inferenceConfig.

Does Spring AI support the Bedrock Converse API?

Yes, through its Bedrock Converse starter; set the Region and model in application properties and use the ChatClient.

How do I keep conversation history with Converse?

Converse is stateless: append each assistant reply to your messages list and send the whole list on the next call, alternating user and assistant turns.

How much does the Converse API cost?

Nothing extra; you pay the model's per-token prices, which the usage field lets you calculate per call.

How do I build a Streamlit chatbot on Bedrock?

Keep messages in st.session_state, call converse_stream, and pass a generator of delta texts to st.chat_message().write_stream.

What is the difference between converse and converse_stream?

converse returns the whole reply at once; converse_stream returns events as text is generated, which is better for chat interfaces.

How do I fix ThrottlingException with Converse?

Use adaptive retries in botocore Config, a cross-Region inference profile, a quota increase, or a queue to smooth traffic.

How do I use prompt caching with Converse?

Add a {"cachePoint": {"type": "default"}} block after the stable part of the system prompt or messages; usage then shows cacheWriteInputTokens and cacheReadInputTokens.

How do I add a guardrail to a Converse call?

Pass guardrailConfig with the guardrail ID and version, and wrap the user text to check in a guardContent block.

How do I unit test code that calls Converse?

Use botocore's Stubber to return a canned converse response or error, and keep a few real integration tests for model and prompt checks.

Can I run Converse calls in parallel?

Yes. boto3 clients are thread-safe; use a thread pool with a modest number of workers and adaptive retries.

What is ChatBedrockConverse in LangChain?

The langchain-aws chat model class that calls the Bedrock Converse API, supporting tools, streaming and structured output.

Does Bedrock remember the conversation?

No. Send the full message history on every Converse call, appending the assistant's reply each turn.

How do I list Bedrock models with boto3?

boto3.client("bedrock").list_foundation_models(), and list_inference_profiles() for profile IDs.

How do I use a Bedrock API key with boto3?

Set the key in the AWS_BEARER_TOKEN_BEDROCK environment variable; recent boto3 versions use it for bedrock-runtime calls.

What permissions does the Converse API need?

bedrock:InvokeModel, plus bedrock:InvokeModelWithResponseStream for converse_stream.

Is the Converse API more expensive than InvokeModel?

No. You pay the same per-token price for the model either way.

What does "Malformed input request" mean in Bedrock?

The InvokeModel body does not match the model provider's format; check the schema or switch to Converse.

How do I pass model-specific parameters in Converse?

Put them in additionalModelRequestFields, for example {"top_k": 200}.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: AI Practitioner (AIF-C01) • Developer – Associate (DVA-C03 from December 1, 2026) • AI Business Strategist (AIB-C01).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. An application must switch between several foundation models on Amazon Bedrock with minimal code changes. Which API fits?

The Bedrock Converse API, which uses one request format for all chat models.

2. Which Bedrock API do you use to create text embeddings?

InvokeModel with an embeddings model such as Amazon Titan Text Embeddings V2.

3. A model must call an external function to look up data before answering. Which capability is this?

Tool use (function calling).

4. Which IAM action allows calls to the Converse API?

bedrock:InvokeModel.

5. What reduces cost and latency when the same long prompt prefix is sent repeatedly?

Prompt caching.

6. What does the temperature inference parameter control?

Randomness of the output: lower is more focused and repeatable, higher is more varied.

Exam codes and dates change often. Our AWS certification changes page and our guide to the AI Business Strategist exam keep track of what is current.

πŸ“š ALSO READ

Where to go next, from prompt caching to embeddings:

📌 Bookmark this; the comparison table and the code blocks are the parts you will come back for.

The shop's assistant now calls Converse with the model ID in a config file. The weekly comparison runs the same twenty customer questions through three models and records usage and latency for each, and the cheapest model that passes the team's checks gets the traffic. Jake's only InvokeModel call left is the one that creates embeddings for the product search.

📌 If you keep one line from this page

Use boto3.client("bedrock-runtime").converse() for every chat model (one format, tools, files, JSON output, streaming with converse_stream), and invoke_model() only for embeddings, image generation and provider-only formats.

Inference profile IDs for current models · bedrock:InvokeModel covers both · send history every turn.

Revision note. Written October 19, 2026. One envelope for every model is the quiet feature that makes comparing them cheap.

Related