Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 1 addition & 39 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,8 @@

DB_PATH = Path.home() / ".claude" / "usage.db"

PRICING = {
"claude-opus-4-7": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-opus-4-6": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-opus-4-5": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-sonnet-4-7": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-sonnet-4-6": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-sonnet-4-5": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-haiku-4-7": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
"claude-haiku-4-6": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
"claude-haiku-4-5": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
}

def get_pricing(model):
if not model:
return None
if model in PRICING:
return PRICING[model]
for key in PRICING:
if model.startswith(key):
return PRICING[key]
# Substring fallback: match model family by keyword
m = model.lower()
if "opus" in m:
return PRICING["claude-opus-4-7"]
if "sonnet" in m:
return PRICING["claude-sonnet-4-6"]
if "haiku" in m:
return PRICING["claude-haiku-4-5"]
return None
from pricing import PRICING, get_pricing, calc_cost

def calc_cost(model, inp, out, cache_read, cache_creation):
p = get_pricing(model)
if not p:
return 0.0
return (
inp * p["input"] / 1_000_000 +
out * p["output"] / 1_000_000 +
cache_read * p["cache_read"] / 1_000_000 +
cache_creation * p["cache_write"] / 1_000_000
)

def fmt(n):
if n >= 1_000_000:
Expand Down
25 changes: 13 additions & 12 deletions dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import sqlite3
from http.server import HTTPServer, BaseHTTPRequestHandler
from pathlib import Path

from pricing import PRICING
from datetime import datetime

DB_PATH = Path.home() / ".claude" / "usage.db"
Expand Down Expand Up @@ -418,17 +420,7 @@ def get_dashboard_data(db_path=DB_PATH):
}

// ── Pricing (Anthropic API, April 2026) ────────────────────────────────────
const PRICING = {
'claude-opus-4-7': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
'claude-opus-4-6': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
'claude-opus-4-5': { input: 5.00, output: 25.00, cache_write: 6.25, cache_read: 0.50 },
'claude-sonnet-4-7': { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
'claude-sonnet-4-6': { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
'claude-sonnet-4-5': { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30 },
'claude-haiku-4-7': { input: 1.00, output: 5.00, cache_write: 1.25, cache_read: 0.10 },
'claude-haiku-4-6': { input: 1.00, output: 5.00, cache_write: 1.25, cache_read: 0.10 },
'claude-haiku-4-5': { input: 1.00, output: 5.00, cache_write: 1.25, cache_read: 0.10 },
};
const PRICING = /*__PRICING_JSON__*/;

function isBillable(model) {
if (!model) return false;
Expand Down Expand Up @@ -1237,6 +1229,15 @@ def get_dashboard_data(db_path=DB_PATH):
"""


def render_html():
"""Inject the Python PRICING table into the HTML so the JS table
can never drift from the Python one."""
return HTML_TEMPLATE.replace(
"/*__PRICING_JSON__*/",
json.dumps(PRICING),
).encode("utf-8")


class DashboardHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
Expand All @@ -1246,7 +1247,7 @@ def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML_TEMPLATE.encode("utf-8"))
self.wfile.write(render_html())

elif self.path == "/api/data":
data = get_dashboard_data()
Expand Down
56 changes: 56 additions & 0 deletions pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Anthropic API pricing — single source of truth for both the Python CLI cost
calculator and the JavaScript dashboard.

USD per million tokens. Updated April 2026.
Source: https://docs.claude.com/en/docs/about-claude/pricing
"""

PRICING = {
"claude-opus-4-7": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-opus-4-6": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-opus-4-5": {"input": 5.00, "output": 25.00, "cache_read": 0.50, "cache_write": 6.25},
"claude-sonnet-4-7": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-sonnet-4-6": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-sonnet-4-5": {"input": 3.00, "output": 15.00, "cache_read": 0.30, "cache_write": 3.75},
"claude-haiku-4-7": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
"claude-haiku-4-6": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
"claude-haiku-4-5": {"input": 1.00, "output": 5.00, "cache_read": 0.10, "cache_write": 1.25},
}


def get_pricing(model):
"""Look up per-MTok pricing for a model name.

Tries exact match, then prefix match, then keyword fallback (any name
containing 'opus'/'sonnet'/'haiku' falls back to the latest of that family).
Returns None for unknown / non-Anthropic models so callers can show 'n/a'.
"""
if not model:
return None
if model in PRICING:
return PRICING[model]
for key in PRICING:
if model.startswith(key):
return PRICING[key]
m = model.lower()
if "opus" in m:
return PRICING["claude-opus-4-7"]
if "sonnet" in m:
return PRICING["claude-sonnet-4-6"]
if "haiku" in m:
return PRICING["claude-haiku-4-5"]
return None


def calc_cost(model, inp, out, cache_read, cache_creation):
"""Cost in USD for one batch of token usage. Returns 0 for unknown models."""
p = get_pricing(model)
if p is None:
return 0.0
return (
(inp or 0) * p["input"] / 1_000_000
+ (out or 0) * p["output"] / 1_000_000
+ (cache_read or 0) * p["cache_read"] / 1_000_000
+ (cache_creation or 0) * p["cache_write"] / 1_000_000
)
20 changes: 10 additions & 10 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,16 @@ class TestPricingParity(unittest.TestCase):
"""Verify CLI and dashboard pricing tables stay in sync."""

def _extract_js_pricing(self):
"""Extract pricing values from the dashboard JS PRICING object."""
import re
prices = {}
for match in re.finditer(
r"'(claude-[^']+)':\s*\{\s*input:\s*([\d.]+),\s*output:\s*([\d.]+)",
HTML_TEMPLATE
):
model, inp, out = match.group(1), float(match.group(2)), float(match.group(3))
prices[model] = {"input": inp, "output": out}
return prices
"""Decode the JSON-injected PRICING table from the rendered HTML.
The HTML_TEMPLATE now carries a placeholder; the real table is
injected at request time by render_html()."""
import re, json
from dashboard import render_html
html = render_html().decode("utf-8")
m = re.search(r"const PRICING = (\{.*?\});", html, re.DOTALL)
if not m:
return {}
return json.loads(m.group(1))

def test_all_cli_models_in_dashboard(self):
from cli import PRICING as CLI_PRICING
Expand Down