Skip to main content
Crashbytes logoCrashbytes
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Browse Articles
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Network
Theme
Browse Articles
Crashbytes logoCrashbytes

Expert insights on web development, technology trends, and programming best practices. Learn from real-world experiences and cutting-edge techniques that help you build better software.

Follow Us

Our Sites

  • 🔮 Predictions
  • 📰 Breaking News
  • 🎨 AI Art
  • 📖 Short Stories
  • View All →
  • Products →

Sitemap

  • Home
  • All Articles
  • Open Source
  • Services
  • About Us
  • Contact
  • Donate Compute

Popular Topics

  • Serverless
  • Cloud Architecture
  • DevOps
  • Kubernetes
  • Platform Engineering

Resources

  • Privacy Policy
  • Terms of Service
  • Sitemap
  • RSS Feed
  • PGP Key

Stay Updated

Get the latest articles, tutorials, and insights delivered to your inbox. Join our community of developers and never miss an update.

© 2021-2026 Crashbytes® by Blackhole Software, LLC. All rights reserved.
| Reg. U.S. Pat. & Tm. Off.

Made for the developer community

  1. Home
  2. /
  3. Articles
  4. /
  5. AWS Bedrock Getting Started with Python — Your First AI API Calls Using the Converse API
TutorialApril 13, 202627 min read• By Michael Eakins

AWS Bedrock Getting Started with Python — Your First AI API Calls Using the Converse API

Learn how to call foundation models like Claude, Llama, and Nova through AWS Bedrock using Python and boto3. Beginner-friendly tutorial covering single prompts, multi-turn conversations, and streaming responses.

AWS Bedrock Getting Started with Python — Your First AI API Calls Using the Converse API

Quick Takeaways

What you'll learn in this article

27 min read
Intermediate
  • 1

    modelId — The model to call. We use Claude Haiku 4.5, which is fast and cheap. The us. prefix indicates a cross-region inference profile — this is the format Bedrock now requires for on-demand model access. You can swap this for any supported model ID without changing anything else.

  • 2

    messages — The conversation history. For a single prompt, it is just one user message.

  • 3

    inferenceConfig — Controls how the model generates its response. maxTokens caps the response length (prevents surprise bills). temperature controls randomness — 0 means deterministic, 1 means creative.

  • 4

    output.message — The assistant's reply, in the same format as your input messages. You can append this directly to your conversation history.

  • 5

    stopReason — Why the model stopped. endturn means it finished naturally. maxtokens means it was cut off. tooluse means it wants to call a function (advanced topic).

Keep reading for detailed implementation, code examples, and real-world results

If you have ever wanted to add AI capabilities to your applications but felt overwhelmed by the idea of hosting models, managing GPUs, or dealing with multiple AI provider APIs, AWS Bedrock is the service that makes all of that disappear. One API call, dozens of foundation models, zero infrastructure to manage.

In this tutorial, you will build three working Python examples that call AI models through Bedrock — from a simple "ask a question" script to a multi-turn chatbot to a real-time streaming response. By the end, you will have working code you can extend into your own projects.

The complete code for this tutorial is available at github.com/CrashBytes/ByteSizedExamples/tree/main/aws-bedrock-getting-started.

What You Will Build

Here is what you will have by the end of this tutorial:

  1. A single-prompt script that sends a question to an AI model and prints the answer
  2. A multi-turn conversation that maintains context across three rounds of back-and-forth chat
  3. A streaming response that prints tokens in real time as the model generates them

All three examples use the Converse API, which is the current recommended way to interact with Bedrock models. It works the same way regardless of which model you choose — Claude, Llama, Nova, Mistral, or any other model available on Bedrock.

Foundation models available on Bedrock

40+

↑ 18%new models added in Dec 2025

Why Bedrock Matters

Before we write any code, let's understand why Bedrock exists and what problem it solves.

Traditionally, using an AI model in your application meant one of two things. You could self-host the model, which requires provisioning GPUs, managing model weights, handling scaling, and dealing with inference optimization. Or you could use a provider-specific API like the Anthropic API or the OpenAI API, which locks you into one vendor and requires separate SDK integrations for each.

Bedrock takes a third approach. It is a fully managed service where AWS handles all the infrastructure. You make API calls through the AWS SDK (boto3 for Python), and AWS handles everything else — model hosting, scaling, availability, and security.

Self-Hosted Models vs AWS Bedrock

Self-Hosted Models

InfrastructureYou manage GPUs
ScalingYou configure
Cost ModelPer hour (even idle)
Model SelectionOne at a time
Setup TimeHours to days

AWS Bedrock

