- 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>
133 lines
4.9 KiB
Python
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 []
|