Track Your LLM API Spend with a Python Token Usage Dashboard
Log tokens and cost for every OpenAI, Anthropic, and Gemini call, then chart spend in Streamlit.
What you'll build
A small Python toolkit that logs tokens and dollar cost for every call you make to OpenAI, Anthropic, and Gemini into SQLite, plus a Streamlit dashboard that shows total spend, cost per model, and cost per request — so you can spot which calls are burning your budget.
Prerequisites
- Python 3.10+ (required by both
openaiandgoogle-genai) - API keys for OpenAI, Anthropic, and Gemini — each provider needs its own account
- Verified against:
openai2.41.1,anthropic0.121.0,google-genai2.17.0,streamlit1.61.0, and each provider's official pricing page as of August 2026 - Commands below are for macOS/Linux; on Windows PowerShell, replace
export X="y"with$env:X="y"
Step 1: Install the SDKs and set your keys
mkdir llm-spend && cd llm-spend
python -m venv .venv && source .venv/bin/activate
pip install openai anthropic google-genai streamlit pandas
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export GEMINI_API_KEY="AIza..."
All three SDKs read their key from these exact environment variable names automatically — no key-passing code needed.
Step 2: Write the usage tracker
Create tracker.py. The price table is USD per 1M tokens, taken from each provider's official pricing page (August 2026) — prices change, so re-check them when you add models.
import sqlite3
import time
DB = "usage.db"
# USD per 1M tokens — verify against official pricing pages before trusting the totals
PRICES = {
"gpt-5.6-terra": {"input": 2.00, "output": 12.00},
"claude-opus-5": {"input": 5.00, "output": 25.00},
"gemini-3.6-flash": {"input": 1.50, "output": 7.50},
}
def init_db():
with sqlite3.connect(DB) as con:
con.execute("""CREATE TABLE IF NOT EXISTS usage (
ts REAL, provider TEXT, model TEXT,
input_tokens INTEGER, output_tokens INTEGER, cost_usd REAL)""")
def log_usage(provider, model, input_tokens, output_tokens):
p = PRICES[model]
cost = (input_tokens * p["input"] + output_tokens * p["output"]) / 1_000_000
with sqlite3.connect(DB) as con:
con.execute("INSERT INTO usage VALUES (?, ?, ?, ?, ?, ?)",
(time.time(), provider, model, input_tokens, output_tokens, cost))
return cost
Step 3: Instrument each provider
Create llm.py. Each wrapper makes the call, reads the provider's usage object, and logs it. The field names differ per provider — that's the whole reason to centralize this.
import anthropic
from google import genai
from openai import OpenAI
from tracker import init_db, log_usage
init_db()
openai_client = OpenAI()
anthropic_client = anthropic.Anthropic()
gemini_client = genai.Client()
def ask_openai(prompt: str) -> str:
r = openai_client.responses.create(model="gpt-5.6-terra", input=prompt)
log_usage("openai", "gpt-5.6-terra", r.usage.input_tokens, r.usage.output_tokens)
return r.output_text
def ask_anthropic(prompt: str) -> str:
r = anthropic_client.messages.create(
model="claude-opus-5",
max_tokens=8192, # caps thinking + answer together; don't lowball it
messages=[{"role": "user", "content": prompt}],
)
log_usage("anthropic", "claude-opus-5", r.usage.input_tokens, r.usage.output_tokens)
# thinking blocks can precede the text block, so filter by type
return next(b.text for b in r.content if b.type == "text")
def ask_gemini(prompt: str) -> str:
r = gemini_client.interactions.create(model="gemini-3.6-flash", input=prompt)
# Gemini reports thinking tokens separately but bills them at the output rate
out = r.usage.total_output_tokens + (r.usage.total_thought_tokens or 0)
log_usage("google", "gemini-3.6-flash", r.usage.total_input_tokens, out)
return r.output_text
Claude's usage.output_tokens already includes thinking tokens; Gemini's Interactions API splits them into total_thought_tokens, so we add them back before pricing.
Step 4: Generate some traffic
Create demo.py and run it with python demo.py:
from llm import ask_anthropic, ask_gemini, ask_openai
q = "In one sentence, why is the sky blue?"
for name, fn in [("OpenAI", ask_openai), ("Anthropic", ask_anthropic), ("Gemini", ask_gemini)]:
print(f"{name}: {fn(q)[:100]}")
Step 5: Build the dashboard
Create dashboard.py:
import sqlite3
import pandas as pd
import streamlit as st
st.title("LLM API Spend")
with sqlite3.connect("usage.db") as con:
df = pd.read_sql_query("SELECT * FROM usage", con)
df["ts"] = pd.to_datetime(df["ts"], unit="s")
c1, c2, c3 = st.columns(3)
c1.metric("Total spend", f"${df.cost_usd.sum():.4f}")
c2.metric("Requests", len(df))
c3.metric("Avg cost / request", f"${df.cost_usd.mean():.4f}")
st.subheader("Spend by model")
st.bar_chart(df.groupby("model")["cost_usd"].sum())
st.subheader("Cost per request over time")
st.scatter_chart(df, x="ts", y="cost_usd", color="model")
st.subheader("Raw log")
st.dataframe(df.sort_values("ts", ascending=False))
Launch it:
streamlit run dashboard.py
Verify it works
python demo.py should print one answer per provider, then confirm the rows landed:
python -c "import sqlite3; [print(r) for r in sqlite3.connect('usage.db').execute(
'SELECT provider, model, input_tokens, output_tokens, round(cost_usd, 6) FROM usage')]"
Expected (token counts will vary):
('openai', 'gpt-5.6-terra', 18, 24, 0.000324)
('anthropic', 'claude-opus-5', 16, 310, 0.00783)
('google', 'gemini-3.6-flash', 11, 245, 0.001854)
streamlit run dashboard.py prints Local URL: http://localhost:8501 and opens a page with three metric tiles, a bar chart of spend per model, and a scatter of cost per request. The Anthropic bar will dominate — Claude Opus 5's $25/1M output rate plus thinking tokens is exactly the kind of thing this dashboard exists to surface.
Troubleshooting
openai.AuthenticationError: Error code: 401 ... Incorrect API key provided— the key isn't in this shell.export OPENAI_API_KEY=...in the same terminal you run Python from (env vars don't persist across sessions), then retry.anthropic.AuthenticationError: Error code: 401 ... {'type': 'authentication_error', 'message': 'invalid x-api-key'}— same cause on the Anthropic side:ANTHROPIC_API_KEYis unset or pasted with whitespace. Re-export it.google.genai.errors.ClientError: 400 INVALID_ARGUMENT ... API key not valid. Please pass a valid API key.— Gemini keys come from Google AI Studio, not Google Cloud Console; make sureGEMINI_API_KEYholds an AI Studio key.sqlite3.OperationalError: no such table: usagewhen opening the dashboard — you ran Streamlit before any calls were logged. Runpython demo.pyfirst sousage.dbexists.
Next steps
- Log cached-token counts (
cache_read_input_tokenson Anthropic,total_cached_tokenson Gemini) — cached input is billed at a steep discount, and separating it shows whether prompt caching is actually working. - Set a daily budget: query today's
SUM(cost_usd)before each call and raise if you're over. - For production, ship the rows to Postgres instead of SQLite and tag each one with a feature or user ID so spend maps to product areas.
Sources & further reading
- API Pricing — developers.openai.com
- Developer quickstart — developers.openai.com
- Gemini API Pricing — ai.google.dev
- Interactions API reference — ai.google.dev
- Pricing — platform.claude.com
- 2026 release notes — docs.streamlit.io
Priya covers AI frameworks, developer productivity tooling, and the startup ecosystem across South and Southeast Asia, bringing a researcher's rigour and a practitioner's empathy to every story. She is deeply sceptical of benchmarks and asks hard questions so her readers don't have to.
Discussion 0
No comments yet
Be the first to weigh in.