Module 2: The Bedrock Cost Model

7. Hands-on: the cost-per-token calculator

Description

Lesson 6 left a precise conclusion: the data missing to cost extract-shipment-manifest-fields — tokens per invocation, monthly volume — is a business decision a human has to declare explicitly, not something any infrastructure analysis tool can infer. This lesson builds the piece that solves exactly that: bedrock_cost_estimate.py, a 100% deterministic Python script, with no random, no datetime.now(), that ran for real to write this lesson, with pytest verifying twelve fixed cases.

Connection to the module

Lessons 2 and 6 gave, respectively, the cited price and the structural reason no existing tool solves this alone. This lesson builds the code that does solve it. Lesson 8, the project that closes the module, uses this same calculator to produce GENAI-COST-PROFILE.md's real cost projection.


Analogy: the produce-market scale

A produce-market scale doesn't tell you the price of "a purchase" — it tells you the price per kilo, and expects you to put the fruit on it before it can tell you how much you're going to pay. There's no shortcut: without putting the product on the scale, the only possible answer is the rate per kilo, never a total. bedrock_cost_estimate.py is exactly that scale, applied to tokens instead of fruit: it knows a model's price per million tokens (lesson 2 gave it to you, cited from AWS), but it needs you to explicitly declare how much "weight" — how many tokens, how many times a month — you're going to put on it before it can tell you a total.


Step 1 — The complete script

scripts/bedrock_cost_estimate.py, at the root of andes-cargo-infra/:

#!/usr/bin/env python3
"""bedrock_cost_estimate.py -- deterministic monthly cost calculator for a
Bedrock on-demand text workload, for Andes Cargo's extract-shipment-manifest-fields.

Three explicit usage inputs, never assumed:
  --input-tokens       average input tokens per request
  --output-tokens      average output tokens per request
  --monthly-requests   the declared monthly volume assumption

Plus a price per model, either resolved from MODEL_PRICING_USD_PER_MILLION_TOKENS
(a small table of prices cited from AWS, see Module 2, lesson 2) or overridden
explicitly with --price-in/--price-out.

Never uses random or datetime.now(). Given the same inputs, this script always
produces the same output -- that determinism is the entire point: see Module 2,
lesson 7 of genai-on-aws-production-guide.
"""

from __future__ import annotations

import argparse
import sys
from dataclasses import dataclass

# Public on-demand prices, US East (N. Virginia), USD per 1,000,000 tokens.
# Verified against the AWS Price List API (pricing.us-east-1.amazonaws.com,
# offer AmazonBedrock, publicationDate 2026-08-13T21:07:07Z).
# These numbers change. Re-verify at aws.amazon.com/bedrock/pricing/ before
# trusting them for a real budget decision -- this table is a citation, not
# a promise. See Module 2, lesson 2.
MODEL_PRICING_USD_PER_MILLION_TOKENS = {
    "amazon.nova-micro-v1:0": {"input": 0.035, "output": 0.14},
    "amazon.nova-lite-v1:0": {"input": 0.06, "output": 0.24},
    "amazon.nova-pro-v1:0": {"input": 0.80, "output": 3.20},
    "amazon.nova-premier-v1:0": {"input": 2.50, "output": 12.50},
}


@dataclass(frozen=True)
class CostEstimate:
    model_id: str
    input_tokens_per_request: int
    output_tokens_per_request: int
    monthly_requests: int
    price_per_million_input: float
    price_per_million_output: float

    @property
    def monthly_input_tokens(self) -> int:
        return self.input_tokens_per_request * self.monthly_requests

    @property
    def monthly_output_tokens(self) -> int:
        return self.output_tokens_per_request * self.monthly_requests

    @property
    def input_cost(self) -> float:
        return (self.monthly_input_tokens / 1_000_000) * self.price_per_million_input

    @property
    def output_cost(self) -> float:
        return (self.monthly_output_tokens / 1_000_000) * self.price_per_million_output

    @property
    def total_monthly_cost(self) -> float:
        return round(self.input_cost + self.output_cost, 2)