InfrastructureFully managed
ScalingAutomatic
Cost ModelPer token (pay per use)
Model Selection40+ models, swap anytime
Setup TimeMinutes

The real power of Bedrock is model flexibility. You can switch from Claude to Llama to Nova by changing a single string — the model ID — in your code. The request and response format stays exactly the same thanks to the Converse API. This means you can benchmark different models, fall back to a cheaper model when costs spike, or upgrade to a more capable model without rewriting your application.

According to my prediction on AI inference cost trends, inference pricing is collapsing rapidly. Bedrock positions you to take advantage of this by letting you swap models as cheaper, better options become available.

What Is the Converse API?

Bedrock offers two ways to invoke models. The older approach is the InvokeModel API, which requires you to construct a different JSON payload for each model provider. Claude expects one format, Llama expects another, and Nova expects yet another. This is painful to maintain.

The Converse API is the newer, recommended approach. It provides a single unified request and response format that works across every model on Bedrock. You send messages in a standard chat format, and you get responses back in a standard format, regardless of which model processes them.

InvokeModel (Legacy) vs Converse API (Recommended)

InvokeModel (Legacy)

Payload FormatDifferent per model
Response FormatDifferent per model
Multi-turnManual construction
Token TrackingModel-specific
StreamingSeparate method

Converse API (Recommended)

Payload FormatUniversal
Response FormatUniversal
Multi-turnBuilt-in messages array
Token TrackingStandardized usage object
Streamingconverse_stream()

AWS explicitly recommends the Converse API over InvokeModel. From their documentation: "We recommend that you use the Converse operation over the InvokeModel operation when supported, because it unifies the inference request across Amazon Bedrock models and simplifies the management of multi-turn conversations."

Throughout this tutorial, we will use the Converse API exclusively.

Advertisement

Prerequisites

Before we start coding, you need four things set up on your machine. If you already have an AWS account and Python installed, you can skip ahead to the Environment Setup section.

1. Python 3.10 or Higher

Check if Python is installed by opening your terminal and running:

python3 --version

If you see Python 3.10 or higher, you are good to go. If not, download Python from python.org/downloads and follow the installer for your operating system.

2. An AWS Account

If you do not have an AWS account, create one at aws.amazon.com/free. The free tier includes enough to follow this tutorial. You will need a credit card on file, but the cost of running these examples is pennies (we are using the cheapest model).

3. AWS CLI Installed

The AWS Command Line Interface lets you configure your credentials. Install it by following the guide for your OS at docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html.

Verify it is installed:

aws --version

4. IAM User with Bedrock Permissions

You need AWS credentials (Access Key ID and Secret Access Key) from an IAM user that has permission to call Bedrock. The simplest approach for learning:

  1. Go to the IAM Console in AWS
  2. Create a new IAM user (or use an existing one)
  3. Attach the managed policy AmazonBedrockFullAccess
  4. Create an access key under the "Security credentials" tab

For production applications, you would use a more restrictive policy. But for learning, the full access policy keeps things simple.

Pie chart data
NameValue
IAM Setup5
Model Access2
Python + boto33
Writing Code15
Testing5

Environment Setup

Let's get everything configured step by step.

Step 1: Configure AWS Credentials

Open your terminal and run:

aws configure

It will ask for four pieces of information:

AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name: us-east-1
Default output format: json

Use us-east-1 as your region. It has the broadest selection of Bedrock models. The output format does not matter for our purposes — json is fine.

Step 2: Enable Model Access in Bedrock

As of September 2025, AWS automatically enables most foundation models for new accounts. However, Anthropic models (Claude) require a one-time use case submission.

  1. Go to the AWS Console and search for "Bedrock"
  2. Click Model access in the left sidebar
  3. If you see models with a "Request access" button, click it
  4. For Anthropic models, fill in the brief use case form (it is approved instantly)

This is a one-time step per AWS account. Once enabled, the models stay available permanently.

Step 3: Clone the Project

git clone https://github.com/CrashBytes/ByteSizedExamples.git
cd ByteSizedExamples/aws-bedrock-getting-started

Step 4: Install Dependencies

pip install -r requirements.txt

This installs boto3, the AWS SDK for Python. It is the only dependency we need.

That is it. Four steps and you are ready to call AI models through Bedrock.

Core Concepts Before We Code

Before diving into the examples, let's understand three concepts that will make the code much clearer.

The Two Bedrock Clients

boto3 has two separate clients for Bedrock, and mixing them up is the most common beginner mistake:

# WRONG — This is for management tasks (listing models, etc.)
bedrock_management = boto3.client("bedrock", region_name="us-east-1")

