The goal
You want to call a model through Amazon Bedrock from a Python script. You'll use your usual AWS credentials, and the Converse API, which works the same way across many models. That means that you can change models later with little code change.
Step 1: check the basics
You need AWS CLI access and a recent boto3:
aws sts get-caller-identity
aws configure get region
pip install --upgrade boto3
python -c "import boto3; print(boto3.__version__)"
Bedrock isn't in every region, and each region offers different models. Check that yours has what you need.
Step 2: find a model
aws bedrock list-foundation-models \
--by-output-modality TEXT \
--query 'modelSummaries[].{id:modelId, provider:providerName, inference:inferenceTypesSupported, status:modelLifecycle.status}' \
--output table
Copy a model ID from the list. Look at the inference column:
| Value | Meaning |
|---|---|
ON_DEMAND | You can call this model ID directly |
INFERENCE_PROFILE | You must call it through an inference profile |
PROVISIONED | Needs provisioned capacity |
For an INFERENCE_PROFILE model, list the profile IDs. They often have a geographic prefix:
aws bedrock list-inference-profiles \
--query 'inferenceProfileSummaries[].{id:inferenceProfileId, name:inferenceProfileName, status:status}' \
--output table
If you use the wrong type, you'll get a ValidationException. See on-demand throughput not supported.
Some models also need access enabled, or terms accepted, for your account. How that works has changed over time, so check the Bedrock console's model access page if you're denied.
Step 3: grant permission
Your identity needs permission to invoke the model. Limit it to the models you use:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeOneModel",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:ap-south-1::foundation-model/<model-id>"
]
}
]
}
The Converse APIs use these same InvokeModel permissions. If you're using an inference profile, you also need to allow the profile's ARN and the underlying foundation models in each of its regions.
Step 4: make the call
Save this as first_call.py:
import os
import boto3
MODEL_ID = os.environ["BEDROCK_MODEL_ID"]
client = boto3.client("bedrock-runtime")
response = client.converse(
modelId=MODEL_ID,
system=[{"text": "You are a concise assistant for IT operations engineers."}],
messages=[
{
"role": "user",
"content": [{"text": "In two sentences, what does an HTTP 429 error mean?"}],
}
],
inferenceConfig={"maxTokens": 300, "temperature": 0.2},
)
text = response["output"]["message"]["content"][0]["text"]
print(text)
print("---")
print("stop reason:", response["stopReason"])
print("usage:", response["usage"])
print("latency ms:", response["metrics"]["latencyMs"])
Run it:
export BEDROCK_MODEL_ID="<model or inference profile id>"
python first_call.py
Notice what isn't there: no API key. boto3 uses your normal AWS credentials, including an instance or container role when it's running on AWS.
What's in the response:
| Field | Meaning |
|---|---|
output.message.content | A list of content blocks. Some models return more than one kind |
stopReason | Why it stopped: end_turn, max_tokens, tool_use or guardrail_intervened |
usage | inputTokens, outputTokens and totalTokens |
metrics.latencyMs | Time taken |
If stopReason is max_tokens, the answer was cut off. See output truncated.
Some models return a reasoning block before the text, so content[0] might not be text. Use this safer way to read it:
blocks = response["output"]["message"]["content"]
text = "".join(b["text"] for b in blocks if "text" in b)
Step 5: have a conversation
The API doesn't remember earlier calls, so your code has to send the history. See chat history basics.
messages = [{"role": "user", "content": [{"text": "My server is named web01."}]}]
r1 = client.converse(modelId=MODEL_ID, messages=messages)
messages.append(r1["output"]["message"])
messages.append({"role": "user", "content": [{"text": "What is my server called?"}]})
r2 = client.converse(modelId=MODEL_ID, messages=messages)
print(r2["output"]["message"]["content"][-1]["text"])
Step 6: stream the reply
For long answers, show text as it arrives:
response = client.converse_stream(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [{"text": "List five ways to reduce EC2 costs."}]}],
inferenceConfig={"maxTokens": 500},
)
for event in response["stream"]:
if "contentBlockDelta" in event:
delta = event["contentBlockDelta"]["delta"]
if "text" in delta:
print(delta["text"], end="", flush=True)
elif "messageStop" in event:
print("\nstop:", event["messageStop"]["stopReason"])
elif "metadata" in event:
print("usage:", event["metadata"]["usage"])
Step 7: handle errors
Add retries for throttling, and handle errors explicitly:
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
client = boto3.client(
"bedrock-runtime",
config=Config(
retries={"mode": "adaptive", "max_attempts": 5},
read_timeout=120,
),
)
try:
response = client.converse(
modelId=MODEL_ID,
messages=[{"role": "user", "content": [{"text": "Hello"}]}],
)
except ClientError as e:
code = e.response["Error"]["Code"]
message = e.response["Error"]["Message"]
print(code, message)
raise
| Error | Likely cause | Fix |
|---|---|---|
AccessDeniedException | IAM, or model access not enabled | See access denied |
ValidationException | Wrong model ID, or inference profile needed | Check Step 2 |
ThrottlingException | Quota reached | See throttling |
ResourceNotFoundException | Model isn't in this region, or it's retired | Check region, and the lifecycle status |
ModelTimeoutException, read timeout | Slow or very long response | Use streaming, and raise read_timeout |
ServiceUnavailableException | Temporary service problem | Retry with backoff |
NoRegionError | No region is set | Set AWS_REGION, or pass region_name |
For a broader list, use the LLM API errors cheat sheet.
Warning
Each call costs money, based on tokens. A loop with a bug can run up a large bill in minutes. Set
maxTokens, and add a budget alert. Before you roll out, estimate the cost for your traffic.
Don't send sensitive data without checking
Check your organization's rules before sending customer or confidential data, and read AWS's data handling terms for Bedrock. Turn on invocation logging if you need an audit trail, but remember that prompts in logs need protection too. For input and output checks, see guardrails.
Prevent it next time
- Keep the model ID in config, not in code, because models get retired. See model retirement errors.
- Use a role, not long-lived access keys.
- Log
usagefor each call, so you can track cost per feature. See per-team cost tracking. - Keep the Bedrock CLI cheat sheet handy.