Skip to content
AI Beginner Tutorial

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.

Priya Nair
Priya Nair
AI & Developer Experience Writer · Aug 9, 2026 · 5 min read
Track Your LLM API Spend with a Python Token Usage Dashboard

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 openai and google-genai)
  • API keys for OpenAI, Anthropic, and Gemini — each provider needs its own account
  • Verified against: openai 2.41.1, anthropic 0.121.0, google-genai 2.17.0, streamlit 1.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_KEY is 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 sure GEMINI_API_KEY holds an AI Studio key.
  • sqlite3.OperationalError: no such table: usage when opening the dashboard — you ran Streamlit before any calls were logged. Run python demo.py first so usage.db exists.

Next steps

  • Log cached-token counts (cache_read_input_tokens on Anthropic, total_cached_tokens on 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

  1. API Pricing — developers.openai.com
  2. Developer quickstart — developers.openai.com
  3. Gemini API Pricing — ai.google.dev
  4. Interactions API reference — ai.google.dev
  5. Pricing — platform.claude.com
  6. 2026 release notes — docs.streamlit.io
Priya Nair
Written by
Priya Nair · AI & Developer Experience Writer

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

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading