By the end of this guide you will have two small Python files: one that converts any pair of token counts into a dollar figure, and one that logs the real cost of every call your project makes to a CSV file you can open in a spreadsheet. From that log you will produce a monthly projection you can actually defend. Budget about forty minutes, most of it spent copying current rates from OpenAI's pricing page and running a handful of test calls.
Counting tokens tells you how big a request is. This guide is the next step in Managing AI API Costs and Tokens: turning that size into money, so you can answer "what will this cost me if a thousand people use it?" with arithmetic instead of a shrug.
Prerequisites
You need Python 3.10 or newer, a working OpenAI API key, and a project folder with a virtual environment. If the environment part is new, Create a Python Virtual Environment for AI walks through it in five minutes. This guide assumes you can already make a successful call — if yours fails on authentication, Fix the 401 Unauthorized Error in OpenAI Python sorts that out first.
Install the two packages this guide uses. Everything else comes from the standard library:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "openai>=1.40" "python-dotenv>=1.0"
pip freeze > requirements.txt
Put your key in a .env file in the project folder:
OPENAI_API_KEY=sk-your-key-here
Then keep it out of version control, because a leaked key is billed to you:
echo ".env" >> .gitignore
Step 1 — Record today's prices in one dict
The single most common mistake in cost code is scattering rate numbers through the script — one in a comment, one hard-coded in a multiplication, one in a spreadsheet nobody opens. Put every rate in one dictionary, in one file, with the date you copied it.
Open OpenAI's pricing page, find the models you actually use, and copy the input and output rates. They are quoted per million tokens — a token being a chunk of text of roughly three-quarters of a word. Do not take rates from this page, from a tutorial, or from memory. Prices move, and a stale number produces a confident, wrong budget.
Create costs.py:
"""costs.py — the only place prices live.
Rates are US dollars per 1,000,000 tokens. Copy them from OpenAI's official
pricing page and update PRICES_CHECKED on the day you do it.
"""
PRICES_CHECKED = "2026-07-24"
PRICES = {
"gpt-4o-mini": {"input": 0.0, "output": 0.0},
"gpt-4o": {"input": 0.0, "output": 0.0},
}
The zeros are deliberate placeholders. In the next step the helper refuses to run while they are still zero, so you cannot silently ship a budget built on nothing. Fill in the four numbers before you go further, and set PRICES_CHECKED to today.
Two details worth noticing while you are on the pricing page. Input and output have separate rates, and the output rate is the higher of the two on every current chat model. And rates differ by model, often by a large multiple between the small and large models in the same family — which is why model is an argument in everything below rather than a constant.
Step 2 — Turn tokens into dollars
Now the arithmetic. One function, three inputs, one dollar figure out. Add this to the bottom of costs.py, below the PRICES dictionary:
def estimate_cost(input_tokens: int, output_tokens: int, model: str = "gpt-4o-mini") -> float:
"""Return the US-dollar cost of one call, given its two token counts."""
try:
rates = PRICES[model]
except KeyError:
raise KeyError(
f"No price recorded for {model!r}. Add it to PRICES in costs.py."
) from None
if rates["input"] == 0.0 and rates["output"] == 0.0:
raise ValueError(
f"Prices for {model!r} are still placeholders. "
f"Copy the current rates from the pricing page first."
)
input_cost = input_tokens / 1_000_000 * rates["input"]
output_cost = output_tokens / 1_000_000 * rates["output"]
return round(input_cost + output_cost, 6)
if __name__ == "__main__":
print(f"1,200 in / 400 out: ${estimate_cost(1200, 400):.6f}")
Run it with python costs.py. Six decimal places is not fussiness — a single short call on a small model can land in the tens of millionths of a dollar, and rounding to cents would show you $0.00 for every request you ever make. Keep the precision until you multiply up to a month.
The two divisions are the part people get wrong. A rate of, say, three dollars per million tokens is 0.000003 dollars per token. Dividing the token count by 1_000_000 first keeps the units obvious and stops you from misplacing a factor of a thousand — the error that turns a fifty-dollar month into a five-cent one on your slide.
Here is the whole pipeline this guide builds, from raw counts to a monthly figure:
If you want an estimate before you spend anything, you need a token count for text you have not sent yet. That is a separate technique, covered in Count Tokens in Python Before You Send; feed its result straight into estimate_cost as the input_tokens argument.
Step 3 — Log what every call really cost
An estimate made before the call is a forecast. The response itself carries the truth: every OpenAI chat response includes a usage object with the exact counts you were billed for. Reading it costs nothing extra and takes one line.
Create log_calls.py next to costs.py:
import csv
import os
from datetime import datetime, timezone
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
from costs import estimate_cost
load_dotenv() # reads .env, which is in .gitignore
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
LOG = Path("usage_log.csv")
FIELDS = ["timestamp", "model", "input_tokens", "output_tokens", "cost_usd", "label"]
def log_row(row: dict) -> None:
"""Append one call to the CSV, writing a header row the first time."""
is_new = not LOG.exists()
with LOG.open("a", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=FIELDS)
if is_new:
writer.writeheader()
writer.writerow(row)
def ask(prompt: str, model: str = "gpt-4o-mini", label: str = "adhoc") -> str:
"""Send one prompt, log its real cost, return the reply text."""
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
max_tokens=400,
)
usage = response.usage
cost = estimate_cost(usage.prompt_tokens, usage.completion_tokens, model)
log_row({
"timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"model": model,
"input_tokens": usage.prompt_tokens,
"output_tokens": usage.completion_tokens,
"cost_usd": f"{cost:.6f}",
"label": label,
})
print(f"{label}: {usage.prompt_tokens} in, {usage.completion_tokens} out, ${cost:.6f}")
return response.choices[0].message.content
if __name__ == "__main__":
ask("Write a one-sentence tagline for a dog-walking app.", label="tagline")
ask("List three reasons a customer might cancel a subscription.", label="churn")
Run python log_calls.py. You get two printed lines and a usage_log.csv you can open in any spreadsheet. The label column is the part people skip and later wish they had: it lets you answer "which feature is eating the budget?" rather than just "how much did I spend?". Give each distinct job in your project its own label from day one.
Notice what the numbers show. Change one call to ask for a long answer and the input count barely moves while the output count multiplies — and output is the more expensive side of the bill. Almost every surprise invoice traces back to that asymmetry, not to prompt length.
If you already cache repeated requests, note that a cache hit never reaches the API and so never appears in this log — you are seeing paid calls only, which is exactly what you want when projecting spend. Cache AI Responses in Python to Cut Costs covers that layer.
Step 4 — Project a month from real calls
A per-call figure of $0.000312 is unpersuasive on its own. What convinces you, or a finance team, is a monthly number derived from calls that actually happened. Create project.py:
import csv
from pathlib import Path
LOG = Path("usage_log.csv")
def project_month(calls_per_day: int, log_path: Path = LOG) -> None:
"""Print a monthly projection from the calls logged so far."""
if not log_path.exists():
raise SystemExit("No usage_log.csv yet — run log_calls.py a few times first.")
rows = list(csv.DictReader(log_path.open(encoding="utf-8")))
if not rows:
raise SystemExit("usage_log.csv has no data rows yet.")
costs = [float(row["cost_usd"]) for row in rows]
inputs = sum(int(row["input_tokens"]) for row in rows)
outputs = sum(int(row["output_tokens"]) for row in rows)
mean = sum(costs) / len(costs)
print(f"sample size : {len(rows)} calls")
print(f"mean cost per call : ${mean:.6f}")
print(f"most expensive call : ${max(costs):.6f}")
print(f"input tokens logged : {inputs:,}")
print(f"output tokens logged : {outputs:,}")
print(f"typical month : ${mean * calls_per_day * 30:.2f}")
print(f"worst-case month : ${max(costs) * calls_per_day * 30:.2f}")
if __name__ == "__main__":
project_month(calls_per_day=200)
Two projections come out, and you should quote both. The typical month uses the mean; the worst-case month assumes every call is as expensive as your priciest logged one. Real usage sits between them, and the distance between the two lines tells you how variable your workload is. A narrow gap means a predictable bill. A wide gap means one long document or one chatty user can double your month, and you should decide in advance what happens when that occurs.
Before you trust the projection, check the sample honestly. If your log contains twenty short prompts but your product also summarises fifty-page PDFs, the mean is fiction. Run each kind of request a few times, keep the labels distinct, and project per label if the shapes differ a lot.
When the projection comes in high, the third lever is usually the fastest win, because output is the expensive side. Lowering max_tokens from 400 to 150 on a summarisation job cuts the dominant half of the bill immediately, and the replies are often better for it. Once you have chosen a number you can live with, Set a Monthly AI API Spending Limit shows how to make your code stop before it exceeds it, rather than merely reporting the damage afterwards.
What each usage field bills for
Every chat response carries these fields on response.usage. Learn what each one means and you will never again guess at a bill.
| Field | What it counts | How it is billed |
|---|---|---|
usage.prompt_tokens | Everything you sent: system message, history, user text | At the model's input rate |
usage.completion_tokens | Only the text the model wrote back | At the higher output rate |
usage.total_tokens | The two above added together | Not billed as a unit — never price with it |
usage.prompt_tokens_details.cached_tokens | The share of input served from the provider's cache | At a reduced input rate; check the pricing page |
The third row is the trap. total_tokens is a convenient single number, so people multiply it by one rate and call it a cost estimate. Because input and output are priced differently, that answer is wrong in whichever direction your traffic leans — and it is wrong by more, not less, as your usage grows.
Troubleshooting
KeyError: No price recorded for 'gpt-4o'— you calledestimate_costwith a model missing fromPRICES. Add an entry for that exact model string, copying both rates from the pricing page. The exact string matters:gpt-4oandgpt-4o-miniare different keys.ValueError: Prices for 'gpt-4o-mini' are still placeholders— the guard from Step 2 is doing its job. Replace the0.0values incosts.pywith today's real rates and updatePRICES_CHECKED.AttributeError: 'NoneType' object has no attribute 'prompt_tokens'— you are streaming the response, and streamed calls omitusageunless you ask for it. Passstream_options={"include_usage": True}and read the usage from the final chunk.FileNotFoundError: usage_log.csv—project.pyran before any call was logged. Runlog_calls.pyfirst; the guard inproject_monthturns this into a readable message rather than a traceback. If tracebacks still throw you, Read a Python Traceback in Five Minutes is worth the detour.- Costs look implausibly small — that is usually correct. Small models are cheap per call, and the number only becomes meaningful once multiplied by monthly volume. Check your arithmetic against a deliberate test: 1,000,000 input tokens should cost exactly the input rate you typed in.
When to use this vs. alternatives
- This CSV approach vs. the provider dashboard. The dashboard shows spend after the fact, aggregated across your whole account, with a reporting delay. Your log shows cost per call, per feature, per model, in real time. Use the dashboard as the authoritative total and your log for every decision about which calls to change.
- This approach vs. estimating before you send. Pre-call counting is the right tool for a gate — refuse or trim a request that is too big before spending anything. Post-call logging is the right tool for a budget, because it uses figures you were actually charged for. Most projects want both, and they share the same
estimate_costfunction. - A CSV file vs. a proper logging service. A CSV is perfect for one developer and a few thousand rows: no setup, opens in a spreadsheet, easy to inspect. Once several people or several machines are writing at once, move to a database or a hosted tool — Log and Monitor AI API Calls in Production covers that transition, and Rate-Limit AI API Calls in a SaaS with Python covers capping spend per customer.
The habit that matters more than any of this code is refreshing your rates. Put a recurring reminder in your calendar, update PRICES and PRICES_CHECKED together, and rerun project.py against your existing log — the projection moves, the code does not. A cost model you maintain for ten minutes a month is worth more than a sophisticated one you wrote once and never touched.
Back to Managing AI API Costs and Tokens.
Related guides
- Count Tokens in Python Before You Send — get the input count that feeds
estimate_costbefore you spend anything. - Cache AI Responses in Python to Cut Costs — the cheapest call is the one you never make twice.
- Set a Monthly AI API Spending Limit — turn your projection into a hard stop your code respects.
- Fix the Context-Length-Exceeded Error in Python — when a prompt is too big to send, never mind too expensive.
- OpenAI vs Anthropic API for Beginners — the same cost maths, with a different usage object to read.