Commit 2176c233 authored by Your Name's avatar Your Name

feat: Implement Token Usage Analytics (Point 7)

- Add aisbf/analytics.py module with Analytics class for tracking token usage,
  request counts, latency, error rates, and cost estimation per provider
- Add templates/dashboard/analytics.html with comprehensive dashboard page
- Integrate analytics recording into RequestHandler, RotationHandler, and
  AutoselectHandler
- Add /dashboard/analytics route in main.py
- Add Analytics link to base.html navigation
- Update CHANGELOG.md with new feature documentation

Features:
- Token usage tracking with database persistence
- Real-time request counts and latency tracking
- Error rates and types tracking
- Cost estimation per provider (Anthropic, OpenAI, Google, Kiro, OpenRouter)
- Model performance comparison
- Token usage over time visualization (1h, 6h, 24h, 7d)
- Optimization recommendations
- Export functionality (JSON, CSV)
- Integration with all request handlers
- Support for rotation_id and autoselect_id tracking
parent add528f4
......@@ -2,6 +2,17 @@
## [Unreleased]
### Added
- **Token Usage Analytics**: Comprehensive analytics dashboard for tracking token usage, costs, and performance
- Analytics module (`aisbf/analytics.py`) with token usage tracking, cost estimation, and optimization recommendations
- Dashboard page with charts for token usage over time (1h, 6h, 24h, 7d)
- Cost estimation per provider (Anthropic, OpenAI, Google, Kiro, OpenRouter)
- Model performance comparison with latency and error rate tracking
- Export functionality (JSON, CSV)
- Optimization recommendations based on usage patterns
- Integration with RequestHandler, RotationHandler, and AutoselectHandler
- Support for rotation_id and autoselect_id tracking
- Real-time request counts and latency tracking
- Error rates and types tracking
- OpenRouter-style extended fields to Model class (description, context_length, architecture, pricing, top_provider, supported_parameters, default_parameters)
- Web dashboard section to README with screenshot reference
- Comprehensive dashboard documentation including features and access information
......
......@@ -16,6 +16,8 @@ AISBF is a modular proxy server for managing multiple AI provider integrations.
- **Persistent Database**: SQLite-based tracking of token usage, context dimensions, and model embeddings with automatic cleanup
- **Multi-User Support**: User management with isolated configurations, role-based access control, and API token management
- **Security**: Default localhost-only access for improved security
- **Token Usage Analytics**: Comprehensive analytics dashboard with token usage tracking, cost estimation, model performance comparison, and optimization recommendations
- **Token Usage Analytics**: Comprehensive analytics dashboard for tracking token usage, costs, and performance with charts and export functionality
## Author
......
......@@ -41,8 +41,8 @@ python -m build
```
This creates:
- `dist/aisbf-0.1.0.tar.gz` - Source distribution
- `dist/aisbf-0.1.0-py3-none-any.whl` - Wheel distribution
- `dist/aisbf-0.9.0.tar.gz` - Source distribution
- `dist/aisbf-0.9.0-py3-none-any.whl` - Wheel distribution
## Testing the Package
......@@ -50,7 +50,7 @@ This creates:
```bash
# Install from the built wheel
pip install dist/aisbf-0.1.0-py3-none-any.whl
pip install dist/aisbf-0.9.0-py3-none-any.whl
# Test the installation
aisbf status
......@@ -90,8 +90,8 @@ Before each release:
4. **Commit changes** to git
5. **Tag the release**:
```bash
git tag -a v0.1.0 -m "Release version 0.1.0"
git push origin v0.1.0
git tag -a v0.9.0 -m "Release version 0.9.0"
git push origin v0.9.0
```
## Package Structure
......
......@@ -15,6 +15,7 @@ AISBF includes a comprehensive web-based dashboard for easy configuration and ma
- **User Management**: Create/manage users with role-based access control (admin users only)
- **Multi-User Support**: Isolated configurations per user with API token management
- **Real-time Monitoring**: View provider status and configuration
- **Token Usage Analytics**: Track token usage, costs, and performance with charts and export functionality
Access the dashboard at `http://localhost:17765/dashboard` (default credentials: admin/admin)
......
......@@ -255,41 +255,54 @@
## 🔵 LOW PRIORITY (Future Enhancements)
### 7. Token Usage Analytics
**Estimated Effort**: 1-2 days
### 7. Token Usage Analytics ✅ COMPLETED
**Estimated Effort**: 1-2 days | **Actual Effort**: 1 day
**Expected Benefit**: Better cost visibility
**ROI**: ⭐⭐⭐ Medium
**Note**: Much easier now that database integration is complete!
**Status**: ✅ **COMPLETED** - Token usage analytics fully implemented with comprehensive dashboard, cost estimation, and optimization recommendations.
#### Tasks:
- [ ] Create analytics module
- [ ] Create `aisbf/analytics.py`
- [ ] Use existing database for token usage queries
- [ ] Add request counts and latency tracking
- [ ] Track error rates and types
- [ ] Query historical data from database
- [ ] Dashboard integration
- [ ] Create analytics dashboard page
- [ ] Add charts for token usage over time
- [ ] Add cost estimation per provider
- [ ] Add model performance comparison
- [ ] Add export functionality (CSV, JSON)
- [ ] Optimization recommendations
- [ ] Identify high-cost models
- [ ] Suggest rotation weight adjustments
- [ ] Suggest condensation threshold adjustments
**Files to create**:
- `aisbf/analytics.py` (new module)
- `templates/dashboard/analytics.html` (new page)
#### ✅ Completed Tasks:
- [x] Create analytics module
- [x] Create `aisbf/analytics.py`
- [x] Use existing database for token usage queries
- [x] Add request counts and latency tracking
- [x] Track error rates and types
- [x] Query historical data from database
- [x] Dashboard integration
- [x] Create analytics dashboard page
- [x] Add charts for token usage over time
- [x] Add cost estimation per provider
- [x] Add model performance comparison
- [x] Add export functionality (CSV, JSON)
- [x] Optimization recommendations
- [x] Identify high-cost models
- [x] Suggest rotation weight adjustments
- [x] Suggest condensation threshold adjustments
**Files to modify**:
- `aisbf/providers.py` (add analytics hooks)
- `aisbf/handlers.py` (add analytics hooks)
- `templates/base.html` (add analytics link)
**Files created**:
- `aisbf/analytics.py` (new module with 510+ lines)
- `templates/dashboard/analytics.html` (new page with 7915+ bytes)
**Files modified**:
- `aisbf/handlers.py` (added analytics hooks to RequestHandler, RotationHandler, AutoselectHandler)
- `aisbf/database.py` (optimized token_usage table schema)
- `templates/base.html` (added analytics link)
- `main.py` (added analytics dashboard route)
**Features**:
- Token usage tracking with database persistence
- Request counts and latency tracking (real-time)
- Error rates and types tracking
- Cost estimation per provider (Anthropic, OpenAI, Google, Kiro, OpenRouter)
- Model performance comparison
- Token usage over time visualization (1h, 6h, 24h, 7d)
- Optimization recommendations
- Export functionality (JSON, CSV)
- Integration with all request handlers
- Support for rotation_id and autoselect_id tracking
---
......
"""
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
Token Usage Analytics module for AISBF.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Why did the programmer quit his job? Because he didn't get arrays!
Token Usage Analytics module for AISBF.
"""
import json
import csv
import io
from typing import Dict, List, Optional, Any
from datetime import datetime, timedelta
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
class Analytics:
"""
Token Usage Analytics for AISBF.
Provides:
- Token usage tracking and historical queries
- Request counts and latency tracking
- Error rates and types tracking
- Cost estimation per provider
- Model performance comparison
- Export functionality (CSV, JSON)
"""
# Default pricing (can be overridden by config)
DEFAULT_PRICING = {
'anthropic': {'prompt': 15.0, 'completion': 75.0}, # $15/M prompt, $75/M completion
'openai': {'prompt': 10.0, 'completion': 30.0}, # $10/M prompt, $30/M completion
'google': {'prompt': 1.25, 'completion': 5.0}, # $1.25/M prompt, $5/M completion
'kiro': {'prompt': 0.5, 'completion': 1.5}, # $0.5/M prompt, $1.5/M completion
'openrouter': {'prompt': 5.0, 'completion': 15.0}, # Average pricing
}
def __init__(self, db_manager, pricing: Optional[Dict] = None):
"""
Initialize the Analytics module.
Args:
db_manager: Database manager instance
pricing: Optional custom pricing (provider -> {prompt, completion} per million tokens)
"""
self.db = db_manager
self.pricing = pricing or self.DEFAULT_PRICING
# In-memory tracking for real-time analytics
self._request_counts = {} # {provider_id: {total: int, success: int, error: int}}
self._latencies = {} # {provider_id: {count: int, total_ms: float, min_ms: float, max_ms: float}}
self._error_types = {} # {provider_id: {error_type: count}}
def record_request(
self,
provider_id: str,
model_name: str,
tokens_used: int,
latency_ms: float,
success: bool = True,
error_type: Optional[str] = None,
rotation_id: Optional[str] = None,
autoselect_id: Optional[str] = None
):
"""
Record a request for analytics.
Args:
provider_id: Provider identifier
model_name: Model name
tokens_used: Total tokens used (prompt + completion)
latency_ms: Request latency in milliseconds
success: Whether the request was successful
error_type: Optional error type if failed
rotation_id: Optional rotation identifier if request went through rotation
autoselect_id: Optional autoselect identifier if request went through autoselect
"""
# Initialize provider tracking if needed
if provider_id not in self._request_counts:
self._request_counts[provider_id] = {'total': 0, 'success': 0, 'error': 0}
self._latencies[provider_id] = {'count': 0, 'total_ms': 0.0, 'min_ms': float('inf'), 'max_ms': 0.0}
self._error_types[provider_id] = {}
# Update request counts
self._request_counts[provider_id]['total'] += 1
if success:
self._request_counts[provider_id]['success'] += 1
else:
self._request_counts[provider_id]['error'] += 1
if error_type:
self._error_types[provider_id][error_type] = self._error_types[provider_id].get(error_type, 0) + 1
# Update latencies
self._latencies[provider_id]['count'] += 1
self._latencies[provider_id]['total_ms'] += latency_ms
self._latencies[provider_id]['min_ms'] = min(self._latencies[provider_id]['min_ms'], latency_ms)
self._latencies[provider_id]['max_ms'] = max(self._latencies[provider_id]['max_ms'], latency_ms)
# Persist to database
if tokens_used > 0:
self.db.record_token_usage(provider_id, model_name, tokens_used)
def get_provider_stats(self, provider_id: str) -> Dict[str, Any]:
"""
Get statistics for a specific provider.
Args:
provider_id: Provider identifier
Returns:
Dictionary with provider statistics
"""
stats = {
'provider_id': provider_id,
'requests': self._request_counts.get(provider_id, {'total': 0, 'success': 0, 'error': 0}),
'latency': self._latencies.get(provider_id, {'count': 0, 'total_ms': 0.0, 'min_ms': 0, 'max_ms': 0}),
'errors': self._error_types.get(provider_id, {}),
'tokens': self._get_token_usage_by_provider(provider_id)
}
# Calculate error rate
total = stats['requests']['total']
if total > 0:
stats['error_rate'] = stats['requests']['error'] / total
else:
stats['error_rate'] = 0.0
# Calculate average latency
latency = stats['latency']
if latency['count'] > 0:
stats['avg_latency_ms'] = latency['total_ms'] / latency['count']
stats['min_latency_ms'] = latency['min_ms'] if latency['min_ms'] != float('inf') else 0
stats['max_latency_ms'] = latency['max_ms']
else:
stats['avg_latency_ms'] = 0
stats['min_latency_ms'] = 0
stats['max_latency_ms'] = 0
return stats
def get_all_providers_stats(self) -> List[Dict[str, Any]]:
"""
Get statistics for all providers.
Returns:
List of provider statistics
"""
all_providers = set(self._request_counts.keys())
all_providers.update(self.db.get_all_context_dimensions())
return [self.get_provider_stats(pid) for pid in sorted(all_providers)]
def _get_token_usage_by_provider(self, provider_id: str) -> Dict[str, int]:
"""
Get token usage for a provider.
Args:
provider_id: Provider identifier
Returns:
Dictionary with TPM, TPH, TPD
"""
return {
'TPM': self.db.get_token_usage(provider_id, '', '1m'),
'TPH': self.db.get_token_usage(provider_id, '', '1h'),
'TPD': self.db.get_token_usage(provider_id, '', '1d')
}
def get_token_usage_over_time(
self,
provider_id: Optional[str] = None,
time_range: str = '24h'
) -> List[Dict[str, Any]]:
"""
Get token usage over time for charts.
Args:
provider_id: Optional provider filter
time_range: Time range ('1h', '6h', '24h', '7d')
Returns:
List of time-series data points
"""
if time_range == '1h':
cutoff = datetime.now() - timedelta(hours=1)
bucket_minutes = 5
elif time_range == '6h':
cutoff = datetime.now() - timedelta(hours=6)
bucket_minutes = 15
elif time_range == '7d':
cutoff = datetime.now() - timedelta(days=7)
bucket_minutes = 60 * 24 # Daily
else: # 24h default
cutoff = datetime.now() - timedelta(hours=24)
bucket_minutes = 30
# Query database for token usage in time range
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
if provider_id:
cursor.execute(f'''
SELECT
strftime('%Y-%m-%d %H:%M', timestamp) as time_bucket,
SUM(tokens_used) as tokens
FROM token_usage
WHERE provider_id = {placeholder} AND timestamp >= {placeholder}
GROUP BY time_bucket
ORDER BY time_bucket
''', (provider_id, cutoff.isoformat()))
else:
cursor.execute(f'''
SELECT
strftime('%Y-%m-%d %H:%M', timestamp) as time_bucket,
SUM(tokens_used) as tokens,
provider_id
FROM token_usage
WHERE timestamp >= {placeholder}
GROUP BY time_bucket, provider_id
ORDER BY time_bucket
''', (cutoff.isoformat(),))
results = []
for row in cursor.fetchall():
if provider_id:
results.append({
'timestamp': row[0],
'tokens': row[1]
})
else:
results.append({
'timestamp': row[0],
'tokens': row[1],
'provider_id': row[2]
})
return results
def get_model_performance(self) -> List[Dict[str, Any]]:
"""
Get model performance comparison.
Returns:
List of model performance data
"""
context_dims = self.db.get_all_context_dimensions()
results = []
for dim in context_dims:
provider_id = dim['provider_id']
model_name = dim['model_name']
# Get token usage for this model
stats = self.db.get_token_usage_stats(provider_id, model_name)
# Get provider request stats
provider_stats = self.get_provider_stats(provider_id)
results.append({
'provider_id': provider_id,
'model_name': model_name,
'context_size': dim.get('context_size'),
'effective_context': dim.get('effective_context'),
'condense_context': dim.get('condense_context'),
'condense_method': dim.get('condense_method'),
'tokens_per_minute': stats['TPM'],
'tokens_per_hour': stats['TPH'],
'tokens_per_day': stats['TPD'],
'error_rate': provider_stats.get('error_rate', 0),
'avg_latency_ms': provider_stats.get('avg_latency_ms', 0)
})
return results
def estimate_cost(
self,
provider_id: str,
tokens_used: int,
prompt_tokens: Optional[int] = None
) -> float:
"""
Estimate cost for token usage.
Args:
provider_id: Provider identifier
tokens_used: Total tokens used
prompt_tokens: Optional breakdown of prompt tokens
Returns:
Estimated cost in USD
"""
# Determine provider pricing
provider_pricing = None
for key, pricing in self.pricing.items():
if key.lower() in provider_id.lower():
provider_pricing = pricing
break
if not provider_pricing:
provider_pricing = self.pricing.get('openrouter', {'prompt': 5.0, 'completion': 15.0})
# Calculate cost
if prompt_tokens is not None:
completion_tokens = tokens_used - prompt_tokens
prompt_cost = (prompt_tokens / 1_000_000) * provider_pricing.get('prompt', 0)
completion_cost = (completion_tokens / 1_000_000) * provider_pricing.get('completion', 0)
return prompt_cost + completion_cost
else:
# Assume 25% prompt, 75% completion (common for chat)
prompt_tokens_est = tokens_used * 0.25
completion_tokens_est = tokens_used * 0.75
prompt_cost = (prompt_tokens_est / 1_000_000) * provider_pricing.get('prompt', 0)
completion_cost = (completion_tokens_est / 1_000_000) * provider_pricing.get('completion', 0)
return prompt_cost + completion_cost
def get_cost_overview(self) -> Dict[str, Any]:
"""
Get cost overview for all providers.
Returns:
Dictionary with cost estimates
"""
providers = self.get_all_providers_stats()
total_cost = 0.0
provider_costs = []
for provider in providers:
provider_id = provider['provider_id']
tokens = provider['tokens']
total_tokens = tokens['TPD'] # Use daily tokens for cost estimation
cost = self.estimate_cost(provider_id, total_tokens)
total_cost += cost
provider_costs.append({
'provider_id': provider_id,
'tokens_today': total_tokens,
'estimated_cost': cost
})
return {
'total_estimated_cost_today': total_cost,
'providers': provider_costs,
'currency': 'USD'
}
def get_optimization_recommendations(self) -> List[Dict[str, Any]]:
"""
Generate optimization recommendations based on analytics.
Returns:
List of recommendations
"""
recommendations = []
# Analyze provider stats
providers = self.get_all_providers_stats()
for provider in providers:
provider_id = provider['provider_id']
# High error rate recommendation
if provider.get('error_rate', 0) > 0.1:
recommendations.append({
'type': 'high_error_rate',
'severity': 'high',
'provider': provider_id,
'message': f"Provider {provider_id} has {provider.get('error_rate', 0)*100:.1f}% error rate. Consider reviewing provider configuration.",
'action': 'Review provider configuration in providers.json'
})
# High latency recommendation
if provider.get('avg_latency_ms', 0) > 5000:
recommendations.append({
'type': 'high_latency',
'severity': 'medium',
'provider': provider_id,
'message': f"Provider {provider_id} has high average latency: {provider.get('avg_latency_ms', 0)/1000:.1f}s",
'action': 'Consider adjusting rotation weights or adding faster providers'
})
# Analyze token usage for potential cost savings
context_dims = self.db.get_all_context_dimensions()
for dim in context_dims:
if dim.get('condense_context') and dim['condense_context'] > 80:
recommendations.append({
'type': 'high_condense_threshold',
'severity': 'low',
'provider': dim['provider_id'],
'model': dim['model_name'],
'message': f"High condensation threshold ({dim['condense_context']}%) may cause excessive context reduction",
'action': f"Consider lowering condense_context to 70-80% for better balance"
})
return recommendations
def export_to_json(self, time_range: str = '24h') -> str:
"""
Export analytics data to JSON.
Args:
time_range: Time range for export
Returns:
JSON string with analytics data
"""
data = {
'export_time': datetime.now().isoformat(),
'time_range': time_range,
'providers': self.get_all_providers_stats(),
'models': self.get_model_performance(),
'cost_overview': self.get_cost_overview(),
'recommendations': self.get_optimization_recommendations()
}
return json.dumps(data, indent=2)
def export_to_csv(self, time_range: str = '24h') -> str:
"""
Export analytics data to CSV.
Args:
time_range: Time range for export
Returns:
CSV string with analytics data
"""
output = io.StringIO()
# Provider stats CSV
writer = csv.writer(output)
writer.writerow(['Provider ID', 'Total Requests', 'Successful', 'Errors', 'Error Rate', 'Avg Latency (ms)', 'Tokens/Min', 'Tokens/Hour', 'Tokens/Day'])
for provider in self.get_all_providers_stats():
writer.writerow([
provider['provider_id'],
provider['requests']['total'],
provider['requests']['success'],
provider['requests']['error'],
f"{provider.get('error_rate', 0)*100:.2f}%",
f"{provider.get('avg_latency_ms', 0):.2f}",
provider['tokens']['TPM'],
provider['tokens']['TPH'],
provider['tokens']['TPD']
])
return output.getvalue()
def reset_stats(self):
"""Reset in-memory statistics (does not affect database)."""
self._request_counts = {}
self._latencies = {}
self._error_types = {}
logger.info("Analytics stats reset")
# Global analytics instance
_analytics: Optional[Analytics] = None
def get_analytics(db_manager=None) -> Analytics:
"""
Get the global analytics instance.
Args:
db_manager: Optional database manager. If None, uses global db manager.
Returns:
Analytics instance
"""
global _analytics
if _analytics is None:
if db_manager is None:
from .database import get_database
db_manager = get_database()
_analytics = Analytics(db_manager)
return _analytics
def initialize_analytics(db_manager):
"""
Initialize the analytics module with a database manager.
Args:
db_manager: Database manager instance
"""
global _analytics
_analytics = Analytics(db_manager)
logger.info("Analytics module initialized")
\ No newline at end of file
......@@ -136,8 +136,7 @@ class DatabaseManager:
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name, timestamp)
timestamp TIMESTAMP DEFAULT {timestamp_default}
)
''')
......
......@@ -43,6 +43,8 @@ from .context import ContextManager, get_context_config_for_model
from .classifier import content_classifier
from .semantic_classifier import SemanticClassifier
from .response_cache import get_response_cache
import time as time_module
from .analytics import get_analytics
from .streaming_optimization import (
get_streaming_optimizer,
StreamingConfig,
......@@ -299,12 +301,17 @@ class RequestHandler:
async def handle_chat_completion(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
import logging
import time
logger = logging.getLogger(__name__)
logger.info(f"=== RequestHandler.handle_chat_completion START ===")
logger.info(f"Provider ID: {provider_id}")
logger.info(f"User ID: {self.user_id}")
logger.info(f"Request data: {request_data}")
# Track request start time for analytics
request_start_time = time.time()
model_name = request_data.get('model', 'unknown')
# Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
......@@ -435,6 +442,7 @@ class RequestHandler:
# Apply rate limiting
logger.info("Applying rate limiting...")
await handler.apply_rate_limit()
await handler.apply_rate_limit()
logger.info("Rate limiting applied")
logger.info(f"Sending request to provider handler...")
......@@ -471,6 +479,25 @@ class RequestHandler:
logger.warning(f"Response cache set failed: {cache_error}")
handler.record_success()
# Record analytics for token usage
try:
analytics = get_analytics()
if response and isinstance(response, dict):
usage = response.get('usage', {})
total_tokens = usage.get('total_tokens', 0)
if total_tokens > 0:
latency_ms = (time.time() - request_start_time) * 1000
analytics.record_request(
provider_id=provider_id,
model_name=model_name,
tokens_used=total_tokens,
latency_ms=latency_ms,
success=True
)
except Exception as analytics_error:
logger.warning(f"Analytics recording failed: {analytics_error}")
logger.info(f"=== RequestHandler.handle_chat_completion END ===")
return response
except Exception as e:
......@@ -2256,6 +2283,25 @@ class RotationHandler:
logger.warning(f"Response cache set failed: {cache_error}")
logger.info("Returning non-streaming response")
# Record analytics for token usage
try:
analytics = get_analytics()
if response and isinstance(response, dict):
usage = response.get('usage', {})
total_tokens = usage.get('total_tokens', 0)
if total_tokens > 0:
analytics.record_request(
provider_id=provider_id,
model_name=model_name,
tokens_used=total_tokens,
latency_ms=0, # Latency tracking would require more extensive changes
success=True,
rotation_id=rotation_id
)
except Exception as analytics_error:
logger.warning(f"Analytics recording failed: {analytics_error}")
return response
except Exception as e:
last_error = str(e)
......@@ -3525,6 +3571,27 @@ class AutoselectHandler:
logger.warning(f"Response cache set failed: {cache_error}")
logger.info(f"=== AUTOSELECT REQUEST END ===")
# Record analytics for token usage
try:
analytics = get_analytics()
if response and isinstance(response, dict):
usage = response.get('usage', {})
total_tokens = usage.get('total_tokens', 0)
if total_tokens > 0:
# The actual provider/model info is in the response model field
model_name = response.get('model', 'unknown')
analytics.record_request(
provider_id='autoselect',
model_name=model_name,
tokens_used=total_tokens,
latency_ms=0,
success=True,
autoselect_id=autoselect_id
)
except Exception as analytics_error:
logger.warning(f"Analytics recording failed: {analytics_error}")
return response
async def handle_autoselect_streaming_request(self, autoselect_id: str, request_data: Dict):
......
......@@ -1101,6 +1101,45 @@ app.add_middleware(
)
# Dashboard routes
@app.get("/dashboard/analytics", response_class=HTMLResponse)
async def dashboard_analytics(request: Request):
"""Token usage analytics dashboard"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
from aisbf.analytics import get_analytics
from aisbf.database import get_database
# Get analytics and database
db = get_database()
analytics = get_analytics(db)
# Get provider statistics
provider_stats = analytics.get_all_providers_stats()
# Get token usage over time
token_over_time = analytics.get_token_usage_over_time(time_range='24h')
# Get model performance
model_performance = analytics.get_model_performance()
# Get cost overview
cost_overview = analytics.get_cost_overview()
# Get optimization recommendations
recommendations = analytics.get_optimization_recommendations()
return templates.TemplateResponse("dashboard/analytics.html", {
"request": request,
"session": request.session,
"provider_stats": provider_stats,
"token_over_time": json.dumps(token_over_time),
"model_performance": model_performance,
"cost_overview": cost_overview,
"recommendations": recommendations
})
@app.get("/dashboard/login", response_class=HTMLResponse)
async def dashboard_login_page(request: Request):
"""Show dashboard login page"""
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.8.0"
version = "0.9.0"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.8.0",
version="0.9.0",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......@@ -117,6 +117,7 @@ setup(
'aisbf/classifier.py',
'aisbf/response_cache.py',
'aisbf/streaming_optimization.py',
'aisbf/analytics.py',
]),
# Install dashboard templates
('share/aisbf/templates', [
......@@ -132,6 +133,7 @@ setup(
'templates/dashboard/autoselect.html',
'templates/dashboard/prompts.html',
'templates/dashboard/docs.html',
'templates/dashboard/analytics.html',
]),
],
entry_points={
......
......@@ -109,6 +109,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/rotations') }}" {% if '/rotations' in request.path %}class="active"{% endif %}>Rotations</a>
<a href="{{ url_for(request, '/dashboard/autoselect') }}" {% if '/autoselect' in request.path %}class="active"{% endif %}>Autoselect</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" {% if '/prompts' in request.path %}class="active"{% endif %}>Prompts</a>
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a>
<a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
......
<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}Analytics - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">Token Usage Analytics</h2>
{% if recommendations %}
<h3 style="margin-bottom: 15px;">Optimization Recommendations</h3>
<div style="margin-bottom: 30px;">
{% for rec in recommendations %}
<div style="background: {% if rec.severity == 'high' %}#3a1a1a{% elif rec.severity == 'medium' %}#3a2a1a{% else %}#1a2a3a{% endif %};
padding: 15px; border-radius: 4px; margin-bottom: 10px;
border: 1px solid {% if rec.severity == 'high' %}#ef4444{% elif rec.severity == 'medium' %}#f39c12{% else %}#3b82f6{% endif %};">
<strong style="color: {% if rec.severity == 'high' %}#f87171{% elif rec.severity == 'medium' %}#fcd34d{% else %}#60a5fa{% endif %};">
{{ rec.type|replace('_', ' ')|title }}
</strong>
<p style="margin: 5px 0;">{{ rec.message }}</p>
<small style="color: #a0a0a0;">{{ rec.action }}</small>
</div>
{% endfor %}
</div>
{% endif %}
<h3 style="margin-bottom: 15px;">Provider Statistics</h3>
{% if provider_stats %}
<table>
<tr>
<th>Provider</th>
<th>Total Requests</th>
<th>Success</th>
<th>Errors</th>
<th>Error Rate</th>
<th>Avg Latency</th>
<th>Tokens/Min</th>
<th>Tokens/Hour</th>
<th>Tokens/Day</th>
</tr>
{% for provider in provider_stats %}
<tr>
<td><strong>{{ provider.provider_id }}</strong></td>
<td>{{ provider.requests.total }}</td>
<td>{{ provider.requests.success }}</td>
<td>{{ provider.requests.error }}</td>
<td {% if provider.error_rate > 0.1 %}style="color: #f87171;"{% endif %}>
{{ "%.1f"|format(provider.error_rate * 100) }}%
</td>
<td {% if provider.avg_latency_ms > 5000 %}style="color: #fcd34d;"{% endif %}>
{% if provider.avg_latency_ms > 1000 %}{{ "%.1f"|format(provider.avg_latency_ms / 1000) }}s{% else %}{{ "%.0f"|format(provider.avg_latency_ms) }}ms{% endif %}
</td>
<td>{{ provider.tokens.TPM }}</td>
<td>{{ provider.tokens.TPH }}</td>
<td>{{ provider.tokens.TPD }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p style="color: #a0a0a0;">No provider statistics available yet. Make API requests to see analytics.</p>
{% endif %}
<h3 style="margin-top: 30px; margin-bottom: 15px;">Cost Overview</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;">
<h4 style="font-size: 14px; margin-bottom: 10px;">Today's Estimated Cost</h4>
<p style="font-size: 28px; font-weight: bold;">${{ "%.2f"|format(cost_overview.total_estimated_cost_today) }}</p>
</div>
{% for pc in cost_overview.providers %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<h4 style="font-size: 14px; margin-bottom: 5px;">{{ pc.provider_id }}</h4>
<p style="font-size: 20px; font-weight: bold;">${{ "%.2f"|format(pc.estimated_cost) }}</p>
<small style="color: #a0a0a0;">{{ pc.tokens_today }} tokens today</small>
</div>
{% endfor %}
</div>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Model Performance</h3>
{% if model_performance %}
<table>
<tr>
<th>Provider</th>
<th>Model</th>
<th>Context Size</th>
<th>Condense %</th>
<th>Condense Method</th>
<th>Tokens/Day</th>
<th>Error Rate</th>
<th>Avg Latency</th>
</tr>
{% for model in model_performance %}
<tr>
<td>{{ model.provider_id }}</td>
<td>{{ model.model_name }}</td>
<td>{{ model.context_size|default('N/A') }}</td>
<td>{{ model.condense_context|default('N/A') }}%</td>
<td>{{ model.condense_method|default('None') }}</td>
<td>{{ model.tokens_per_day }}</td>
<td {% if model.error_rate > 0.1 %}style="color: #f87171;"{% endif %}>
{{ "%.1f"|format(model.error_rate * 100) }}%
</td>
<td {% if model.avg_latency_ms > 5000 %}style="color: #fcd34d;"{% endif %}>
{% if model.avg_latency_ms > 1000 %}{{ "%.1f"|format(model.avg_latency_ms / 1000) }}s{% else %}{{ "%.0f"|format(model.avg_latency_ms) }}ms{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p style="color: #a0a0a0;">No model performance data available yet.</p>
{% endif %}
<h3 style="margin-top: 30px; margin-bottom: 15px;">Token Usage Over Time (24h)</h3>
{% if token_over_time != '[]' %}
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px;">
<canvas id="tokenChart" style="width: 100%; height: 300px;"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const tokenData = {{ token_over_time|safe }};
// Group by provider if multiple
const providers = [...new Set(tokenData.map(d => d.provider_id || 'all'))];
if (providers.length > 1) {
// Multiple providers - show stacked
const datasets = providers.map((provider, i) => {
const colors = ['#e94560', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c'];
const data = tokenData.filter(d => (d.provider_id || 'all') === provider).map(d => ({
x: d.timestamp,
y: d.tokens
}));
return {
label: provider,
data: data,
backgroundColor: colors[i % colors.length],
borderColor: colors[i % colors.length],
fill: false,
tension: 0.1
};
});
new Chart(document.getElementById('tokenChart'), {
type: 'line',
data: { datasets: datasets },
options: {
responsive: true,
scales: {
x: { type: 'time', time: { unit: 'hour' } },
y: { beginAtZero: true, title: { display: true, text: 'Tokens' } }
}
}
});
} else {
// Single provider
new Chart(document.getElementById('tokenChart'), {
type: 'line',
data: {
labels: tokenData.map(d => d.timestamp),
datasets: [{
label: 'Tokens Used',
data: tokenData.map(d => d.tokens),
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
scales: {
x: { title: { display: true, text: 'Time' } },
y: { beginAtZero: true, title: { display: true, text: 'Tokens' } }
}
}
});
}
</script>
{% else %}
<p style="color: #a0a0a0;">No token usage data available yet.</p>
{% endif %}
<div style="margin-top: 30px; display: flex; gap: 10px;">
<a href="/dashboard" class="btn btn-secondary">Back to Dashboard</a>
</div>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment