Module 6: Finops For Tokens
4. Hands-on: the per-token cost calculator, integrated into the gate
Description
scripts/bedrock_cost_estimate.py (Module 2, lesson 7) already deterministically calculates a token workload's projected monthly cost. Until now, it was always run by hand, to write a document (GENAI-COST-PROFILE.md). This lesson adds one new argument, --monthly-budget, and turns it into a real step in the inherited cost-check job (finops-and-cost-guardrails-guide, Module 3) — the first time this script stops being an analysis tool and becomes part of a gate that can stop a merge.
Connection to the module
Lesson 2 explained why no tfplan.json can solve this — there's no resource representing an invocation. Lesson 3 wrote the one piece that can live in cost-policy/ (the allocation tag). This lesson builds the mechanism that resolves what's left: an explicitly declared volume assumption, checked against an explicitly declared budget too — the exact same pattern scripts/check-cost-threshold.sh (finops-and-cost-guardrails-guide, Module 3, lesson 5) already established for Infracost's delta, applied here to a number Infracost could never calculate.
Analogy: the customs officer who rejects a declaration that exceeds what's allowed
You already know the grocery scale from Module 2, lesson 7 — it won't tell you the price until you declare how much you're going to weigh. This lesson adds a second piece to that same scene: imagine that, besides weighing your purchase, the store's system has a spending limit you configured yourself ahead of time ("warn me, or flat-out don't let me pay, if this purchase exceeds $50") — like a prepaid card's overspending warning. The scale (bedrock_cost_estimate.py, unchanged) still weighs and calculates the exact price; what's new is the automatic comparison against the limit you declared, and the refusal to let the purchase through if it exceeds it. --monthly-budget is exactly that limit — a number Andes Cargo declares, not something the script guesses.
Step 1 — --monthly-budget, the only new argument
The complete script is still the same one from Module 2, lesson 7 — same cited price table, same CostEstimate class, same estimate() function, same format_report(). The only thing that changes is main(), with an optional argument added at the end:
parser.add_argument(
"--monthly-budget", type=float, default=None, dest="monthly_budget",
help=(
"optional: fail (exit 1) if total_monthly_cost exceeds this USD amount "
"-- the token budget gate (Module 6, lesson 4)"
),
)
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))
if args.monthly_budget is not None and e.total_monthly_cost > args.monthly_budget:
print(
f"::error::token-budget-check failed: projected monthly cost "
f"${e.total_monthly_cost:,.2f} exceeds the ${args.monthly_budget:,.2f} budget "
f"declared for this volume assumption ({args.monthly_requests:,} requests/month).",
file=sys.stderr,
)
return 1
return 0
Three decisions, each traceable to a piece you already know:
default=None, optional — if nobody passes--monthly-budget, the script behaves exactly as it did in Module 2: it calculates and reports, never failing. This preserves, with no change, every previous use of the script (including the one that producedGENAI-COST-PROFILE.md).e.total_monthly_cost > args.monthly_budget, strictly greater than — the exact same conventioncheck-cost-threshold.shalready established (finops-and-cost-guardrails-guide, Module 3, lesson 5): a cost equal to the budget passes the gate; the threshold marks the limit of what's tolerated, not the first forbidden value.::error::onstderr— the same GitHub Actions annotation formatcheck-cost-threshold.shalready uses, so the message appears as a visible annotation on the step, not just as text lost in a log.
Step 2 — The pytest suite, two new cases
scripts/test_bedrock_cost_estimate.py gains two cases, added at the end of the twelve Module 2 already left:
def test_cli_within_budget_passes(capsys):
"""Module 6, lesson 4: the stress scenario (GENAI-COST-PROFILE.md, section 4,
5,000 escalated manifests/month) stays under a $5.00 declared budget."""
exit_code = main(
[
"--model", "amazon.nova-lite-v1:0",
"--input-tokens", "800",
"--output-tokens", "150",
"--monthly-requests", "5000",
"--monthly-budget", "5.00",
]
)
captured = capsys.readouterr()
assert exit_code == 0
assert "$0.42" in captured.out
def test_cli_over_budget_fails(capsys):
"""Module 6, lesson 6: a deliberately disproportionate volume assumption
(100,000 escalated manifests/month) trips the same $5.00 budget."""
exit_code = main(
[
"--model", "amazon.nova-lite-v1:0",
"--input-tokens", "800",
"--output-tokens", "150",
"--monthly-requests", "100000",
"--monthly-budget", "5.00",
]
)
captured = capsys.readouterr()
assert exit_code == 1
assert "token-budget-check failed" in captured.err
pytest test_bedrock_cost_estimate.py -v
What to expect (literal — really run):
============================= test session starts ==============================
collected 14 items
test_bedrock_cost_estimate.py::test_nova_lite_andes_cargo_baseline PASSED [ 7%]
test_bedrock_cost_estimate.py::test_nova_micro_is_cheaper_than_nova_lite_at_same_volume PASSED [ 14%]
test_bedrock_cost_estimate.py::test_cost_scales_linearly_with_declared_volume PASSED [ 21%]
test_bedrock_cost_estimate.py::test_zero_declared_volume_is_zero_cost PASSED [ 28%]
test_bedrock_cost_estimate.py::test_unknown_model_without_explicit_price_raises PASSED [ 35%]
test_bedrock_cost_estimate.py::test_unknown_model_with_explicit_price_override_works PASSED [ 42%]
test_bedrock_cost_estimate.py::test_negative_token_count_is_rejected PASSED [ 50%]
test_bedrock_cost_estimate.py::test_negative_monthly_requests_is_rejected PASSED [ 57%]
test_bedrock_cost_estimate.py::test_nova_premier_reference_price_matches_the_cited_figure PASSED [ 64%]
test_bedrock_cost_estimate.py::test_report_contains_the_total_line PASSED [ 71%]
test_bedrock_cost_estimate.py::test_cli_end_to_end PASSED [ 78%]
test_bedrock_cost_estimate.py::test_cli_rejects_unknown_model_with_nonzero_exit PASSED [ 85%]
test_bedrock_cost_estimate.py::test_cli_within_budget_passes PASSED [ 92%]
test_bedrock_cost_estimate.py::test_cli_over_budget_fails PASSED [100%]
============================== 14 passed in 0.03s ==============================
Fourteen out of fourteen — the twelve from Module 2, intact, plus this lesson's two new ones. None of the original twelve needed any change: the --monthly-budget argument is additive, default=None, so every previous call to main() still behaves exactly the same.
Step 3 — Running the gate, by hand, against the declared stress scenario
python3 bedrock_cost_estimate.py \
--model amazon.nova-lite-v1:0 \
--input-tokens 800 \
--output-tokens 150 \
--monthly-requests 5000 \
--monthly-budget 5.00
What to expect (literal — really run):
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
Exit code 0 — no error message, exactly the same report Module 2's lesson 8 already produced, now with a declared budget of $5.00 the result ($0.42) stays well under. $5.00 isn't an arbitrary number: it's the same order of magnitude as COST_THRESHOLD_USD (the general Infracost gate's threshold, finops-and-cost-guardrails-guide Module 3) — a deliberate consistency decision between this pipeline's two thresholds, not a coincidence.
Step 4 — The new step, inside cost-check
.github/workflows/ci.yml gains one more step, inside the already-existing cost-check job — never a new job:
- name: Enforce the cost threshold
run: ./scripts/check-cost-threshold.sh before-cost-breakdown.json after-cost-breakdown.json 5.00
+
+ # Module 6, lesson 4 -- the ONE new step this guide adds to the inherited
+ # cost gate. --monthly-requests is the volume declared in
+ # GENAI-COST-PROFILE.md, section 4 (the stress scenario) -- update both in
+ # the same Pull Request, always. See Module 6, lesson 6 for what happens
+ # when the two go out of sync on purpose.
+ - name: Enforce the token budget (scripts/bedrock_cost_estimate.py)
+ run: |
+ python3 scripts/bedrock_cost_estimate.py \
+ --model amazon.nova-lite-v1:0 \
+ --input-tokens 800 \
+ --output-tokens 150 \
+ --monthly-requests 5000 \
+ --monthly-budget 5.00
Notice what did not change: not cost-estimate, not cost-tags, not any security job. cost-check still has needs: cost-estimate, still downloads the same Infracost artifacts, still runs check-cost-threshold.sh first — the new step gets added afterward, as an additional, independent check, over data Infracost never touched. cost-check's two steps answer completely different questions: "did the cost of declared infrastructure rise too much?" (Infracost, HCL) versus "does the declared token volume exceed the budget?" (bedrock_cost_estimate.py, a number no HCL contains) — both run inside the same job because they're, in essence, the same business question ("is this change going to cost more than acceptable?"), applied to two different cost axes.
Why the volume is hardcoded in the YAML, and what that means
It's worth stating this precisely, because it's a design decision, not an oversight: --monthly-requests 5000 is written directly in ci.yml, not read from GENAI-COST-PROFILE.md at runtime. This means the two documents — the YAML and the Markdown — have to stay synchronized by hand, exactly the same kind of discipline finops-and-cost-guardrails-guide already requires between COST-PROFILE.md and infracost-usage.yml (that guide's Module 2, lesson 6): no tool enforces the sync automatically, the responsibility is the team's, on every Pull Request. This module's lesson 6 demonstrates, with real evidence, what happens when someone changes one of the two documents without the other.
Common mistakes
Passing --monthly-budget without first running the script without that argument, to see the real number first (a workflow mistake). What happens: someone, in a hurry, writes a threshold directly without having first seen what the real projected cost is. How to spot it: if your first attempt with --monthly-budget already includes a specific value, without having run the script once without it. How to fix it: the correct discipline — the same this guide's Module 2, lesson 8 already followed — is to run first without --monthly-budget to see the real number, and then decide a budget informed by that number, never the other way around.
Forgetting --monthly-budget is strictly >, not >=, and being surprised that a cost exactly equal to the budget passes (reading the comparison backwards). What happens: someone expects total_monthly_cost == monthly_budget to fail the gate. How to spot it: if you run the script with a cost exactly equal to the declared budget, and the exit code is 0 when you expected 1. How to fix it: the condition is e.total_monthly_cost > args.monthly_budget — the same convention check-cost-threshold.sh already established, and that finops-and-cost-guardrails-guide Module 3, lesson 7, Exercise 1 already precisely explained: the threshold marks the limit of what's tolerated, not the first forbidden value.
Changing GENAI-COST-PROFILE.md without updating ci.yml's --monthly-requests (or vice versa), and not noticing the divergence until the gate gives an unexpected result. What happens: someone updates the volume assumption in the document, but forgets the corresponding number in the YAML. How to spot it: the gate keeps running against a volume that no longer matches what the document declares. How to fix it: the two numbers have to change together, in the same Pull Request — exactly the scenario this module's lesson 6 deliberately demonstrates, with evidence of what happens when they are correctly synced.
Exercises
Exercise 1 — Run the script with GENAI-COST-PROFILE.md's realistic scenario (40 invocations/month) and the same $5.00 budget. Predict the exit code before running it, and check.
See solution
Exit code 0 — the same result Module 2's lesson 8, Step 2 already showed: at 40 invocations/month, the projected cost rounds to $0.00, well under any reasonable budget. python3 bedrock_cost_estimate.py --model amazon.nova-lite-v1:0 --input-tokens 800 --output-tokens 150 --monthly-requests 40 --monthly-budget 5.00 confirms this with no surprises.
Exercise 2 — Calculate, without running the script, the minimum budget (rounded to two decimals) that would let the stress scenario pass (5,000 invocations/month, $0.42) but would fail at 6,000 invocations/month. Verify your calculation by running the script with 6,000 invocations and your proposed budget.
See solution
Cost scales linearly with volume (Module 2, lesson 7, Exercise 1): at 6,000 invocations, the cost would be $0.42 × (6000/5000) = $0.504, which rounds to $0.50. Any budget between $0.42 (inclusive, because the comparison is >) and $0.50 (exclusive) would satisfy the condition — for example, --monthly-budget 0.45 would let 5,000 invocations pass ($0.42 > $0.45 is false) but would fail at 6,000 ($0.50 > $0.45 is true).
Exercise 3 — Explain why cost-check's two steps (Infracost and bedrock_cost_estimate.py) are in the same job, and not in two separate jobs like cost-estimate/cost-tags. Use the "one responsibility, one job" criterion finops-and-cost-guardrails-guide Module 4, lesson 7 already established to justify your answer.
See solution
Both of cost-check's steps answer the same high-level business question — "is this change going to cost more than acceptable?" —, just applied to two different cost axes (declared infrastructure vs. declared token volume). cost-tags, on the other hand, answers a question of a completely different nature — "can this spend be attributed to someone?" —, which is why it lives in its own job, with no needs: toward any other, exactly as finops-and-cost-guardrails-guide Module 4, lesson 7 already explained for tags. Merging the two threshold steps in the same job (cost-check) keeps the "this exceeds what's acceptable" logic together, while keeping cost-tags separate keeps the "this can be billed to someone" logic independent — two questions, two jobs, each with a single responsibility.
Summary and next step
This lesson extended scripts/bedrock_cost_estimate.py with --monthly-budget, an optional argument that fails (exit code 1) if the projected cost exceeds the declared budget — without changing a single line of the script's previous behavior. You confirmed, with real pytest (14 passed), that the twelve original cases stay intact and the two new ones verify the budget's PASS/FAIL. You added exactly one new step to the inherited cost-check job, running against the 5,000 invocations/month volume GENAI-COST-PROFILE.md already declared — PASS, code 0, $0.42 well under $5.00.
Before moving on you should be able to: explain why --monthly-budget is optional and default=None; write from memory the exact condition that triggers the FAIL; and explain why the CI step's volume and GENAI-COST-PROFILE.md's have to be synced by hand.
Lesson 5 closes lesson 3's FAIL: it adds Workload=GenAIExtraction to the real HCL, and confirms, with real conftest, that bedrock-budget.rego goes from FAIL to PASS.
Resources
- Python Docs —
argparse— reference for the optional argument added in this lesson. finops-and-cost-guardrails-guide, Module 3, lesson 5 (check-cost-threshold.sh) — the exact origin of the::error::threshold pattern this lesson reapplies in Python.- This course, Module 2, lesson 7 — the complete origin of
bedrock_cost_estimate.py, extended here without rewriting any of its existing pieces. - pytest —
capsys— the fixture used in this lesson's two new cases to checkstdout/stderrseparately.