Your first Amazon Bedrock model call

Outcome: You get a repeatable response from Amazon Bedrock on your own machine, read its token-usage metadata, and know how to recover from access and throttling errors.

Prerequisites: Lesson 0 complete (tools installed, AWS identity verified), Lesson 1 concepts (token, inference, prompt)

Cost: Small but not free. Running the sample makes one real Amazon Bedrock inference call (a couple hundred tokens) billed to your AWS account. Check current per-token prices on the linked Bedrock pricing page before running.

Verified on: Jul 15, 2026

Outcome

By the end of this lesson you will send one prompt to a hosted model through Amazon Bedrock and get its reply back on your own machine. Amazon Bedrock is the AWS (Amazon Web Services) service that hosts language models behind a single API, so you never manage the model yourself. API stands for Application Programming Interface, the defined way one program talks to another. You will also read the call’s token-usage numbers and know how to fix the two errors beginners hit most: missing model access and throttling.

The model this course uses is Anthropic’s Claude Haiku 4.5, chosen because it is fast, inexpensive, and good at the structured, tool-using work the capstone needs.

Mental model

This is the “call the connector once” moment. In a visual tool you pick a connector, provide credentials, send one request, and read the response. Here the connector is Amazon Bedrock, the request is your prompt, and the response is the model’s reply plus some metadata.

Two ideas carry over directly:

  • The Converse API is Bedrock’s one standard request format for chatting with any supported model. You send a list of messages; you get a message back. It is the connector’s “send message” action.
  • The usage metadata returned with every reply (input tokens, output tokens, total) is your run history for cost. Every call reports exactly what it consumed, so spend is never a mystery.

Where the analogy stops: a visual connector usually hides credentials and region inside the connection setup. Here you keep the model ID and region in a small configuration file you can read, so switching models or regions is a one-line change and nothing sensitive is hard-coded.

Prerequisites and cost

  • Lesson 0 complete: tools installed and aws sts get-caller-identity returns your sandbox account.
  • Bedrock model access granted for Claude Haiku 4.5 in the us-east-1 region. In the Bedrock console, open Model access, choose Modify model access, select the Anthropic model, and submit. Anthropic models also require a one-time use-case form the first time. Access can take a few minutes to apply. The console-walkthrough video below shows this flow.
  • The course code checked out, with dependencies installed. From the agent/ folder run uv sync once.

Cost: running the sample makes one real, billable inference call. It is tiny (a couple hundred tokens), but it is not free. On-demand Bedrock pricing is per-million-tokens, with input and output priced separately. Check the current numbers on the Amazon Bedrock pricing page before you run it. This course does not print dollar figures, because prices change; the pricing page is the source of truth. Pricing page location verified on 2026-07-15.

Steps

Run these from the agent/ folder of the course.

  1. Copy the environment file. Run cp .env.example .env. This file holds your model ID and region. It is ignored by Git so it never gets committed.

  2. Confirm the values. Open .env and confirm MODEL_ID is the Claude Haiku 4.5 cross-region inference profile and AWS_REGION is us-east-1. The sample file already contains the correct defaults from the research log.

  3. Sign in to AWS. Run aws sso login so your terminal has fresh short-lived credentials. Bedrock uses these to authorize the call.

  4. Read the code below so you can explain every line before you run it.

  5. Run the call. From agent/, run:

    uv run python -m intake.bedrock_call

    This prints a cost note, sends one prompt, and prints the reply plus token usage.

Smallest code sample

Two files do the work. First, config.py reads the model ID and region from your environment, with the researched defaults as a fallback. Keeping these out of the code is why you can change models by editing .env instead of editing source.

"""Shared configuration, read from environment variables.

Standard library only. Keeping model id, region, and memory id out of source
(AGENTS.md: "Keep model ID, region, memory ID ... configurable") means a learner
switches environments by changing `.env`, not by editing code.
"""

from __future__ import annotations

import os
from dataclasses import dataclass

# Defaults match docs/research-log.md. They are safe to read but every call that
# spends money still requires explicit opt-in, so a default here cannot bill you.
_DEFAULT_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0"
_DEFAULT_REGION = "us-east-1"


class ConfigError(RuntimeError):
    """A required configuration value is missing or empty."""


