Dateien
AegisSight-Monitor-Verwaltung/src/routers/token_usage.py
Claude Dev 7cd36959b0 feat: Credits-System — Token-Usage-Router, Budget-Verwaltung, Frontend-Übersicht
- Neuer Router /api/token-usage mit Overview, Org-Detail, Monatsstatistik
- Budget-Felder (credits_total, cost_per_credit, token_budget_usd) bei Lizenz-Erstellung
- Token-Nutzung Sub-Tab in Org-Detail mit Verbrauchsbalken und Monatstabelle
- Dashboard Stat-Card für API-Kosten gesamt
- CSS Dark-Theme Styling für Token-Komponenten
2026-03-17 23:59:49 +01:00

165 Zeilen
6.9 KiB
Python

"""Token-Usage & Budget-Verwaltung."""
import logging
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from auth import get_current_admin
from database import get_db
logger = logging.getLogger("verwaltung.token_usage")
router = APIRouter(prefix="/api/token-usage", tags=["Token-Usage"])
@router.get("/overview")
async def get_usage_overview(admin=Depends(get_current_admin)):
"""Token-Verbrauch aller Organisationen."""
db = await get_db()
try:
cursor = await db.execute("""
SELECT
o.id, o.name, o.slug,
l.credits_total, l.credits_used, l.cost_per_credit,
l.token_budget_usd, l.budget_warning_percent,
COALESCE(SUM(r.total_cost_usd), 0) as total_cost,
COALESCE(SUM(r.input_tokens), 0) as total_input_tokens,
COALESCE(SUM(r.output_tokens), 0) as total_output_tokens,
COALESCE(SUM(r.api_calls), 0) as total_api_calls,
COUNT(r.id) as total_refreshes
FROM organizations o
LEFT JOIN licenses l ON l.organization_id = o.id AND l.status = 'active'
LEFT JOIN refresh_log r ON r.tenant_id = o.id AND r.status = 'completed'
GROUP BY o.id
""")
rows = await cursor.fetchall()
result = []
for row in rows:
credits_total = row["credits_total"] or 0
credits_used = row["credits_used"] or 0
credits_remaining = max(0, int(credits_total - credits_used)) if credits_total else None
percent_used = round((credits_used / credits_total) * 100, 1) if credits_total and credits_total > 0 else None
budget_usd = row["token_budget_usd"]
cost = row["total_cost"]
budget_percent = round((cost / budget_usd) * 100, 1) if budget_usd and budget_usd > 0 else None
result.append({
"org_id": row["id"],
"org_name": row["name"],
"org_slug": row["slug"],
"credits_total": credits_total,
"credits_used": round(credits_used, 1),
"credits_remaining": credits_remaining,
"credits_percent_used": percent_used,
"token_budget_usd": budget_usd,
"total_cost_usd": round(cost, 2),
"budget_percent_used": budget_percent,
"budget_warning_percent": row["budget_warning_percent"] or 80,
"total_input_tokens": row["total_input_tokens"],
"total_output_tokens": row["total_output_tokens"],
"total_api_calls": row["total_api_calls"],
"total_refreshes": row["total_refreshes"],
"cost_per_credit": row["cost_per_credit"],
})
return result
finally:
await db.close()
@router.get("/{org_id}")
async def get_org_usage(org_id: int, admin=Depends(get_current_admin)):
"""Monatliche Token-Nutzung einer Organisation."""
db = await get_db()
try:
cursor = await db.execute(
"SELECT * FROM token_usage_monthly WHERE organization_id = ? ORDER BY year_month DESC",
(org_id,))
rows = await cursor.fetchall()
return [{
"year_month": row["year_month"],
"input_tokens": row["input_tokens"],
"output_tokens": row["output_tokens"],
"cache_creation_tokens": row["cache_creation_tokens"],
"cache_read_tokens": row["cache_read_tokens"],
"total_cost_usd": round(row["total_cost_usd"], 2),
"api_calls": row["api_calls"],
"refresh_count": row["refresh_count"],
} for row in rows]
finally:
await db.close()
@router.get("/{org_id}/current")
async def get_org_current_usage(org_id: int, admin=Depends(get_current_admin)):
"""Aktueller Monat + Budget-Auslastung."""
db = await get_db()
try:
year_month = datetime.now().strftime("%Y-%m")
cursor = await db.execute(
"SELECT * FROM token_usage_monthly WHERE organization_id = ? AND year_month = ?",
(org_id, year_month))
usage = await cursor.fetchone()
cursor = await db.execute(
"SELECT credits_total, credits_used, cost_per_credit, token_budget_usd, budget_warning_percent FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1",
(org_id,))
lic = await cursor.fetchone()
credits_total = lic["credits_total"] if lic else None
credits_used = lic["credits_used"] if lic else 0
return {
"year_month": year_month,
"usage": {
"input_tokens": usage["input_tokens"] if usage else 0,
"output_tokens": usage["output_tokens"] if usage else 0,
"total_cost_usd": round(usage["total_cost_usd"], 2) if usage else 0,
"api_calls": usage["api_calls"] if usage else 0,
"refresh_count": usage["refresh_count"] if usage else 0,
},
"budget": {
"credits_total": credits_total,
"credits_used": round(credits_used, 1) if credits_used else 0,
"credits_remaining": max(0, int(credits_total - credits_used)) if credits_total else None,
"credits_percent_used": round((credits_used / credits_total) * 100, 1) if credits_total and credits_total > 0 else None,
"token_budget_usd": lic["token_budget_usd"] if lic else None,
"cost_per_credit": lic["cost_per_credit"] if lic else None,
"budget_warning_percent": lic["budget_warning_percent"] if lic else 80,
},
}
finally:
await db.close()
@router.put("/budget/{license_id}")
async def update_budget(license_id: int, data: dict, admin=Depends(get_current_admin)):
"""Budget einer Lizenz setzen/ändern."""
db = await get_db()
try:
cursor = await db.execute("SELECT id FROM licenses WHERE id = ?", (license_id,))
if not await cursor.fetchone():
raise HTTPException(status_code=404, detail="Lizenz nicht gefunden")
fields = []
values = []
for key in ("token_budget_usd", "credits_total", "cost_per_credit", "budget_warning_percent"):
if key in data:
fields.append(f"{key} = ?")
values.append(data[key])
if "credits_used" in data:
fields.append("credits_used = ?")
values.append(data["credits_used"])
if not fields:
raise HTTPException(status_code=400, detail="Keine Felder zum Aktualisieren")
values.append(license_id)
await db.execute(f"UPDATE licenses SET {', '.join(fields)} WHERE id = ?", values)
await db.commit()
logger.info(f"Budget für Lizenz {license_id} aktualisiert: {data}")
return {"ok": True}
finally:
await db.close()