ai & tokens
This commit is contained in:
@@ -251,3 +251,325 @@ def data_quality_report(request):
|
|||||||
context.update(admin_context)
|
context.update(admin_context)
|
||||||
|
|
||||||
return render(request, 'admin/reports/data_quality.html', context)
|
return render(request, 'admin/reports/data_quality.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def token_usage_report(request):
|
||||||
|
"""Comprehensive token usage analytics with multi-dimensional insights"""
|
||||||
|
from igny8_core.business.billing.models import CreditUsageLog
|
||||||
|
from igny8_core.auth.models import Account
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Date filter setup
|
||||||
|
days_filter = request.GET.get('days', '30')
|
||||||
|
try:
|
||||||
|
days = int(days_filter)
|
||||||
|
except ValueError:
|
||||||
|
days = 30
|
||||||
|
|
||||||
|
start_date = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
# Base queryset - filter for records with token data
|
||||||
|
logs = CreditUsageLog.objects.filter(
|
||||||
|
created_at__gte=start_date,
|
||||||
|
tokens_input__isnull=False,
|
||||||
|
tokens_output__isnull=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Total statistics
|
||||||
|
total_tokens_input = logs.aggregate(total=Sum('tokens_input'))['total'] or 0
|
||||||
|
total_tokens_output = logs.aggregate(total=Sum('tokens_output'))['total'] or 0
|
||||||
|
total_tokens = total_tokens_input + total_tokens_output
|
||||||
|
total_calls = logs.count()
|
||||||
|
avg_tokens_per_call = total_tokens / total_calls if total_calls > 0 else 0
|
||||||
|
|
||||||
|
# Token usage by model
|
||||||
|
token_by_model = logs.values('model_used').annotate(
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
total_cost=Sum('cost_usd')
|
||||||
|
).order_by('-total_tokens_input')[:10]
|
||||||
|
|
||||||
|
# Add total_tokens to each model and sort by total
|
||||||
|
for model in token_by_model:
|
||||||
|
model['total_tokens'] = (model['total_tokens_input'] or 0) + (model['total_tokens_output'] or 0)
|
||||||
|
model['avg_tokens'] = model['total_tokens'] / model['call_count'] if model['call_count'] > 0 else 0
|
||||||
|
model['model'] = model['model_used'] # Add alias for template
|
||||||
|
token_by_model = sorted(token_by_model, key=lambda x: x['total_tokens'], reverse=True)
|
||||||
|
|
||||||
|
# Token usage by function/operation
|
||||||
|
token_by_function = logs.values('operation_type').annotate(
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
total_cost=Sum('cost_usd')
|
||||||
|
).order_by('-total_tokens_input')[:10]
|
||||||
|
|
||||||
|
# Add total_tokens to each function and sort by total
|
||||||
|
for func in token_by_function:
|
||||||
|
func['total_tokens'] = (func['total_tokens_input'] or 0) + (func['total_tokens_output'] or 0)
|
||||||
|
func['avg_tokens'] = func['total_tokens'] / func['call_count'] if func['call_count'] > 0 else 0
|
||||||
|
func['function'] = func['operation_type'] # Add alias for template
|
||||||
|
token_by_function = sorted(token_by_function, key=lambda x: x['total_tokens'], reverse=True)
|
||||||
|
|
||||||
|
# Token usage by account (top consumers)
|
||||||
|
token_by_account = logs.values('account__name', 'account_id').annotate(
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
total_cost=Sum('cost_usd')
|
||||||
|
).order_by('-total_tokens_input')[:15]
|
||||||
|
|
||||||
|
# Add total_tokens to each account and sort by total
|
||||||
|
for account in token_by_account:
|
||||||
|
account['total_tokens'] = (account['total_tokens_input'] or 0) + (account['total_tokens_output'] or 0)
|
||||||
|
token_by_account = sorted(token_by_account, key=lambda x: x['total_tokens'], reverse=True)[:15]
|
||||||
|
|
||||||
|
# Daily token trends (time series)
|
||||||
|
daily_data = []
|
||||||
|
daily_labels = []
|
||||||
|
for i in range(days):
|
||||||
|
day = timezone.now().date() - timedelta(days=days-i-1)
|
||||||
|
day_logs = logs.filter(created_at__date=day)
|
||||||
|
day_tokens_input = day_logs.aggregate(total=Sum('tokens_input'))['total'] or 0
|
||||||
|
day_tokens_output = day_logs.aggregate(total=Sum('tokens_output'))['total'] or 0
|
||||||
|
day_tokens = day_tokens_input + day_tokens_output
|
||||||
|
daily_labels.append(day.strftime('%m/%d'))
|
||||||
|
daily_data.append(int(day_tokens))
|
||||||
|
|
||||||
|
# Token efficiency metrics (CreditUsageLog doesn't have error field, so assume all successful)
|
||||||
|
success_rate = 100.0
|
||||||
|
successful_tokens = total_tokens
|
||||||
|
wasted_tokens = 0
|
||||||
|
|
||||||
|
# Create tokens_by_status for template compatibility
|
||||||
|
tokens_by_status = [{
|
||||||
|
'error': None,
|
||||||
|
'total_tokens': total_tokens,
|
||||||
|
'call_count': total_calls,
|
||||||
|
'avg_tokens': avg_tokens_per_call
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Peak usage times (hour of day)
|
||||||
|
hourly_usage = logs.extra(
|
||||||
|
select={'hour': "EXTRACT(hour FROM created_at)"}
|
||||||
|
).values('hour').annotate(
|
||||||
|
token_input=Sum('tokens_input'),
|
||||||
|
token_output=Sum('tokens_output'),
|
||||||
|
call_count=Count('id')
|
||||||
|
).order_by('hour')
|
||||||
|
|
||||||
|
# Add total token_count for each hour
|
||||||
|
for hour_data in hourly_usage:
|
||||||
|
hour_data['token_count'] = (hour_data['token_input'] or 0) + (hour_data['token_output'] or 0)
|
||||||
|
|
||||||
|
# Cost efficiency
|
||||||
|
total_cost = logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00')
|
||||||
|
cost_per_1k_tokens = (total_cost / (total_tokens / 1000)) if total_tokens > 0 else Decimal('0.00')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'title': 'Token Usage Report',
|
||||||
|
'days_filter': days,
|
||||||
|
'total_tokens': int(total_tokens),
|
||||||
|
'total_calls': total_calls,
|
||||||
|
'avg_tokens_per_call': round(avg_tokens_per_call, 2),
|
||||||
|
'token_by_model': list(token_by_model),
|
||||||
|
'token_by_function': list(token_by_function),
|
||||||
|
'token_by_account': list(token_by_account),
|
||||||
|
'daily_labels': json.dumps(daily_labels),
|
||||||
|
'daily_data': json.dumps(daily_data),
|
||||||
|
'tokens_by_status': list(tokens_by_status),
|
||||||
|
'success_rate': round(success_rate, 2),
|
||||||
|
'successful_tokens': int(successful_tokens),
|
||||||
|
'wasted_tokens': int(wasted_tokens),
|
||||||
|
'hourly_usage': list(hourly_usage),
|
||||||
|
'total_cost': float(total_cost),
|
||||||
|
'cost_per_1k_tokens': float(cost_per_1k_tokens),
|
||||||
|
'current_app': '_reports', # For active menu state
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge with admin context
|
||||||
|
from igny8_core.admin.site import admin_site
|
||||||
|
admin_context = admin_site.each_context(request)
|
||||||
|
context.update(admin_context)
|
||||||
|
|
||||||
|
return render(request, 'admin/reports/token_usage.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
def ai_cost_analysis(request):
|
||||||
|
"""Multi-dimensional AI cost analysis with model pricing, trends, and predictions"""
|
||||||
|
from igny8_core.business.billing.models import CreditUsageLog
|
||||||
|
from igny8_core.auth.models import Account
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Date filter setup
|
||||||
|
days_filter = request.GET.get('days', '30')
|
||||||
|
try:
|
||||||
|
days = int(days_filter)
|
||||||
|
except ValueError:
|
||||||
|
days = 30
|
||||||
|
|
||||||
|
start_date = timezone.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
# Base queryset - filter for records with cost data
|
||||||
|
logs = CreditUsageLog.objects.filter(
|
||||||
|
created_at__gte=start_date,
|
||||||
|
cost_usd__isnull=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Overall cost metrics
|
||||||
|
total_cost = logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00')
|
||||||
|
total_calls = logs.count()
|
||||||
|
avg_cost_per_call = logs.aggregate(avg=Avg('cost_usd'))['avg'] or Decimal('0.00')
|
||||||
|
total_tokens_input = logs.aggregate(total=Sum('tokens_input'))['total'] or 0
|
||||||
|
total_tokens_output = logs.aggregate(total=Sum('tokens_output'))['total'] or 0
|
||||||
|
total_tokens = total_tokens_input + total_tokens_output
|
||||||
|
|
||||||
|
# Cost by model with efficiency metrics
|
||||||
|
cost_by_model = logs.values('model_used').annotate(
|
||||||
|
total_cost=Sum('cost_usd'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
avg_cost=Avg('cost_usd'),
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output')
|
||||||
|
).order_by('-total_cost')
|
||||||
|
|
||||||
|
# Add cost efficiency (cost per 1K tokens) for each model
|
||||||
|
for model in cost_by_model:
|
||||||
|
model['total_tokens'] = (model['total_tokens_input'] or 0) + (model['total_tokens_output'] or 0)
|
||||||
|
model['avg_tokens'] = model['total_tokens'] / model['call_count'] if model['call_count'] > 0 else 0
|
||||||
|
model['model'] = model['model_used'] # Add alias for template
|
||||||
|
if model['total_tokens'] and model['total_tokens'] > 0:
|
||||||
|
model['cost_per_1k_tokens'] = float(model['total_cost']) / (model['total_tokens'] / 1000)
|
||||||
|
else:
|
||||||
|
model['cost_per_1k_tokens'] = 0
|
||||||
|
|
||||||
|
# Cost by account (top spenders)
|
||||||
|
cost_by_account = logs.values('account__name', 'account_id').annotate(
|
||||||
|
total_cost=Sum('cost_usd'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output'),
|
||||||
|
avg_cost=Avg('cost_usd')
|
||||||
|
).order_by('-total_cost')[:15]
|
||||||
|
|
||||||
|
# Add total_tokens to each account
|
||||||
|
for account in cost_by_account:
|
||||||
|
account['total_tokens'] = (account['total_tokens_input'] or 0) + (account['total_tokens_output'] or 0)
|
||||||
|
|
||||||
|
# Cost by function/operation
|
||||||
|
cost_by_function = logs.values('operation_type').annotate(
|
||||||
|
total_cost=Sum('cost_usd'),
|
||||||
|
call_count=Count('id'),
|
||||||
|
avg_cost=Avg('cost_usd'),
|
||||||
|
total_tokens_input=Sum('tokens_input'),
|
||||||
|
total_tokens_output=Sum('tokens_output')
|
||||||
|
).order_by('-total_cost')[:10]
|
||||||
|
|
||||||
|
# Add total_tokens and function alias
|
||||||
|
for func in cost_by_function:
|
||||||
|
func['total_tokens'] = (func['total_tokens_input'] or 0) + (func['total_tokens_output'] or 0)
|
||||||
|
func['function'] = func['operation_type'] # Add alias for template
|
||||||
|
|
||||||
|
# Daily cost trends (time series)
|
||||||
|
daily_cost_data = []
|
||||||
|
daily_cost_labels = []
|
||||||
|
daily_call_data = []
|
||||||
|
|
||||||
|
for i in range(days):
|
||||||
|
day = timezone.now().date() - timedelta(days=days-i-1)
|
||||||
|
day_logs = logs.filter(created_at__date=day)
|
||||||
|
day_cost = day_logs.aggregate(total=Sum('cost_usd'))['total'] or Decimal('0.00')
|
||||||
|
day_calls = day_logs.count()
|
||||||
|
|
||||||
|
daily_cost_labels.append(day.strftime('%m/%d'))
|
||||||
|
daily_cost_data.append(float(day_cost))
|
||||||
|
daily_call_data.append(day_calls)
|
||||||
|
|
||||||
|
# Cost prediction (simple linear extrapolation)
|
||||||
|
if len(daily_cost_data) > 7:
|
||||||
|
recent_avg_daily = sum(daily_cost_data[-7:]) / 7
|
||||||
|
projected_monthly = recent_avg_daily * 30
|
||||||
|
else:
|
||||||
|
projected_monthly = 0
|
||||||
|
|
||||||
|
# Failed requests cost (CreditUsageLog doesn't track errors, so no failed cost)
|
||||||
|
failed_cost = Decimal('0.00')
|
||||||
|
|
||||||
|
# Cost anomalies (calls costing > 3x average)
|
||||||
|
if avg_cost_per_call > 0:
|
||||||
|
anomaly_threshold = float(avg_cost_per_call) * 3
|
||||||
|
anomalies = logs.filter(cost_usd__gt=anomaly_threshold).values(
|
||||||
|
'model_used', 'operation_type', 'account__name', 'cost_usd', 'tokens_input', 'tokens_output', 'created_at'
|
||||||
|
).order_by('-cost_usd')[:10]
|
||||||
|
# Add aliases and calculate total tokens for each anomaly
|
||||||
|
for anomaly in anomalies:
|
||||||
|
anomaly['model'] = anomaly['model_used']
|
||||||
|
anomaly['function'] = anomaly['operation_type']
|
||||||
|
anomaly['cost'] = anomaly['cost_usd']
|
||||||
|
anomaly['tokens'] = (anomaly['tokens_input'] or 0) + (anomaly['tokens_output'] or 0)
|
||||||
|
else:
|
||||||
|
anomalies = []
|
||||||
|
|
||||||
|
# Model comparison matrix
|
||||||
|
model_comparison = []
|
||||||
|
for model_data in cost_by_model:
|
||||||
|
model_name = model_data['model']
|
||||||
|
model_comparison.append({
|
||||||
|
'model': model_name,
|
||||||
|
'total_cost': float(model_data['total_cost']),
|
||||||
|
'calls': model_data['call_count'],
|
||||||
|
'avg_cost': float(model_data['avg_cost']),
|
||||||
|
'total_tokens': model_data['total_tokens'],
|
||||||
|
'cost_per_1k': model_data['cost_per_1k_tokens'],
|
||||||
|
})
|
||||||
|
|
||||||
|
# Cost distribution percentages
|
||||||
|
if total_cost > 0:
|
||||||
|
for item in cost_by_model:
|
||||||
|
item['cost_percentage'] = float((item['total_cost'] / total_cost) * 100)
|
||||||
|
|
||||||
|
# Peak cost hours
|
||||||
|
hourly_cost = logs.extra(
|
||||||
|
select={'hour': "EXTRACT(hour FROM created_at)"}
|
||||||
|
).values('hour').annotate(
|
||||||
|
total_cost=Sum('cost_usd'),
|
||||||
|
call_count=Count('id')
|
||||||
|
).order_by('hour')
|
||||||
|
|
||||||
|
# Cost efficiency score (CreditUsageLog doesn't track errors, assume all successful)
|
||||||
|
successful_cost = total_cost
|
||||||
|
efficiency_score = 100.0
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'title': 'AI Cost Analysis',
|
||||||
|
'days_filter': days,
|
||||||
|
'total_cost': float(total_cost),
|
||||||
|
'total_calls': total_calls,
|
||||||
|
'avg_cost_per_call': float(avg_cost_per_call),
|
||||||
|
'total_tokens': int(total_tokens),
|
||||||
|
'cost_by_model': list(cost_by_model),
|
||||||
|
'cost_by_account': list(cost_by_account),
|
||||||
|
'cost_by_function': list(cost_by_function),
|
||||||
|
'daily_cost_labels': json.dumps(daily_cost_labels),
|
||||||
|
'daily_cost_data': json.dumps(daily_cost_data),
|
||||||
|
'daily_call_data': json.dumps(daily_call_data),
|
||||||
|
'projected_monthly': round(projected_monthly, 2),
|
||||||
|
'failed_cost': float(failed_cost),
|
||||||
|
'wasted_percentage': float((failed_cost / total_cost * 100) if total_cost > 0 else 0),
|
||||||
|
'anomalies': list(anomalies),
|
||||||
|
'model_comparison': model_comparison,
|
||||||
|
'hourly_cost': list(hourly_cost),
|
||||||
|
'efficiency_score': round(efficiency_score, 2),
|
||||||
|
'successful_cost': float(successful_cost),
|
||||||
|
'current_app': '_reports', # For active menu state
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge with admin context
|
||||||
|
from igny8_core.admin.site import admin_site
|
||||||
|
admin_context = admin_site.each_context(request)
|
||||||
|
context.update(admin_context)
|
||||||
|
|
||||||
|
return render(request, 'admin/reports/ai_cost_analysis.html', context)
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ class Igny8AdminSite(UnfoldAdminSite):
|
|||||||
"""Get admin URLs with dashboard and reports available"""
|
"""Get admin URLs with dashboard and reports available"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .dashboard import admin_dashboard
|
from .dashboard import admin_dashboard
|
||||||
from .reports import revenue_report, usage_report, content_report, data_quality_report
|
from .reports import (
|
||||||
|
revenue_report, usage_report, content_report, data_quality_report,
|
||||||
|
token_usage_report, ai_cost_analysis
|
||||||
|
)
|
||||||
|
|
||||||
urls = super().get_urls()
|
urls = super().get_urls()
|
||||||
custom_urls = [
|
custom_urls = [
|
||||||
@@ -33,6 +36,8 @@ class Igny8AdminSite(UnfoldAdminSite):
|
|||||||
path('reports/usage/', self.admin_view(usage_report), name='report_usage'),
|
path('reports/usage/', self.admin_view(usage_report), name='report_usage'),
|
||||||
path('reports/content/', self.admin_view(content_report), name='report_content'),
|
path('reports/content/', self.admin_view(content_report), name='report_content'),
|
||||||
path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'),
|
path('reports/data-quality/', self.admin_view(data_quality_report), name='report_data_quality'),
|
||||||
|
path('reports/token-usage/', self.admin_view(token_usage_report), name='report_token_usage'),
|
||||||
|
path('reports/ai-cost-analysis/', self.admin_view(ai_cost_analysis), name='report_ai_cost_analysis'),
|
||||||
]
|
]
|
||||||
return custom_urls + urls
|
return custom_urls + urls
|
||||||
|
|
||||||
@@ -125,121 +130,109 @@ class Igny8AdminSite(UnfoldAdminSite):
|
|||||||
# Define our custom groups with their models (using object_name)
|
# Define our custom groups with their models (using object_name)
|
||||||
# Organized by business function - Material icons configured in Unfold
|
# Organized by business function - Material icons configured in Unfold
|
||||||
custom_groups = {
|
custom_groups = {
|
||||||
'Accounts & Users': {
|
'Accounts & Tenancy': {
|
||||||
'models': [
|
'models': [
|
||||||
('igny8_core_auth', 'Account'),
|
('igny8_core_auth', 'Account'),
|
||||||
('igny8_core_auth', 'User'),
|
('igny8_core_auth', 'User'),
|
||||||
('igny8_core_auth', 'Site'),
|
('igny8_core_auth', 'Site'),
|
||||||
('igny8_core_auth', 'Sector'),
|
('igny8_core_auth', 'Sector'),
|
||||||
('igny8_core_auth', 'SiteUserAccess'),
|
('igny8_core_auth', 'SiteUserAccess'),
|
||||||
('igny8_core_auth', 'Plan'),
|
],
|
||||||
('igny8_core_auth', 'Subscription'),
|
},
|
||||||
('igny8_core_auth', 'PasswordResetToken'),
|
'Global Resources': {
|
||||||
|
'models': [
|
||||||
('igny8_core_auth', 'Industry'),
|
('igny8_core_auth', 'Industry'),
|
||||||
('igny8_core_auth', 'IndustrySector'),
|
('igny8_core_auth', 'IndustrySector'),
|
||||||
('igny8_core_auth', 'SeedKeyword'),
|
('igny8_core_auth', 'SeedKeyword'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Billing & Tenancy': {
|
'Plans and Billing': {
|
||||||
'models': [
|
'models': [
|
||||||
|
('igny8_core_auth', 'Plan'),
|
||||||
|
('igny8_core_auth', 'Subscription'),
|
||||||
('billing', 'Invoice'),
|
('billing', 'Invoice'),
|
||||||
('billing', 'Payment'),
|
('billing', 'Payment'),
|
||||||
('billing', 'CreditTransaction'),
|
|
||||||
('billing', 'CreditUsageLog'),
|
|
||||||
('billing', 'CreditPackage'),
|
('billing', 'CreditPackage'),
|
||||||
('billing', 'PaymentMethodConfig'),
|
('billing', 'PaymentMethodConfig'),
|
||||||
('billing', 'AccountPaymentMethod'),
|
('billing', 'AccountPaymentMethod'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Credits': {
|
||||||
|
'models': [
|
||||||
|
('billing', 'CreditTransaction'),
|
||||||
|
('billing', 'CreditUsageLog'),
|
||||||
('billing', 'CreditCostConfig'),
|
('billing', 'CreditCostConfig'),
|
||||||
('billing', 'PlanLimitUsage'),
|
('billing', 'PlanLimitUsage'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Writer Module': {
|
'Content Planning': {
|
||||||
'models': [
|
'models': [
|
||||||
('writer', 'Content'),
|
|
||||||
('writer', 'Tasks'),
|
|
||||||
('writer', 'Images'),
|
|
||||||
('writer', 'ContentTaxonomy'),
|
|
||||||
('writer', 'ContentAttribute'),
|
|
||||||
('writer', 'ContentTaxonomyRelation'),
|
|
||||||
('writer', 'ContentClusterMap'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Planner': {
|
|
||||||
'models': [
|
|
||||||
('planner', 'Clusters'),
|
|
||||||
('planner', 'Keywords'),
|
('planner', 'Keywords'),
|
||||||
|
('planner', 'Clusters'),
|
||||||
('planner', 'ContentIdeas'),
|
('planner', 'ContentIdeas'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Publishing': {
|
'Content Generation': {
|
||||||
'models': [
|
'models': [
|
||||||
|
('writer', 'Tasks'),
|
||||||
|
('writer', 'Content'),
|
||||||
|
('writer', 'Images'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Taxonomy & Organization': {
|
||||||
|
'models': [
|
||||||
|
('writer', 'ContentTaxonomy'),
|
||||||
|
('writer', 'ContentTaxonomyRelation'),
|
||||||
|
('writer', 'ContentClusterMap'),
|
||||||
|
('writer', 'ContentAttribute'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Publishing & Integration': {
|
||||||
|
'models': [
|
||||||
|
('integration', 'SiteIntegration'),
|
||||||
|
('integration', 'SyncEvent'),
|
||||||
('publishing', 'PublishingRecord'),
|
('publishing', 'PublishingRecord'),
|
||||||
|
('system', 'PublishingChannel'),
|
||||||
('publishing', 'DeploymentRecord'),
|
('publishing', 'DeploymentRecord'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Optimization': {
|
'AI & Automation': {
|
||||||
'models': [
|
|
||||||
('optimization', 'OptimizationTask'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Automation': {
|
|
||||||
'models': [
|
'models': [
|
||||||
|
('system', 'IntegrationSettings'),
|
||||||
|
('system', 'AIPrompt'),
|
||||||
|
('system', 'Strategy'),
|
||||||
|
('system', 'AuthorProfile'),
|
||||||
|
('system', 'APIKey'),
|
||||||
|
('system', 'WebhookConfig'),
|
||||||
('automation', 'AutomationConfig'),
|
('automation', 'AutomationConfig'),
|
||||||
('automation', 'AutomationRun'),
|
('automation', 'AutomationRun'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Integration': {
|
'System Settings': {
|
||||||
'models': [
|
'models': [
|
||||||
('integration', 'SiteIntegration'),
|
('contenttypes', 'ContentType'),
|
||||||
('integration', 'SyncEvent'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'AI Framework': {
|
|
||||||
'models': [
|
|
||||||
('ai', 'AITaskLog'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'System Configuration': {
|
|
||||||
'models': [
|
|
||||||
('system', 'AIPrompt'),
|
|
||||||
('system', 'Strategy'),
|
|
||||||
('system', 'AuthorProfile'),
|
|
||||||
('system', 'ContentTemplate'),
|
('system', 'ContentTemplate'),
|
||||||
('system', 'TaxonomyConfig'),
|
('system', 'TaxonomyConfig'),
|
||||||
('system', 'SystemSetting'),
|
('system', 'SystemSetting'),
|
||||||
('system', 'ContentTypeConfig'),
|
('system', 'ContentTypeConfig'),
|
||||||
('system', 'PublishingChannel'),
|
|
||||||
('system', 'APIKey'),
|
|
||||||
('system', 'WebhookConfig'),
|
|
||||||
('system', 'NotificationConfig'),
|
('system', 'NotificationConfig'),
|
||||||
('system', 'AuditLog'),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Celery Results': {
|
'Django Admin': {
|
||||||
'models': [
|
|
||||||
('django_celery_results', 'TaskResult'),
|
|
||||||
('django_celery_results', 'GroupResult'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Content Types': {
|
|
||||||
'models': [
|
|
||||||
('contenttypes', 'ContentType'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Administration': {
|
|
||||||
'models': [
|
|
||||||
('admin', 'LogEntry'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'Authentication and Authorization': {
|
|
||||||
'models': [
|
'models': [
|
||||||
('auth', 'Group'),
|
('auth', 'Group'),
|
||||||
('auth', 'Permission'),
|
('auth', 'Permission'),
|
||||||
|
('igny8_core_auth', 'PasswordResetToken'),
|
||||||
|
('sessions', 'Session'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'Sessions': {
|
'Tasks & Logging': {
|
||||||
'models': [
|
'models': [
|
||||||
('sessions', 'Session'),
|
('ai', 'AITaskLog'),
|
||||||
|
('system', 'AuditLog'),
|
||||||
|
('admin', 'LogEntry'),
|
||||||
|
('django_celery_results', 'TaskResult'),
|
||||||
|
('django_celery_results', 'GroupResult'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -292,6 +285,20 @@ class Igny8AdminSite(UnfoldAdminSite):
|
|||||||
'view_only': True,
|
'view_only': True,
|
||||||
'perms': {'view': True},
|
'perms': {'view': True},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'Token Usage Report',
|
||||||
|
'object_name': 'TokenUsageReport',
|
||||||
|
'admin_url': '/admin/reports/token-usage/',
|
||||||
|
'view_only': True,
|
||||||
|
'perms': {'view': True},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'AI Cost Analysis',
|
||||||
|
'object_name': 'AICostAnalysis',
|
||||||
|
'admin_url': '/admin/reports/ai-cost-analysis/',
|
||||||
|
'view_only': True,
|
||||||
|
'perms': {'view': True},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
349
backend/igny8_core/templates/admin/reports/ai_cost_analysis.html
Normal file
349
backend/igny8_core/templates/admin/reports/ai_cost_analysis.html
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-8 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">AI Cost Analysis</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Comprehensive cost breakdown with model pricing and predictions</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form method="get" class="flex gap-2 items-center">
|
||||||
|
<label class="text-sm text-gray-600 dark:text-gray-400">Time Period:</label>
|
||||||
|
<select name="days" onchange="this.form.submit()" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option value="7" {% if days_filter == 7 %}selected{% endif %}>Last 7 Days</option>
|
||||||
|
<option value="30" {% if days_filter == 30 %}selected{% endif %}>Last 30 Days</option>
|
||||||
|
<option value="60" {% if days_filter == 60 %}selected{% endif %}>Last 60 Days</option>
|
||||||
|
<option value="90" {% if days_filter == 90 %}selected{% endif %}>Last 90 Days</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
|
||||||
|
<div class="bg-gradient-to-br from-blue-500 to-blue-600 rounded-lg shadow-lg p-6 text-white">
|
||||||
|
<h3 class="text-sm font-medium opacity-90 mb-2">Total Cost</h3>
|
||||||
|
<p class="text-3xl font-bold">${{ total_cost|floatformat:2 }}</p>
|
||||||
|
<p class="text-xs opacity-75 mt-1">{{ total_calls }} API calls</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Avg Cost/Call</h3>
|
||||||
|
<p class="text-3xl font-bold text-gray-900 dark:text-white">${{ avg_cost_per_call|floatformat:4 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Projected Monthly</h3>
|
||||||
|
<p class="text-3xl font-bold text-purple-600 dark:text-purple-400">${{ projected_monthly|floatformat:2 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Based on last 7 days</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Efficiency Score</h3>
|
||||||
|
<p class="text-3xl font-bold text-green-600 dark:text-green-400">{{ efficiency_score }}%</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Successful cost ratio</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Wasted Cost</h3>
|
||||||
|
<p class="text-3xl font-bold text-red-600 dark:text-red-400">${{ failed_cost|floatformat:2 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ wasted_percentage|floatformat:1 }}% of total</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cost Trends Chart -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Daily Cost & Volume Trends</h2>
|
||||||
|
<canvas id="costTrendsChart" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Comparison Matrix -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Model Cost Comparison & Efficiency</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Model</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Total Cost</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">% of Total</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">API Calls</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Avg Cost</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Total Tokens</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Cost/1K Tokens</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for model in cost_by_model %}
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
<span class="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
||||||
|
{{ model.model|default:"Unknown" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right font-semibold text-gray-900 dark:text-white">
|
||||||
|
${{ model.total_cost|floatformat:2 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ model.cost_percentage|floatformat:1 }}%
|
||||||
|
<div class="mt-1 w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||||
|
<div class="bg-blue-600 dark:bg-blue-400 h-1.5 rounded-full" style="width: {{ model.cost_percentage }}%"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ model.call_count }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-900 dark:text-white">
|
||||||
|
${{ model.avg_cost|floatformat:4 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ model.total_tokens|floatformat:0 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-900 dark:text-white">
|
||||||
|
${{ model.cost_per_1k_tokens|floatformat:4 }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Two Column Layout -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
|
<!-- Cost by Account -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Top Spenders (By Account)</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Account</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Cost</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Calls</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for account in cost_by_account %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
<a href="/admin/igny8_core_auth/account/{{ account.account_id }}/" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
{{ account.account__name|default:"Unknown" }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right font-semibold text-gray-900 dark:text-white">
|
||||||
|
${{ account.total_cost|floatformat:2 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ account.call_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cost by Function -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Cost by Function/Operation</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Function</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Cost</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Calls</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for func in cost_by_function %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ func.function|default:"Unknown" }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right font-semibold text-gray-900 dark:text-white">
|
||||||
|
${{ func.total_cost|floatformat:2 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ func.call_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cost Anomalies -->
|
||||||
|
{% if anomalies %}
|
||||||
|
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg shadow p-6 mb-8">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-6 w-6 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<h3 class="text-lg font-semibold text-amber-800 dark:text-amber-200 mb-3">Cost Anomalies Detected</h3>
|
||||||
|
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">The following API calls had unusually high costs (>3x average):</p>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-amber-200 dark:divide-amber-800">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Model</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Function</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Account</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Cost</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Tokens</th>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-amber-700 dark:text-amber-300 uppercase">Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-amber-200 dark:divide-amber-800">
|
||||||
|
{% for anomaly in anomalies %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-2 text-sm text-amber-900 dark:text-amber-100">{{ anomaly.model }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-amber-900 dark:text-amber-100">{{ anomaly.function }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-amber-900 dark:text-amber-100">{{ anomaly.account__name }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right font-bold text-amber-900 dark:text-amber-100">${{ anomaly.cost|floatformat:2 }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right text-amber-800 dark:text-amber-200">{{ anomaly.tokens|floatformat:0 }}</td>
|
||||||
|
<td class="px-4 py-2 text-sm text-amber-800 dark:text-amber-200">{{ anomaly.created_at|date:"M d, H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Hourly Cost Distribution -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Peak Cost Hours</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Hour of Day</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Total Cost</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">API Calls</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Activity Level</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for hour in hourly_cost %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{{ hour.hour|floatformat:0 }}:00 - {{ hour.hour|add:1|floatformat:0 }}:00
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-900 dark:text-white">
|
||||||
|
${{ hour.total_cost|floatformat:2 }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{{ hour.call_count }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm">
|
||||||
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
{% widthratio hour.total_cost total_cost 100 as percentage %}
|
||||||
|
<div class="bg-gradient-to-r from-blue-500 to-purple-600 h-2 rounded-full" style="width: {% if percentage > 100 %}100{% else %}{{ percentage }}{% endif %}%"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Cost & Volume Trends Chart
|
||||||
|
const ctx = document.getElementById('costTrendsChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ daily_cost_labels|safe }},
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Daily Cost ($)',
|
||||||
|
data: {{ daily_cost_data|safe }},
|
||||||
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API Calls',
|
||||||
|
data: {{ daily_call_data|safe }},
|
||||||
|
borderColor: 'rgb(168, 85, 247)',
|
||||||
|
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true,
|
||||||
|
yAxisID: 'y1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
interaction: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
if (context.datasetIndex === 0) {
|
||||||
|
label += '$' + context.parsed.y.toFixed(2);
|
||||||
|
} else {
|
||||||
|
label += context.parsed.y;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'left',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Cost ($)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return '$' + value.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: 'linear',
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'API Calls'
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
drawOnChartArea: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
218
backend/igny8_core/templates/admin/reports/token_usage.html
Normal file
218
backend/igny8_core/templates/admin/reports/token_usage.html
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="mb-8 flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Token Usage Report</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Multi-dimensional token consumption analytics</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form method="get" class="flex gap-2 items-center">
|
||||||
|
<label class="text-sm text-gray-600 dark:text-gray-400">Time Period:</label>
|
||||||
|
<select name="days" onchange="this.form.submit()" class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option value="7" {% if days_filter == 7 %}selected{% endif %}>Last 7 Days</option>
|
||||||
|
<option value="30" {% if days_filter == 30 %}selected{% endif %}>Last 30 Days</option>
|
||||||
|
<option value="60" {% if days_filter == 60 %}selected{% endif %}>Last 60 Days</option>
|
||||||
|
<option value="90" {% if days_filter == 90 %}selected{% endif %}>Last 90 Days</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Tokens</h3>
|
||||||
|
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ total_tokens|floatformat:0 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ total_calls }} API calls</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Avg Tokens/Call</h3>
|
||||||
|
<p class="text-3xl font-bold text-gray-900 dark:text-white">{{ avg_tokens_per_call|floatformat:0 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Success Rate</h3>
|
||||||
|
<p class="text-3xl font-bold text-green-600 dark:text-green-400">{{ success_rate }}%</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ successful_tokens|floatformat:0 }} tokens</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Total Cost</h3>
|
||||||
|
<p class="text-3xl font-bold text-gray-900 dark:text-white">${{ total_cost|floatformat:2 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">${{ cost_per_1k_tokens|floatformat:4 }} per 1K</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token Trends Chart -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Token Usage Trends</h2>
|
||||||
|
<canvas id="tokenTrendsChart" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Two Column Layout -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
|
<!-- Token Usage by Model -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Token Usage by Model</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Model</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Tokens</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Calls</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Cost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for item in token_by_model %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">{{ item.model|default:"Unknown" }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-white">{{ item.total_tokens|floatformat:0 }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">{{ item.call_count }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-white">${{ item.total_cost|floatformat:2 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Token Usage by Function -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Token Usage by Function</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Function</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Tokens</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Calls</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for item in token_by_function %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">{{ item.function|default:"Unknown" }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-900 dark:text-white">{{ item.total_tokens|floatformat:0 }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right text-gray-600 dark:text-gray-400">{{ item.call_count }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Token Consumers -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Top Token Consumers (By Account)</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Account</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Tokens</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">API Calls</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Total Cost</th>
|
||||||
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Avg Tokens</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{% for consumer in token_by_account %}
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
<a href="/admin/igny8_core_auth/account/{{ consumer.account_id }}/" class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300">
|
||||||
|
{{ consumer.account__name|default:"Unknown Account" }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-900 dark:text-white font-semibold">{{ consumer.total_tokens|floatformat:0 }}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">{{ consumer.call_count }}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-900 dark:text-white">${{ consumer.total_cost|floatformat:2 }}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||||
|
{% widthratio consumer.total_tokens consumer.call_count 1 %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Efficiency Metrics -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Efficiency Metrics</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Successful Tokens</h3>
|
||||||
|
<p class="text-2xl font-bold text-green-600 dark:text-green-400">{{ successful_tokens|floatformat:0 }}</p>
|
||||||
|
<div class="mt-2 w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div class="bg-green-600 dark:bg-green-400 h-2 rounded-full" style="width: {{ success_rate }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Wasted Tokens</h3>
|
||||||
|
<p class="text-2xl font-bold text-red-600 dark:text-red-400">{{ wasted_tokens|floatformat:0 }}</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">From failed requests</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Hourly Peak Usage</h3>
|
||||||
|
<p class="text-2xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{% for hour in hourly_usage %}
|
||||||
|
{% if forloop.first or hour.token_count > hourly_usage.0.token_count %}
|
||||||
|
{{ hour.hour|floatformat:0 }}:00
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Most active hour</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Token Trends Chart
|
||||||
|
const ctx = document.getElementById('tokenTrendsChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: {{ daily_labels|safe }},
|
||||||
|
datasets: [{
|
||||||
|
label: 'Daily Token Usage',
|
||||||
|
data: {{ daily_data|safe }},
|
||||||
|
borderColor: 'rgb(59, 130, 246)',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user