@dataclass(frozen=True)
class Config:
    """Resolved configuration for one run.

    `memory_id` is optional because most lessons never touch AgentCore Memory;
    only Lesson 6 requires it, and it fails loudly there if unset.
    """

    model_id: str
    region: str
    memory_id: str | None

    def require_memory_id(self) -> str:
        """Return the memory id or explain exactly what to set.

        AgentCore Memory has no local emulation, so this cannot be defaulted.
        """
        if not self.memory_id:
            raise ConfigError(
                "MEMORY_ID is not set. AgentCore Memory is a cloud resource with "
                "no local emulation. Create one in your AWS account and set "
                "MEMORY_ID in your environment (see .env.example)."
            )
        return self.memory_id


def load_config() -> Config:
    """Build a Config from the current environment.

    Model id and region fall back to the researched defaults; memory id does not,
    because a wrong memory id would point at the wrong account's data.
    """
    return Config(
        model_id=os.environ.get("MODEL_ID", _DEFAULT_MODEL_ID).strip()
        or _DEFAULT_MODEL_ID,
        region=os.environ.get("AWS_REGION", _DEFAULT_REGION).strip()
        or _DEFAULT_REGION,
        memory_id=(os.environ.get("MEMORY_ID") or "").strip() or None,
    )

Second, bedrock_call.py makes the actual call. Read call_model: it creates a Bedrock client bound to your region with explicit timeouts and a small retry limit, sends one message through the Converse API, and turns the two most common errors into plain-language guidance instead of a raw stack trace. _parse_reply pulls the answer text and the token counts out of the response.

"""Lesson 2: one Amazon Bedrock Converse call, no agent framework yet.

This is the smallest possible "call a model" step: send one message, read the
reply and the token-usage metadata, and turn the two errors a beginner hits most
(no model access, throttling) into plain-language remediation instead of a stack
trace.

Running this DOES call AWS and costs a small amount of money, so it is guarded
behind `if __name__ == "__main__"` with a printed cost note.
"""

from __future__ import annotations

from dataclasses import dataclass

import boto3
from botocore.config import Config as BotoConfig
from botocore.exceptions import ClientError

from intake.config import Config, load_config

# Bound the network call. Without these a hung connection blocks forever and a
# transient error retries indefinitely. Small, explicit limits are the lesson.
_READ_TIMEOUT_SECONDS = 30
_CONNECT_TIMEOUT_SECONDS = 10
_MAX_RETRIES = 2

# Keep the demo cheap and bounded: a short answer is all we need to see usage.
_MAX_OUTPUT_TOKENS = 200


@dataclass(frozen=True)
class ModelReply:
    """The parts of a Converse response the lesson inspects."""

    text: str
    input_tokens: int
    output_tokens: int
    total_tokens: int


class ModelAccessError(RuntimeError):
    """The account cannot invoke this model yet (needs model access granted)."""


class ModelThrottledError(RuntimeError):
    """Bedrock throttled the request; retry later or slow down."""


def call_model(prompt: str, config: Config | None = None) -> ModelReply:
    """Send one prompt to Bedrock via the Converse API and return the reply.

    Raises ModelAccessError / ModelThrottledError with remediation text for the
    two most common beginner failures; other ClientErrors propagate unchanged so
    they are not silently swallowed.
    """
    cfg = config or load_config()
    client = boto3.client(
        "bedrock-runtime",
        region_name=cfg.region,
        config=BotoConfig(
            read_timeout=_READ_TIMEOUT_SECONDS,
            connect_timeout=_CONNECT_TIMEOUT_SECONDS,
            retries={"max_attempts": _MAX_RETRIES, "mode": "standard"},
        ),
    )

    try:
        response = client.converse(
            modelId=cfg.model_id,
            messages=[{"role": "user", "content": [{"text": prompt}]}],
            inferenceConfig={"maxTokens": _MAX_OUTPUT_TOKENS},
        )
    except ClientError as err:
        code = err.response.get("Error", {}).get("Code", "")
        if code == "AccessDeniedException":
            raise ModelAccessError(
                f"Access denied for model '{cfg.model_id}' in region "
                f"'{cfg.region}'. In the Bedrock console open 'Model access', "
                "enable this Anthropic model, and complete the one-time use-case "
                "form. Access can take a few minutes to apply."
            ) from err
        if code == "ThrottlingException":
            raise ModelThrottledError(
                "Bedrock throttled this request. Wait a few seconds and retry, "
                "or request a quota increase if it persists."
            ) from err
        raise

    return _parse_reply(response)


