nanoClaw/backend/routes/stats.py

138 lines
4.5 KiB
Python

"""Token statistics API routes"""
from datetime import date, timedelta, datetime, timezone
from flask import Blueprint, request, g
from sqlalchemy import func, extract
from backend.models import TokenUsage, Message, Conversation
from backend.utils.helpers import ok, err
from backend import db
bp = Blueprint("stats", __name__)
def _utc_today():
"""Get today's date in UTC to match stored timestamps."""
return datetime.now(timezone.utc).date()
@bp.route("/api/stats/tokens", methods=["GET"])
def token_stats():
"""Get token usage statistics"""
user = g.current_user
period = request.args.get("period", "daily")
today = _utc_today()
if period == "daily":
stats = TokenUsage.query.filter_by(user_id=user.id, date=today).all()
# Hourly breakdown from Message table
hourly = _build_hourly_stats(user.id, today)
result = {
"period": "daily",
"date": today.isoformat(),
"prompt_tokens": sum(s.prompt_tokens for s in stats),
"completion_tokens": sum(s.completion_tokens for s in stats),
"total_tokens": sum(s.total_tokens for s in stats),
"hourly": hourly,
"by_model": {
s.model: {
"prompt": s.prompt_tokens,
"completion": s.completion_tokens,
"total": s.total_tokens
} for s in stats
}
}
elif period == "weekly":
start_date = today - timedelta(days=6)
stats = TokenUsage.query.filter(
TokenUsage.user_id == user.id,
TokenUsage.date >= start_date,
TokenUsage.date <= today
).all()
result = _build_period_result(stats, "weekly", start_date, today, 7)
elif period == "monthly":
start_date = today - timedelta(days=29)
stats = TokenUsage.query.filter(
TokenUsage.user_id == user.id,
TokenUsage.date >= start_date,
TokenUsage.date <= today
).all()
result = _build_period_result(stats, "monthly", start_date, today, 30)
else:
return err(400, "invalid period")
return ok(result)
def _build_period_result(stats, period, start_date, end_date, days):
"""Build result for period-based statistics"""
daily_data = {}
for s in stats:
d = s.date.isoformat()
if d not in daily_data:
daily_data[d] = {"prompt": 0, "completion": 0, "total": 0}
daily_data[d]["prompt"] += s.prompt_tokens
daily_data[d]["completion"] += s.completion_tokens
daily_data[d]["total"] += s.total_tokens
# Fill missing dates
for i in range(days):
d = (end_date - timedelta(days=days - 1 - i)).isoformat()
if d not in daily_data:
daily_data[d] = {"prompt": 0, "completion": 0, "total": 0}
# Aggregate by model
by_model = {}
for s in stats:
if s.model not in by_model:
by_model[s.model] = {"prompt": 0, "completion": 0, "total": 0}
by_model[s.model]["prompt"] += s.prompt_tokens
by_model[s.model]["completion"] += s.completion_tokens
by_model[s.model]["total"] += s.total_tokens
return {
"period": period,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
"prompt_tokens": sum(s.prompt_tokens for s in stats),
"completion_tokens": sum(s.completion_tokens for s in stats),
"total_tokens": sum(s.total_tokens for s in stats),
"daily": daily_data,
"by_model": by_model,
}
def _build_hourly_stats(user_id, day):
"""Build hourly token breakdown for a given day (UTC) from Message table."""
day_start = datetime.combine(day, datetime.min.time()).replace(tzinfo=timezone.utc)
day_end = datetime.combine(day, datetime.max.time()).replace(tzinfo=timezone.utc)
conv_ids = (
db.session.query(Conversation.id)
.filter(Conversation.user_id == user_id)
.subquery()
)
rows = (
db.session.query(
extract("hour", Message.created_at).label("hour"),
func.sum(Message.token_count).label("total"),
)
.filter(
Message.conversation_id.in_(conv_ids),
Message.role == "assistant",
Message.created_at >= day_start,
Message.created_at <= day_end,
)
.group_by(extract("hour", Message.created_at))
.all()
)
hourly = {}
for r in rows:
h = int(r.hour)
hourly[str(h)] = {"total": r.total or 0}
return hourly