Files
Aether/_deprecated_py_src/services/rate_limit/adaptive_rpm.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 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)
2026-04-03 16:26:16 +08:00

756 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
自适应 RPM 调整器 - 基于置信度衰减的 RPM 限制学习
核心算法:多次观察确认 + 置信度衰减
- 收到 429 时记录观察(本地 RPM + 上游 header 限制值)
- 多次一致的观察才确认限制header 需 2 次,无 header 需 3 次)
- confidence 随时间自然衰减,限制永远不会固化
- confidence 低于阈值时停止本地 RPM 限制执行,让上游 429 透传
设计原则:
1. 限制永远不固化 -- confidence 需要持续的 429 观察来维持
2. 即使有上游 header 也要多次确认
3. 学习期间 429 直接透传给客户端
4. 优先使用上游 header 声明的限制值,而非本地 RPM 计数
"""
from __future__ import annotations
from datetime import datetime, timezone
from statistics import median
from typing import Any, cast
from sqlalchemy.orm import Session
from src.config.constants import RPMDefaults
from src.core.batch_committer import get_batch_committer
from src.core.logger import logger
from src.models.database import ProviderAPIKey
from src.services.rate_limit.detector import RateLimitInfo, RateLimitType
class AdaptiveStrategy:
"""自适应策略类型"""
AIMD = "aimd" # 加性增-乘性减 (Additive Increase Multiplicative Decrease)
CONSERVATIVE = "conservative" # 保守策略(只减不增)
AGGRESSIVE = "aggressive" # 激进策略(快速探测)
class AdaptiveRPMManager:
"""
自适应 RPM 管理器
核心算法:多次观察确认 + 置信度衰减
- 收到 429 时记录观察(本地 RPM 计数 + 上游 header 限制值)
- 有 header 的观察需 MIN_HEADER_CONFIRMATIONS 次一致才确认
- 无 header 的观察需 MIN_CONSISTENT_OBSERVATIONS 次一致才确认
- confidence 随时间自然衰减CONFIDENCE_DECAY_PER_MINUTE
- confidence < ENFORCEMENT_CONFIDENCE_THRESHOLD 时停止本地限制执行
扩容条件(满足任一即可):
1. 利用率扩容:窗口内高利用率比例 >= 60%,且当前限制 < 边界
2. 探测性扩容:距上次 429 超过 30 分钟,可以尝试突破边界
"""
# 默认配置
DEFAULT_INITIAL_LIMIT = RPMDefaults.INITIAL_LIMIT
MIN_RPM_LIMIT = RPMDefaults.MIN_RPM_LIMIT
MAX_RPM_LIMIT = RPMDefaults.MAX_RPM_LIMIT
# AIMD 参数
INCREASE_STEP = RPMDefaults.INCREASE_STEP
# 滑动窗口参数
UTILIZATION_WINDOW_SIZE = RPMDefaults.UTILIZATION_WINDOW_SIZE
UTILIZATION_WINDOW_SECONDS = RPMDefaults.UTILIZATION_WINDOW_SECONDS
UTILIZATION_THRESHOLD = RPMDefaults.UTILIZATION_THRESHOLD
HIGH_UTILIZATION_RATIO = RPMDefaults.HIGH_UTILIZATION_RATIO
MIN_SAMPLES_FOR_DECISION = RPMDefaults.MIN_SAMPLES_FOR_DECISION
# 探测性扩容参数
PROBE_INCREASE_INTERVAL_MINUTES = RPMDefaults.PROBE_INCREASE_INTERVAL_MINUTES
PROBE_INCREASE_MIN_REQUESTS = RPMDefaults.PROBE_INCREASE_MIN_REQUESTS
# 记录历史数量
MAX_HISTORY_RECORDS = 20
# 置信度学习参数
MIN_CONSISTENT_OBSERVATIONS = RPMDefaults.MIN_CONSISTENT_OBSERVATIONS
MIN_HEADER_CONFIRMATIONS = RPMDefaults.MIN_HEADER_CONFIRMATIONS
OBSERVATION_CONSISTENCY_THRESHOLD = RPMDefaults.OBSERVATION_CONSISTENCY_THRESHOLD
HEADER_LIMIT_SAFETY_MARGIN = RPMDefaults.HEADER_LIMIT_SAFETY_MARGIN
OBSERVATION_LIMIT_SAFETY_MARGIN = RPMDefaults.OBSERVATION_LIMIT_SAFETY_MARGIN
ENFORCEMENT_CONFIDENCE_THRESHOLD = RPMDefaults.ENFORCEMENT_CONFIDENCE_THRESHOLD
CONFIDENCE_DECAY_PER_MINUTE = RPMDefaults.CONFIDENCE_DECAY_PER_MINUTE
def __init__(self, strategy: str = AdaptiveStrategy.AIMD):
"""
初始化自适应 RPM 管理器
Args:
strategy: 调整策略
"""
self.strategy = strategy
@staticmethod
def _persist_metadata_update(db: Session) -> None:
"""
延后持久化自适应学习元数据,避免在请求热路径上同步 flush。
这些更新只修改既有 ProviderAPIKey 行上的统计/学习字段,不依赖数据库生成值。
请求级事务会在中间件统一 commit后台会话则由 BatchCommitter 负责后续提交。
"""
get_batch_committer().mark_dirty(db)
# ==================== 429 处理 ====================
def handle_429_error(
self,
db: Session,
key: ProviderAPIKey,
rate_limit_info: RateLimitInfo,
current_rpm: int | None = None,
) -> int | None:
"""
处理 429 错误,记录观察并基于一致性评估是否设置限制
不再单次 429 就设限,而是:
1. 记录 429 观察(本地 RPM + 上游 header 限制值)
2. 评估历史观察的一致性
3. 一致性达标时设置 learned_rpm_limit 并赋予 confidence
4. 一致性不够时保持学习期429 透传给客户端)
Returns:
调整后的 RPM 限制,或 None学习期间
"""
is_adaptive = key.rpm_limit is None
if not is_adaptive:
logger.debug(f"Key {key.id} 设置了固定 RPM 限制 ({key.rpm_limit}),跳过自适应调整")
return int(key.rpm_limit) # type: ignore[arg-type]
# 更新 429 统计
key.last_429_at = datetime.now(timezone.utc) # type: ignore[assignment]
key.last_429_type = rate_limit_info.limit_type # type: ignore[assignment]
# 清空利用率采样窗口
key.utilization_samples = [] # type: ignore[assignment]
if rate_limit_info.limit_type == RateLimitType.RPM:
key.rpm_429_count = int(key.rpm_429_count or 0) + 1 # type: ignore[assignment]
upstream_limit = rate_limit_info.limit_value
# 记录 429 观察
self._record_429_observation(key, current_rpm, upstream_limit)
# 评估观察一致性,决定是否设置/更新限制
evaluated_limit, confidence = self._evaluate_observations(key)
old_limit = key.learned_rpm_limit
if evaluated_limit is not None and confidence >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD:
# 一致性达标,设置限制
self._record_adjustment(
key,
old_limit=old_limit or 0,
new_limit=evaluated_limit,
reason="rpm_429",
current_rpm=current_rpm,
upstream_limit=upstream_limit,
confidence=round(confidence, 3),
learning_source="header" if upstream_limit else "observation",
)
key.learned_rpm_limit = evaluated_limit # type: ignore[assignment]
# 更新 last_rpm_peak优先使用 upstream header
if upstream_limit and upstream_limit > 0:
key.last_rpm_peak = upstream_limit # type: ignore[assignment]
elif current_rpm and current_rpm > 0:
key.last_rpm_peak = current_rpm # type: ignore[assignment]
logger.warning(
f"[RPM] 限制已确认: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"上游 header: {upstream_limit} | "
f"调整: {old_limit} -> {evaluated_limit} | "
f"confidence: {confidence:.2f}"
)
else:
# 一致性不够,保持学习期
logger.info(
f"[RPM] 学习中: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"上游 header: {upstream_limit} | "
f"观察已记录,暂不设限"
)
elif rate_limit_info.limit_type == RateLimitType.CONCURRENT:
key.concurrent_429_count = int(key.concurrent_429_count or 0) + 1 # type: ignore[assignment]
logger.info(
f"[CONCURRENT] 并发限制触发: Key {key.id[:8]}... | "
f"不调整 RPM 限制(这是并发问题,非 RPM 问题)"
)
else:
# 未知类型:保守处理(仅在已有学习值时减少)
old_limit = key.learned_rpm_limit
if old_limit is not None:
logger.warning(
f"[UNKNOWN] 未知429类型: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"保守减少 RPM: {old_limit} -> {max(int(old_limit * 0.95), self.MIN_RPM_LIMIT)}"
)
else:
logger.info(
f"[UNKNOWN] 未知429类型: Key {key.id[:8]}... | "
f"当前 RPM: {current_rpm} | "
f"无学习值,跳过调整"
)
if old_limit is not None:
new_limit = max(int(old_limit * 0.95), self.MIN_RPM_LIMIT)
self._record_adjustment(
key,
old_limit=int(old_limit),
new_limit=new_limit,
reason="unknown_429",
current_rpm=current_rpm,
)
key.learned_rpm_limit = new_limit # type: ignore[assignment]
self._persist_metadata_update(db)
return key.learned_rpm_limit if key.learned_rpm_limit is not None else None
# ==================== 观察记录与评估 ====================
def _record_429_observation(
self,
key: ProviderAPIKey,
current_rpm: int | None,
upstream_limit: int | None,
) -> None:
"""在 adjustment_history 中记录一次 429 观察"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
observation: dict[str, Any] = {
"type": "429_observation",
"timestamp": datetime.now(timezone.utc).isoformat(),
"current_rpm": current_rpm,
"upstream_limit": upstream_limit,
}
history.append(observation)
key.adjustment_history = self._trim_history(history) # type: ignore[assignment]
def _evaluate_observations(self, key: ProviderAPIKey) -> tuple[int | None, float]:
"""
评估历史 429 观察的一致性,决定是否确认限制
优先使用有 header 的观察upstream_limit其次使用纯本地观察current_rpm
Returns:
(limit, confidence):
- limit: 新确认的限制值,或 None一致性不够不设/不更新限制)
- confidence: 置信度分数 0.0~1.0
"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
observations = [h for h in history if h.get("type") == "429_observation"]
if not observations:
return None, 0.0
# 优先评估有 header 的观察
header_obs = [
o
for o in observations
if o.get("upstream_limit") is not None and o["upstream_limit"] > 0
]
if len(header_obs) >= self.MIN_HEADER_CONFIRMATIONS:
recent = header_obs[-self.MIN_HEADER_CONFIRMATIONS * 2 :]
values = [o["upstream_limit"] for o in recent]
last_n = values[-self.MIN_HEADER_CONFIRMATIONS :]
if self._check_consistency(last_n):
limit_val = int(median(last_n) * self.HEADER_LIMIT_SAFETY_MARGIN)
limit_val = max(limit_val, self.MIN_RPM_LIMIT)
limit_val = min(limit_val, self.MAX_RPM_LIMIT)
return limit_val, 0.8
# 其次评估纯本地观察(无 header
local_obs = [
o for o in observations if o.get("current_rpm") is not None and o["current_rpm"] > 0
]
if len(local_obs) >= self.MIN_CONSISTENT_OBSERVATIONS:
recent = local_obs[-self.MIN_CONSISTENT_OBSERVATIONS * 2 :]
values = [o["current_rpm"] for o in recent]
last_n = values[-self.MIN_CONSISTENT_OBSERVATIONS :]
if self._check_consistency(last_n):
limit_val = int(median(last_n) * self.OBSERVATION_LIMIT_SAFETY_MARGIN)
limit_val = max(limit_val, self.MIN_RPM_LIMIT)
limit_val = min(limit_val, self.MAX_RPM_LIMIT)
return limit_val, 0.6
# 一致性不够,不设/不更新限制(已有的 learned_rpm_limit 不在此处处理)
return None, 0.0
def _check_consistency(self, values: list[int]) -> bool:
"""检查一组数值是否在 OBSERVATION_CONSISTENCY_THRESHOLD 偏差范围内"""
if not values:
return False
med = median(values)
if med <= 0:
return False
return all(abs(v - med) / med <= self.OBSERVATION_CONSISTENCY_THRESHOLD for v in values)
# ==================== 置信度计算 ====================
def get_confidence(self, key: ProviderAPIKey) -> float:
"""
计算当前 confidence 分数0.0~1.0),包含时间衰减
confidence 基于最后一次 429 评估的基础值,随时间自然衰减。
确保限制永远不会固化:长时间没有新 429 观察 → confidence 降至 0。
Returns:
当前 confidence0.0~1.0
"""
if key.learned_rpm_limit is None:
return 0.0
# 从历史中获取基础 confidence
base_confidence = self._get_base_confidence(key)
if base_confidence <= 0:
return 0.0
# 时间衰减
if key.last_429_at is not None:
last_429_at = cast(datetime, key.last_429_at)
minutes_since = max(
0.0, (datetime.now(timezone.utc) - last_429_at).total_seconds() / 60.0
)
time_decay = minutes_since * self.CONFIDENCE_DECAY_PER_MINUTE
else:
time_decay = 1.0 # 没有 429 记录,直接衰减到 0
final = max(0.0, base_confidence - time_decay)
return min(final, 1.0)
def is_enforcement_active(self, key: ProviderAPIKey) -> bool:
"""confidence 是否达到执行阈值,达标才执行本地 RPM 限制"""
return self.get_confidence(key) >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD
def get_effective_limit(self, key: ProviderAPIKey) -> int | None:
"""
获取 key 当前有效的 RPM 限制(统一入口)
- rpm_limit=NULL自适应learned_rpm_limit + confidence 达标才返回
- rpm_limit=数字(固定):直接返回固定值
- 其余情况返回 None不限制
"""
if key.rpm_limit is not None:
return int(key.rpm_limit)
# 自适应模式
if key.learned_rpm_limit is not None and self.is_enforcement_active(key):
return int(key.learned_rpm_limit)
return None
def _get_base_confidence(self, key: ProviderAPIKey) -> float:
"""从最近的 adjustment 记录中获取基础 confidence"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
# 从最新的 adjustment 记录中查找 confidence
for record in reversed(history):
if record.get("type") != "429_observation" and "confidence" in record:
return float(record["confidence"])
# 没有 confidence 记录(旧数据迁移):尝试从观察中重新评估
evaluated_limit, confidence = self._evaluate_observations(key)
if confidence > 0:
return confidence
# 有 learned_rpm_limit 但无法从观察中确认(旧数据),给予低基线置信度
if key.learned_rpm_limit is not None:
return 0.3
return 0.0
# ==================== 成功处理 ====================
def handle_success(
self,
db: Session,
key: ProviderAPIKey,
current_rpm: int,
) -> int | None:
"""
处理成功请求,基于滑动窗口利用率考虑增加 RPM 限制
Returns:
调整后的 RPM 限制(如果有调整),否则返回 None
"""
is_adaptive = key.rpm_limit is None
if not is_adaptive:
return None
# 未碰壁学习前,不主动设置限制
if key.learned_rpm_limit is None:
return None
# confidence 太低时不做扩容逻辑(系统已在自由运行模式)
confidence = self.get_confidence(key)
if confidence < self.ENFORCEMENT_CONFIDENCE_THRESHOLD:
return None
current_limit = int(key.learned_rpm_limit)
# 获取已知边界(上次触发 429 时的 RPM
known_boundary = key.last_rpm_peak
# 计算当前利用率
utilization = float(current_rpm / current_limit) if current_limit > 0 else 0.0
now = datetime.now(timezone.utc)
now_ts = now.timestamp()
# 更新滑动窗口
samples = self._update_utilization_window(key, now_ts, utilization)
# 检查是否满足扩容条件
increase_reason = self._check_increase_conditions(key, samples, now, known_boundary)
if increase_reason and current_limit < self.MAX_RPM_LIMIT:
old_limit = current_limit
is_probe = increase_reason == "probe_increase"
new_limit = self._increase_limit(current_limit, known_boundary, is_probe)
# 如果没有实际增长(已达边界),跳过
if new_limit <= old_limit:
return None
# 计算窗口统计用于日志
avg_util = sum(s["util"] for s in samples) / len(samples) if samples else 0
high_util_count = sum(1 for s in samples if s["util"] >= self.UTILIZATION_THRESHOLD)
high_util_ratio = high_util_count / len(samples) if samples else 0
boundary_info = f"边界: {known_boundary}" if known_boundary else "无边界"
logger.info(
f"[INCREASE] {increase_reason}: Key {key.id[:8]}... | "
f"窗口采样: {len(samples)} | "
f"平均利用率: {avg_util:.1%} | "
f"高利用率比例: {high_util_ratio:.1%} | "
f"{boundary_info} | "
f"调整: {old_limit} -> {new_limit}"
)
# 记录调整历史
self._record_adjustment(
key,
old_limit=old_limit,
new_limit=new_limit,
reason=increase_reason,
avg_utilization=round(avg_util, 2),
high_util_ratio=round(high_util_ratio, 2),
sample_count=len(samples),
current_rpm=current_rpm,
known_boundary=known_boundary,
confidence=round(confidence, 3),
)
# 更新限制
key.learned_rpm_limit = new_limit # type: ignore[assignment]
# 如果是探测性扩容,更新探测时间
if is_probe:
key.last_probe_increase_at = now # type: ignore[assignment]
# 扩容后清空采样窗口,重新开始收集
key.utilization_samples = [] # type: ignore[assignment]
self._persist_metadata_update(db)
return new_limit
# 定期持久化采样数据每5个采样保存一次
if len(samples) % 5 == 0:
self._persist_metadata_update(db)
return None
# ==================== 滑动窗口 ====================
def _update_utilization_window(
self, key: ProviderAPIKey, now_ts: float, utilization: float
) -> list[dict[str, Any]]:
"""更新利用率滑动窗口"""
samples: list[dict[str, Any]] = list(key.utilization_samples or [])
samples.append({"ts": now_ts, "util": round(utilization, 3)})
cutoff_ts = now_ts - self.UTILIZATION_WINDOW_SECONDS
samples = [s for s in samples if s["ts"] > cutoff_ts]
if len(samples) > self.UTILIZATION_WINDOW_SIZE:
samples = samples[-self.UTILIZATION_WINDOW_SIZE :]
key.utilization_samples = samples # type: ignore[assignment]
return samples
# ==================== 扩容条件 ====================
def _check_increase_conditions(
self,
key: ProviderAPIKey,
samples: list[dict[str, Any]],
now: datetime,
known_boundary: int | None = None,
) -> str | None:
"""检查是否满足扩容条件"""
if self._is_in_cooldown(key):
return None
current_limit = int(key.learned_rpm_limit or self.DEFAULT_INITIAL_LIMIT)
# 条件1滑动窗口扩容不超过边界
if len(samples) >= self.MIN_SAMPLES_FOR_DECISION:
high_util_count = sum(1 for s in samples if s["util"] >= self.UTILIZATION_THRESHOLD)
high_util_ratio = high_util_count / len(samples)
if high_util_ratio >= self.HIGH_UTILIZATION_RATIO:
if known_boundary:
if current_limit < known_boundary:
return "high_utilization"
else:
return "high_utilization"
# 条件2探测性扩容
if self._should_probe_increase(key, samples, now):
return "probe_increase"
return None
def _should_probe_increase(
self, key: ProviderAPIKey, samples: list[dict[str, Any]], now: datetime
) -> bool:
"""检查是否应该进行探测性扩容"""
probe_interval_seconds = self.PROBE_INCREASE_INTERVAL_MINUTES * 60
if key.last_429_at:
last_429_at = cast(datetime, key.last_429_at)
time_since_429 = (now - last_429_at).total_seconds()
if time_since_429 < probe_interval_seconds:
return False
if key.last_probe_increase_at:
last_probe = cast(datetime, key.last_probe_increase_at)
time_since_probe = (now - last_probe).total_seconds()
if time_since_probe < probe_interval_seconds:
return False
if len(samples) < self.PROBE_INCREASE_MIN_REQUESTS:
return False
avg_util = sum(s["util"] for s in samples) / len(samples)
if avg_util < 0.3:
return False
return True
def _is_in_cooldown(self, key: ProviderAPIKey) -> bool:
"""检查是否在 429 错误后的冷却期内"""
if key.last_429_at is None:
return False
last_429_at = cast(datetime, key.last_429_at)
time_since_429 = (datetime.now(timezone.utc) - last_429_at).total_seconds()
cooldown_seconds = RPMDefaults.COOLDOWN_AFTER_429_MINUTES * 60
return bool(time_since_429 < cooldown_seconds)
# ==================== 限制调整 ====================
def _increase_limit(
self,
current_limit: int,
known_boundary: int | None = None,
is_probe: bool = False,
) -> int:
"""增加 RPM 限制(考虑边界保护)"""
if is_probe:
new_limit = current_limit + 1
else:
new_limit = current_limit + self.INCREASE_STEP
if known_boundary:
if new_limit > known_boundary:
new_limit = known_boundary
new_limit = min(new_limit, self.MAX_RPM_LIMIT)
if new_limit <= current_limit:
return current_limit
return new_limit
# ==================== 历史记录 ====================
def _record_adjustment(
self,
key: ProviderAPIKey,
old_limit: int,
new_limit: int,
reason: str,
**extra_data: Any,
) -> None:
"""记录 RPM 调整历史"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
record = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"old_limit": old_limit,
"new_limit": new_limit,
"reason": reason,
**extra_data,
}
history.append(record)
key.adjustment_history = self._trim_history(history) # type: ignore[assignment]
def _trim_history(self, history: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
截断历史记录,优先保留 429_observation学习数据源
策略:超出 MAX_HISTORY_RECORDS 时,先淘汰最旧的非观察记录,
仍超限则淘汰最旧的观察记录。
"""
if len(history) <= self.MAX_HISTORY_RECORDS:
return history
observations = [h for h in history if h.get("type") == "429_observation"]
adjustments = [h for h in history if h.get("type") != "429_observation"]
# 按时间戳排序(最新在后)
observations.sort(key=lambda h: h.get("timestamp", ""))
adjustments.sort(key=lambda h: h.get("timestamp", ""))
# 保留尽可能多的观察记录:先缩减 adjustment再缩减 observation
overflow = len(history) - self.MAX_HISTORY_RECORDS
trim_adj = min(overflow, len(adjustments))
adjustments = adjustments[trim_adj:]
overflow -= trim_adj
if overflow > 0:
observations = observations[overflow:]
merged = observations + adjustments
merged.sort(key=lambda h: h.get("timestamp", ""))
return merged
# ==================== 统计与管理 ====================
def get_adjustment_stats(self, key: ProviderAPIKey) -> dict[str, Any]:
"""获取调整统计信息"""
history: list[dict[str, Any]] = list(key.adjustment_history or [])
samples: list[dict[str, Any]] = list(key.utilization_samples or [])
is_adaptive = key.rpm_limit is None
effective_limit = self.get_effective_limit(key) if is_adaptive else int(key.rpm_limit) # type: ignore
avg_utilization: float | None = None
high_util_ratio: float | None = None
if samples:
avg_utilization = sum(s["util"] for s in samples) / len(samples)
high_util_count = sum(1 for s in samples if s["util"] >= self.UTILIZATION_THRESHOLD)
high_util_ratio = high_util_count / len(samples)
last_429_at_str: str | None = None
if key.last_429_at:
last_429_at_str = cast(datetime, key.last_429_at).isoformat()
last_probe_at_str: str | None = None
if key.last_probe_increase_at:
last_probe_at_str = cast(datetime, key.last_probe_increase_at).isoformat()
known_boundary = key.last_rpm_peak
# 观察统计
observations = [h for h in history if h.get("type") == "429_observation"]
header_observations = [
o
for o in observations
if o.get("upstream_limit") is not None and o["upstream_limit"] > 0
]
latest_upstream = header_observations[-1]["upstream_limit"] if header_observations else None
confidence = self.get_confidence(key) if is_adaptive else None
enforcement_active = (
confidence >= self.ENFORCEMENT_CONFIDENCE_THRESHOLD if confidence is not None else None
)
return {
"adaptive_mode": is_adaptive,
"rpm_limit": key.rpm_limit,
"effective_limit": effective_limit,
"learned_limit": key.learned_rpm_limit,
# 边界记忆相关
"known_boundary": known_boundary,
"concurrent_429_count": int(key.concurrent_429_count or 0),
"rpm_429_count": int(key.rpm_429_count or 0),
"last_429_at": last_429_at_str,
"last_429_type": key.last_429_type,
"adjustment_count": len(history),
"recent_adjustments": history[-5:] if history else [],
# 滑动窗口相关
"window_sample_count": len(samples),
"window_avg_utilization": round(avg_utilization, 3) if avg_utilization else None,
"window_high_util_ratio": round(high_util_ratio, 3) if high_util_ratio else None,
"utilization_threshold": self.UTILIZATION_THRESHOLD,
"high_util_ratio_threshold": self.HIGH_UTILIZATION_RATIO,
"min_samples_for_decision": self.MIN_SAMPLES_FOR_DECISION,
# 探测性扩容相关
"last_probe_increase_at": last_probe_at_str,
"probe_increase_interval_minutes": self.PROBE_INCREASE_INTERVAL_MINUTES,
# 置信度相关
"learning_confidence": round(confidence, 3) if confidence is not None else None,
"enforcement_active": enforcement_active,
"observation_count": len(observations),
"header_observation_count": len(header_observations),
"latest_upstream_limit": latest_upstream,
}
def reset_learning(self, db: Session, key: ProviderAPIKey) -> None:
"""重置学习状态(管理员功能)"""
logger.info(f"[RESET] 重置学习状态: Key {key.id[:8]}...")
key.learned_rpm_limit = None # type: ignore[assignment]
key.concurrent_429_count = 0 # type: ignore[assignment]
key.rpm_429_count = 0 # type: ignore[assignment]
key.last_429_at = None # type: ignore[assignment]
key.last_429_type = None # type: ignore[assignment]
key.last_rpm_peak = None # type: ignore[assignment]
key.adjustment_history = [] # type: ignore[assignment]
key.utilization_samples = [] # type: ignore[assignment]
key.last_probe_increase_at = None # type: ignore[assignment]
self._persist_metadata_update(db)
# 全局单例
_adaptive_rpm_manager: AdaptiveRPMManager | None = None
def get_adaptive_rpm_manager() -> AdaptiveRPMManager:
"""获取全局自适应 RPM 管理器单例"""
global _adaptive_rpm_manager
if _adaptive_rpm_manager is None:
_adaptive_rpm_manager = AdaptiveRPMManager()
return _adaptive_rpm_manager
# 向后兼容别名
AdaptiveConcurrencyManager = AdaptiveRPMManager
get_adaptive_manager = get_adaptive_rpm_manager