def estimate(
    model_id: str,
    input_tokens_per_request: int,
    output_tokens_per_request: int,
    monthly_requests: int,
    price_per_million_input: float | None = None,
    price_per_million_output: float | None = None,
) -> CostEstimate:
    if input_tokens_per_request < 0 or output_tokens_per_request < 0 or monthly_requests < 0:
        raise ValueError("token counts and monthly_requests must be >= 0")

    if price_per_million_input is None or price_per_million_output is None:
        if model_id not in MODEL_PRICING_USD_PER_MILLION_TOKENS:
            raise ValueError(
                f"unknown model_id {model_id!r}; pass --price-in/--price-out explicitly, "
                f"or use one of: {', '.join(MODEL_PRICING_USD_PER_MILLION_TOKENS)}"
            )
        cited = MODEL_PRICING_USD_PER_MILLION_TOKENS[model_id]
        if price_per_million_input is None:
            price_per_million_input = cited["input"]
        if price_per_million_output is None:
            price_per_million_output = cited["output"]

    return CostEstimate(
        model_id=model_id,
        input_tokens_per_request=input_tokens_per_request,
        output_tokens_per_request=output_tokens_per_request,
        monthly_requests=monthly_requests,
        price_per_million_input=price_per_million_input,
        price_per_million_output=price_per_million_output,
    )


def format_report(e: CostEstimate) -> str:
    lines = [
        f"Model                          {e.model_id}",
        f"Input tokens / request         {e.input_tokens_per_request:,}",
        f"Output tokens / request        {e.output_tokens_per_request:,}",
        f"Monthly requests (declared)    {e.monthly_requests:,}",
        "",
        f"Monthly input tokens           {e.monthly_input_tokens:,}",
        f"Monthly output tokens          {e.monthly_output_tokens:,}",
        "",
        f"Input cost   (${e.price_per_million_input:.4f}/1M tok)    ${e.input_cost:,.2f}",
        f"Output cost  (${e.price_per_million_output:.4f}/1M tok)    ${e.output_cost:,.2f}",
        "-" * 52,
        f"TOTAL MONTHLY COST                           ${e.total_monthly_cost:,.2f}",
    ]
    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Deterministic monthly cost estimate for a Bedrock on-demand text workload."
    )
    parser.add_argument(
        "--model", required=True, dest="model_id", help="Bedrock model id, e.g. amazon.nova-lite-v1:0"
    )
    parser.add_argument("--input-tokens", required=True, type=int, dest="input_tokens_per_request")
    parser.add_argument("--output-tokens", required=True, type=int, dest="output_tokens_per_request")
    parser.add_argument("--monthly-requests", required=True, type=int)
    parser.add_argument(
        "--price-in", type=float, default=None, dest="price_per_million_input",
        help="override: USD per 1M input tokens",
    )
    parser.add_argument(
        "--price-out", type=float, default=None, dest="price_per_million_output",
        help="override: USD per 1M output tokens",
    )
    args = parser.parse_args(argv)

    try:
        e = estimate(
            args.model_id,
            args.input_tokens_per_request,
            args.output_tokens_per_request,
            args.monthly_requests,
            args.price_per_million_input,
            args.price_per_million_output,
        )
    except ValueError as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1

    print(format_report(e))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Notice three design decisions, each directly traceable to a previous lesson in this module. First, the three usage inputs (--input-tokens, --output-tokens, --monthly-requests) are required=True — the script refuses to guess a volume, exactly the discipline lesson 6 demanded. Second, MODEL_PRICING_USD_PER_MILLION_TOKENS is a small table, with a comment citing its exact source and verification date — never a "magic" number with no origin. Third, --price-in/--price-out allow overriding the cited price — for the day AWS's price changes, or to model a model that isn't in the table yet, without having to edit the script's source code.