# RIGHT — This is for invoking models (sending prompts)
bedrock_runtime = boto3.client("bedrock-runtime", region_name="us-east-1")

You almost always want bedrock-runtime. The bedrock client (without -runtime) is for administrative operations like listing available models or checking model access status. If you try to call converse() on the wrong client, you will get a confusing "operation not found" error.

Model IDs

Every model on Bedrock has a unique identifier string. Here are some common ones:

Bar chart data
modelcost
Claude Haiku 4.50.25
Nova Lite0.06
Nova Micro0.04
Llama 3.1 8B0.22
Claude Sonnet 4.63
Mistral Large 32

The chart above shows approximate costs per million input tokens for popular Bedrock models. For this tutorial, we use Claude Haiku 4.5 — fast, capable, and affordable at $0.25 per million input tokens. At that price, you could send roughly 4 million tokens (about 3 million words) for one dollar.

The Messages Format

The Converse API uses a chat-style messages array. Each message has a role and content:

messages = [
    {
        "role": "user",        # Your message
        "content": [{"text": "Hello, how are you?"}]
    },
    {
        "role": "assistant",   # Model's reply
        "content": [{"text": "I'm doing well! How can I help?"}]
    },
    {
        "role": "user",        # Your follow-up
        "content": [{"text": "Tell me about Bedrock."}]
    }
]

Two rules to remember. First, the content field is always a list of content blocks, not a plain string. Even for simple text, you wrap it in [{"text": "..."}]. Second, roles must alternate between user and assistant. You cannot have two consecutive messages from the same role.

Example 1: Your First Bedrock Call

Let's start with the simplest possible interaction — send one question, get one answer.

Open main.py in the project. The single_prompt() function contains this example. Let's walk through every line.

Creating the Client

import boto3
from botocore.exceptions import ClientError

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

This creates a Bedrock Runtime client configured for the us-east-1 region. The region_name parameter is important — it must match a region where Bedrock is available. If you omit it, boto3 uses whatever region you set in aws configure.

Making the API Call

response = bedrock.converse(
    modelId="us.anthropic.claude-haiku-4-5-20251001-v1:0",
    messages=[
        {
            "role": "user",
            "content": [{"text": "What is Amazon Bedrock? Explain in 2-3 sentences."}],
        }
    ],
    inferenceConfig={
        "maxTokens": 256,
        "temperature": 0.5,
    },
)

Let's break down each parameter:

  • modelId — The model to call. We use Claude Haiku 4.5, which is fast and cheap. The us. prefix indicates a cross-region inference profile — this is the format Bedrock now requires for on-demand model access. You can swap this for any supported model ID without changing anything else.
  • messages — The conversation history. For a single prompt, it is just one user message.
  • inferenceConfig — Controls how the model generates its response. maxTokens caps the response length (prevents surprise bills). temperature controls randomness — 0 means deterministic, 1 means creative.

Reading the Response

answer = response["output"]["message"]["content"][0]["text"]
usage = response["usage"]

The response is a nested dictionary. The actual generated text lives at response["output"]["message"]["content"][0]["text"]. The usage field tells you how many tokens were consumed — useful for tracking costs.

Running It

python main.py single

You should see output like:

Using model: us.anthropic.claude-haiku-4-5-20251001-v1:0
Region: us-east-1

============================================================
EXAMPLE 1: Single Prompt
============================================================

Question: What is Amazon Bedrock?

Answer: Amazon Bedrock is a fully managed AWS service that provides
API access to foundation models from leading AI companies like
Anthropic, Meta, and Amazon, allowing developers to build generative
AI applications without managing infrastructure.

Tokens used: 25 input, 64 output
Stop reason: end_turn

The stop reason: end_turn means the model finished naturally. If you see max_tokens instead, the response was truncated — increase maxTokens in the inference config.

Model completed its response naturally

end_turn

↑ 89%tokens used total

Congratulations — you just made your first Bedrock API call. One function call, one response. That is all it takes.

Example 2: Multi-Turn Conversation

A single prompt is useful, but most real applications need conversation — the ability for the model to remember what was said earlier and build on it. This is how chatbots, tutoring systems, and customer support agents work.

The key concept here is that the Converse API is stateless. Bedrock does not remember your previous API calls. If you want the model to have context from earlier messages, you must send the full conversation history with every request. This sounds wasteful, but it gives you complete control over what the model sees.

How Conversation History Works

Here is the pattern:

Request 1: [user1]
Request 2: [user1, assistant1, user2]
Request 3: [user1, assistant1, user2, assistant2, user3]

Each request includes everything that came before. The model reads the full history and generates a response that takes all of it into account.

The Conversation Code

