Fix analytics

parent 006441ea
...@@ -562,6 +562,34 @@ class Analytics: ...@@ -562,6 +562,34 @@ class Analytics:
) )
return total_cost return total_cost
def _normalize_time_window(
self,
time_range: Optional[str] = None,
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> tuple[datetime, datetime, str]:
now = datetime.now()
if from_datetime and to_datetime:
normalized_range = 'custom' if time_range not in ['yesterday'] else time_range
return from_datetime, to_datetime, normalized_range
if time_range == '1h':
return now - timedelta(hours=1), now, '1h'
if time_range == '6h':
return now - timedelta(hours=6), now, '6h'
if time_range == 'yesterday':
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
return today - timedelta(days=1), today - timedelta(microseconds=1), 'yesterday'
if time_range == '7d':
return now - timedelta(days=7), now, '7d'
if time_range == '30d':
return now - timedelta(days=30), now, '30d'
if time_range == '90d':
return now - timedelta(days=90), now, '90d'
return now - timedelta(hours=24), now, '24h'
def get_token_usage_by_date_range( def get_token_usage_by_date_range(
self, self,
provider_id: Optional[str] = None, provider_id: Optional[str] = None,
...@@ -584,8 +612,7 @@ class Analytics: ...@@ -584,8 +612,7 @@ class Analytics:
Returns: Returns:
Dictionary with token counts and cost estimates Dictionary with token counts and cost estimates
""" """
start = from_datetime or (datetime.now() - timedelta(days=1)) start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
end = to_datetime or datetime.now()
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
...@@ -686,10 +713,7 @@ class Analytics: ...@@ -686,10 +713,7 @@ class Analytics:
""" """
Get statistics for all providers. Get statistics for all providers.
""" """
# Use local time to match how MySQL CURRENT_TIMESTAMP stores data start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
now_local = datetime.now()
start = (from_datetime if from_datetime else (now_local - timedelta(days=1)))
end = (to_datetime if to_datetime else now_local)
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
...@@ -824,10 +848,10 @@ class Analytics: ...@@ -824,10 +848,10 @@ class Analytics:
Returns: Returns:
List of time-series data points List of time-series data points
""" """
# Determine time range cutoff, end_time, normalized_time_range = self._normalize_time_window(time_range, from_datetime, to_datetime)
if time_range == 'custom' and from_datetime and to_datetime:
cutoff = from_datetime # Determine time range bucket size
end_time = to_datetime if normalized_time_range == 'custom':
# Calculate bucket size based on range # Calculate bucket size based on range
total_minutes = (end_time - cutoff).total_seconds() / 60 total_minutes = (end_time - cutoff).total_seconds() / 60
if total_minutes <= 60: if total_minutes <= 60:
...@@ -842,39 +866,21 @@ class Analytics: ...@@ -842,39 +866,21 @@ class Analytics:
bucket_minutes = 60 * 24 # daily bucket_minutes = 60 * 24 # daily
else: # > 30 days else: # > 30 days
bucket_minutes = 60 * 24 * 7 # weekly bucket_minutes = 60 * 24 * 7 # weekly
elif time_range == '1h': elif normalized_time_range == '1h':
cutoff = datetime.now() - timedelta(hours=1)
end_time = datetime.now()
bucket_minutes = 5 bucket_minutes = 5
elif time_range == '6h': elif normalized_time_range == '6h':
cutoff = datetime.now() - timedelta(hours=6)
end_time = datetime.now()
bucket_minutes = 15 bucket_minutes = 15
elif time_range == '24h': elif normalized_time_range == '24h':
cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
bucket_minutes = 30 bucket_minutes = 30
elif time_range == 'yesterday': elif normalized_time_range == 'yesterday':
# Yesterday: from 00:00:00 to 23:59:59 of previous day
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
cutoff = today - timedelta(days=1)
end_time = today - timedelta(microseconds=1)
bucket_minutes = 30 bucket_minutes = 30
elif time_range == '7d': elif normalized_time_range == '7d':
cutoff = datetime.now() - timedelta(days=7)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily bucket_minutes = 60 * 24 # Daily
elif time_range == '30d': elif normalized_time_range == '30d':
cutoff = datetime.now() - timedelta(days=30)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily bucket_minutes = 60 * 24 # Daily
elif time_range == '90d': elif normalized_time_range == '90d':
cutoff = datetime.now() - timedelta(days=90)
end_time = datetime.now()
bucket_minutes = 60 * 24 # Daily bucket_minutes = 60 * 24 # Daily
else: # Default 24h else: # Default 24h
cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
bucket_minutes = 30 bucket_minutes = 30
# Query database for token usage in time range # Query database for token usage in time range
...@@ -1228,8 +1234,7 @@ class Analytics: ...@@ -1228,8 +1234,7 @@ class Analytics:
rotation_filter: Optional[str] = None, rotation_filter: Optional[str] = None,
autoselect_filter: Optional[str] = None autoselect_filter: Optional[str] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
start = from_datetime or (datetime.now() - timedelta(days=1)) start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
end = to_datetime or datetime.now()
provider_stats = self.get_all_providers_stats( provider_stats = self.get_all_providers_stats(
from_datetime, from_datetime,
...@@ -1266,6 +1271,9 @@ class Analytics: ...@@ -1266,6 +1271,9 @@ class Analytics:
if not tier_info: if not tier_info:
continue continue
if tier_info.get('source') == 'default':
continue
cached_usage = None cached_usage = None
try: try:
cached_row = self.db.get_provider_usage(None if user_filter == -1 else user_filter, provider_id) cached_row = self.db.get_provider_usage(None if user_filter == -1 else user_filter, provider_id)
...@@ -1275,19 +1283,22 @@ class Analytics: ...@@ -1275,19 +1283,22 @@ class Analytics:
cached_usage = None cached_usage = None
runtime_quota = self._derive_quota_from_usage(provider_id, cached_usage) runtime_quota = self._derive_quota_from_usage(provider_id, cached_usage)
free_limit = max(int((runtime_quota or {}).get('limit') or tier_info.get('limit', 0) or 0), 1) free_limit = int((runtime_quota or {}).get('limit') or tier_info.get('limit', 0) or 0)
limit_type = (runtime_quota or {}).get('limit_type') or tier_info.get('limit_type') or 'requests' limit_type = (runtime_quota or {}).get('limit_type') or tier_info.get('limit_type') or 'requests'
period = (runtime_quota or {}).get('period') or tier_info.get('period') or 'month' period = (runtime_quota or {}).get('period') or tier_info.get('period') or 'month'
if free_limit <= 0:
continue
if limit_type == 'tokens': if limit_type == 'tokens':
usage_amount = usage.get('tokens_used', 0) or 0 usage_amount = usage.get('tokens_used', 0) or 0
else: else:
usage_amount = (runtime_quota or {}).get('used') or usage.get('request_count', 0) or 0 usage_amount = usage.get('request_count', 0) or 0
if usage_amount <= free_limit: if usage_amount <= free_limit:
continue continue
extra_free_tiers = usage_amount // free_limit extra_free_tiers = max((usage_amount - 1) // free_limit, 0)
if extra_free_tiers <= 0: if extra_free_tiers <= 0:
continue continue
...@@ -1352,9 +1363,7 @@ class Analytics: ...@@ -1352,9 +1363,7 @@ class Analytics:
Returns: Returns:
Dictionary with cost estimates Dictionary with cost estimates
""" """
# Use date range for token usage if specified start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
start = from_datetime or (datetime.now() - timedelta(days=1))
end = to_datetime or datetime.now()
# Get providers that have data # Get providers that have data
providers = self.get_all_providers_stats(from_datetime, to_datetime, user_filter, providers = self.get_all_providers_stats(from_datetime, to_datetime, user_filter,
...@@ -1558,9 +1567,7 @@ class Analytics: ...@@ -1558,9 +1567,7 @@ class Analytics:
Per-rotation breakdown: which provider/model received what share of hits and tokens. Per-rotation breakdown: which provider/model received what share of hits and tokens.
Returns list of {rotation_id, entries: [{provider_id, model_name, requests, tokens, hit_pct, token_pct, avg_latency_ms}]} Returns list of {rotation_id, entries: [{provider_id, model_name, requests, tokens, hit_pct, token_pct, avg_latency_ms}]}
""" """
now = datetime.now() start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
start = from_datetime or (now - timedelta(days=1))
end = to_datetime or now
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
...@@ -1628,9 +1635,7 @@ class Analytics: ...@@ -1628,9 +1635,7 @@ class Analytics:
Returns list of {autoselect_id, entries: [{model_name, requests, tokens, hit_pct, token_pct, avg_latency_ms}]} Returns list of {autoselect_id, entries: [{model_name, requests, tokens, hit_pct, token_pct, avg_latency_ms}]}
Selection latency is stored as latency_ms on the 'autoselect' provider_id rows. Selection latency is stored as latency_ms on the 'autoselect' provider_id rows.
""" """
now = datetime.now() start, end, _ = self._normalize_time_window('custom' if from_datetime or to_datetime else '24h', from_datetime, to_datetime)
start = from_datetime or (now - timedelta(days=1))
end = to_datetime or now
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
......
...@@ -880,8 +880,11 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)): ...@@ -880,8 +880,11 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"request": request, "request": request,
"session": request.session, "session": request.session,
"__version__": __version__, "__version__": __version__,
"user_providers_data": providers_data, "user_providers_data": list(providers_data.values()),
"user_providers_bootstrap_json": _json_parse_bootstrap(providers_data), "user_providers_bootstrap_json": _json_parse_bootstrap([
{"provider_id": provider_id, "config": provider_config}
for provider_id, provider_config in providers_data.items()
]),
"studio_capability_choices": serialize_studio_capability_choices(), "studio_capability_choices": serialize_studio_capability_choices(),
"studio_adapter_choices": serialize_studio_adapter_choices(), "studio_adapter_choices": serialize_studio_adapter_choices(),
"studio_adapter_profile_choices": serialize_studio_adapter_profile_choices(), "studio_adapter_profile_choices": serialize_studio_adapter_profile_choices(),
...@@ -2293,6 +2296,21 @@ async def dashboard_analytics( ...@@ -2293,6 +2296,21 @@ async def dashboard_analytics(
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
from_datetime = today - timedelta(days=1) from_datetime = today - timedelta(days=1)
to_datetime = today - timedelta(microseconds=1) to_datetime = today - timedelta(microseconds=1)
elif time_range == '1h':
from_datetime = datetime.now() - timedelta(hours=1)
to_datetime = datetime.now()
elif time_range == '6h':
from_datetime = datetime.now() - timedelta(hours=6)
to_datetime = datetime.now()
elif time_range == '7d':
from_datetime = datetime.now() - timedelta(days=7)
to_datetime = datetime.now()
elif time_range == '30d':
from_datetime = datetime.now() - timedelta(days=30)
to_datetime = datetime.now()
elif time_range == '90d':
from_datetime = datetime.now() - timedelta(days=90)
to_datetime = datetime.now()
elif time_range == 'custom': elif time_range == 'custom':
if not from_datetime or not to_datetime: if not from_datetime or not to_datetime:
time_range = '24h' time_range = '24h'
......
...@@ -607,9 +607,9 @@ window.onclick = function(event) { ...@@ -607,9 +607,9 @@ window.onclick = function(event) {
<p style="font-size: 28px; font-weight: bold;" id="savings-amount">{{ currency_symbol }}{{ "%.2f"|format((optimization_savings.total_cost_saved if optimization_savings else 0) or 0) }}</p> <p style="font-size: 28px; font-weight: bold;" id="savings-amount">{{ currency_symbol }}{{ "%.2f"|format((optimization_savings.total_cost_saved if optimization_savings else 0) or 0) }}</p>
<small style="color: rgba(255,255,255,0.8);"> <small style="color: rgba(255,255,255,0.8);">
{% if optimization_savings and optimization_savings.provider_equivalents %} {% if optimization_savings and optimization_savings.provider_equivalents %}
Provider free-tier equivalents + optimization Configured free-tier equivalents in selected period
{% else %} {% else %}
Cache hits & optimization No configured free-tier savings in selected period
{% endif %} {% endif %}
</small> </small>
</div> </div>
...@@ -646,8 +646,8 @@ window.onclick = function(event) { ...@@ -646,8 +646,8 @@ window.onclick = function(event) {
<div style="background: var(--bg-page); padding: 16px; border-radius: 8px; margin-bottom: 20px;"> <div style="background: var(--bg-page); padding: 16px; border-radius: 8px; margin-bottom: 20px;">
<h4 style="margin: 0 0 10px 0;">Free-Tier Equivalent Savings</h4> <h4 style="margin: 0 0 10px 0;">Free-Tier Equivalent Savings</h4>
<p style="margin: 0 0 10px 0; color: var(--color-muted);"> <p style="margin: 0 0 10px 0; color: var(--color-muted);">
The table below estimates provider-side free-tier equivalents using known upstream free limits such as weekly or monthly request allowances. The table below only uses explicitly configured provider free-tier limits. Generic defaults are excluded to avoid overstated savings on subscription or bundled plans.
When the filtered usage exceeds a provider's base free allowance, the overflow is counted as additional free-tier-equivalent capacity and translated into token and premium-value equivalents. Extra free-tier equivalents are counted only for complete additional quota blocks beyond the first included tier within the selected period.
</p> </p>
<table> <table>
<tr> <tr>
......
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