Step 2 — Running the calculator, for real

python3 bedrock_cost_estimate.py \
  --model amazon.nova-lite-v1:0 \
  --input-tokens 800 \
  --output-tokens 150 \
  --monthly-requests 5000

What to expect (literal — run for real, same environment as this guide):

Model                          amazon.nova-lite-v1:0
Input tokens / request         800
Output tokens / request        150
Monthly requests (declared)    5,000

Monthly input tokens           4,000,000
Monthly output tokens          750,000

Input cost   ($0.0600/1M tok)    $0.24
Output cost  ($0.2400/1M tok)    $0.18
----------------------------------------------------
TOTAL MONTHLY COST                           $0.42

This command's three inputs (800 input tokens, 150 output, 5,000 invocations a month) are the initial volume assumption lesson 8 formalizes in GENAI-COST-PROFILE.md — a typical free-text manifest (something like the body of an email describing a shipment) runs around that input size, and a structured five-field response (shipmentId, origin, destination, weight, and a confidence field) runs around that output size. Now compare it against Nova Micro, the catalog's cheapest model, at the exact same volume:

python3 bedrock_cost_estimate.py \
  --model amazon.nova-micro-v1:0 \
  --input-tokens 800 \
  --output-tokens 150 \
  --monthly-requests 5000

What to expect (literal):

Model                          amazon.nova-micro-v1:0
Input tokens / request         800
Output tokens / request        150
Monthly requests (declared)    5,000

Monthly input tokens           4,000,000
Monthly output tokens          750,000

Input cost   ($0.0350/1M tok)    $0.14
Output cost  ($0.1400/1M tok)    $0.11
----------------------------------------------------
TOTAL MONTHLY COST                           $0.25

At the exact same volume, Nova Micro costs $0.25/month against Nova Lite's $0.42/month — a real difference, but, at this volume, neither figure is high. This is a finding worth stating out loud: at the low volume ADR-001 prescribes for an escalation path, Bedrock's per-token cost, even with the most expensive model in this comparison, remains a minimal fraction of any real budget — this module's central concern was never "this is expensive today," it was "nobody had done the math yet."

And, to close the loop with lesson 6, a deliberate attempt to break the script — a model not in the table, with no explicit price:

python3 bedrock_cost_estimate.py \
  --model not-a-real-model \
  --input-tokens 800 \
  --output-tokens 150 \
  --monthly-requests 5000

What to expect (literal):

error: unknown model_id 'not-a-real-model'; pass --price-in/--price-out explicitly, or use one of: amazon.nova-micro-v1:0, amazon.nova-lite-v1:0, amazon.nova-pro-v1:0, amazon.nova-premier-v1:0

The script exits with code 1, without making up a price or assuming a default one — the same antipattern, avoided here in code, lesson 6 named in the abstract: never guess a piece of data only a human can declare.


Step 3 — The pytest suite, fixed cases, zero randomness

scripts/test_bedrock_cost_estimate.py:

"""pytest suite for bedrock_cost_estimate.py -- fixed, deterministic cases only.

No random, no datetime.now(): the same inputs must always produce the same
$ output, every time this file runs, on any machine. Run with:
    pytest scripts/test_bedrock_cost_estimate.py -v
"""

import pytest

from bedrock_cost_estimate import (
    MODEL_PRICING_USD_PER_MILLION_TOKENS,
    estimate,
    format_report,
    main,
)


def test_nova_lite_andes_cargo_baseline():
    """Andes Cargo's declared assumption for extract-shipment-manifest-fields:
    800 input tokens / 150 output tokens per escalated manifest, 5,000
    escalated manifests/month, Amazon Nova Lite on-demand."""
    e = estimate("amazon.nova-lite-v1:0", 800, 150, 5000)
    assert e.monthly_input_tokens == 4_000_000
    assert e.monthly_output_tokens == 750_000
    assert e.input_cost == pytest.approx(0.24)
    assert e.output_cost == pytest.approx(0.18)
    assert e.total_monthly_cost == pytest.approx(0.42)