The multi_turn_conversation() function in main.py demonstrates this. Here is the core logic:

system_prompt = [{"text": "You are a friendly cloud computing tutor."}]
messages = []

questions = [
    "What are the three most popular AWS services?",
    "Which one would I use to host a simple website?",
    "How much would that cost per month for a small blog?",
]

We start with an empty messages list and a system prompt. The system prompt sets the model's personality and stays constant across all turns.

for question in questions:
    # Add the user's question to the history
    messages.append({
        "role": "user",
        "content": [{"text": question}],
    })

    # Send the FULL history to Bedrock
    response = bedrock.converse(
        modelId=MODEL_ID,
        system=system_prompt,
        messages=messages,
        inferenceConfig={"maxTokens": 300, "temperature": 0.7},
    )

    # Extract the assistant's reply and add it to history
    assistant_message = response["output"]["message"]
    messages.append(assistant_message)

Notice the pattern: append the user message, call Bedrock with the full list, then append the assistant's reply. By the third turn, the messages list contains all six messages (three from the user, three from the assistant), so the model knows the full context when answering "How much would that cost?"

Running It

python main.py conversation

Expected output:

--- Turn 1 ---
You: What are the three most popular AWS services?
Assistant: The three most popular AWS services are EC2 (virtual servers),
S3 (object storage), and Lambda (serverless functions)...

--- Turn 2 ---
You: Which one would I use to host a simple website?
Assistant: For a simple website, S3 is your best bet! You can host
static websites directly from an S3 bucket...

--- Turn 3 ---
You: How much would that cost per month for a small blog?
Assistant: For a small blog on S3, you're looking at about $1-3 per month...

Conversation length: 6 messages

Notice how in Turn 3, the model correctly answers about S3 hosting costs — even though the question "How much would that cost?" does not mention S3. It knows from the conversation history that we were talking about S3 website hosting.

Turn 1

Initial Question

User asks about popular AWS services. Messages: 1 user message.

Turn 2

Follow-Up

User asks which to use for a website. Messages: 2 user + 1 assistant.

Turn 3

Context-Dependent

User asks about cost. Model uses full context to answer about S3 specifically. Messages: 3 user + 2 assistant.

Cost Implications of Conversation History

There is an important cost consideration with multi-turn conversations. Because you send the full history with every request, the number of input tokens grows with each turn. By turn 10, you are sending all 10 previous exchanges as input tokens.

For a learning project, this is negligible. But for production chatbots with long conversations, you will want strategies like conversation summarization or sliding window truncation to keep costs manageable. My article on AI infrastructure cost optimization strategies covers this in depth.

Line chart data
turntokens
Turn 150
Turn 2150
Turn 3300
Turn 4500
Turn 5750
Turn 61050
Turn 71400
Turn 81800

The chart above illustrates how input token count grows with each conversation turn. Each turn includes all previous messages, so the growth accelerates over time.

Example 3: Streaming Responses

When you use converse(), the API waits until the model finishes generating its entire response before returning anything. For short answers, this is fine. But for longer responses, the user stares at a blank screen for several seconds.

Streaming solves this. Instead of waiting for the complete response, converse_stream() sends text chunks as the model generates them. This is the "typing" effect you see in ChatGPT, Claude's web interface, and most modern AI chat applications.

How Streaming Works

The converse_stream() method returns a response object with a stream key. This stream is an iterator that yields events as they happen:

response = bedrock.converse_stream(
    modelId=MODEL_ID,
    messages=[
        {
            "role": "user",
            "content": [{"text": "Write a short poem about cloud computing."}],
        }
    ],
    inferenceConfig={"maxTokens": 256, "temperature": 0.8},
)

Instead of reading response["output"], you iterate over response["stream"]:

for event in response["stream"]:
    if "contentBlockDelta" in event:
        text_chunk = event["contentBlockDelta"]["delta"]["text"]
        print(text_chunk, end="", flush=True)

    if "messageStop" in event:
        print(f"\nStop reason: {event['messageStop']['stopReason']}")

    if "metadata" in event:
        usage = event["metadata"]["usage"]
        print(f"Tokens: {usage['inputTokens']} in, {usage['outputTokens']} out")

There are three event types that matter:

  1. contentBlockDelta — A new chunk of text. This fires many times as the model generates tokens. The flush=True on the print statement ensures each chunk appears immediately rather than buffering.

  2. messageStop — The model is done. Contains the stopReason (same as the non-streaming version).

  3. metadata — Token usage statistics. This always arrives last, after the model finishes.

Running It

python main.py stream

You will see the text appear word by word, just like a live chat interface:

Streaming response: In clouds of data, vast and wide,
Where servers hum with quiet pride,
Our thoughts ascend on virtual wings,
While infrastructure does its things.

Stop reason: end_turn
Tokens used: 22 input, 38 output

The difference from the non-streaming examples is subtle in a terminal. But in a web application, streaming is critical for user experience. Nobody wants to wait 5 seconds staring at a loading spinner when the model could be showing text progressively.

Bar chart data
metricstreamingnonStreaming
Time to First Token0.33.2
Perceived Latency0.53.2
Total Generation3.23.2

The total generation time is the same either way. But with streaming, the user sees the first token in about 300 milliseconds instead of waiting for the full 3+ seconds. That difference completely changes the perceived responsiveness of your application.

Advertisement

Understanding the Response Object

Let's take a closer look at what Bedrock returns, since understanding this structure is essential for building real applications.

Non-Streaming Response

{
    "output": {
        "message": {
            "role": "assistant",
            "content": [{"text": "The model's response text here."}]
        }
    },
    "stopReason": "end_turn",
    "usage": {
        "inputTokens": 25,
        "outputTokens": 64,
        "totalTokens": 89
    },
    "metrics": {
        "latencyMs": 1175
    }
}

Key fields:

  • output.message — The assistant's reply, in the same format as your input messages. You can append this directly to your conversation history.
  • stopReason — Why the model stopped. end_turn means it finished naturally. max_tokens means it was cut off. tool_use means it wants to call a function (advanced topic).
  • usage — Token counts for cost tracking. Input tokens are what you sent, output tokens are what the model generated.
  • metrics.latencyMs — How long the request took in milliseconds.

The Stop Reason Matters

Always check stopReason in production code:

Bar chart data
reasonmeaning
end_turn100
max_tokens75
tool_use50
  • end_turn — The model completed its thought. This is what you want.
  • max_tokens — The response was truncated because it hit the maxTokens limit. The answer is incomplete. You may want to call the model again with a higher limit or ask it to continue.
  • tool_use — The model wants to call an external function. This is used in agent-style applications (beyond the scope of this beginner tutorial).

Available Models on Bedrock

One of Bedrock's strongest features is the breadth of models available. As of February 2026, you can access models from over a dozen providers through a single API.

Pie chart data
NameValue
Anthropic (Claude)6
Amazon (Nova)8
Meta (Llama)10
Mistral AI5
DeepSeek3
Cohere4
Others12

To switch models in our code, you only need to change the MODEL_ID string:

# Amazon's own model — widely available, good for general tasks
MODEL_ID = "us.amazon.nova-lite-v1:0"

# Meta's Llama 3.1 — open-weight, good performance
MODEL_ID = "us.meta.llama3-1-8b-instruct-v1:0"

# Mistral's large model — strong for code and reasoning
MODEL_ID = "us.mistral.mistral-large-2407-v1:0"

Everything else in the code stays exactly the same. The Converse API handles the translation between its universal format and each model's native format behind the scenes.

You can see the full list of supported models and their IDs in the Bedrock documentation. You can also list models programmatically:

# Use the management client (NOT runtime) for this
bedrock_mgmt = boto3.client("bedrock", region_name="us-east-1")
response = bedrock_mgmt.list_foundation_models()

for model in response["modelSummaries"]:
    print(f"{model['modelId']} — {model['providerName']}")

Pricing: What Will This Cost?

Bedrock uses on-demand pricing by default — you pay per token with no upfront commitment. Different models have wildly different prices, so choosing the right model for your use case matters.

Bar chart data
modelinputCost
Nova Micro0.035
Nova Lite0.06
Claude Haiku 4.50.25
Llama 3.1 8B0.22
Mistral Large 32
Claude Sonnet 4.63
Claude Opus 4.615

Prices above are per million input tokens. For context, one million tokens is roughly 750,000 words — about 10 full-length novels. The examples in this tutorial use perhaps 1,000 tokens total, which costs a fraction of a cent.

For anyone tracking how Amazon Bedrock pricing compares to the broader market, my analysis of how reasoning model pricing is evolving shows that competition is driving costs down rapidly across all providers.

Cost Tracking in Code

The usage field in every Converse API response gives you exact token counts:

usage = response["usage"]
input_cost = usage["inputTokens"] * (0.25 / 1_000_000)   # Claude Haiku rate
output_cost = usage["outputTokens"] * (1.25 / 1_000_000)  # Output is more expensive
total_cost = input_cost + output_cost
print(f"This call cost: ${total_cost:.6f}")

Output tokens are typically 4-5x more expensive than input tokens. Always set maxTokens to a reasonable limit to prevent runaway costs.

Common Mistakes and Troubleshooting

Here are the issues beginners hit most often, along with their solutions.

Mistake 1: Using the Wrong Client

Symptom: AttributeError: 'Bedrock' object has no attribute 'converse'

Cause: You created boto3.client("bedrock") instead of boto3.client("bedrock-runtime").

Fix:

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

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

Mistake 2: Model Access Not Enabled

Symptom: AccessDeniedException: You don't have access to the model with the specified model ID.

Cause: The model has not been enabled in your Bedrock console, or you have not submitted the Anthropic use case form.

Fix: Go to the AWS Console, navigate to Bedrock, and click "Model access" in the sidebar. Enable the model you want to use.

Mistake 3: Wrong Region

Symptom: ValidationException: The provided model identifier is invalid.

Cause: The model ID is correct, but the model is not available in your region.

Fix: Switch to us-east-1, which has the broadest model availability:

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

Mistake 4: Response Truncated

Symptom: The model's response ends mid-sentence. stopReason is max_tokens.

Cause: The maxTokens limit in your inferenceConfig is too low.

Fix: Increase the limit:

inferenceConfig={"maxTokens": 1024}  # or 2048, 4096, etc.

Mistake 5: Content Format Error

Symptom: ValidationException: messages.0.content must be a list

Cause: You passed a string instead of a list of content blocks.

Fix:

# Wrong
"content": {"text": "Hello"}

# Right
"content": [{"text": "Hello"}]

Mistake 6: Consecutive Same-Role Messages

Symptom: ValidationException: messages must alternate between user and assistant roles

Cause: You have two user messages in a row without an assistant message between them.

Fix: Always append the assistant's response to your messages list before adding the next user message.

Wrong client (bedrock vs bedrock-runtime)35.0%
Model access not enabled25.0%
Wrong region20.0%
Content format errors12.0%
Other8.0%

The progress bar above shows the approximate frequency of each error among beginners based on common Stack Overflow and AWS forum questions. Getting the client name right eliminates over a third of all issues.

IAM Permissions Deep Dive

For this tutorial, we used the AmazonBedrockFullAccess managed policy. That is fine for learning. But for production applications, you should use the principle of least privilege.

Here is the minimal IAM policy for invoking models only:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "bedrock:InvokeModel",
        "bedrock:InvokeModelWithResponseStream"
      ],
      "Resource": "arn:aws:bedrock:*::foundation-model/*"
    }
  ]
}

Two things to note. First, bedrock:InvokeModel covers both invoke_model() AND converse(). The bedrock:InvokeModelWithResponseStream permission covers both invoke_model_with_response_stream() AND converse_stream(). There are no separate IAM actions for the Converse API — it piggybacks on the InvokeModel actions.

Second, you can restrict the Resource to a specific model if you want to prevent someone from accidentally calling an expensive model:

"Resource": "arn:aws:bedrock:us-east-1:*:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0"

This policy only allows invoking Claude Haiku in us-east-1. Any attempt to call Claude Opus or any other model would be denied.

For a deeper understanding of how AI services fit into enterprise infrastructure, the article on AI model deployment strategies for production covers IAM patterns, model governance, and production architecture.

Understanding Tokens — The Currency of AI APIs

If you are new to AI APIs, the word "token" will come up constantly. Tokens are how models measure input and output, and they are how you get billed. Understanding tokens is essential for cost management.

A token is not the same as a word. Tokens are chunks of text that the model processes. Common short words like "the", "is", and "and" are each one token. Longer words get split into multiple tokens. As a rough rule of thumb, one token is about three-quarters of a word in English. So 1,000 tokens is roughly 750 words.

Here is how token counting works in practice:

# This sentence is about 10 tokens:
"What is Amazon Bedrock?"

# This paragraph is about 50 tokens:
"Amazon Bedrock is a fully managed service that provides API
access to foundation models from leading AI companies through
a single unified API."

Every Bedrock API call has two token counts that matter:

  • Input tokens — Everything you send to the model: your message, conversation history, system prompt. You pay for these at the input token rate.
  • Output tokens — Everything the model generates in its response. You pay for these at the output token rate, which is typically 4-5x higher than input.

The usage field in every Converse API response tells you exactly how many tokens were consumed:

usage = response["usage"]
print(f"Input:  {usage['inputTokens']} tokens")
print(f"Output: {usage['outputTokens']} tokens")
print(f"Total:  {usage['totalTokens']} tokens")

Per 1,000 tokens (English text)

~750 words

↑ 4%output tokens cost 4-5x more than input

Why Token Limits Matter

The maxTokens parameter in your inferenceConfig caps how many output tokens the model can generate. This serves two purposes.