def _parse_reply(response: dict) -> ModelReply:
    """Pull answer text and token usage out of a Converse response.

    Validate the shape at this boundary rather than trusting the SDK dict deep in
    caller code.
    """
    content = response["output"]["message"]["content"]
    text = "".join(block.get("text", "") for block in content)
    usage = response["usage"]
    return ModelReply(
        text=text,
        input_tokens=usage["inputTokens"],
        output_tokens=usage["outputTokens"],
        total_tokens=usage["totalTokens"],
    )


if __name__ == "__main__":
    # COST NOTE: this makes a real, billable Bedrock inference call in your
    # configured AWS account. It is tiny (a couple hundred tokens) but not free.
    print("COST NOTE: this calls Amazon Bedrock and bills your AWS account.")
    reply = call_model("In one sentence, what is an automation intake request?")
    print("\nReply:", reply.text)
    print(
        f"Tokens: input={reply.input_tokens} "
        f"output={reply.output_tokens} total={reply.total_tokens}"
    )

The <MODEL_ID> and <AWS_REGION> used here come from your .env file (see config.py); they are never hard-coded into the call.

Expected output

This step calls Amazon Bedrock, which needs your credentials and spends a small amount, so it cannot be run for you here. The block below is the shape of the output, not a transcript. The reply wording is generated fresh each run, so your exact sentence and exact token counts will differ (output shape; exact text and numbers vary per run):

COST NOTE: this calls Amazon Bedrock and bills your AWS account.

Reply: <one sentence of model-generated text describing an automation intake request>
Tokens: input=<N> output=<N> total=<N>

The checkpoint is met when you see a non-empty Reply: line and a Tokens: line where total equals input + output. Run the command a second time: the sentence changes slightly but the shape stays the same. That variation is the probabilistic behavior from Lesson 1, seen firsthand.

One common failure

Symptom: the command ends with a message like “Access denied for model ‘…’. In the Bedrock console open ‘Model access’…” instead of a reply.

Diagnosis: your AWS account has valid credentials but has not been granted access to this specific model in this region. Bedrock models are off by default and must be enabled per account and region. The code catches Bedrock’s AccessDeniedException and rewrites it as that plain-language message, so this is expected, recoverable behavior, not a crash.

Fix: open the Bedrock console in us-east-1, go to Model access, enable Claude Haiku 4.5, and complete the one-time Anthropic use-case form if prompted. Wait a few minutes for access to apply, run aws sso login if your session has expired, then run the command again.

A second common failure is a throttling message (“Bedrock throttled this request. Wait a few seconds and retry…”). That means you sent requests faster than your account’s rate limit allows. Wait a few seconds and retry, or request a quota increase if it keeps happening. The code catches Bedrock’s ThrottlingException and surfaces this guidance for you.

Why this works

Three deliberate choices make this first call reliable. The model ID and region live in configuration, so the same code runs against any model or region without edits and nothing sensitive is baked into source. The network call has explicit timeouts and a bounded retry count, so a hung connection fails in seconds instead of hanging forever, and a transient blip retries a couple of times instead of endlessly. And the two most common errors are caught and translated into actions you can take, so a beginner sees “enable model access” rather than a forty-line traceback. These are the same boundary habits you will reuse at every network call in the course.

Verify it yourself

  1. Run uv run python -m intake.bedrock_call from agent/ and confirm you get a Reply: line and a Tokens: line.
  2. Run it a second time and confirm the reply wording changes while the token line still adds up (input + output = total).
  3. Temporarily set a wrong region in .env (for example a region where you have not enabled the model), run the command, and confirm you get the plain-language access message rather than a crash. Then restore AWS_REGION=us-east-1.

Passing all three means you have a repeatable Bedrock call and you have seen both the success path and the main failure path.

Cleanup

There is nothing to delete. This lesson creates no standing cloud resource; each run is a single call that finishes immediately. To stop any further spend, simply do not run the command again. If you want to end your AWS session, run aws sso logout. Keep your .env file for the next lesson; it is already ignored by Git, so it will not be committed.

Go deeper (2)