def test_nova_micro_is_cheaper_than_nova_lite_at_same_volume():
    lite = estimate("amazon.nova-lite-v1:0", 800, 150, 5000)
    micro = estimate("amazon.nova-micro-v1:0", 800, 150, 5000)
    assert micro.total_monthly_cost < lite.total_monthly_cost
    assert micro.total_monthly_cost == pytest.approx(0.25)


def test_cost_scales_linearly_with_declared_volume():
    base = estimate("amazon.nova-lite-v1:0", 800, 150, 5000)
    doubled = estimate("amazon.nova-lite-v1:0", 800, 150, 10000)
    assert doubled.total_monthly_cost == pytest.approx(base.total_monthly_cost * 2)


def test_zero_declared_volume_is_zero_cost():
    e = estimate("amazon.nova-lite-v1:0", 800, 150, 0)
    assert e.total_monthly_cost == 0.0


def test_unknown_model_without_explicit_price_raises():
    with pytest.raises(ValueError):
        estimate("some.unlisted-model", 800, 150, 5000)


def test_unknown_model_with_explicit_price_override_works():
    e = estimate(
        "some.unlisted-model", 1000, 200, 1000,
        price_per_million_input=1.0, price_per_million_output=2.0,
    )
    assert e.input_cost == pytest.approx(1.0)
    assert e.output_cost == pytest.approx(0.4)
    assert e.total_monthly_cost == pytest.approx(1.40)


def test_negative_token_count_is_rejected():
    with pytest.raises(ValueError):
        estimate("amazon.nova-lite-v1:0", -1, 150, 5000)


def test_negative_monthly_requests_is_rejected():
    with pytest.raises(ValueError):
        estimate("amazon.nova-lite-v1:0", 800, 150, -5000)


def test_nova_premier_reference_price_matches_the_cited_figure():
    """Cross-check against the $2.50/$12.50 per 1M token figure cited in
    Module 2, lesson 2, verified directly against the AWS Price List API."""
    pricing = MODEL_PRICING_USD_PER_MILLION_TOKENS["amazon.nova-premier-v1:0"]
    assert pricing["input"] == 2.50
    assert pricing["output"] == 12.50


def test_report_contains_the_total_line():
    e = estimate("amazon.nova-lite-v1:0", 800, 150, 5000)
    report = format_report(e)
    assert "TOTAL MONTHLY COST" in report
    assert "$0.42" in report


def test_cli_end_to_end(capsys):
    exit_code = main(
        [
            "--model", "amazon.nova-lite-v1:0",
            "--input-tokens", "800",
            "--output-tokens", "150",
            "--monthly-requests", "5000",
        ]
    )
    captured = capsys.readouterr()
    assert exit_code == 0
    assert "$0.42" in captured.out


def test_cli_rejects_unknown_model_with_nonzero_exit(capsys):
    exit_code = main(
        [
            "--model", "not-a-real-model",
            "--input-tokens", "800",
            "--output-tokens", "150",
            "--monthly-requests", "5000",
        ]
    )
    captured = capsys.readouterr()
    assert exit_code == 1
    assert "error:" in captured.err

Twelve cases, each testing a specific behavior: Andes Cargo's baseline, the comparison between models, linear scaling with volume, the zero-volume edge case, rejecting an unknown model (with and without an explicit replacement price), rejecting negative inputs, cross-checking Nova Premier's price cited in lesson 2, the report's format, and the command-line interface's complete flow, including its error case.

pytest test_bedrock_cost_estimate.py -v

What to expect (literal — run for real):

============================= test session starts ==============================
platform darwin -- Python 3.13.7, pytest-7.4.4, pluggy-1.6.0
collected 12 items

