hm_ai_qc_report_tool/modules/usage/routes.py
nickviljoen e910e00edf Add Usage Dashboard with token tracking, cost estimates, and filters
- New UsageLog model tracking every LLM API call (provider, model,
  tokens, estimated cost, user, module, check name)
- Instrument LLMConfig.call_vision_api() to auto-log each call
- New /usage tab in nav bar with dashboard showing:
  - Summary cards (total calls, tokens, estimated cost)
  - Breakdowns by provider, model, tool, and user
  - Recent API calls table
  - Time filters (All Time, 30 Days, 7 Days, Today)
- Cost estimates based on per-model token pricing
- Pass logged-in user through executor context for tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 18:17:21 +02:00

133 lines
4.9 KiB
Python

"""Usage Dashboard Routes."""
import logging
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, jsonify
from sqlalchemy import func, desc
from core.models.usage_log import UsageLog
from core.models.database import db
logger = logging.getLogger(__name__)
usage_bp = Blueprint('usage', __name__, url_prefix='/usage',
template_folder='templates')
@usage_bp.route('/')
@usage_bp.route('/index')
def index():
"""Render usage dashboard."""
return render_template('usage/dashboard.html', active_tab='usage')
@usage_bp.route('/api/stats')
def api_stats():
"""Get usage statistics as JSON, with optional time filter."""
try:
days = request.args.get('days', type=int)
query = UsageLog.query
if days:
cutoff = datetime.utcnow() - timedelta(days=days)
query = query.filter(UsageLog.timestamp >= cutoff)
# Total stats
total_calls = query.count()
total_tokens = db.session.query(func.sum(UsageLog.tokens_used)).filter(
*_time_filter(days)
).scalar() or 0
total_cost = db.session.query(func.sum(UsageLog.estimated_cost_usd)).filter(
*_time_filter(days)
).scalar() or 0
# By provider
by_provider = db.session.query(
UsageLog.provider,
func.count(UsageLog.id).label('calls'),
func.sum(UsageLog.tokens_used).label('tokens'),
func.sum(UsageLog.estimated_cost_usd).label('cost')
).filter(*_time_filter(days)).group_by(UsageLog.provider).all()
# By model
by_model = db.session.query(
UsageLog.provider,
UsageLog.model,
func.count(UsageLog.id).label('calls'),
func.sum(UsageLog.tokens_used).label('tokens'),
func.sum(UsageLog.estimated_cost_usd).label('cost')
).filter(*_time_filter(days)).group_by(UsageLog.provider, UsageLog.model).all()
# By module (tool)
by_module = db.session.query(
UsageLog.module,
func.count(UsageLog.id).label('calls'),
func.sum(UsageLog.tokens_used).label('tokens'),
func.sum(UsageLog.estimated_cost_usd).label('cost')
).filter(*_time_filter(days)).group_by(UsageLog.module).all()
# By user
by_user = db.session.query(
UsageLog.user,
func.count(UsageLog.id).label('calls'),
func.sum(UsageLog.tokens_used).label('tokens'),
func.sum(UsageLog.estimated_cost_usd).label('cost')
).filter(*_time_filter(days)).group_by(UsageLog.user).all()
# Daily usage (last 30 days max)
daily_cutoff = datetime.utcnow() - timedelta(days=min(days or 30, 30))
daily = db.session.query(
func.date(UsageLog.timestamp).label('date'),
func.count(UsageLog.id).label('calls'),
func.sum(UsageLog.tokens_used).label('tokens'),
func.sum(UsageLog.estimated_cost_usd).label('cost')
).filter(UsageLog.timestamp >= daily_cutoff).group_by(
func.date(UsageLog.timestamp)
).order_by(func.date(UsageLog.timestamp)).all()
# Recent calls
recent = query.order_by(desc(UsageLog.timestamp)).limit(20).all()
return jsonify({
'summary': {
'total_calls': total_calls,
'total_tokens': int(total_tokens),
'total_cost': round(float(total_cost), 4)
},
'by_provider': [
{'provider': r.provider, 'calls': r.calls,
'tokens': int(r.tokens or 0), 'cost': round(float(r.cost or 0), 4)}
for r in by_provider
],
'by_model': [
{'provider': r.provider, 'model': r.model, 'calls': r.calls,
'tokens': int(r.tokens or 0), 'cost': round(float(r.cost or 0), 4)}
for r in by_model
],
'by_module': [
{'module': r.module or 'unknown', 'calls': r.calls,
'tokens': int(r.tokens or 0), 'cost': round(float(r.cost or 0), 4)}
for r in by_module
],
'by_user': [
{'user': r.user or 'anonymous', 'calls': r.calls,
'tokens': int(r.tokens or 0), 'cost': round(float(r.cost or 0), 4)}
for r in by_user
],
'daily': [
{'date': str(r.date), 'calls': r.calls,
'tokens': int(r.tokens or 0), 'cost': round(float(r.cost or 0), 4)}
for r in daily
],
'recent': [r.to_dict() for r in recent]
})
except Exception as e:
logger.error(f"Usage stats error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
def _time_filter(days):
"""Build time filter conditions."""
if days:
cutoff = datetime.utcnow() - timedelta(days=days)
return [UsageLog.timestamp >= cutoff]
return []