Add API cost tracking to admin dashboard

Track estimated Anthropic API costs per request across all Claude API
call sites (recommender, analyze, artist-dive, generate-playlist, crate,
rabbit-hole, playlist-fix, timeline, compatibility). Log token usage and
estimated cost to the app logger. Aggregate costs in admin stats endpoint
and display total/today costs and token usage in the admin dashboard.
This commit is contained in:
root
2026-03-31 20:51:51 -05:00
parent 0ee8f9a144
commit f2b8dadbf8
7 changed files with 104 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
import logging
import io
import re
from datetime import datetime, timezone, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -113,6 +114,28 @@ async def get_stats(
for row in user_stats_result.all()
]
# Calculate API costs from log buffer
total_cost = 0.0
today_cost = 0.0
total_tokens_in = 0
total_tokens_out = 0
for log in _log_buffer:
if 'API_COST' in log.get('message', ''):
msg = log['message']
cost_match = re.search(r'cost=\$([0-9.]+)', msg)
if cost_match:
c = float(cost_match.group(1))
total_cost += c
# Check if today
if log['timestamp'][:10] == now.strftime('%Y-%m-%d'):
today_cost += c
input_match = re.search(r'input=(\d+)', msg)
output_match = re.search(r'output=(\d+)', msg)
if input_match:
total_tokens_in += int(input_match.group(1))
if output_match:
total_tokens_out += int(output_match.group(1))
return {
"users": {
"total": total_users,
@@ -131,6 +154,12 @@ async def get_stats(
"saved": saved_recs,
"disliked": disliked_recs,
},
"api_costs": {
"total_estimated": round(total_cost, 4),
"today_estimated": round(today_cost, 4),
"total_input_tokens": total_tokens_in,
"total_output_tokens": total_tokens_out,
},
"user_breakdown": user_breakdown,
}

View File

@@ -1,6 +1,9 @@
import json
import logging
import anthropic
api_logger = logging.getLogger("app")
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
@@ -144,6 +147,12 @@ Return ONLY the JSON. Include exactly 5 recommendations."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user=system|endpoint=compatibility")
try:
text = message.content[0].text.strip()
if text.startswith("```"):

View File

@@ -1,6 +1,9 @@
import json
import logging
import anthropic
api_logger = logging.getLogger("app")
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
@@ -114,6 +117,12 @@ Return ONLY the JSON object."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=playlist_fix")
response_text = message.content[0].text.strip()
# Handle potential markdown code blocks
if response_text.startswith("```"):

View File

@@ -250,6 +250,12 @@ Return ONLY the JSON object."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=artist_dive")
response_text = message.content[0].text.strip()
if response_text.startswith("```"):
response_text = response_text.split("\n", 1)[1]
@@ -346,6 +352,12 @@ Return ONLY the JSON object."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=generate_playlist")
response_text = message.content[0].text.strip()
if response_text.startswith("```"):
response_text = response_text.split("\n", 1)[1]
@@ -464,6 +476,12 @@ Only recommend real songs. Return ONLY the JSON array."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=crate")
response_text = message.content[0].text.strip()
if response_text.startswith("```"):
response_text = response_text.split("\n", 1)[1]
@@ -598,6 +616,12 @@ Only use real songs. Return ONLY the JSON."""
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=rabbit_hole")
response_text = message.content[0].text.strip()
if response_text.startswith("```"):
response_text = response_text.split("\n", 1)[1]

View File

@@ -106,6 +106,13 @@ Return ONLY a valid JSON object with "decades" and "insight" keys. No other text
messages=[{"role": "user", "content": prompt}],
)
# Track API cost (Haiku: $0.80/M input, $4/M output)
api_logger = logging.getLogger("app")
input_tokens = message.usage.input_tokens
output_tokens = message.usage.output_tokens
cost = (input_tokens * 0.80 / 1_000_000) + (output_tokens * 4 / 1_000_000)
api_logger.info(f"API_COST|model=claude-haiku|input={input_tokens}|output={output_tokens}|cost=${cost:.4f}|user={user.id}|endpoint=timeline")
response_text = message.content[0].text.strip()
# Try to extract JSON if wrapped in markdown code blocks