test_bedrock_cost_estimate.py::test_nova_lite_andes_cargo_baseline PASSED [  8%]
test_bedrock_cost_estimate.py::test_nova_micro_is_cheaper_than_nova_lite_at_same_volume PASSED [ 16%]
test_bedrock_cost_estimate.py::test_cost_scales_linearly_with_declared_volume PASSED [ 25%]
test_bedrock_cost_estimate.py::test_zero_declared_volume_is_zero_cost PASSED [ 33%]
test_bedrock_cost_estimate.py::test_unknown_model_without_explicit_price_raises PASSED [ 41%]
test_bedrock_cost_estimate.py::test_unknown_model_with_explicit_price_override_works PASSED [ 50%]
test_bedrock_cost_estimate.py::test_negative_token_count_is_rejected PASSED [ 58%]
test_bedrock_cost_estimate.py::test_negative_monthly_requests_is_rejected PASSED [ 66%]
test_bedrock_cost_estimate.py::test_nova_premier_reference_price_matches_the_cited_figure PASSED [ 75%]
test_bedrock_cost_estimate.py::test_report_contains_the_total_line PASSED [ 83%]
test_bedrock_cost_estimate.py::test_cli_end_to_end PASSED                [ 91%]
test_bedrock_cost_estimate.py::test_cli_rejects_unknown_model_with_nonzero_exit PASSED [100%]

============================== 12 passed in 0.03s ==============================

Twelve out of twelve, in under a tenth of a second — because there's no network, no disk beyond reading the source code itself, and no external dependency. Run this exact command on your own machine, with the same code: the result should be identical, always, no exceptions. That is, literally, the definition of determinism this lesson promised from its first line.


Why round() matters more than it seems

Notice a real detail that came up while building these test cases, not anticipated in advance: Nova Micro's case ($0.14 input + $0.105 output) mathematically adds up to $0.245 — but the report shows $0.25, not $0.245, because total_monthly_cost rounds to two decimals with Python's round(). This isn't a bug in the script — it's real floating-point arithmetic: 0.14 + 0.105 isn't represented exactly in binary, and Python's round(..., 2) uses "banker's rounding" (to the nearest even) over that imperfect representation. The test_nova_micro_is_cheaper_than_nova_lite_at_same_volume test verifies the real number the script produces (0.25), not the number a pocket calculator with exact decimal precision would have given (0.245) — a small but real lesson on why "run the code and see what it produces" is different from "assume what it should produce."


Common mistakes

Writing a test that verifies a "nice" number instead of the real number the code produces (floating-point-arithmetic-expectation mistake). What happens: someone, writing a new test for their own case, calculates the expected total by hand with a regular calculator and hardcodes it, without running the script first. How to spot it: if your test fails with a cent-level difference you didn't expect. How to fix it: as this lesson showed with the Nova Micro case, floating-point arithmetic can round differently than a hand-done calculation. Run bedrock_cost_estimate.py first, with real --model/--input-tokens/--output-tokens/--monthly-requests, and use the number it actually produces as your test's expected value — never the other way around.

Hardcoding a price directly in a call to estimate(), instead of using the cited table (maintenance mistake). What happens: someone, needing a quick calculation for a new model, writes estimate("amazon.nova-lite-v1:0", 800, 150, 5000, price_per_million_input=0.06, price_per_million_output=0.24) instead of letting the script resolve the price from MODEL_PRICING_USD_PER_MILLION_TOKENS. How to spot it: if your code has a price number written directly, instead of a reference to the model. How to fix it: use --price-in/--price-out (or the equivalent Python parameters) only for models that aren't in the table, or to deliberately test a hypothetical price. For any already-listed model, letting the script resolve the price from the cited table guarantees that, if AWS's price changes and you update the table once, every future calculation automatically uses the correct number.

