mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +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)
530 lines
21 KiB
Python
530 lines
21 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any
|
||
|
||
from sqlalchemy import func
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.models.database import Usage, User
|
||
|
||
|
||
class UsageCacheAnalysisMixin:
|
||
"""缓存分析方法"""
|
||
|
||
@staticmethod
|
||
def analyze_cache_affinity_ttl(
|
||
db: Session,
|
||
user_id: str | None = None,
|
||
api_key_id: str | None = None,
|
||
hours: int = 168,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
分析用户请求间隔分布,推荐合适的缓存亲和性 TTL
|
||
|
||
通过分析同一用户连续请求之间的时间间隔,判断用户的使用模式:
|
||
- 高频用户(间隔短):5 分钟 TTL 足够
|
||
- 中频用户:15-30 分钟 TTL
|
||
- 低频用户(间隔长):需要 60 分钟 TTL
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 指定用户 ID(可选,为空则分析所有用户)
|
||
api_key_id: 指定 API Key ID(可选)
|
||
hours: 分析最近多少小时的数据
|
||
|
||
Returns:
|
||
包含分析结果的字典
|
||
"""
|
||
from sqlalchemy import text
|
||
|
||
# 计算时间范围
|
||
start_date = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||
|
||
# 构建 SQL 查询 - 使用窗口函数计算请求间隔
|
||
# 按 user_id 或 api_key_id 分组,计算同一组内连续请求的时间差
|
||
group_by_field = "api_key_id" if api_key_id else "user_id"
|
||
|
||
# 构建过滤条件
|
||
filter_clause = ""
|
||
if user_id or api_key_id:
|
||
filter_clause = f"AND {group_by_field} = :filter_id"
|
||
|
||
sql = text(f"""
|
||
WITH user_requests AS (
|
||
SELECT
|
||
{group_by_field} as group_id,
|
||
created_at,
|
||
LAG(created_at) OVER (
|
||
PARTITION BY {group_by_field}
|
||
ORDER BY created_at
|
||
) as prev_request_at
|
||
FROM usage
|
||
WHERE status = 'completed'
|
||
AND created_at > :start_date
|
||
AND {group_by_field} IS NOT NULL
|
||
{filter_clause}
|
||
),
|
||
intervals AS (
|
||
SELECT
|
||
group_id,
|
||
EXTRACT(EPOCH FROM (created_at - prev_request_at)) / 60.0 as interval_minutes
|
||
FROM user_requests
|
||
WHERE prev_request_at IS NOT NULL
|
||
),
|
||
user_stats AS (
|
||
SELECT
|
||
group_id,
|
||
COUNT(*) as request_count,
|
||
COUNT(*) FILTER (WHERE interval_minutes <= 5) as within_5min,
|
||
COUNT(*) FILTER (WHERE interval_minutes > 5 AND interval_minutes <= 15) as within_15min,
|
||
COUNT(*) FILTER (WHERE interval_minutes > 15 AND interval_minutes <= 30) as within_30min,
|
||
COUNT(*) FILTER (WHERE interval_minutes > 30 AND interval_minutes <= 60) as within_60min,
|
||
COUNT(*) FILTER (WHERE interval_minutes > 60) as over_60min,
|
||
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY interval_minutes) as median_interval,
|
||
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY interval_minutes) as p75_interval,
|
||
PERCENTILE_CONT(0.90) WITHIN GROUP (ORDER BY interval_minutes) as p90_interval,
|
||
AVG(interval_minutes) as avg_interval,
|
||
MIN(interval_minutes) as min_interval,
|
||
MAX(interval_minutes) as max_interval
|
||
FROM intervals
|
||
GROUP BY group_id
|
||
HAVING COUNT(*) >= 2
|
||
)
|
||
SELECT * FROM user_stats
|
||
ORDER BY request_count DESC
|
||
""")
|
||
|
||
params: dict[str, Any] = {
|
||
"start_date": start_date,
|
||
}
|
||
if user_id:
|
||
params["filter_id"] = user_id
|
||
elif api_key_id:
|
||
params["filter_id"] = api_key_id
|
||
|
||
result = db.execute(sql, params)
|
||
rows = result.fetchall()
|
||
|
||
# 收集所有 user_id 以便批量查询用户信息
|
||
group_ids = [row[0] for row in rows]
|
||
|
||
# 如果是按 user_id 分组,查询用户信息
|
||
user_info_map: dict[str, dict[str, str]] = {}
|
||
if group_by_field == "user_id" and group_ids:
|
||
users = db.query(User).filter(User.id.in_(group_ids)).all()
|
||
for user in users:
|
||
user_info_map[str(user.id)] = {
|
||
"username": str(user.username),
|
||
"email": str(user.email) if user.email else "",
|
||
}
|
||
|
||
# 处理结果
|
||
users_analysis = []
|
||
for row in rows:
|
||
# row 是一个 tuple,按查询顺序访问
|
||
(
|
||
group_id,
|
||
request_count,
|
||
within_5min,
|
||
within_15min,
|
||
within_30min,
|
||
within_60min,
|
||
over_60min,
|
||
median_interval,
|
||
p75_interval,
|
||
p90_interval,
|
||
avg_interval,
|
||
min_interval,
|
||
max_interval,
|
||
) = row
|
||
|
||
# 计算推荐 TTL
|
||
recommended_ttl = UsageCacheAnalysisMixin._calculate_recommended_ttl(
|
||
p75_interval, p90_interval
|
||
)
|
||
|
||
# 获取用户信息
|
||
user_info = user_info_map.get(str(group_id), {})
|
||
|
||
# 计算各区间占比
|
||
total_intervals = request_count
|
||
users_analysis.append(
|
||
{
|
||
"group_id": group_id,
|
||
"username": user_info.get("username"),
|
||
"email": user_info.get("email"),
|
||
"request_count": request_count,
|
||
"interval_distribution": {
|
||
"within_5min": within_5min,
|
||
"within_15min": within_15min,
|
||
"within_30min": within_30min,
|
||
"within_60min": within_60min,
|
||
"over_60min": over_60min,
|
||
},
|
||
"interval_percentages": {
|
||
"within_5min": round(within_5min / total_intervals * 100, 1),
|
||
"within_15min": round(within_15min / total_intervals * 100, 1),
|
||
"within_30min": round(within_30min / total_intervals * 100, 1),
|
||
"within_60min": round(within_60min / total_intervals * 100, 1),
|
||
"over_60min": round(over_60min / total_intervals * 100, 1),
|
||
},
|
||
"percentiles": {
|
||
"p50": round(float(median_interval), 2) if median_interval else None,
|
||
"p75": round(float(p75_interval), 2) if p75_interval else None,
|
||
"p90": round(float(p90_interval), 2) if p90_interval else None,
|
||
},
|
||
"avg_interval_minutes": (
|
||
round(float(avg_interval), 2) if avg_interval else None
|
||
),
|
||
"min_interval_minutes": (
|
||
round(float(min_interval), 2) if min_interval else None
|
||
),
|
||
"max_interval_minutes": (
|
||
round(float(max_interval), 2) if max_interval else None
|
||
),
|
||
"recommended_ttl_minutes": recommended_ttl,
|
||
"recommendation_reason": UsageCacheAnalysisMixin._get_ttl_recommendation_reason(
|
||
recommended_ttl, p75_interval, p90_interval
|
||
),
|
||
}
|
||
)
|
||
|
||
# 汇总统计
|
||
ttl_distribution = {"5min": 0, "15min": 0, "30min": 0, "60min": 0}
|
||
for analysis in users_analysis:
|
||
ttl = analysis["recommended_ttl_minutes"]
|
||
if ttl <= 5:
|
||
ttl_distribution["5min"] += 1
|
||
elif ttl <= 15:
|
||
ttl_distribution["15min"] += 1
|
||
elif ttl <= 30:
|
||
ttl_distribution["30min"] += 1
|
||
else:
|
||
ttl_distribution["60min"] += 1
|
||
|
||
return {
|
||
"analysis_period_hours": hours,
|
||
"total_users_analyzed": len(users_analysis),
|
||
"ttl_distribution": ttl_distribution,
|
||
"users": users_analysis,
|
||
}
|
||
|
||
@staticmethod
|
||
def _calculate_recommended_ttl(
|
||
p75_interval: float | None,
|
||
p90_interval: float | None,
|
||
) -> int:
|
||
"""
|
||
根据请求间隔分布计算推荐的缓存 TTL
|
||
|
||
策略:
|
||
- 如果 90% 的请求间隔都在 5 分钟内 -> 5 分钟 TTL
|
||
- 如果 75% 的请求间隔在 15 分钟内 -> 15 分钟 TTL
|
||
- 如果 75% 的请求间隔在 30 分钟内 -> 30 分钟 TTL
|
||
- 否则 -> 60 分钟 TTL
|
||
"""
|
||
if p90_interval is None or p75_interval is None:
|
||
return 5 # 默认值
|
||
|
||
# 如果 90% 的间隔都在 5 分钟内
|
||
if p90_interval <= 5:
|
||
return 5
|
||
|
||
# 如果 75% 的间隔在 15 分钟内
|
||
if p75_interval <= 15:
|
||
return 15
|
||
|
||
# 如果 75% 的间隔在 30 分钟内
|
||
if p75_interval <= 30:
|
||
return 30
|
||
|
||
# 低频用户,需要更长的 TTL
|
||
return 60
|
||
|
||
@staticmethod
|
||
def _get_ttl_recommendation_reason(
|
||
ttl: int,
|
||
p75_interval: float | None,
|
||
p90_interval: float | None,
|
||
) -> str:
|
||
"""生成 TTL 推荐理由"""
|
||
if p75_interval is None or p90_interval is None:
|
||
return "数据不足,使用默认值"
|
||
|
||
if ttl == 5:
|
||
return f"高频用户:90% 的请求间隔在 {p90_interval:.1f} 分钟内"
|
||
elif ttl == 15:
|
||
return f"中高频用户:75% 的请求间隔在 {p75_interval:.1f} 分钟内"
|
||
elif ttl == 30:
|
||
return f"中频用户:75% 的请求间隔在 {p75_interval:.1f} 分钟内"
|
||
else:
|
||
return f"低频用户:75% 的请求间隔为 {p75_interval:.1f} 分钟,建议使用长 TTL"
|
||
|
||
@staticmethod
|
||
def get_cache_hit_analysis(
|
||
db: Session,
|
||
user_id: str | None = None,
|
||
api_key_id: str | None = None,
|
||
hours: int = 168,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
分析缓存命中情况
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
user_id: 指定用户 ID(可选)
|
||
api_key_id: 指定 API Key ID(可选)
|
||
hours: 分析最近多少小时的数据
|
||
|
||
Returns:
|
||
缓存命中分析结果
|
||
"""
|
||
start_date = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||
|
||
# 基础查询
|
||
query = db.query(
|
||
func.count(Usage.id).label("total_requests"),
|
||
func.sum(Usage.input_tokens).label("total_input_tokens"),
|
||
func.sum(Usage.cache_read_input_tokens).label("total_cache_read_tokens"),
|
||
func.sum(Usage.cache_creation_input_tokens).label("total_cache_creation_tokens"),
|
||
func.sum(Usage.cache_read_cost_usd).label("total_cache_read_cost"),
|
||
func.sum(Usage.cache_creation_cost_usd).label("total_cache_creation_cost"),
|
||
).filter(
|
||
Usage.status == "completed",
|
||
Usage.created_at >= start_date,
|
||
)
|
||
|
||
if user_id:
|
||
query = query.filter(Usage.user_id == user_id)
|
||
if api_key_id:
|
||
query = query.filter(Usage.api_key_id == api_key_id)
|
||
|
||
result = query.first()
|
||
|
||
if result is None:
|
||
total_requests = 0
|
||
total_input_tokens = 0
|
||
total_cache_read_tokens = 0
|
||
total_cache_creation_tokens = 0
|
||
total_cache_read_cost = 0.0
|
||
total_cache_creation_cost = 0.0
|
||
else:
|
||
total_requests = result.total_requests or 0
|
||
total_input_tokens = result.total_input_tokens or 0
|
||
total_cache_read_tokens = result.total_cache_read_tokens or 0
|
||
total_cache_creation_tokens = result.total_cache_creation_tokens or 0
|
||
total_cache_read_cost = float(result.total_cache_read_cost or 0)
|
||
total_cache_creation_cost = float(result.total_cache_creation_cost or 0)
|
||
|
||
# 计算缓存命中率(按 token 数)
|
||
# 总输入上下文 = input_tokens + cache_read_tokens(因为 input_tokens 不含 cache_read)
|
||
# 或者如果 input_tokens 已经包含 cache_read,则直接用 input_tokens
|
||
# 这里假设 cache_read_tokens 是额外的,命中率 = cache_read / (input + cache_read)
|
||
total_context_tokens = total_input_tokens + total_cache_read_tokens
|
||
cache_hit_rate = 0.0
|
||
if total_context_tokens > 0:
|
||
cache_hit_rate = total_cache_read_tokens / total_context_tokens * 100
|
||
|
||
# 计算节省的费用
|
||
# 缓存读取价格是正常输入价格的 10%,所以节省了 90%
|
||
# 节省 = cache_read_tokens * (正常价格 - 缓存价格) = cache_read_cost * 9
|
||
# 因为 cache_read_cost 是按 10% 价格算的,如果按 100% 算就是 10 倍
|
||
estimated_savings = total_cache_read_cost * 9 # 节省了 90%
|
||
|
||
# 统计有缓存命中的请求数
|
||
requests_with_cache_hit = db.query(func.count(Usage.id)).filter(
|
||
Usage.status == "completed",
|
||
Usage.created_at >= start_date,
|
||
Usage.cache_read_input_tokens > 0,
|
||
)
|
||
if user_id:
|
||
requests_with_cache_hit = requests_with_cache_hit.filter(Usage.user_id == user_id)
|
||
if api_key_id:
|
||
requests_with_cache_hit = requests_with_cache_hit.filter(Usage.api_key_id == api_key_id)
|
||
requests_with_cache_hit_count = int(requests_with_cache_hit.scalar() or 0)
|
||
|
||
return {
|
||
"analysis_period_hours": hours,
|
||
"total_requests": total_requests,
|
||
"requests_with_cache_hit": requests_with_cache_hit_count,
|
||
"request_cache_hit_rate": (
|
||
round(requests_with_cache_hit_count / total_requests * 100, 2)
|
||
if total_requests > 0
|
||
else 0
|
||
),
|
||
"total_input_tokens": total_input_tokens,
|
||
"total_cache_read_tokens": total_cache_read_tokens,
|
||
"total_cache_creation_tokens": total_cache_creation_tokens,
|
||
"token_cache_hit_rate": round(cache_hit_rate, 2),
|
||
"total_cache_read_cost_usd": round(total_cache_read_cost, 4),
|
||
"total_cache_creation_cost_usd": round(total_cache_creation_cost, 4),
|
||
"estimated_savings_usd": round(estimated_savings, 4),
|
||
}
|
||
|
||
@staticmethod
|
||
def get_interval_timeline(
|
||
db: Session,
|
||
hours: int = 24,
|
||
limit: int = 10000,
|
||
user_id: str | None = None,
|
||
include_user_info: bool = False,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
获取请求间隔时间线数据,用于散点图展示
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
hours: 分析最近多少小时的数据(默认24小时)
|
||
limit: 最大返回数据点数量(默认10000)
|
||
user_id: 指定用户 ID(可选,为空则返回所有用户)
|
||
include_user_info: 是否包含用户信息(用于管理员多用户视图)
|
||
|
||
Returns:
|
||
包含时间线数据点的字典,每个数据点包含 model 字段用于按模型区分颜色
|
||
"""
|
||
from sqlalchemy import text
|
||
|
||
start_date = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||
|
||
# 构建用户过滤条件
|
||
user_filter = "AND u.user_id = :user_id" if user_id else ""
|
||
|
||
# 根据是否需要用户信息选择不同的查询
|
||
if include_user_info and not user_id:
|
||
# 管理员视图:返回带用户信息的数据点
|
||
# 使用按比例采样,保持每个用户的数据量比例不变
|
||
sql = text(f"""
|
||
WITH request_intervals AS (
|
||
SELECT
|
||
u.created_at,
|
||
u.user_id,
|
||
u.model,
|
||
usr.username,
|
||
LAG(u.created_at) OVER (
|
||
PARTITION BY u.user_id
|
||
ORDER BY u.created_at
|
||
) as prev_request_at
|
||
FROM usage u
|
||
LEFT JOIN users usr ON u.user_id = usr.id
|
||
WHERE u.status = 'completed'
|
||
AND u.created_at > :start_date
|
||
AND u.user_id IS NOT NULL
|
||
{user_filter}
|
||
),
|
||
filtered_intervals AS (
|
||
SELECT
|
||
created_at,
|
||
user_id,
|
||
model,
|
||
username,
|
||
EXTRACT(EPOCH FROM (created_at - prev_request_at)) / 60.0 as interval_minutes,
|
||
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) as rn
|
||
FROM request_intervals
|
||
WHERE prev_request_at IS NOT NULL
|
||
AND EXTRACT(EPOCH FROM (created_at - prev_request_at)) / 60.0 <= 120
|
||
),
|
||
total_count AS (
|
||
SELECT COUNT(*) as cnt FROM filtered_intervals
|
||
),
|
||
user_totals AS (
|
||
SELECT user_id, COUNT(*) as user_cnt FROM filtered_intervals GROUP BY user_id
|
||
),
|
||
user_limits AS (
|
||
SELECT
|
||
ut.user_id,
|
||
CASE WHEN tc.cnt <= :limit THEN ut.user_cnt
|
||
ELSE GREATEST(CEIL(ut.user_cnt::float * :limit / tc.cnt), 1)::int
|
||
END as user_limit
|
||
FROM user_totals ut, total_count tc
|
||
)
|
||
SELECT
|
||
fi.created_at,
|
||
fi.user_id,
|
||
fi.model,
|
||
fi.username,
|
||
fi.interval_minutes
|
||
FROM filtered_intervals fi
|
||
JOIN user_limits ul ON fi.user_id = ul.user_id
|
||
WHERE fi.rn <= ul.user_limit
|
||
ORDER BY fi.created_at
|
||
""")
|
||
else:
|
||
# 普通视图:返回时间、间隔和模型信息
|
||
sql = text(f"""
|
||
WITH request_intervals AS (
|
||
SELECT
|
||
u.created_at,
|
||
u.user_id,
|
||
u.model,
|
||
LAG(u.created_at) OVER (
|
||
PARTITION BY u.user_id
|
||
ORDER BY u.created_at
|
||
) as prev_request_at
|
||
FROM usage u
|
||
WHERE u.status = 'completed'
|
||
AND u.created_at > :start_date
|
||
AND u.user_id IS NOT NULL
|
||
{user_filter}
|
||
)
|
||
SELECT
|
||
created_at,
|
||
model,
|
||
EXTRACT(EPOCH FROM (created_at - prev_request_at)) / 60.0 as interval_minutes
|
||
FROM request_intervals
|
||
WHERE prev_request_at IS NOT NULL
|
||
AND EXTRACT(EPOCH FROM (created_at - prev_request_at)) / 60.0 <= 120
|
||
ORDER BY created_at
|
||
LIMIT :limit
|
||
""")
|
||
|
||
params: dict[str, Any] = {"start_date": start_date, "limit": limit}
|
||
if user_id:
|
||
params["user_id"] = user_id
|
||
|
||
result = db.execute(sql, params)
|
||
rows = result.fetchall()
|
||
|
||
# 转换为时间线数据点
|
||
points = []
|
||
users_map: dict[str, str] = {} # user_id -> username
|
||
models_set: set = set() # 收集所有出现的模型
|
||
|
||
if include_user_info and not user_id:
|
||
for row in rows:
|
||
created_at, row_user_id, model, username, interval_minutes = row
|
||
point_data: dict[str, Any] = {
|
||
"x": created_at.isoformat(),
|
||
"y": round(float(interval_minutes), 2),
|
||
"user_id": str(row_user_id),
|
||
}
|
||
if model:
|
||
point_data["model"] = model
|
||
models_set.add(model)
|
||
points.append(point_data)
|
||
if row_user_id and username:
|
||
users_map[str(row_user_id)] = username
|
||
else:
|
||
for row in rows:
|
||
created_at, model, interval_minutes = row
|
||
point_data = {"x": created_at.isoformat(), "y": round(float(interval_minutes), 2)}
|
||
if model:
|
||
point_data["model"] = model
|
||
models_set.add(model)
|
||
points.append(point_data)
|
||
|
||
response: dict[str, Any] = {
|
||
"analysis_period_hours": hours,
|
||
"total_points": len(points),
|
||
"points": points,
|
||
}
|
||
|
||
if include_user_info and not user_id:
|
||
response["users"] = users_map
|
||
|
||
# 如果有模型信息,返回模型列表
|
||
if models_set:
|
||
response["models"] = sorted(models_set)
|
||
|
||
return response
|