mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
143 lines
5.2 KiB
Python
143 lines
5.2 KiB
Python
"""Admin comparison stats routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.api.base.admin_adapter import AdminApiAdapter
|
|
from src.api.base.context import ApiRequestContext
|
|
from src.config.constants import CacheTTL
|
|
from src.database import get_db
|
|
from src.services.system.stats_aggregator import AggregatedStats, StatsFilter, query_stats_hybrid
|
|
from src.services.system.time_range import TimeRangeParams
|
|
from src.utils.cache_decorator import cache_result
|
|
|
|
from .common import pipeline
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class AdminComparisonAdapter(AdminApiAdapter):
|
|
def __init__(
|
|
self,
|
|
current_start: date,
|
|
current_end: date,
|
|
comparison_type: Literal["period", "year"],
|
|
timezone_name: str | None,
|
|
tz_offset_minutes: int | None,
|
|
) -> None:
|
|
self.current_start = current_start
|
|
self.current_end = current_end
|
|
self.comparison_type = comparison_type
|
|
self.timezone_name = timezone_name
|
|
self.tz_offset_minutes = tz_offset_minutes
|
|
|
|
@cache_result(
|
|
key_prefix="admin:stats:comparison",
|
|
ttl=CacheTTL.ADMIN_USAGE_AGGREGATION,
|
|
user_specific=False,
|
|
vary_by=[
|
|
"current_start",
|
|
"current_end",
|
|
"comparison_type",
|
|
"timezone_name",
|
|
"tz_offset_minutes",
|
|
],
|
|
)
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
|
if self.current_start > self.current_end:
|
|
raise HTTPException(status_code=400, detail="current_start must be <= current_end")
|
|
|
|
days = (self.current_end - self.current_start).days + 1
|
|
|
|
def _safe_year_shift(value: date) -> date:
|
|
try:
|
|
return value.replace(year=value.year - 1)
|
|
except ValueError:
|
|
return value.replace(year=value.year - 1, day=28)
|
|
|
|
if self.comparison_type == "period":
|
|
comparison_end = self.current_start - timedelta(days=1)
|
|
comparison_start = comparison_end - timedelta(days=days - 1)
|
|
else:
|
|
comparison_start = _safe_year_shift(self.current_start)
|
|
comparison_end = _safe_year_shift(self.current_end)
|
|
|
|
current_range = TimeRangeParams(
|
|
start_date=self.current_start,
|
|
end_date=self.current_end,
|
|
timezone=self.timezone_name,
|
|
tz_offset_minutes=self.tz_offset_minutes or 0,
|
|
).validate_and_resolve()
|
|
comparison_range = TimeRangeParams(
|
|
start_date=comparison_start,
|
|
end_date=comparison_end,
|
|
timezone=self.timezone_name,
|
|
tz_offset_minutes=self.tz_offset_minutes or 0,
|
|
).validate_and_resolve()
|
|
|
|
current_stats = query_stats_hybrid(context.db, current_range, filters=StatsFilter())
|
|
comparison_stats = query_stats_hybrid(context.db, comparison_range, filters=StatsFilter())
|
|
|
|
def _stats_payload(stats: AggregatedStats) -> dict[str, Any]:
|
|
total_tokens = (
|
|
stats.input_tokens
|
|
+ stats.output_tokens
|
|
+ stats.cache_creation_tokens
|
|
+ stats.cache_read_tokens
|
|
)
|
|
return {
|
|
"total_requests": stats.total_requests,
|
|
"total_tokens": total_tokens,
|
|
"total_cost": float(stats.total_cost),
|
|
"actual_total_cost": float(stats.actual_total_cost),
|
|
"avg_response_time_ms": float(stats.avg_response_time_ms),
|
|
"error_requests": stats.error_requests,
|
|
}
|
|
|
|
def _pct_change(current: float, previous: float) -> float | None:
|
|
if previous == 0:
|
|
return None if current != 0 else 0.0
|
|
return round((current - previous) / previous * 100, 2)
|
|
|
|
current_payload = _stats_payload(current_stats)
|
|
comparison_payload = _stats_payload(comparison_stats)
|
|
changes = {
|
|
key: _pct_change(float(current_payload[key]), float(comparison_payload[key]))
|
|
for key in current_payload.keys()
|
|
}
|
|
|
|
return {
|
|
"current": current_payload,
|
|
"comparison": comparison_payload,
|
|
"change_percent": changes,
|
|
"current_start": self.current_start.isoformat(),
|
|
"current_end": self.current_end.isoformat(),
|
|
"comparison_start": comparison_start.isoformat(),
|
|
"comparison_end": comparison_end.isoformat(),
|
|
}
|
|
|
|
|
|
@router.get("/comparison")
|
|
async def get_comparison(
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
current_start: date = Query(...),
|
|
current_end: date = Query(...),
|
|
comparison_type: Literal["period", "year"] = Query("period"),
|
|
timezone_name: str | None = Query(None, alias="timezone"),
|
|
tz_offset_minutes: int | None = Query(0),
|
|
) -> Any:
|
|
adapter = AdminComparisonAdapter(
|
|
current_start=current_start,
|
|
current_end=current_end,
|
|
comparison_type=comparison_type,
|
|
timezone_name=timezone_name,
|
|
tz_offset_minutes=tz_offset_minutes,
|
|
)
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|