Confusing "deterministic" with "the price never changes" (word-scope mistake). What happens: someone reads "this calculator is deterministic" and concludes this script's dollar result is a permanent truth. How to spot it: if you cite this lesson's result ($0.42/month) months later without re-verifying Nova Lite's price. How to fix it: determinism, in this lesson, means the same input always produces the same output — not that the input (the price cited in MODEL_PRICING_USD_PER_MILLION_TOKENS) is permanent. The price is marked VARIABLE, exactly as lesson 2 already warned; the script's own comment tells you to re-verify it against aws.amazon.com/bedrock/pricing/ before any real decision.


Exercises

Exercise 1 — Run the calculator with your own volume assumption for extract-shipment-manifest-fields, different from Andes Cargo's. Pick a hypothetical monthly volume (for example, 20,000 invocations/month, a much larger-scale scenario) and run the script with Nova Lite. Does the result scale linearly against the 5,000 case from Step 2, as test_cost_scales_linearly_with_declared_volume predicted?

See solution

Yes — 20,000 invocations is 4 times Step 2's volume of 5,000, so the total cost should also be 4 times $0.42, meaning $1.68/month. This confirms in practice what this lesson's test already verified in code: given the same average invocation size (fixed input and output tokens), total cost scales linearly with declared volume — there's no volume discount and no additional fixed cost in the On-Demand model that would break that proportion.

Exercise 2 — Explain why the script rejects a negative monthly_requests, instead of simply calculating a negative cost. What real problem, beyond "negative numbers don't make sense for a volume," does this validation prevent?

See solution

A negative volume doesn't represent any real business scenario — nobody deliberately declares "-500 invocations a month" —, so, if that value showed up, it's most likely the result of a typo, a bad subtraction in some other system feeding this script, or a confused unit (for example, subtracting one month's volume from another's by mistake). Rejecting the value with an explicit ValueError, instead of just calculating a nonsensical negative total, forces that error to be noticed and fixed the moment it happens, instead of silently propagating into a document like GENAI-COST-PROFILE.md with a number nobody would question at a glance.

Exercise 3 — Predict what would happen if you ran pytest twice in a row, without changing a single line of code. Based on the absence of random and datetime.now() in bedrock_cost_estimate.py, would you expect any difference between the two runs?

See solution

No, no difference — all twelve cases should pass exactly the same way, with the exact same values verified in every assert, on any run, on any machine, at any time. That is, literally, the property that makes this script deterministic: since no calculation depends on an external source of randomness or the system clock, each assert's result depends only on the fixed values each test declares — the same guarantee, applied here to a cost script, you already saw with terraform plan on a new resource in this guide's Module 1 and Module 3.


Summary and next step

This lesson built bedrock_cost_estimate.py, the calculator that solves exactly what lesson 6 left defined as missing: three explicit volume and size inputs, never inferred, combined with the public price cited in lesson 2, to produce a deterministic monthly total. You ran the script for real, over Andes Cargo's initial assumption ($0.42/month with Nova Lite, $0.25/month with Nova Micro, at 5,000 monthly invocations) and confirmed, with twelve pytest cases, that the behavior is correct, including its edge and error cases.

Before moving on you should be able to: explain why the script's three usage inputs are required, with no default value; run the calculator with your own volume assumption and predict whether the result scales linearly; and explain the difference between "deterministic" (same input, same output, always) and "permanent" (the cited price never changes — it does change).

Lesson 8 takes this calculator and uses it to produce the module's final deliverable: GENAI-COST-PROFILE.md, with the chosen model, the declared volume assumption, the real monthly projection, and the honest result of lesson 5's Infracost attempt — all in a single document.

Resources

  1. Python Docs — argparse — reference for the command-line interface this script uses.
  2. Python Docs — round() — reference for the rounding behavior that explains this lesson's difference between $0.245 and $0.25.
  3. pytest — approx — the function used in every test in this lesson to compare floating-point numbers without failures from binary imprecision.
  4. This module, lessons 2 and 6 — the source of the prices cited in MODEL_PRICING_USD_PER_MILLION_TOKENS and of the exact reason this script, not Infracost, solves this problem.