Módulo 6: Finops For Tokens
4. Manos a la obra: la calculadora de costo por token, integrada al gate
Descripción
scripts/bedrock_cost_estimate.py (Módulo 2, lección 7) ya calcula, de forma determinista, el costo mensual proyectado de una carga de tokens. Hasta ahora, siempre se corrió a mano, para escribir un documento (GENAI-COST-PROFILE.md). Esta lección le agrega un argumento nuevo, --monthly-budget, y la convierte en un step real del job cost-check heredado (finops-and-cost-guardrails-guide, Módulo 3) — la primera vez que este script deja de ser una herramienta de análisis y se convierte en parte de un gate que puede detener un merge.
Conexión con el módulo
La lección 2 explicó por qué ningún tfplan.json puede resolver esto — no hay recurso que represente una invocación. La lección 3 escribió la única pieza que sí puede vivir en cost-policy/ (el tag de asignación). Esta lección construye el mecanismo que resuelve lo que queda: un supuesto de volumen, declarado explícitamente, verificado contra un presupuesto también declarado explícitamente — el mismo patrón exacto que scripts/check-cost-threshold.sh (finops-and-cost-guardrails-guide, Módulo 3, lección 5) ya estableció para el delta de Infracost, aplicado aquí a un número que Infracost nunca pudo calcular.
Analogía: el guardia de aduana que rechaza una declaración que excede lo permitido
Ya conoces la báscula de verdulería del Módulo 2, lección 7 —no dice el precio hasta que declaras cuánto vas a pesar—. Esta lección agrega una segunda pieza a esa misma escena: imagina que, además de pesar tu compra, el sistema de la tienda tiene un límite de gasto que tú mismo configuraste de antemano ("avísame, o directamente no me dejes pagar, si esta compra supera $50") — como el aviso de gasto excesivo de una tarjeta prepaga. La báscula (bedrock_cost_estimate.py, sin cambios) sigue pesando y calculando el precio exacto; lo nuevo es la comparación automática contra el límite que tú declaraste, y la negativa a dejar pasar la compra si lo supera. --monthly-budget es exactamente ese límite — un número que Andes Cargo declara, no algo que el script adivina.
Paso 1 — --monthly-budget, el único argumento nuevo
El script completo sigue siendo el mismo de la lección 7 del Módulo 2 — misma tabla de precios citada, misma clase CostEstimate, misma función estimate(), mismo format_report(). Lo único que cambia es main(), con un argumento opcional agregado al final:
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
Tres decisiones, cada una trazable a una pieza que ya conoces:
default=None, opcional — si nadie pasa--monthly-budget, el script se comporta exactamente igual que en el Módulo 2: calcula y reporta, sin fallar nunca. Esto preserva, sin ningún cambio, cada uso anterior del script (incluido el que produjoGENAI-COST-PROFILE.md).e.total_monthly_cost > args.monthly_budget, estrictamente mayor — la misma convención exacta quecheck-cost-threshold.shya estableció (finops-and-cost-guardrails-guide, Módulo 3, lección 5): un costo igual al presupuesto pasa el gate; el umbral marca el límite de lo tolerado, no el primer valor prohibido.::error::enstderr— el mismo formato de anotación de GitHub Actions quecheck-cost-threshold.shya usa, para que el mensaje aparezca como una anotación visible sobre el step, no solo como texto perdido en un log.
Paso 2 — El suite de pytest, dos casos nuevos
scripts/test_bedrock_cost_estimate.py gana dos casos, agregados al final de los doce que el Módulo 2 ya dejó:
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
Qué esperar (literal — corrido de verdad):
============================= 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 ==============================
Catorce de catorce — los doce del Módulo 2, intactos, más los dos nuevos de esta lección. Ninguno de los doce originales necesitó ningún cambio: el argumento --monthly-budget es aditivo, default=None, así que cada llamada anterior a main() sigue comportándose exactamente igual.
Paso 3 — Corriendo el gate, a mano, contra el escenario de estrés declarado
python3 bedrock_cost_estimate.py \
--model amazon.nova-lite-v1:0 \
--input-tokens 800 \
--output-tokens 150 \
--monthly-requests 5000 \
--monthly-budget 5.00
Qué esperar (literal — corrido de verdad):
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
Código de salida 0 — sin ningún mensaje de error, exactamente el mismo reporte que la lección 8 del Módulo 2 ya produjo, ahora con un presupuesto de $5,00 declarado que el resultado ($0,42) queda muy por debajo. $5,00 no es un número arbitrario: es el mismo orden de magnitud que COST_THRESHOLD_USD (el umbral del gate general de Infracost, finops-and-cost-guardrails-guide Módulo 3) — una decisión deliberada de consistencia entre los dos umbrales de este pipeline, no una coincidencia.
Paso 4 — El nuevo step, dentro de cost-check
.github/workflows/ci.yml gana un step más, dentro del job cost-check que ya existe —nunca un job nuevo—:
- 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
Fíjate en lo que no cambió: ni cost-estimate, ni cost-tags, ni ningún job de seguridad. cost-check sigue teniendo needs: cost-estimate, sigue descargando los mismos artefactos de Infracost, sigue corriendo check-cost-threshold.sh primero — el step nuevo se agrega después, como una verificación adicional e independiente, sobre un dato que Infracost nunca tocó. Los dos steps de cost-check responden preguntas completamente distintas: "¿el costo de infraestructura declarada subió demasiado?" (Infracost, HCL) frente a "¿el volumen de tokens declarado excede el presupuesto?" (bedrock_cost_estimate.py, un número que ningún HCL contiene) — ambas corren dentro del mismo job porque las dos son, en esencia, la misma pregunta de negocio ("¿este cambio va a costar más de lo aceptable?"), aplicada a dos ejes de costo distintos.
Por qué el volumen está hardcodeado en el YAML, y qué significa eso
Vale la pena decirlo con precisión, porque es una decisión de diseño, no un descuido: --monthly-requests 5000 está escrito directamente en ci.yml, no leído de GENAI-COST-PROFILE.md en tiempo de ejecución. Esto significa que los dos documentos —el YAML y el Markdown— tienen que mantenerse sincronizados a mano, exactamente el mismo tipo de disciplina que finops-and-cost-guardrails-guide ya exige entre COST-PROFILE.md y infracost-usage.yml (Módulo 2, lección 6 de esa guía): ninguna herramienta obliga la sincronía automáticamente, la responsabilidad es del equipo, en cada Pull Request. La lección 6 de este módulo demuestra, con evidencia real, qué pasa cuando alguien cambia uno de los dos documentos sin el otro.
Errores comunes
Pasar --monthly-budget sin haber corrido antes el script sin ese argumento, para ver el número real primero (de flujo de trabajo). Qué pasa: alguien, apurado, escribe directamente un umbral sin haber visto antes cuál es el costo proyectado real. Cómo detectarlo: si tu primer intento con --monthly-budget ya incluye un valor específico, sin haber corrido el script una vez sin él. Cómo corregirlo: la disciplina correcta —la misma que el Módulo 2, lección 8 de esta guía ya siguió— es correr primero sin --monthly-budget para ver el número real, y después decidir un presupuesto informado por ese número, nunca al revés.
Olvidar que --monthly-budget es estrictamente >, no >=, y sorprenderse de que un costo exactamente igual al presupuesto pase (de leer la comparación al revés). Qué pasa: alguien espera que total_monthly_cost == monthly_budget falle el gate. Cómo detectarlo: si corres el script con un costo exactamente igual al presupuesto declarado, y el código de salida es 0 cuando esperabas 1. Cómo corregirlo: la condición es e.total_monthly_cost > args.monthly_budget — la misma convención que check-cost-threshold.sh ya estableció, y que el Ejercicio 1 de finops-and-cost-guardrails-guide Módulo 3, lección 7 ya explicó con precisión: el umbral marca el límite de lo tolerado, no el primer valor prohibido.
Cambiar GENAI-COST-PROFILE.md sin actualizar el --monthly-requests de ci.yml (o viceversa), y no notar la divergencia hasta que el gate da un resultado inesperado. Qué pasa: alguien actualiza el supuesto de volumen en el documento, pero olvida el número correspondiente en el YAML. Cómo detectarlo: el gate sigue corriendo contra un volumen que ya no coincide con lo que el documento declara. Cómo corregirlo: los dos números tienen que cambiar juntos, en el mismo Pull Request — exactamente el escenario que la lección 6 de este módulo demuestra a propósito, con evidencia de lo que pasa cuando sí se sincronizan correctamente.
Ejercicios
Ejercicio 1 — Corre el script con el escenario realista de GENAI-COST-PROFILE.md (40 invocaciones/mes) y el mismo presupuesto de $5,00. Predice el código de salida antes de correrlo, y verifica.
Ver solución
Código de salida 0 — el mismo resultado que el Paso 2 de la lección 8 del Módulo 2 ya mostró: a 40 invocaciones/mes, el costo proyectado redondea a $0,00, muy por debajo de cualquier presupuesto razonable. python3 bedrock_cost_estimate.py --model amazon.nova-lite-v1:0 --input-tokens 800 --output-tokens 150 --monthly-requests 40 --monthly-budget 5.00 confirma esto sin ninguna sorpresa.
Ejercicio 2 — Calcula, sin correr el script, el presupuesto mínimo (redondeado a dos decimales) que dejaría pasar el escenario de estrés (5.000 invocaciones/mes, $0,42) pero fallaría ante 6.000 invocaciones/mes. Verifica tu cálculo corriendo el script con 6.000 invocaciones y tu presupuesto propuesto.
Ver solución
El costo escala linealmente con el volumen (Módulo 2, lección 7, Ejercicio 1): a 6.000 invocaciones, el costo sería $0,42 × (6000/5000) = $0,504, que redondea a $0,50. Cualquier presupuesto entre $0,42 (inclusive, porque la comparación es >) y $0,50 (exclusive) cumpliría la condición — por ejemplo, --monthly-budget 0.45 dejaría pasar 5.000 invocaciones ($0,42 > $0,45 es falso) pero fallaría ante 6.000 ($0,50 > $0,45 es verdadero).
Ejercicio 3 — Explica por qué los dos steps de cost-check (Infracost y bedrock_cost_estimate.py) están en el mismo job, y no en dos jobs separados como cost-estimate/cost-tags. Usa el criterio de "una responsabilidad, un job" que finops-and-cost-guardrails-guide Módulo 4, lección 7 ya estableció para justificar tu respuesta.
Ver solución
Ambos steps de cost-check responden a la misma pregunta de negocio de alto nivel —"¿este cambio va a costar más de lo aceptable?"—, solo que aplicada a dos ejes de costo distintos (infraestructura declarada vs. volumen de tokens declarado). cost-tags, en cambio, responde una pregunta de naturaleza completamente distinta —"¿este gasto se puede atribuir a alguien?"—, razón por la cual vive en su propio job, sin needs: hacia ningún otro, exactamente como finops-and-cost-guardrails-guide Módulo 4, lección 7 ya explicó para tags. Fusionar los dos steps de umbral en el mismo job (cost-check) mantiene junta la lógica de "esto excede lo aceptable", mientras que separar cost-tags mantiene independiente la lógica de "esto se puede facturar a alguien" — dos preguntas, dos jobs, cada uno con una sola responsabilidad.
Resumen y siguiente paso
Esta lección extendió scripts/bedrock_cost_estimate.py con --monthly-budget, un argumento opcional que falla (código de salida 1) si el costo proyectado excede el presupuesto declarado — sin cambiar ni una línea del comportamiento anterior del script. Confirmaste, con pytest real (14 passed), que los doce casos originales siguen intactos y los dos nuevos verifican el PASS/FAIL del presupuesto. Agregaste un solo step nuevo al job cost-check heredado, corriendo contra el volumen de 5.000 invocaciones/mes que GENAI-COST-PROFILE.md ya declaró — PASS, código 0, $0,42 muy por debajo de $5,00.
Antes de avanzar deberías poder: explicar por qué --monthly-budget es opcional y default=None; escribir de memoria la condición exacta que dispara el FAIL; y explicar por qué el volumen del step de CI y el de GENAI-COST-PROFILE.md tienen que sincronizarse a mano.
La lección 5 cierra el FAIL de la lección 3: agrega Workload=GenAIExtraction al HCL real, y confirma, con conftest real, que bedrock-budget.rego pasa de FAIL a PASS.
Recursos
- Python Docs —
argparse— referencia del argumento opcional agregado en esta lección. finops-and-cost-guardrails-guide, Módulo 3, lección 5 (check-cost-threshold.sh) — el origen exacto del patrón de umbral con::error::que esta lección reaplica en Python.- Este curso, Módulo 2, lección 7 — el origen completo de
bedrock_cost_estimate.py, extendido aquí sin reescribir ninguna de sus piezas existentes. - pytest —
capsys— la fixture usada en los dos casos nuevos de esta lección para verificarstdout/stderrpor separado.