Spaces:
Sleeping
Sleeping
| """ | |
| Token usage tracking for cost analysis and monitoring. | |
| Tracks: | |
| - Input/output tokens per request | |
| - Cost per request | |
| - Cumulative token usage | |
| - Feature-specific usage | |
| """ | |
| import logging | |
| from datetime import datetime | |
| from typing import Dict, List, Optional | |
| from dataclasses import dataclass, asdict | |
| from threading import Lock | |
| logger = logging.getLogger(__name__) | |
| class TokenRecord: | |
| """Record of a single token usage event.""" | |
| timestamp: datetime | |
| feature: str # 'text_triage', 'image_scan', 'video_analysis' | |
| input_tokens: int | |
| output_tokens: int | |
| total_tokens: int | |
| cost_usd: float | |
| owner_id: str | |
| model: str # 'gemini-1.5-pro', etc. | |
| status: str # 'success', 'error', 'rate_limited' | |
| latency_ms: float | |
| class TokenTracker: | |
| """Track token usage for cost analysis and monitoring.""" | |
| def __init__(self): | |
| self.records: List[TokenRecord] = [] | |
| self.lock = Lock() | |
| # Pricing (Gemini 1.5 Flash - update as needed) | |
| # https://ai.google.dev/pricing | |
| self.pricing = { | |
| "gemini-1.5-flash": { | |
| "input_cost_per_million": 0.075, | |
| "output_cost_per_million": 0.30 | |
| }, | |
| "gemini-1.5-pro": { | |
| "input_cost_per_million": 1.50, | |
| "output_cost_per_million": 6.00 | |
| }, | |
| "gemini-2.0-flash": { | |
| "input_cost_per_million": 0.10, | |
| "output_cost_per_million": 0.40 | |
| } | |
| } | |
| def calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float: | |
| """Calculate cost in USD for token usage.""" | |
| if model not in self.pricing: | |
| logger.warning(f"Unknown model: {model}, using gemini-1.5-flash pricing") | |
| model = "gemini-1.5-flash" | |
| pricing = self.pricing[model] | |
| input_cost = (input_tokens / 1_000_000) * pricing["input_cost_per_million"] | |
| output_cost = (output_tokens / 1_000_000) * pricing["output_cost_per_million"] | |
| return input_cost + output_cost | |
| def log_tokens( | |
| self, | |
| feature: str, | |
| input_tokens: int, | |
| output_tokens: int, | |
| owner_id: str, | |
| model: str = "gemini-1.5-flash", | |
| status: str = "success", | |
| latency_ms: float = 0.0 | |
| ) -> TokenRecord: | |
| """Log a token usage event.""" | |
| total_tokens = input_tokens + output_tokens | |
| cost = self.calculate_cost(model, input_tokens, output_tokens) | |
| record = TokenRecord( | |
| timestamp=datetime.utcnow(), | |
| feature=feature, | |
| input_tokens=input_tokens, | |
| output_tokens=output_tokens, | |
| total_tokens=total_tokens, | |
| cost_usd=cost, | |
| owner_id=owner_id, | |
| model=model, | |
| status=status, | |
| latency_ms=latency_ms | |
| ) | |
| with self.lock: | |
| self.records.append(record) | |
| # Log with structured logging | |
| logger.info( | |
| "Token usage recorded", | |
| extra={ | |
| "feature": feature, | |
| "input_tokens": input_tokens, | |
| "output_tokens": output_tokens, | |
| "total_tokens": total_tokens, | |
| "cost_usd": round(cost, 4), | |
| "owner_id": owner_id, | |
| "model": model, | |
| "status": status, | |
| "latency_ms": latency_ms | |
| } | |
| ) | |
| return record | |
| def get_stats(self, owner_id: Optional[str] = None) -> Dict: | |
| """Get token usage statistics.""" | |
| with self.lock: | |
| if owner_id: | |
| records = [r for r in self.records if r.owner_id == owner_id] | |
| else: | |
| records = self.records | |
| if not records: | |
| return { | |
| "total_requests": 0, | |
| "total_tokens": 0, | |
| "total_cost_usd": 0.0, | |
| "average_tokens_per_request": 0, | |
| "average_cost_per_request": 0.0 | |
| } | |
| total_requests = len(records) | |
| total_tokens = sum(r.total_tokens for r in records) | |
| total_cost = sum(r.cost_usd for r in records) | |
| return { | |
| "total_requests": total_requests, | |
| "total_tokens": total_tokens, | |
| "total_cost_usd": round(total_cost, 4), | |
| "average_tokens_per_request": total_tokens // total_requests if total_requests else 0, | |
| "average_cost_per_request": round(total_cost / total_requests, 4) if total_requests else 0.0, | |
| "by_feature": self._get_feature_breakdown(records), | |
| "by_status": self._get_status_breakdown(records) | |
| } | |
| def _get_feature_breakdown(self, records: List[TokenRecord]) -> Dict: | |
| """Get token usage by feature.""" | |
| breakdown = {} | |
| for record in records: | |
| if record.feature not in breakdown: | |
| breakdown[record.feature] = { | |
| "requests": 0, | |
| "tokens": 0, | |
| "cost_usd": 0.0 | |
| } | |
| breakdown[record.feature]["requests"] += 1 | |
| breakdown[record.feature]["tokens"] += record.total_tokens | |
| breakdown[record.feature]["cost_usd"] += record.cost_usd | |
| # Round costs | |
| for feature in breakdown: | |
| breakdown[feature]["cost_usd"] = round(breakdown[feature]["cost_usd"], 4) | |
| return breakdown | |
| def _get_status_breakdown(self, records: List[TokenRecord]) -> Dict: | |
| """Get token usage by status.""" | |
| breakdown = {} | |
| for record in records: | |
| if record.status not in breakdown: | |
| breakdown[record.status] = { | |
| "count": 0, | |
| "total_tokens": 0, | |
| "total_cost_usd": 0.0 | |
| } | |
| breakdown[record.status]["count"] += 1 | |
| breakdown[record.status]["total_tokens"] += record.total_tokens | |
| breakdown[record.status]["total_cost_usd"] += record.cost_usd | |
| # Round costs | |
| for status in breakdown: | |
| breakdown[status]["total_cost_usd"] = round(breakdown[status]["total_cost_usd"], 4) | |
| return breakdown | |
| def get_recent_records(self, limit: int = 100) -> List[Dict]: | |
| """Get recent token usage records.""" | |
| with self.lock: | |
| recent = sorted(self.records, key=lambda r: r.timestamp, reverse=True)[:limit] | |
| return [asdict(r) for r in recent] | |
| def export_csv(self, filepath: str, owner_id: Optional[str] = None): | |
| """Export token records to CSV for analysis.""" | |
| import csv | |
| with self.lock: | |
| if owner_id: | |
| records = [r for r in self.records if r.owner_id == owner_id] | |
| else: | |
| records = self.records | |
| if not records: | |
| logger.warning(f"No records to export for {owner_id or 'all users'}") | |
| return | |
| with open(filepath, 'w', newline='') as f: | |
| writer = csv.DictWriter(f, fieldnames=TokenRecord.__dataclass_fields__.keys()) | |
| writer.writeheader() | |
| for record in records: | |
| writer.writerow(asdict(record)) | |
| logger.info(f"Exported {len(records)} token records to {filepath}") | |
| # Global instance | |
| token_tracker = TokenTracker() | |