First, it prevents the model from generating extremely long responses that cost more than you intended. Without a limit, a model could theoretically generate thousands of tokens for a simple question.

Second, it acts as a safety net for agentic loops. If you build an application that calls Bedrock in a loop (like a chatbot processing many messages), a maxTokens limit prevents any single call from consuming your entire budget.

Start with 256 for simple question-and-answer use cases. Use 1,024 for paragraph-length responses. Use 4,096 or higher for long-form content generation like summaries or reports.

Token Costs in Practice

Let's do some real math. Using Claude Haiku 4.5 at $0.25 per million input tokens and $1.25 per million output tokens:

Bar chart data
scenariocost
Simple Q&A (50 in, 100 out)0.00014
Chat turn (500 in, 200 out)0.00038
Long summary (2000 in, 1000 out)0.00175
Full doc analysis (8000 in, 2000 out)0.0045

Even the most expensive scenario — analyzing a full document and generating a long summary — costs less than half a cent. For learning and experimentation, Bedrock with Claude Haiku is practically free. The examples in this tutorial cost a fraction of a penny total.

How to Choose the Right Model

With over 40 models available, how do you pick the right one? Here is a simple decision framework for beginners.

Start Cheap, Scale Up

Always start with the cheapest model that might work. If the results are not good enough, move up one tier. Do not start with the most expensive model — you will waste money on tasks that a smaller model handles perfectly well.

Bar chart data
tiercost
Tier 1: Micro4
Tier 2: Lite6
Tier 3: Standard25
Tier 4: Pro300
Tier 5: Flagship1500

The chart shows relative cost per million tokens across model tiers. The jump from Tier 3 to Tier 4 is dramatic — make sure you actually need the extra capability before upgrading.

Matching Models to Tasks

Different models excel at different tasks. Here is a practical guide:

Simple classification, extraction, formatting: Use Amazon Nova Micro or Nova Lite. These are the cheapest models and handle structured tasks well. If you need to extract dates from text, classify customer feedback as positive or negative, or format data into JSON, these are your go-to models.

General question answering and chat: Use Claude Haiku 4.5 or Meta Llama 3.1 8B. These are affordable models that provide quality responses for conversational use cases. The examples in this tutorial use Claude Haiku for this reason.

Complex reasoning, analysis, code generation: Use Claude Sonnet 4.6 or Mistral Large 3. These models handle multi-step reasoning, code generation, and detailed analysis. They cost more but produce significantly better results for complex tasks.

Maximum quality, no budget constraints: Use Claude Opus 4.6. This is the most capable model on Bedrock but also the most expensive. Reserve it for tasks where quality is critical and cost is secondary.

Budget-Friendly Stack vs Quality-First Stack

Budget-Friendly Stack

ClassificationNova Micro ($0.04/M)
ChatClaude Haiku ($0.25/M)
Code GenLlama 3.1 70B ($0.72/M)
Monthly BudgetUnder $50

Quality-First Stack

ClassificationClaude Haiku ($0.25/M)
ChatClaude Sonnet ($3.0/M)
Code GenClaude Opus ($15.0/M)
Monthly Budget$500+

The Model Swap Advantage

This is where Bedrock's Converse API really shines. Because the API format is identical across all models, you can A/B test models by changing a single string. Build your application with Claude Haiku, test it thoroughly, and then swap in Claude Sonnet for specific endpoints that need better quality. You can even route different requests to different models at runtime based on task complexity.

def get_model_for_task(task_type):
    """Choose the cheapest model that handles this task well."""
    if task_type == "classify":
        return "us.amazon.nova-micro-v1:0"
    elif task_type == "chat":
        return "us.anthropic.claude-haiku-4-5-20251001-v1:0"
    elif task_type == "analyze":
        return "us.anthropic.claude-sonnet-4-6-20250514-v1:0"
    else:
        return "us.anthropic.claude-haiku-4-5-20251001-v1:0"

This pattern — cost-aware model routing — is how production Bedrock applications keep costs down while delivering quality where it matters.

What To Learn Next

You now have three working patterns for calling AI models through Bedrock. Here is where to go from here:

System Prompts

We used a system prompt in the conversation example. System prompts are powerful — they let you control the model's tone, format, personality, and constraints. Experiment with different system prompts to see how dramatically they change the model's behavior.

Tool Use (Function Calling)

The Converse API supports tool definitions, where you describe functions the model can "call." The model decides when to call a function based on the user's request, and you execute the function and pass the result back. This is how AI agents work.

Bedrock Knowledge Bases (RAG)

If you want the model to answer questions about your own documents, Bedrock Knowledge Bases let you upload PDFs, web pages, or databases and query them through a retrieval-augmented generation (RAG) pipeline. The model reads relevant document chunks before answering.

Bedrock Guardrails

For production applications, Guardrails let you define content policies (no PII, no harmful content, topic restrictions) that are enforced automatically on every model call. You do not need to build this filtering yourself.

Step 1

Single Prompts (This Tutorial)

Call a model, get a response. Foundation of everything else.

Step 2

Multi-Turn + System Prompts

Build interactive chat applications with controlled behavior.

Step 3

Tool Use / Function Calling

Let the model call external functions. Build AI agents.

Step 4

RAG with Knowledge Bases

Answer questions from your own documents and data.

Step 5

Production Guardrails + Monitoring

Deploy safely with content filtering and usage tracking.

Conclusion

AWS Bedrock removes the infrastructure complexity from working with AI models. With Python and boto3, you can send prompts, have conversations, and stream responses in a few lines of code. The Converse API means you never have to learn model-specific payload formats — one pattern works for Claude, Llama, Nova, Mistral, and every other model on the platform.

You built three working examples today:

  1. Single prompt — The foundation of every Bedrock application
  2. Multi-turn conversation — Stateless API, stateful conversations through history management
  3. Streaming — Real-time token delivery for responsive user experiences

The complete code is at github.com/CrashBytes/ByteSizedExamples/tree/main/aws-bedrock-getting-started. Clone it, run the examples, swap in different models, and start building.

The latest AWS AI announcements from re:Invent 2025 showed that Bedrock is becoming the center of AWS's AI strategy, with new models, agent frameworks, and enterprise features shipping every month. Getting comfortable with the Converse API now puts you ahead of the curve as these capabilities expand.

Advertisement

Was this article helpful?

Your feedback helps us improve our content and create more valuable resources

We appreciate honest feedback - it helps us serve you better

Work with us

This analysis is what we do for clients

CrashBytes consults on enterprise AI strategy and implementation, builds custom web and mobile software, and places senior engineers on corp-to-corp engagements.

See Services

Enjoyed this? Get the next one.

Join developers getting CrashBytes articles, tutorials, and predictions in their inbox. No spam, unsubscribe anytime.

Related Topics

TutorialAWSPythonAICloud ComputingBedrock
Back to Articles
← PreviousThe SaaSpocalypse Two Months Later - Who Survived, Who Pivoted, and What Comes NextNext →The UK Kills the AI Copyright Opt-Out — Inside the Global Training Data Licensing Battle

From across the CrashBytes network

More than the blog — predictions, news, fiction, and AI art.

PredictionCustom AI Chips Reach Commodity Status by Q4 2027: Cloud Provider Competition Drives Democratization
NewsWeek In Review July 19-25, 2026 - The Week The Money Moved To The Metering Layer
Short StoryThe Answer Key
AI ArtThe Room That Remembers

Continue Your Learning Journey

Explore more articles related to Tutorial and expand your knowledge.

📄Tutorial

Build a Cost-Aware Multi-Model AI Router in TypeScript

A complete hands-on tutorial for routing prompts to the cheapest capable LLM in TypeScript. Build a classifier, a model registry, a fallback ladder, and per-request cost telemetry that survives the May 2026 price war.

25 min readRead more
📄Tutorial

Building a Production Async Agent Queue in TypeScript with Bun and Mistral Work Mode: A 2026 Tutorial

An end-to-end tutorial for engineers shipping long-running coding agents in production. We build a TypeScript queue on Bun that submits agent jobs to Mistral Le Chat Work mode and OpenAI background mode, polls or receives webhooks for completion, enforces per-job cost ceilings, and exposes a small status dashboard — the operating model behind the new async-coding paradigm.

26 min readRead more
📄Tutorial

Migrating Your Coding Agent from GPT-5 to DeepSeek V4: A TypeScript Tutorial

A practical, end-to-end migration guide for engineers running production coding agents on GPT-5 who want to evaluate or move to DeepSeek V4 — the open-source frontier model that landed Friday claiming the strongest agentic coding scores in the open ecosystem. Covers API differences, tool-calling adaptation, streaming, the agent loop, evaluation, and the real cost math.

25 min readRead more
📄Tutorial

Build an AI PR Reviewer in C# — Part 3: CI/CD Pipelines, GitHub Actions, and GitLab CI

Learn how to deploy your AI PR reviewer as an automated CI/CD pipeline using GitHub Actions and GitLab CI. Part 3 covers platform abstraction with interfaces, the Octokit SDK for posting inline review comments, token chunking for large diffs, rate limiting, unit testing with xUnit, and complete YAML pipeline configurations.

34 min readRead more