ai & tokens

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-19 17:06:01 +00:00
parent 98e68f6bd8
commit e041cb8e65
4 changed files with 962 additions and 66 deletions

View File

@@ -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)

View File

@@ -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},
},
], ],
}) })

View 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 %}

View 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 %}