fix: refine analytics cost and provider quota insights

parent 18da1eed
This diff is collapsed.
......@@ -272,6 +272,7 @@ async def _refresh_provider_usage_if_stale(provider_id: str, user_id):
return
usage_data = await handler.get_usage()
if usage_data:
usage_data = handler.normalize_usage_data(usage_data)
db.save_provider_usage(user_id, provider_id, usage_data)
_apply_usage_disable(db, user_id, provider_id, usage_data)
except Exception as e:
......
......@@ -94,6 +94,11 @@ class ProviderConfig(BaseModel):
cache_ttl: Optional[int] = None # Cache TTL in seconds for Google Context Caching API
min_cacheable_tokens: Optional[int] = 1024 # Minimum token count for content to be cacheable (default matches OpenAI)
prompt_cache_key: Optional[str] = None # Optional cache key for OpenAI's load balancer routing optimization
free_tier_limit: Optional[int] = None # Upstream provider free-tier quota amount
free_tier_period: Optional[str] = None # Upstream free-tier period: day, week, month
free_tier_limit_type: Optional[str] = None # requests or tokens
premium_reference_monthly_cost: Optional[float] = None # Equivalent premium value for one extra upstream free tier
free_tier_description: Optional[str] = None # Human description for upstream free tier
# Response caching control
enable_response_cache: Optional[bool] = None # Enable/disable response caching for this provider (None = use global default)
......@@ -421,7 +426,7 @@ class Config:
print(f"Created default config file: {dst}")
# Copy markdown prompt files if they don't exist
for prompt_file in ['condensation_conversational.md', 'condensation_semantic.md', 'autoselect.md']:
for prompt_file in ['condensation_conversational.md', 'condensation_semantic.md', 'autoselect.md', 'STUDIO_SYSTEM.md']:
src = source_dir / prompt_file
dst = config_dir / prompt_file
......
......@@ -25,6 +25,50 @@ from typing import Dict, Optional, Any
logger = logging.getLogger(__name__)
def _coerce_cost_value(value: Any) -> Optional[float]:
if value is None or value == '':
return None
try:
if isinstance(value, dict):
for key in ('usd', 'amount', 'value', 'total'):
if key in value:
coerced = _coerce_cost_value(value[key])
if coerced is not None:
return coerced
return None
if isinstance(value, str):
cleaned = value.strip().replace('$', '')
if cleaned.lower().startswith('usd '):
cleaned = cleaned[4:]
return float(cleaned)
return float(value)
except (TypeError, ValueError):
return None
def _find_nested_cost(obj: Any) -> Optional[float]:
if isinstance(obj, dict):
prioritized_keys = (
'cost', 'total_cost', 'total_price', 'price', 'amount', 'usd_cost',
'billed_cost', 'request_cost', 'charge', 'credits_cost'
)
for key in prioritized_keys:
if key in obj:
coerced = _coerce_cost_value(obj.get(key))
if coerced is not None:
return coerced
for value in obj.values():
nested = _find_nested_cost(value)
if nested is not None:
return nested
elif isinstance(obj, list):
for item in obj:
nested = _find_nested_cost(item)
if nested is not None:
return nested
return None
def extract_cost_from_response(response: Dict[str, Any], provider_id: str) -> Optional[float]:
"""
Extract actual cost from provider response if available.
......@@ -40,49 +84,18 @@ def extract_cost_from_response(response: Dict[str, Any], provider_id: str) -> Op
return None
try:
# AWS Bedrock - may include cost in usage
if provider_id in ['amazon', 'bedrock', 'aws']:
usage = response.get('usage', {})
if isinstance(usage, dict):
cost = usage.get('cost')
if cost is not None:
return float(cost)
# Cohere - has billed_units but not direct cost
# Would need pricing config to convert
if provider_id == 'cohere':
meta = response.get('meta', {})
if isinstance(meta, dict):
billed_units = meta.get('billed_units', {})
if billed_units:
# Return None - we'll calculate from tokens
# Could enhance this to calculate from billed_units
pass
# Replicate - has prediction time
if provider_id == 'replicate':
metrics = response.get('metrics', {})
if isinstance(metrics, dict):
predict_time = metrics.get('predict_time')
if predict_time:
# Would need pricing per second to calculate
# Return None for now - calculate from tokens
pass
# Check for generic cost fields that some providers might use
for cost_field in ['cost', 'price', 'amount', 'total_cost']:
if cost_field in response:
cost = response[cost_field]
if cost is not None:
return float(cost)
# Check in usage object
usage = response.get('usage', {})
if isinstance(usage, dict) and cost_field in usage:
cost = usage[cost_field]
if cost is not None:
return float(cost)
provider_id_lower = (provider_id or '').lower()
if 'x_openai_metadata' in response:
cost = _find_nested_cost(response['x_openai_metadata'])
if cost is not None:
return cost
if provider_id_lower in ['amazon', 'bedrock', 'aws', 'openrouter', 'openai', 'codex', 'claude', 'anthropic', 'google', 'qwen', 'kiro', 'kilo', 'replicate', 'cohere']:
cost = _find_nested_cost(response)
if cost is not None:
return cost
return None
except Exception as e:
......@@ -108,20 +121,7 @@ def extract_cost_from_streaming_chunk(chunk: Dict[str, Any], provider_id: str) -
return None
try:
# Check if this is a final chunk with usage/cost info
usage = chunk.get('usage', {})
if isinstance(usage, dict):
# Try to extract cost from usage
cost = usage.get('cost')
if cost is not None:
return float(cost)
# Some providers might include cost at top level in final chunk
cost = chunk.get('cost')
if cost is not None:
return float(cost)
return None
return _find_nested_cost(chunk)
except Exception as e:
logger.debug(f"Error extracting cost from {provider_id} streaming chunk: {e}")
......
This diff is collapsed.
......@@ -14,6 +14,8 @@ _IPNet = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
# failures are not cached so they are retried on the next request.
_CACHE_TTL = 30 * 24 * 3600 # 30 days
_subnet_cache: Dict[str, Tuple[str, float]] = {}
_failure_cache: Dict[str, float] = {}
_FAILURE_TTL = 15 * 60
def _fallback_prefix(addr: _IPAddr) -> _IPNet:
......@@ -69,12 +71,25 @@ async def get_ip_country(ip: str) -> Optional[str]:
if cached is not None:
return cached
failure_expires = _failure_cache.get(ip)
if failure_expires and time.time() < failure_expires:
logger.debug("geo lookup: skipping %s due to recent failure backoff", ip)
return None
logger.debug("geo lookup: querying ipapi.co for %s", ip)
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"https://ipapi.co/{ip}/json/")
if response.status_code != 200:
logger.debug("geo lookup: %s returned HTTP %d", ip, response.status_code)
retry_after = response.headers.get('Retry-After') or response.headers.get('retry-after')
wait_seconds = _FAILURE_TTL
if retry_after:
try:
wait_seconds = max(int(retry_after), _FAILURE_TTL)
except (TypeError, ValueError):
pass
_failure_cache[ip] = time.time() + wait_seconds
return None
data = response.json()
......@@ -99,6 +114,7 @@ async def get_ip_country(ip: str) -> Optional[str]:
except Exception as exc:
logger.debug("geo lookup: %s failed with %s: %s", ip, type(exc).__name__, exc)
_failure_cache[ip] = time.time() + _FAILURE_TTL
return None
......
......@@ -38,6 +38,7 @@ from .models import ChatCompletionRequest, ChatCompletionResponse
from .providers import get_provider_handler, RateLimitError
from .config import config
from .studio import infer_model_capabilities
from .studio_adapters import effective_studio_adapter, infer_studio_adapter_profile, adapt_studio_payload_with_profile
from .utils import (
count_messages_tokens,
split_messages_into_chunks,
......@@ -121,6 +122,66 @@ class RequestHandler:
self.user_rotations = {}
self.user_autoselects = {}
def _get_provider_config(self, provider_id: str):
if self.user_id and provider_id in getattr(self, 'user_providers', {}):
return self.user_providers[provider_id]
return self.config.get_provider(provider_id)
def _get_provider_type(self, provider_id: str) -> str:
provider_config = self._get_provider_config(provider_id)
if isinstance(provider_config, dict):
return provider_config.get('type', 'openai') or 'openai'
return getattr(provider_config, 'type', 'openai') or 'openai'
def _adapt_studio_workflow_payload(self, provider_id: str, endpoint_path: str, body: Dict) -> Dict:
payload = dict(body or {})
provider_type = (self._get_provider_type(provider_id) or '').lower()
model_metadata = payload.get('_studio_model_metadata') if isinstance(payload.get('_studio_model_metadata'), dict) else {}
if provider_id and '_studio_provider_id' not in payload:
payload['_studio_provider_id'] = provider_id
if model_metadata.get('provider_endpoint') and '_studio_provider_endpoint' not in payload:
payload['_studio_provider_endpoint'] = model_metadata.get('provider_endpoint')
adapter = effective_studio_adapter(provider_type, model_metadata)
profile = infer_studio_adapter_profile(provider_id, provider_type, model_metadata)
def first_value(*keys):
for key in keys:
value = payload.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
return None
if endpoint_path == 'v1/video/dub':
selected = first_value('video_model', 'stt_model', 'tts_model', 'model')
if selected:
payload['model'] = selected
elif endpoint_path == 'v1/audio/clone':
selected = first_value('model', 'tts_model')
if selected:
payload['model'] = selected
elif endpoint_path == 'v1/audio/convert':
selected = first_value('model', 'audio_model', 'tts_model', 'stt_model')
if selected:
payload['model'] = selected
elif endpoint_path in {'v1/audio/split', 'v1/audio/denoise'}:
selected = first_value('model', 'audio_model')
if selected:
payload['model'] = selected
elif endpoint_path in {'v1/images/faceswap', 'v1/images/outfit'}:
selected = first_value('model', 'image_model', 'video_model')
if selected:
payload['model'] = selected
elif endpoint_path in {'v1/images/to3d', 'v1/images/from3d', 'v1/video/to3d', 'v1/video/from3d', 'v1/3d/generate'}:
selected = first_value('model', 'render_model', 'image_model', 'video_model')
if selected:
payload['model'] = selected
payload = adapt_studio_payload_with_profile(adapter, profile, endpoint_path, payload)
payload.pop('_studio_model_metadata', None)
payload.pop('_studio_provider_id', None)
payload.pop('_studio_provider_endpoint', None)
return payload
def _load_user_configs(self):
"""Load user-specific configurations from database"""
self.reload_user_config()
......@@ -662,7 +723,8 @@ class RequestHandler:
token_id=getattr(request.state, 'token_id', None),
prompt_tokens=prompt_tokens if prompt_tokens > 0 else None,
completion_tokens=completion_tokens if completion_tokens > 0 else None,
actual_cost=actual_cost
actual_cost=actual_cost,
analytics_kind='execution'
)
# Record context dimensions for model performance tracking
......@@ -715,7 +777,8 @@ class RequestHandler:
token_id=getattr(request.state, 'token_id', None),
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
actual_cost=None
actual_cost=None,
analytics_kind='execution'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for failed request failed: {analytics_error}")
......@@ -1355,7 +1418,8 @@ class RequestHandler:
token_id=getattr(request.state, 'token_id', None),
prompt_tokens=effective_context,
completion_tokens=completion_tokens,
actual_cost=None # Streaming responses typically don't include cost
actual_cost=None,
analytics_kind='execution'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for streaming request failed: {analytics_error}")
......@@ -1393,7 +1457,8 @@ class RequestHandler:
token_id=getattr(request.state, 'token_id', None),
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
actual_cost=None
actual_cost=None,
analytics_kind='execution'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for failed streaming request failed: {analytics_error}")
......@@ -1678,6 +1743,7 @@ class RequestHandler:
import httpx
import logging
logger = logging.getLogger(__name__)
body = self._adapt_studio_workflow_payload(provider_id, endpoint_path, body)
# Support user-defined providers (dict format) and global providers (object format)
if self.user_id and provider_id in self.user_providers:
......@@ -2990,7 +3056,8 @@ class RotationHandler:
token_id=token_id,
prompt_tokens=prompt_tokens if prompt_tokens > 0 else None,
completion_tokens=completion_tokens if completion_tokens > 0 else None,
actual_cost=actual_cost
actual_cost=actual_cost,
analytics_kind='execution'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording failed: {analytics_error}")
......@@ -3065,7 +3132,8 @@ class RotationHandler:
token_id=token_id,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
actual_cost=None
actual_cost=None,
analytics_kind='selector'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for failed rotation failed: {analytics_error}")
......@@ -3735,7 +3803,8 @@ class RotationHandler:
token_id=token_id,
prompt_tokens=prompt_tokens if prompt_tokens > 0 else None,
completion_tokens=completion_tokens_count if completion_tokens_count > 0 else None,
actual_cost=None
actual_cost=None,
analytics_kind='execution'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for streaming rotation failed: {analytics_error}")
......@@ -4852,7 +4921,8 @@ class AutoselectHandler:
token_id=token_id,
prompt_tokens=prompt_tokens if prompt_tokens > 0 else None,
completion_tokens=completion_tokens if completion_tokens > 0 else None,
actual_cost=None
actual_cost=None,
analytics_kind='selector'
)
except Exception as analytics_error:
logger.warning(f"Analytics recording for autoselect failed: {analytics_error}")
......
......@@ -89,6 +89,11 @@ class Provider(BaseModel):
is_subscription: bool = False # If True, pricing is 0 (subscription-based provider)
price_per_million_prompt: Optional[float] = None # Price per million prompt tokens (USD)
price_per_million_completion: Optional[float] = None # Price per million completion tokens (USD)
free_tier_limit: Optional[int] = None
free_tier_period: Optional[str] = None
free_tier_limit_type: Optional[str] = None
premium_reference_monthly_cost: Optional[float] = None
free_tier_description: Optional[str] = None
class ErrorTracking(BaseModel):
failures: int
......
......@@ -1541,6 +1541,24 @@ class BaseProviderHandler:
"""Fetch current usage/quota data from the provider. Returns None if unsupported."""
return None
def normalize_usage_data(self, usage_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Normalize provider usage/quota data into a shared schema when possible."""
if not usage_data or not isinstance(usage_data, dict):
return usage_data
normalized = dict(usage_data)
free_tier = normalized.get('free_tier')
if isinstance(free_tier, dict):
normalized['free_tier'] = {
'limit': free_tier.get('limit'),
'used': free_tier.get('used'),
'remaining': free_tier.get('remaining'),
'period': free_tier.get('period'),
'limit_type': free_tier.get('limit_type', 'requests'),
'source': free_tier.get('source', 'provider')
}
return normalized
async def handle_request_with_batching(self, model: str, messages: List[Dict], max_tokens: Optional[int] = None,
temperature: Optional[float] = 1.0, stream: Optional[bool] = False,
tools: Optional[List[Dict]] = None, tool_choice: Optional[Union[str, Dict]] = None) -> Union[Dict, object]:
......
......@@ -957,6 +957,28 @@ class ClaudeProviderHandler(BaseProviderHandler):
return True
return False
def normalize_usage_data(self, usage_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
normalized = super().normalize_usage_data(usage_data)
if not normalized or not isinstance(normalized, dict):
return normalized
utilization = normalized.get('quota_7d_utilization')
if utilization is not None:
try:
util_float = float(str(utilization).rstrip('%'))
normalized['free_tier'] = {
'limit': 100,
'used': max(util_float, 0.0),
'remaining': max(100.0 - util_float, 0.0),
'period': 'week',
'limit_type': 'percent',
'source': 'provider'
}
except (TypeError, ValueError):
pass
return normalized
async def _ensure_session(self):
"""Ensure session is initialized and valid before making requests."""
......
......@@ -996,7 +996,24 @@ class CodexProviderHandler(BaseProviderHandler):
data = response.json()
import json as _json
logger.debug(f"CodexProviderHandler: Usage raw body for {self.provider_id}:\n{_json.dumps(data, indent=2)}")
return data
normalized = dict(data)
weekly_limit = data.get('weekly_requests_limit') or data.get('weekly_limit') or data.get('free_requests_per_week')
weekly_used = data.get('weekly_requests_used') or data.get('used_requests') or data.get('requests_used')
if weekly_limit is not None:
try:
weekly_limit_int = int(weekly_limit)
weekly_used_int = int(weekly_used or 0)
normalized['free_tier'] = {
'limit': weekly_limit_int,
'used': weekly_used_int,
'remaining': max(weekly_limit_int - weekly_used_int, 0),
'period': 'week',
'limit_type': 'requests',
'source': 'provider'
}
except (TypeError, ValueError):
pass
return self.normalize_usage_data(normalized)
except Exception as e:
logger.warning(f"CodexProviderHandler: Failed to fetch usage: {e}")
return None
......@@ -29,7 +29,7 @@ import json
import uuid
import logging
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union
from ...config import config
from ...models import Model
......@@ -366,6 +366,30 @@ class KiroProviderHandler(BaseProviderHandler):
return openai_response
def normalize_usage_data(self, usage_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
normalized = super().normalize_usage_data(usage_data)
if not normalized or not isinstance(normalized, dict):
return normalized
monthly_limit = normalized.get('monthly_requests_limit') or normalized.get('free_requests_per_month')
monthly_used = normalized.get('monthly_requests_used') or normalized.get('used_requests')
if monthly_limit is not None:
try:
monthly_limit_int = int(monthly_limit)
monthly_used_int = int(monthly_used or 0)
normalized['free_tier'] = {
'limit': monthly_limit_int,
'used': monthly_used_int,
'remaining': max(monthly_limit_int - monthly_used_int, 0),
'period': 'month',
'limit_type': 'requests',
'source': 'provider'
}
except (TypeError, ValueError):
pass
return normalized
async def _handle_streaming_request(self, kiro_api_url: str, payload: dict, headers: dict, model: str):
"""Handle streaming request to Kiro API."""
logger = logging.getLogger(__name__)
......
This diff is collapsed.
{
"providers": {
"codex": {
"id": "codex",
"name": "Codex / ChatGPT OAuth2",
"endpoint": "https://chatgpt.com/backend-api",
"type": "codex",
"api_key_required": false,
"is_subscription": true,
"free_tier_limit": 1,
"free_tier_period": "week",
"free_tier_limit_type": "requests",
"premium_reference_monthly_cost": 20,
"free_tier_description": "Codex upstream weekly free-tier equivalent",
"rate_limit": 0,
"models": []
},
"kiro": {
"id": "kiro",
"name": "Kiro CLI",
"endpoint": "https://api.kiro.dev",
"type": "kiro",
"api_key_required": false,
"is_subscription": true,
"free_tier_limit": 50,
"free_tier_period": "month",
"free_tier_limit_type": "requests",
"premium_reference_monthly_cost": 19,
"free_tier_description": "Kiro upstream 50 requests/month free tier",
"rate_limit": 0,
"models": []
},
"claude": {
"id": "claude",
"name": "Claude OAuth2",
"endpoint": "https://api.anthropic.com",
"type": "claude",
"api_key_required": false,
"is_subscription": true,
"free_tier_limit": 1,
"free_tier_period": "month",
"free_tier_limit_type": "requests",
"premium_reference_monthly_cost": 20,
"free_tier_description": "Claude upstream subscription/free-tier equivalent",
"rate_limit": 0,
"models": []
},
"kilo": {
"id": "kilo",
"name": "Kilocode (OAuth2)",
......@@ -7,6 +52,11 @@
"type": "kilo",
"api_key_required": false,
"is_subscription": true,
"free_tier_limit": 1,
"free_tier_period": "month",
"free_tier_limit_type": "requests",
"premium_reference_monthly_cost": 0,
"free_tier_description": "Kilo upstream free-tier/subscription equivalent",
"rate_limit": 0,
"kilo_config": {
"credentials_file": "~/.kilo_credentials.json"
......
......@@ -155,6 +155,7 @@ setup(
'config/autoselect.md',
'config/condensation_conversational.md',
'config/condensation_semantic.md',
'config/STUDIO_SYSTEM.md',
'config/aisbf.json',
]),
# Install aisbf package to share directory for venv installation
......@@ -307,6 +308,7 @@ setup(
'templates/dashboard/providers.html',
'templates/dashboard/pricing.html',
'templates/dashboard/paypal_connect.html',
'templates/dashboard/provider_quotas.html',
'templates/dashboard/error.html',
'templates/dashboard/forgot_password.html',
'templates/dashboard/rate_limits.html',
......
......@@ -603,8 +603,14 @@ window.onclick = function(event) {
<div style="background: #3498db; color: white; padding: 20px; border-radius: 8px;">
<h4 style="font-size: 14px; margin-bottom: 10px;">💰 Estimated Savings</h4>
<p style="font-size: 28px; font-weight: bold;" id="savings-amount">{{ currency_symbol }}0.00</p>
<small style="color: rgba(255,255,255,0.8);">From cache hits & optimization</small>
<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);">
{% if optimization_savings and optimization_savings.provider_equivalents %}
Provider free-tier equivalents + optimization
{% else %}
Cache hits & optimization
{% endif %}
</small>
</div>
{% for pc in cost_overview.providers %}
......@@ -635,6 +641,48 @@ window.onclick = function(event) {
</div>
</div>
{% if optimization_savings.provider_equivalents %}
<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>
<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.
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.
</p>
<table>
<tr>
<th>Provider</th>
<th>Used Tokens</th>
<th>Requests</th>
<th>Measured Usage</th>
<th>Free Limit</th>
<th>Extra Free Tiers</th>
<th>Equivalent Saved Tokens</th>
<th>Premium Reference</th>
<th>Equivalent Saved Cost</th>
</tr>
{% for item in optimization_savings.provider_equivalents %}
<tr>
<td><strong>{{ item.provider_id }}</strong></td>
<td>{{ format_tokens(item.tokens_used) }}</td>
<td>{{ item.request_count }}</td>
<td>{{ item.usage_amount }} {{ item.free_tier_limit_type }}</td>
<td>{{ item.free_tier_limit }}/{{ item.free_tier_period }} {{ item.free_tier_limit_type }} <small style="color: var(--color-muted);">({{ item.quota_source }})</small></td>
<td>{{ item.extra_free_tiers }}</td>
<td>{{ format_tokens(item.equivalent_saved_tokens) }}</td>
<td>
{% if item.premium_reference_name %}
{{ item.premium_reference_name }} ({{ currency_symbol }}{{ "%.2f"|format(item.premium_reference_monthly_cost) }}/mo)
{% else %}
N/A
{% endif %}
</td>
<td>{{ currency_symbol }}{{ "%.2f"|format(item.equivalent_saved_cost) }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if optimization_savings.savings_by_type %}
<table>
<tr>
......@@ -666,26 +714,9 @@ window.onclick = function(event) {
</table>
{% endif %}
{% else %}
<p style="color: var(--color-muted);">No optimization savings recorded yet. Enable deduplication, condensation, batching, or caching to see savings.</p>
<p style="color: var(--color-muted);">No optimization savings recorded yet. Increase usage on providers with known upstream free tiers, or enable deduplication, condensation, batching, or caching to see savings.</p>
{% endif %}
<script>
// Fetch and display savings estimation
fetch('{{ url_for(request, "/dashboard/response-cache/stats") }}')
.then(response => response.json())
.then(data => {
if (data.enabled && data.hits > 0) {
// Estimate savings: assume average cost of $0.002 per cached request
const avgCostPerRequest = 0.002;
const estimatedSavings = data.hits * avgCostPerRequest;
document.getElementById('savings-amount').textContent = '{{ currency_symbol }}' + estimatedSavings.toFixed(2);
}
})
.catch(error => {
console.error('Error fetching cache stats:', error);
});
</script>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Model Performance</h3>
{% if model_performance %}
<table>
......@@ -1015,9 +1046,10 @@ function confirmDeleteAnalytics(scope) {
<div style="margin-top: 30px; display: flex; gap: 10px; flex-wrap: wrap;">
<a href="/dashboard" class="btn btn-secondary" data-i18n="analytics_page.back">Back to Dashboard</a>
<a href="{{ url_for(request, '/dashboard/analytics/provider-quotas') }}" class="btn btn-secondary">Provider Quota Debug</a>
{% if is_admin %}
<a href="{{ url_for(request, '/dashboard/rate-limits') }}" class="btn">Rate Limits</a>
<a href="{{ url_for(request, '/dashboard/response-cache') }}" class="btn">Response Cache</a>
{% endif %}
</div>
{% endblock %}
\ No newline at end of file
{% endblock %}
{% extends "base.html" %}
{% block title %}Provider Quotas - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 20px;">Provider Quota Debug</h2>
<p style="color: var(--color-muted); margin-bottom: 20px;">
Debug view for normalized provider quota/free-tier data persisted from upstream usage endpoints. This page is intentionally not linked from the main navigation.
</p>
{% if provider_rows %}
<table>
<tr>
<th>Provider</th>
<th>Configured Free Tier</th>
<th>Normalized Runtime Quota</th>
<th>User Snapshot</th>
{% if is_admin %}<th>Global Snapshot</th>{% endif %}
</tr>
{% for row in provider_rows %}
<tr>
<td><strong>{{ row.provider_id }}</strong></td>
<td>
{% if row.free_tier_info %}
<div><strong>{{ row.free_tier_info.description or row.provider_id }}</strong></div>
<div style="color: var(--color-muted); font-size: 13px;">
{{ row.free_tier_info.limit }}/{{ row.free_tier_info.period }} {{ row.free_tier_info.limit_type }}
· {{ row.free_tier_info.source }}
</div>
{% else %}
<span style="color: var(--color-muted);">N/A</span>
{% endif %}
</td>
<td>
{% if row.normalized_quota %}
<div class="code">{{ row.normalized_quota | tojson(indent=2) }}</div>
{% else %}
<span style="color: var(--color-muted);">N/A</span>
{% endif %}
</td>
<td>
{% if row.user_snapshot %}
<div style="margin-bottom: 8px; color: var(--color-muted); font-size: 13px;">{{ row.user_snapshot.last_updated or 'unknown time' }}</div>
<div class="code">{{ row.user_snapshot.usage_data | tojson(indent=2) }}</div>
{% else %}
<span style="color: var(--color-muted);">N/A</span>
{% endif %}
</td>
{% if is_admin %}
<td>
{% if row.global_snapshot %}
<div style="margin-bottom: 8px; color: var(--color-muted); font-size: 13px;">{{ row.global_snapshot.last_updated or 'unknown time' }}</div>
<div class="code">{{ row.global_snapshot.usage_data | tojson(indent=2) }}</div>
{% else %}
<span style="color: var(--color-muted);">N/A</span>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<p style="color: var(--color-muted);">No provider quota data available.</p>
{% endif %}
<div style="margin-top: 24px; display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for(request, '/dashboard/analytics') }}" class="btn btn-secondary">Back to Analytics</a>
</div>
{% endblock %}
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