|
|
"""Statistics tracking for API usage and token counts.""" |
|
|
|
|
|
import time |
|
|
from collections import defaultdict, deque |
|
|
from threading import Lock |
|
|
from typing import Dict, Any |
|
|
from dataclasses import dataclass, field |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class RequestStats: |
|
|
"""Statistics for a single request.""" |
|
|
timestamp: float |
|
|
prompt_tokens: int |
|
|
completion_tokens: int |
|
|
total_tokens: int |
|
|
model: str |
|
|
finish_reason: str |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class AggregateStats: |
|
|
"""Aggregate statistics.""" |
|
|
total_requests: int = 0 |
|
|
total_prompt_tokens: int = 0 |
|
|
total_completion_tokens: int = 0 |
|
|
total_tokens: int = 0 |
|
|
requests_by_model: Dict[str, int] = field(default_factory=lambda: defaultdict(int)) |
|
|
tokens_by_model: Dict[str, int] = field(default_factory=lambda: defaultdict(int)) |
|
|
finish_reasons: Dict[str, int] = field(default_factory=lambda: defaultdict(int)) |
|
|
recent_requests: deque = field(default_factory=lambda: deque(maxlen=100)) |
|
|
|
|
|
|
|
|
class StatsTracker: |
|
|
"""Thread-safe statistics tracker.""" |
|
|
|
|
|
def __init__(self): |
|
|
self._lock = Lock() |
|
|
self._stats = AggregateStats() |
|
|
self._start_time = time.time() |
|
|
|
|
|
def record_request(self, stats: RequestStats): |
|
|
"""Record a request's statistics.""" |
|
|
with self._lock: |
|
|
self._stats.total_requests += 1 |
|
|
self._stats.total_prompt_tokens += stats.prompt_tokens |
|
|
self._stats.total_completion_tokens += stats.completion_tokens |
|
|
self._stats.total_tokens += stats.total_tokens |
|
|
self._stats.requests_by_model[stats.model] += 1 |
|
|
self._stats.tokens_by_model[stats.model] += stats.total_tokens |
|
|
self._stats.finish_reasons[stats.finish_reason] += 1 |
|
|
self._stats.recent_requests.append(stats) |
|
|
|
|
|
def get_stats(self) -> Dict[str, Any]: |
|
|
"""Get current statistics.""" |
|
|
with self._lock: |
|
|
uptime_seconds = time.time() - self._start_time |
|
|
uptime_hours = uptime_seconds / 3600 |
|
|
|
|
|
|
|
|
avg_prompt_tokens = ( |
|
|
self._stats.total_prompt_tokens / self._stats.total_requests |
|
|
if self._stats.total_requests > 0 else 0 |
|
|
) |
|
|
avg_completion_tokens = ( |
|
|
self._stats.total_completion_tokens / self._stats.total_requests |
|
|
if self._stats.total_requests > 0 else 0 |
|
|
) |
|
|
avg_total_tokens = ( |
|
|
self._stats.total_tokens / self._stats.total_requests |
|
|
if self._stats.total_requests > 0 else 0 |
|
|
) |
|
|
|
|
|
|
|
|
requests_per_hour = ( |
|
|
self._stats.total_requests / uptime_hours |
|
|
if uptime_hours > 0 else 0 |
|
|
) |
|
|
|
|
|
|
|
|
tokens_per_hour = ( |
|
|
self._stats.total_tokens / uptime_hours |
|
|
if uptime_hours > 0 else 0 |
|
|
) |
|
|
|
|
|
return { |
|
|
"uptime_seconds": int(uptime_seconds), |
|
|
"uptime_hours": round(uptime_hours, 2), |
|
|
"total_requests": self._stats.total_requests, |
|
|
"total_prompt_tokens": self._stats.total_prompt_tokens, |
|
|
"total_completion_tokens": self._stats.total_completion_tokens, |
|
|
"total_tokens": self._stats.total_tokens, |
|
|
"average_prompt_tokens": round(avg_prompt_tokens, 2), |
|
|
"average_completion_tokens": round(avg_completion_tokens, 2), |
|
|
"average_total_tokens": round(avg_total_tokens, 2), |
|
|
"requests_per_hour": round(requests_per_hour, 2), |
|
|
"tokens_per_hour": round(tokens_per_hour, 2), |
|
|
"requests_by_model": dict(self._stats.requests_by_model), |
|
|
"tokens_by_model": dict(self._stats.tokens_by_model), |
|
|
"finish_reasons": dict(self._stats.finish_reasons), |
|
|
"recent_requests_count": len(self._stats.recent_requests), |
|
|
} |
|
|
|
|
|
def reset(self): |
|
|
"""Reset all statistics.""" |
|
|
with self._lock: |
|
|
self._stats = AggregateStats() |
|
|
self._start_time = time.time() |
|
|
|
|
|
|
|
|
|
|
|
_stats_tracker = StatsTracker() |
|
|
|
|
|
|
|
|
def get_stats_tracker() -> StatsTracker: |
|
|
"""Get the global stats tracker instance.""" |
|
|
return _stats_tracker |
|
|
|
|
|
|