Commit 2765eea2 authored by Your Name's avatar Your Name

0.99.40

parent bebce4e7
......@@ -366,6 +366,7 @@ class Analytics:
provider_id: Optional provider filter
from_datetime: Start datetime
to_datetime: End datetime
user_filter: Optional user ID to filter by (-1 for global only)
Returns:
Dictionary with token counts and cost estimates
......@@ -380,21 +381,36 @@ class Analytics:
formatted_start = self._format_timestamp(start)
formatted_end = self._format_timestamp(end)
# Build user condition
if user_filter == -1:
user_condition = " AND user_id IS NULL"
params_suffix = []
elif user_filter is not None:
user_condition = f" AND user_id = {placeholder}"
params_suffix = [user_filter]
else:
user_condition = ""
params_suffix = []
if provider_id:
params = [provider_id, formatted_start, formatted_end] + params_suffix
cursor.execute(f'''
SELECT SUM(tokens_used) as total_tokens
FROM token_usage
WHERE provider_id = {placeholder} AND timestamp >= {placeholder} AND timestamp <= {placeholder}
''', (provider_id, formatted_start, formatted_end))
{user_condition}
''', params)
row = cursor.fetchone()
total_tokens = row[0] if row and row[0] else 0
else:
params = [formatted_start, formatted_end] + params_suffix
cursor.execute(f'''
SELECT provider_id, SUM(tokens_used) as total_tokens
FROM token_usage
WHERE timestamp >= {placeholder} AND timestamp <= {placeholder}
{user_condition}
GROUP BY provider_id
''', (formatted_start, formatted_end))
''', params)
provider_tokens = {}
total_tokens = 0
......@@ -931,6 +947,7 @@ class Analytics:
Args:
from_datetime: Optional start datetime for filtering
to_datetime: Optional end datetime for filtering
user_filter: Optional user ID to filter by (-1 for global only)
Returns:
Dictionary with cost estimates
......@@ -940,10 +957,10 @@ class Analytics:
end = to_datetime or datetime.now()
# Get token usage by date range
range_usage = self.get_token_usage_by_date_range(None, start, end)
range_usage = self.get_token_usage_by_date_range(None, start, end, user_filter)
# Get providers that have data
providers = self.get_all_providers_stats(from_datetime, to_datetime)
providers = self.get_all_providers_stats(from_datetime, to_datetime, user_filter)
total_cost = 0.0
provider_costs = []
......
......@@ -1171,6 +1171,9 @@ class ResponseCache:
# Per-provider statistics
self.provider_stats = {}
# Per-user statistics
self.user_stats = {}
# Initialize backends
self.redis_client = None
......@@ -1346,12 +1349,13 @@ class ResponseCache:
self._memory_cache.pop(lru_key, None)
self._memory_timestamps.pop(lru_key, None)
def get(self, request_data: Dict) -> Optional[Any]:
def get(self, request_data: Dict, user_id: Optional[int] = None) -> Optional[Any]:
"""
Get cached response for a request.
Args:
request_data: The request data dict
user_id: Optional user ID for tracking statistics
Returns:
Cached response dict or None if not found
......@@ -1369,6 +1373,7 @@ class ResponseCache:
if data:
self.stats['hits'] += 1
self._update_provider_stats(provider_id, 'hits')
self._update_user_stats(user_id, 'hits')
logger.debug(f"Cache hit (Redis): {cache_key}")
return self._deserialize_response(data)
elif self.backend == 'sqlite' and self.sqlite_backend:
......@@ -1377,6 +1382,7 @@ class ResponseCache:
if data:
self.stats['hits'] += 1
self._update_provider_stats(provider_id, 'hits')
self._update_user_stats(user_id, 'hits')
logger.debug(f"Cache hit (SQLite): {cache_key}")
return data
elif self.backend == 'mysql' and self.mysql_backend:
......@@ -1385,6 +1391,7 @@ class ResponseCache:
if data:
self.stats['hits'] += 1
self._update_provider_stats(provider_id, 'hits')
self._update_user_stats(user_id, 'hits')
logger.debug(f"Cache hit (MySQL): {cache_key}")
return data
elif self.backend == 'memory':
......@@ -1407,11 +1414,13 @@ class ResponseCache:
self.stats['hits'] += 1
self._update_provider_stats(provider_id, 'hits')
self._update_user_stats(user_id, 'hits')
logger.debug(f"Cache hit (Memory): {cache_key}")
return self._memory_cache[cache_key]
self.stats['misses'] += 1
self._update_provider_stats(provider_id, 'misses')
self._update_user_stats(user_id, 'misses')
logger.debug(f"Cache miss: {cache_key}")
return None
......@@ -1420,7 +1429,7 @@ class ResponseCache:
logger.warning(f"Cache get error: {e}")
return None
def set(self, request_data: Dict, response: Any, ttl: Optional[int] = None) -> None:
def set(self, request_data: Dict, response: Any, ttl: Optional[int] = None, user_id: Optional[int] = None) -> None:
"""
Cache a response.
......@@ -1428,6 +1437,7 @@ class ResponseCache:
request_data: The request data dict
response: The response to cache (dict or object)
ttl: TTL in seconds (uses default if None)
user_id: Optional user ID for tracking statistics
"""
if not self.enabled:
return
......@@ -1473,6 +1483,7 @@ class ResponseCache:
logger.debug(f"Cached response (Memory): {cache_key} (TTL: {ttl_value}s)")
self.stats['sets'] += 1
self._update_user_stats(user_id, 'sets')
except Exception as e:
self.stats['errors'] += 1
......@@ -1509,6 +1520,55 @@ class ResponseCache:
except Exception as e:
self.stats['errors'] += 1
logger.warning(f"Cache delete error: {e}")
def _update_provider_stats(self, provider_id: str, stat_type: str) -> None:
"""Update per-provider statistics"""
if provider_id not in self.provider_stats:
self.provider_stats[provider_id] = {
'hits': 0,
'misses': 0,
'sets': 0,
'deletes': 0,
'errors': 0
}
self.provider_stats[provider_id][stat_type] += 1
def _update_user_stats(self, user_id: Optional[int], stat_type: str) -> None:
"""Update per-user statistics"""
if user_id is None:
return
user_key = f"user:{user_id}"
if user_key not in self.user_stats:
self.user_stats[user_key] = {
'hits': 0,
'misses': 0,
'sets': 0,
'deletes': 0,
'errors': 0
}
self.user_stats[user_key][stat_type] += 1
def get_user_stats(self, user_id: int) -> Dict[str, Any]:
"""Get cache statistics for a specific user"""
user_key = f"user:{user_id}"
if user_key not in self.user_stats:
return {
'enabled': self.enabled,
'hits': 0,
'misses': 0,
'hit_rate': 0.0,
'total_requests': 0,
'backend': self.backend
}
stats = self.user_stats[user_key].copy()
total = stats['hits'] + stats['misses']
stats['hit_rate'] = stats['hits'] / total if total > 0 else 0.0
stats['total_requests'] = total
stats['enabled'] = self.enabled
stats['backend'] = self.backend
return stats
def clear(self) -> None:
"""Clear all cached responses"""
......
......@@ -345,7 +345,7 @@ class RequestHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
cached_response = response_cache.get(request_data)
cached_response = response_cache.get(request_data, user_id=self.user_id)
if cached_response:
logger.info(f"Cache hit for request to provider {provider_id}")
return cached_response
......@@ -479,7 +479,7 @@ class RequestHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
response_cache.set(request_data, response)
response_cache.set(request_data, response, user_id=self.user_id)
logger.debug(f"Cached chunked response for request to provider {provider_id}")
except Exception as cache_error:
logger.warning(f"Response cache set failed for chunked request: {cache_error}")
......@@ -521,7 +521,7 @@ class RequestHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
response_cache.set(request_data, response)
response_cache.set(request_data, response, user_id=self.user_id)
logger.debug(f"Cached response for request to provider {provider_id}")
except Exception as cache_error:
logger.warning(f"Response cache set failed: {cache_error}")
......@@ -2353,7 +2353,7 @@ class RotationHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
cached_response = response_cache.get(request_data)
cached_response = response_cache.get(request_data, user_id=self.user_id)
if cached_response:
logger.info(f"Cache hit for rotation request {rotation_id}")
return cached_response
......@@ -2863,7 +2863,7 @@ class RotationHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
response_cache.set(request_data, response)
response_cache.set(request_data, response, user_id=self.user_id)
logger.debug(f"Cached chunked response for rotation request {rotation_id}")
except Exception as cache_error:
logger.warning(f"Response cache set failed for chunked request: {cache_error}")
......@@ -2935,7 +2935,7 @@ class RotationHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
response_cache.set(request_data, response)
response_cache.set(request_data, response, user_id=self.user_id)
logger.debug(f"Cached response for rotation request {rotation_id}")
except Exception as cache_error:
logger.warning(f"Response cache set failed: {cache_error}")
......@@ -4275,7 +4275,7 @@ class AutoselectHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
cached_response = response_cache.get(request_data)
cached_response = response_cache.get(request_data, user_id=self.user_id)
if cached_response:
logger.info(f"Cache hit for autoselect request {autoselect_id}")
return cached_response
......@@ -4431,7 +4431,7 @@ class AutoselectHandler:
aisbf_config = self.config.get_aisbf_config()
if aisbf_config and aisbf_config.response_cache and aisbf_config.response_cache.enabled:
response_cache = get_response_cache(aisbf_config.response_cache.model_dump())
response_cache.set(request_data, response)
response_cache.set(request_data, response, user_id=self.user_id)
logger.debug(f"Cached response for autoselect request {autoselect_id}")
except Exception as cache_error:
logger.warning(f"Response cache set failed: {cache_error}")
......
......@@ -697,24 +697,47 @@ class AdaptiveRateLimiter:
logger.info(f"[AdaptiveRateLimiter {self.provider_id}] Reset to initial state")
# Global adaptive rate limiters registry
# Global adaptive rate limiters registry - now supports user-specific limiters
# Key format: "provider_id" (global) or "user:user_id:provider_id" (user-specific)
_adaptive_rate_limiters: Dict[str, AdaptiveRateLimiter] = {}
def get_adaptive_rate_limiter(provider_id: str, config: Dict = None) -> AdaptiveRateLimiter:
"""Get or create an adaptive rate limiter for a provider."""
def get_adaptive_rate_limiter(provider_id: str, config: Dict = None, user_id: Optional[int] = None) -> AdaptiveRateLimiter:
"""Get or create an adaptive rate limiter for a provider, optionally user-specific."""
global _adaptive_rate_limiters
if provider_id not in _adaptive_rate_limiters:
_adaptive_rate_limiters[provider_id] = AdaptiveRateLimiter(provider_id, config)
# Create key based on whether user_id is provided
if user_id is not None:
key = f"user:{user_id}:{provider_id}"
else:
key = provider_id
return _adaptive_rate_limiters[provider_id]
if key not in _adaptive_rate_limiters:
_adaptive_rate_limiters[key] = AdaptiveRateLimiter(key, config)
return _adaptive_rate_limiters[key]
def get_all_adaptive_rate_limiters() -> Dict[str, AdaptiveRateLimiter]:
"""Get all adaptive rate limiters."""
def get_all_adaptive_rate_limiters(user_id: Optional[int] = None) -> Dict[str, AdaptiveRateLimiter]:
"""Get all adaptive rate limiters, optionally filtered by user."""
global _adaptive_rate_limiters
return _adaptive_rate_limiters
if user_id is None:
# Return all limiters (admin view)
return _adaptive_rate_limiters
else:
# Return only limiters for this user + global ones
user_prefix = f"user:{user_id}:"
filtered = {}
for key, limiter in _adaptive_rate_limiters.items():
if key.startswith(user_prefix) or not key.startswith("user:"):
# Strip the user prefix for display to user
if key.startswith(user_prefix):
display_key = key[len(user_prefix):]
else:
display_key = key
filtered[display_key] = limiter
return filtered
class BaseProviderHandler:
......@@ -756,7 +779,7 @@ class BaseProviderHandler:
adaptive_config = None
if config.aisbf and config.aisbf.adaptive_rate_limiting:
adaptive_config = config.aisbf.adaptive_rate_limiting.dict()
self.adaptive_limiter = get_adaptive_rate_limiter(provider_id, adaptive_config)
self.adaptive_limiter = get_adaptive_rate_limiter(provider_id, adaptive_config, user_id)
def parse_429_response(self, response_data: Union[Dict, str], headers: Dict = None) -> Optional[int]:
"""
......
......@@ -22,7 +22,12 @@
<!-- Cache Statistics -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;">Cache Statistics</h3>
{% if is_admin %}
<h3 style="margin-bottom: 20px;">Global Cache Statistics</h3>
{% else %}
<h3 style="margin-bottom: 20px;">Your Personal Cache Impact</h3>
<p style="color: #a0a0a0; margin-bottom: 20px;">These statistics show how the response cache has benefited your requests personally.</p>
{% endif %}
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
......@@ -32,26 +37,29 @@
</div>
</div>
{% if is_admin %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Backend</div>
<div style="font-size: 24px; font-weight: bold;">{{ stats.backend|upper }}</div>
</div>
{% endif %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Cache Hits</div>
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">{% if is_admin %}Cache Hits{% else %}Your Cache Hits{% endif %}</div>
<div style="font-size: 24px; font-weight: bold; color: #4ade80;">{{ stats.hits }}</div>
</div>
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Cache Misses</div>
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">{% if is_admin %}Cache Misses{% else %}Your Cache Misses{% endif %}</div>
<div style="font-size: 24px; font-weight: bold; color: #ef4444;">{{ stats.misses }}</div>
</div>
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Hit Rate</div>
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">{% if is_admin %}Hit Rate{% else %}Your Hit Rate{% endif %}</div>
<div style="font-size: 24px; font-weight: bold; color: #60a5fa;">{{ "%.1f"|format(stats.hit_rate * 100) }}%</div>
</div>
{% if is_admin %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Cache Size</div>
<div style="font-size: 24px; font-weight: bold;">{{ stats.size }}</div>
......@@ -61,9 +69,16 @@
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Evictions</div>
<div style="font-size: 24px; font-weight: bold; color: #f59e0b;">{{ stats.evictions }}</div>
</div>
{% else %}
<div style="background: #0f3460; padding: 15px; border-radius: 8px;">
<div style="color: #a0a0a0; font-size: 14px; margin-bottom: 5px;">Total Requests</div>
<div style="font-size: 24px; font-weight: bold; color: #a78bfa;">{{ stats.total_requests }}</div>
</div>
{% endif %}
</div>
</div>
{% if is_admin %}
<!-- Cache Actions -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 20px;">Cache Actions</h3>
......@@ -78,6 +93,7 @@
</button>
</div>
</div>
{% endif %}
<!-- Error Display -->
{% if stats.error %}
......
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