Bedrock & Claude
Bedrock is a second front door to the same Claude models you already know. This chapter is the core inference loop: Converse, streaming, usage, and the raw invoke_model escape hatch.
- AWS credentials (
aws configure) + Bedrock model access enabled in your region +pip install boto3 - AWS credentials (
aws configure) +pip install boto3
Learning objectives
- Call Claude on Bedrock with the Converse API — the modern, model-agnostic way.
- Read the response and usage (token counts) for cost tracking.
- Stream tokens as they generate for responsive UIs.
- Map every Anthropic-SDK concept (system, messages, max_tokens) onto Bedrock.
code/aws2-bedrock-basics/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.Converse: one API for every model essential
Bedrock's Converse API is the recommended entry point. It gives one consistent request shape across all providers, so switching from Claude to another model is a one-line change. The messages format will feel familiar from the Anthropic SDK.
converse.pyimport boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
resp = brt.converse(
modelId=MODEL,
system=[{"text": "You are a careful SRE assistant. Be concise."}],
messages=[{"role": "user", "content": [{"text": "In one sentence, what is Amazon Bedrock?"}]}],
inferenceConfig={"maxTokens": 200, "temperature": 0.2},
)
print(resp["output"]["message"]["content"][0]["text"])
u = resp["usage"]
print("tokens in/out:", u["inputTokens"], u["outputTokens"])
Amazon Bedrock is a fully managed AWS service that provides API access to foundation models — including Anthropic's Claude — for building generative-AI apps.
tokens in/out: 28 41
This is the whole point of the chapter in one screen: call Claude — the same model family you know from the Anthropic SDK — but through Amazon Bedrock instead. Bedrock is AWS's managed door to many foundation models; here you send it a question and print Claude's answer plus how many tokens it used.
import boto3pulls in AWS's official Python library. Everything you do with AWS from Python goes through boto3.boto3.client("bedrock-runtime", region_name="us-east-1")creates the Bedrock runtime client — the object that actually sends inference requests.bedrock-runtimeis the 'run a model' service (a separatebedrockservice handles setup/admin).region_namepicks which AWS data-center region answers you, because model access is enabled per region.MODEL = "anthropic.claude-3-5-sonnet-..."is Bedrock's model ID — the exact Claude version to call. On Bedrock you name models by these IDs rather than short names.brt.converse(...)is the Converse API — the modern, model-agnostic call.system=[{"text": ...}]sets the standing instruction (persona/rules).messages=[{"role": "user", "content": [{"text": ...}]}]is the actual question. Notecontentis a list of blocks, each a{"text": ...}dict — that list shape is what later lets you add images or tool calls the same way.inferenceConfig={"maxTokens": 200, "temperature": 0.2}are the knobs:maxTokenscaps how long the reply can be;temperaturenear 0 makes answers more focused and repeatable (higher = more creative/random).resp["output"]["message"]["content"][0]["text"]reaches into the nested response to pull out the first text block — Claude's actual sentence.resp["usage"]holdsinputTokens/outputTokens, which you print for cost tracking (you pay per token).
What the output means: The first printed line is Claude's one-sentence answer. The second line, tokens in/out: 28 41, means the request cost 28 input tokens and the reply used 41 output tokens — multiply those by the model's per-token price to get the dollar cost.
Try this: Change the user question, or bump temperature to 0.9 and run twice — at high temperature you'll notice the wording changes between runs; at 0.2 it stays steady. (Running for real needs AWS credentials and Bedrock model access — see the box at the top.)
system, messages, maxTokens — identical concepts. The content is a list of blocks ({"text": ...}), which is what lets Bedrock handle images and tool calls with the same shape.Streaming essential
For anything a human waits on, stream. converse_stream yields events as the model generates. You accumulate contentBlockDelta events for the text.
stream.pyimport boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0"
stream = brt.converse_stream(
modelId=MODEL,
messages=[{"role": "user", "content": [{"text": "Count to five slowly."}]}],
inferenceConfig={"maxTokens": 100},
)
for event in stream["stream"]:
if "contentBlockDelta" in event:
print(event["contentBlockDelta"]["delta"]["text"], end="", flush=True)
print()
Same Converse call, but instead of waiting for the whole answer, this streams it — you get the text piece by piece as Claude generates it. That's what makes a chat UI feel alive, showing words as they arrive instead of a long pause then a wall of text.
- The client, model ID, and
messagesare set up exactly like Lab W2.1 — nothing new about how you talk to Bedrock. brt.converse_stream(...)is the streaming twin ofconverse. Instead of one finished reply, it hands back a stream of events that arrive over time.for event in stream["stream"]:loops over those events as they come in. Each event is a small dict describing something that just happened (text arrived, message started, message ended, etc.).if "contentBlockDelta" in event:keeps only the events that carry a new chunk of text. A delta means 'a little more of the answer'.event["contentBlockDelta"]["delta"]["text"]is that chunk.print(..., end="", flush=True)prints each chunk with no line break (end="") and forces it to the screen immediately (flush=True) so the text appears live. The final bareprint()just adds a newline at the end.
What the output means: On screen you'd see the answer type itself out — "1... 2... 3..." appearing progressively — rather than all at once. The end result is the same text as a non-streamed call; only the delivery is incremental.
Try this: Add a counter that increments inside the if and print it at the end to see how many text deltas the reply arrived in — a short answer might be a handful, a long one dozens.
InvokeModel: the raw escape hatch intermediate
Before Converse existed, you called invoke_model with each provider's native JSON body. You will still see it in older code and when you need a Claude-specific field Converse does not surface. For Claude the body is the Anthropic Messages format verbatim.
invoke_model.pyimport boto3, json
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
body = {
"anthropic_version": "bedrock-2023-05-31", # required for Claude on Bedrock
"max_tokens": 200,
"messages": [{"role": "user", "content": "Say hello."}],
}
resp = brt.invoke_model(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
body=json.dumps(body),
)
payload = json.loads(resp["body"].read())
print(payload["content"][0]["text"])
This is the older, lower-level way to call a model on Bedrock: invoke_model. Instead of the tidy Converse shape, you hand Bedrock the provider's native JSON body yourself. For Claude that body is the raw Anthropic Messages format. You'll meet this in legacy code and when you need a Claude-specific field Converse doesn't expose.
import boto3, json— you now also needjsonbecause you build the request body as a dict and turn it into a JSON string by hand.- The
bodydict is Claude's native format."anthropic_version": "bedrock-2023-05-31"is a required tag telling Bedrock which Claude request schema you're using.max_tokensandmessageslook like the Anthropic SDK — note the snake_case names here vs. Converse'smaxTokens. brt.invoke_model(modelId=..., body=json.dumps(body))sends it.json.dumps(body)converts your Python dict into the JSON text string the API expects.payload = json.loads(resp["body"].read())reverses that on the way back: the response body is a raw stream, so you.read()the bytes andjson.loadsparses them into a Python dict.payload["content"][0]["text"]pulls the answer out of that dict — the same list-of-blocks shape Claude always returns.
What the output means: Prints Claude's reply to "Say hello." The result is identical to what Converse would give; the difference is purely that you assembled and parsed the JSON yourself.
Try this: Compare this block to Lab W2.1 side by side. Everything Converse did automatically — naming fields, encoding JSON, decoding the response — you did manually here. That's exactly why the tip below says prefer Converse for new code and keep invoke_model for special cases.
converse/converse_stream for new code. Reach for invoke_model only for provider-specific parameters or to match legacy code.Exercise W2.1 — Port your Anthropic call
Context: The Converse API is deliberately close to, but not identical to, the direct Anthropic SDK shape. Porting a call you already know is the fastest way to internalize the differences — list-of-blocks content, separate system, and usage on the response.
Your task: Take a Claude call you wrote against the Anthropic SDK and rewrite it with converse, confirm the output and token counts match your expectation, then add streaming so the answer prints as it generates.
Requirements:
- Reproduce an existing Anthropic-SDK call using
bedrock-runtimeconverse - Confirm the reply text is equivalent and read token counts from
resp["usage"] - Convert the call to
converse_streamand printcontentBlockDeltatext as it arrives - Note the shape differences you hit: content is a list of blocks and
systemis its own top-level field
💡 Hint: Map the SDK's plain-string message content onto Converse's [{"text": ...}] blocks first — that one transform fixes most porting errors.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: The Converse API is Bedrock's unified, provider-agnostic way to chat with any model, and its one quirk trips up every newcomer: content is always a list of blocks, never a bare string. Getting the first call right anchors everything that follows.
Your task: Call converse on bedrock-runtime with a single user message and print the reply text.
Requirements:
- Use a
bedrock-runtimeclient with a region - Pass a
messageslist where content is[{"text": ...}], not a raw string - Set an
inferenceConfig(e.g.maxTokens,temperature) - Read the answer from
output.message.content[0].text
💡 Hint: Both the request content and the response content are lists of typed blocks — the text you want is inside the first block, not at the top level.
Show solution
Converse content is always a list of blocks; the answer is at output.message.content[0].text.
import boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = brt.converse(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role": "user", "content": [{"text": "Define latency in one line."}]}],
inferenceConfig={"maxTokens": 128, "temperature": 0.2},
)
print(resp["output"]["message"]["content"][0]["text"])
Context: A system prompt is how you set the model's role and constraints, and usage on every response is your running meter for cost. Wiring both in early means every later call is already observable.
Your task: Add a system block to steer the model's behaviour and print the input and output token counts from the response.
Requirements:
- Pass
systemas a list of{"text": ...}blocks, mirroring the message-content shape - The system instruction visibly shapes the reply (e.g. 'answer in exactly one sentence')
- Read
inputTokensandoutputTokensfromresp["usage"] - Understand that
usageis what you log for per-call cost tracking
💡 Hint: system is a sibling of messages at the top level of the call — and it takes the same list-of-blocks shape as message content.
Show solution
system is a list of {"text": ...} blocks; resp["usage"] carries token counts for cost tracking.
import boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = brt.converse(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
system=[{"text": "You answer in exactly one sentence."}],
messages=[{"role": "user", "content": [{"text": "What is a VPC?"}]}],
inferenceConfig={"maxTokens": 128},
)
u = resp["usage"]
print(u["inputTokens"], u["outputTokens"])
Context: For a chat UI, waiting for the full completion before showing anything feels broken. ConverseStream emits incremental events so text appears as it's generated — but you have to pick the right event type out of the stream.
Your task: Use converse_stream and print text as it arrives, filtering for contentBlockDelta events.
Requirements:
- Call
converse_streaminstead ofconverse - Iterate the
streamgenerator of event dicts - Select only
contentBlockDeltaevents and readdelta.text - Print incrementally (no newline per delta) so the output reads as one flowing response
💡 Hint: The stream carries several event kinds (start/delta/stop/metadata); the running text lives specifically under a delta event's delta.text.
Show solution
The stream yields event dicts; the incremental text is under contentBlockDelta.delta.text.
import boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
resp = brt.converse_stream(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=[{"role":"user","content":[{"text":"Count to five."}]}],
)
for event in resp["stream"]:
if "contentBlockDelta" in event:
print(event["contentBlockDelta"]["delta"]["text"], end="")
Context: Converse normalizes every provider, but sometimes you need a provider-specific field it doesn't expose. InvokeModel is the escape hatch: you hand Bedrock the model's native request body verbatim — and for Claude that body has one non-negotiable field.
Your task: Call the same model through invoke_model using the native Claude request body, including the required anthropic_version, and parse the JSON response.
Requirements:
- Build the native (snake_case) Claude body with
max_tokensandmessages - Include
anthropic_versionset to"bedrock-2023-05-31"— the call fails without it - Serialize the body to a JSON string for the
bodyparameter - Read and
json.loadsthe streamingbodyof the response, then pull text fromcontent[0].text
💡 Hint: Unlike Converse's tidy dict, invoke_model gives you a raw byte stream back — you must .read() and decode the JSON yourself.
Show solution
invoke_model takes a JSON string body in the provider-native (snake_case) shape; anthropic_version is required.
import boto3, json
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
body = {
"anthropic_version": "bedrock-2023-05-31", # required
"max_tokens": 128,
"messages": [{"role":"user","content":"Say hi."}],
}
resp = brt.invoke_model(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
body=json.dumps(body),
)
out = json.loads(resp["body"].read())
print(out["content"][0]["text"])
Context: Scattering raw converse calls across a codebase means no single place to add logging, retries, or guardrails later. A thin wrapper collapses each call site to one line and gives you exactly that seam.
Your task: Wrap converse in ask(prompt) that returns the reply text and logs token usage, so every call site is a single call.
Requirements:
asktakes a prompt (and a defaulted model id) and returns just the answer text- Token usage is logged from
resp["usage"]on every call - The message-list construction and
inferenceConfigare hidden inside the wrapper - The wrapper is the natural home for future retries, guardrails, or metrics
💡 Hint: Centralizing the call is the whole point — keep the public surface a plain string in, string out, and let usage logging be a side effect.
Show solution
Centralizing the call gives one place to log usage and later add retries or guardrails.
import boto3, logging
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
log = logging.getLogger("llm")
def ask(prompt, model="anthropic.claude-3-5-sonnet-20241022-v2:0"):
r = brt.converse(
modelId=model,
messages=[{"role":"user","content":[{"text": prompt}]}],
inferenceConfig={"maxTokens": 512},
)
u = r["usage"]
log.info("tokens in=%s out=%s", u["inputTokens"], u["outputTokens"])
return r["output"]["message"]["content"][0]["text"]
# print(ask("Summarize the CAP theorem."))
Context: Converse is stateless: it has no memory of prior turns, so a support bot only sounds coherent because you replay the transcript each call. Owning that history — and bounding it — is what keeps follow-ups contextual without an unbounded cost.
Your task: Maintain the messages list across turns for a support endpoint: append the user turn, call converse, append the assistant turn, and cap history length.
Requirements:
- Each turn appends the user message, calls converse, then appends the returned assistant
messageverbatim - History is capped (e.g. trim to the last N turns) to bound context size and cost
- The assistant turn stored is the full
output.message, so it round-trips as valid input next call - The transcript-management state machine is exercisable offline, independent of the network call
- Understand that Bedrock keeps no session — the client owns the entire conversation
💡 Hint: Store the model's own output.message object back into the list unchanged; slicing to the last 2 * max_turns entries is enough to bound it.
Show solution
Converse is stateless: you own the transcript. Append both roles each turn and trim to bound cost.
import boto3
brt = boto3.client("bedrock-runtime", region_name="us-east-1")
class Chat:
def __init__(self, max_turns=10):
self.messages, self.max = [], max_turns * 2
def send(self, text):
self.messages.append({"role":"user","content":[{"text": text}]})
r = brt.converse(
modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
messages=self.messages,
inferenceConfig={"maxTokens": 512},
)
msg = r["output"]["message"]
self.messages.append(msg) # keep assistant turn
self.messages = self.messages[-self.max:] # bound history
return msg["content"][0]["text"]
# offline: history bookkeeping without a network call
c = Chat(max_turns=1)
c.messages = [{"role":"user","content":[{"text":"a"}]}]*5
c.messages = c.messages[-c.max:]
print(len(c.messages)) # 2
✓ Checkpoint — you can move on when you can…
- Call Claude on Bedrock with
converseand read the text + usage. - Stream a response and explain what a
contentBlockDeltais. - Explain when you would drop to
invoke_modelinstead of Converse. - Map
system/messages/maxTokensbetween the Anthropic SDK and Bedrock.
Knowledge check check yourself
Why does the course recommend the Converse API over invoke_model for new code, and when would you still reach for invoke_model?
Show answer
When you stream a Bedrock response with converse_stream, what is a contentBlockDelta and why do you filter for it?