2026-02-04 02:04:54 +08:00
|
|
|
"""Admin quota usage stats routes."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
from src.api.base.admin_adapter import AdminApiAdapter
|
|
|
|
|
from src.api.base.context import ApiRequestContext
|
2026-03-03 22:04:40 +08:00
|
|
|
from src.config.constants import CacheTTL
|
2026-02-04 02:04:54 +08:00
|
|
|
from src.core.enums import ProviderBillingType
|
|
|
|
|
from src.database import get_db
|
|
|
|
|
from src.models.database import Provider
|
2026-03-03 22:04:40 +08:00
|
|
|
from src.utils.cache_decorator import cache_result
|
2026-02-04 02:04:54 +08:00
|
|
|
|
|
|
|
|
from .common import pipeline
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdminQuotaUsageAdapter(AdminApiAdapter):
|
2026-03-03 22:04:40 +08:00
|
|
|
@cache_result(
|
|
|
|
|
key_prefix="admin:stats:providers:quota_usage",
|
|
|
|
|
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
|
|
|
|
|
user_specific=False,
|
|
|
|
|
)
|
2026-02-04 02:04:54 +08:00
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
|
|
|
|
db = context.db
|
|
|
|
|
providers = (
|
|
|
|
|
db.query(Provider)
|
|
|
|
|
.filter(
|
|
|
|
|
(Provider.billing_type == ProviderBillingType.MONTHLY_QUOTA)
|
|
|
|
|
| (Provider.monthly_quota_usd.isnot(None))
|
|
|
|
|
)
|
|
|
|
|
.all()
|
|
|
|
|
)
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
for provider in providers:
|
2026-03-08 16:44:16 +08:00
|
|
|
quota = float(provider.monthly_quota_usd or 0)
|
|
|
|
|
used = float(provider.monthly_used_usd or 0)
|
2026-02-04 02:04:54 +08:00
|
|
|
remaining = max(quota - used, 0.0)
|
|
|
|
|
usage_percent = round((used / quota) * 100, 2) if quota > 0 else 0.0
|
|
|
|
|
|
|
|
|
|
reset_at = provider.quota_last_reset_at
|
|
|
|
|
if reset_at:
|
|
|
|
|
days_elapsed = max(1, (now - reset_at).days)
|
|
|
|
|
else:
|
|
|
|
|
days_elapsed = max(1, now.day - 1)
|
|
|
|
|
|
|
|
|
|
daily_rate = used / days_elapsed if used > 0 else 0.0
|
|
|
|
|
estimated_exhaust_at = None
|
|
|
|
|
if daily_rate > 0 and remaining > 0:
|
|
|
|
|
estimated_exhaust_at = now + timedelta(days=remaining / daily_rate)
|
|
|
|
|
if provider.quota_expires_at:
|
|
|
|
|
if not estimated_exhaust_at or provider.quota_expires_at < estimated_exhaust_at:
|
|
|
|
|
estimated_exhaust_at = provider.quota_expires_at
|
|
|
|
|
|
|
|
|
|
result.append(
|
|
|
|
|
{
|
|
|
|
|
"id": provider.id,
|
|
|
|
|
"name": provider.name,
|
|
|
|
|
"quota_usd": float(quota),
|
|
|
|
|
"used_usd": float(used),
|
|
|
|
|
"remaining_usd": float(remaining),
|
|
|
|
|
"usage_percent": usage_percent,
|
|
|
|
|
"quota_expires_at": (
|
|
|
|
|
provider.quota_expires_at.isoformat() if provider.quota_expires_at else None
|
|
|
|
|
),
|
|
|
|
|
"estimated_exhaust_at": (
|
|
|
|
|
estimated_exhaust_at.isoformat() if estimated_exhaust_at else None
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result.sort(key=lambda x: x["usage_percent"], reverse=True)
|
|
|
|
|
return {"providers": result}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/providers/quota-usage")
|
|
|
|
|
async def get_quota_usage(
|
|
|
|
|
request: Request,
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
) -> Any:
|
|
|
|
|
adapter = AdminQuotaUsageAdapter()
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|