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)
425 lines
15 KiB
Python
425 lines
15 KiB
Python
"""
|
||
速率限制检测器 - 解析429响应头,区分并发限制和RPM限制
|
||
"""
|
||
|
||
from datetime import datetime, timezone
|
||
|
||
from src.core.logger import logger
|
||
|
||
|
||
class RateLimitType:
|
||
"""速率限制类型"""
|
||
|
||
CONCURRENT = "concurrent" # 并发限制
|
||
RPM = "rpm" # 每分钟请求数限制
|
||
DAILY = "daily" # 每日限制
|
||
MONTHLY = "monthly" # 每月限制
|
||
UNKNOWN = "unknown" # 未知类型
|
||
|
||
|
||
class RateLimitInfo:
|
||
"""速率限制信息"""
|
||
|
||
def __init__(
|
||
self,
|
||
limit_type: str,
|
||
retry_after: int | None = None,
|
||
limit_value: int | None = None,
|
||
remaining: int | None = None,
|
||
reset_at: datetime | None = None,
|
||
current_usage: int | None = None,
|
||
raw_headers: dict[str, str] | None = None,
|
||
):
|
||
self.limit_type = limit_type
|
||
self.retry_after = retry_after # 需要等待的秒数
|
||
self.limit_value = limit_value # 限制值
|
||
self.remaining = remaining # 剩余配额
|
||
self.reset_at = reset_at # 重置时间
|
||
self.current_usage = current_usage # 当前使用量
|
||
self.raw_headers = raw_headers or {}
|
||
|
||
def __repr__(self) -> str:
|
||
return (
|
||
f"RateLimitInfo(type={self.limit_type}, "
|
||
f"retry_after={self.retry_after}, "
|
||
f"limit={self.limit_value}, "
|
||
f"remaining={self.remaining})"
|
||
)
|
||
|
||
|
||
class RateLimitDetector:
|
||
"""
|
||
速率限制检测器
|
||
|
||
支持的提供商:
|
||
- Anthropic Claude API
|
||
- OpenAI API
|
||
- 通用 HTTP 标准头
|
||
"""
|
||
|
||
@staticmethod
|
||
def detect_from_headers(
|
||
headers: dict[str, str],
|
||
provider_name: str = "unknown",
|
||
current_usage: int | None = None,
|
||
) -> RateLimitInfo:
|
||
"""
|
||
从响应头中检测速率限制类型
|
||
|
||
Args:
|
||
headers: 429响应的HTTP头
|
||
provider_name: 提供商名称(用于选择解析策略)
|
||
current_usage: 当前使用量(RPM 计数,用于启发式判断是否为并发限制)
|
||
|
||
Returns:
|
||
RateLimitInfo对象
|
||
"""
|
||
# 标准化header key (转小写)
|
||
headers_lower = {k.lower(): v for k, v in headers.items()}
|
||
|
||
# 根据提供商选择解析策略
|
||
if "anthropic" in provider_name.lower() or "claude" in provider_name.lower():
|
||
return RateLimitDetector._parse_anthropic_headers(headers_lower, current_usage)
|
||
elif "openai" in provider_name.lower():
|
||
return RateLimitDetector._parse_openai_headers(headers_lower, current_usage)
|
||
else:
|
||
return RateLimitDetector._parse_generic_headers(headers_lower, current_usage)
|
||
|
||
@staticmethod
|
||
def _parse_anthropic_headers(
|
||
headers: dict[str, str],
|
||
current_usage: int | None = None,
|
||
) -> RateLimitInfo:
|
||
"""
|
||
解析 Anthropic Claude API 的速率限制头
|
||
|
||
常见头部:
|
||
- anthropic-ratelimit-requests-limit: 50
|
||
- anthropic-ratelimit-requests-remaining: 0
|
||
- anthropic-ratelimit-requests-reset: 2024-01-01T00:00:00Z
|
||
- anthropic-ratelimit-tokens-limit: 100000
|
||
- anthropic-ratelimit-tokens-remaining: 50000
|
||
- retry-after: 60
|
||
"""
|
||
retry_after = RateLimitDetector._parse_retry_after(headers)
|
||
|
||
# 获取请求限制信息
|
||
requests_limit = RateLimitDetector._parse_int(
|
||
headers.get("anthropic-ratelimit-requests-limit")
|
||
)
|
||
requests_remaining = RateLimitDetector._parse_int(
|
||
headers.get("anthropic-ratelimit-requests-remaining")
|
||
)
|
||
requests_reset = RateLimitDetector._parse_datetime(
|
||
headers.get("anthropic-ratelimit-requests-reset")
|
||
)
|
||
|
||
# 判断限制类型
|
||
# 1. 明确的 RPM 限制:请求数剩余为 0
|
||
if requests_remaining is not None and requests_remaining == 0:
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=requests_limit,
|
||
remaining=requests_remaining,
|
||
reset_at=requests_reset,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 2. 并发限制判断(多条件策略)
|
||
# 注意:current_usage 是 RPM 计数(当前分钟请求数),不是真正的并发数
|
||
#
|
||
# 判断条件(满足任一即可):
|
||
# A. 强判断:remaining > 0 且 retry_after <= 30(Provider 明确告知还有配额但需要等待)
|
||
# B. 弱判断:只有 retry_after <= 5 且缺少 remaining 头(短等待时间是并发限制的典型特征)
|
||
#
|
||
# 选择保守的 retry_after 阈值:
|
||
# - 强判断用 30 秒(有 remaining 头时)
|
||
# - 弱判断用 5 秒(无 remaining 头时,更保守)
|
||
is_likely_concurrent = False
|
||
concurrent_reason = ""
|
||
|
||
# 条件 A:remaining > 0 且 retry_after <= 30
|
||
if (
|
||
requests_remaining is not None
|
||
and requests_remaining > 0
|
||
and retry_after is not None
|
||
and retry_after <= 30
|
||
):
|
||
is_likely_concurrent = True
|
||
concurrent_reason = (
|
||
f"remaining={requests_remaining} > 0, retry_after={retry_after}s <= 30s"
|
||
)
|
||
|
||
# 条件 B:无 remaining 头但 retry_after 很短(<= 5 秒)
|
||
elif requests_remaining is None and retry_after is not None and retry_after <= 5:
|
||
is_likely_concurrent = True
|
||
concurrent_reason = f"no remaining header, retry_after={retry_after}s <= 5s"
|
||
|
||
if is_likely_concurrent:
|
||
logger.info(f"检测到并发限制: {concurrent_reason}")
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.CONCURRENT,
|
||
retry_after=retry_after,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 3. 默认视为 RPM 限制(更保守的处理)
|
||
# 无法明确区分时,视为 RPM 限制让系统降低 RPM
|
||
# 这比误判为并发限制(不降 RPM)更安全
|
||
if retry_after is not None or requests_limit is not None:
|
||
logger.info(
|
||
f"无法明确区分限制类型,保守视为 RPM 限制: "
|
||
f"remaining={requests_remaining}, retry_after={retry_after}"
|
||
)
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=requests_limit,
|
||
remaining=requests_remaining,
|
||
reset_at=requests_reset,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 4. 完全没有信息,标记为未知
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.UNKNOWN,
|
||
retry_after=retry_after,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
@staticmethod
|
||
def _parse_openai_headers(
|
||
headers: dict[str, str],
|
||
current_usage: int | None = None,
|
||
) -> RateLimitInfo:
|
||
"""
|
||
解析 OpenAI API 的速率限制头
|
||
|
||
常见头部:
|
||
- x-ratelimit-limit-requests: 3500
|
||
- x-ratelimit-remaining-requests: 0
|
||
- x-ratelimit-reset-requests: 2024-01-01T00:00:00Z
|
||
- x-ratelimit-limit-tokens: 90000
|
||
- x-ratelimit-remaining-tokens: 50000
|
||
- retry-after: 60
|
||
"""
|
||
retry_after = RateLimitDetector._parse_retry_after(headers)
|
||
|
||
# 获取请求限制信息
|
||
requests_limit = RateLimitDetector._parse_int(headers.get("x-ratelimit-limit-requests"))
|
||
requests_remaining = RateLimitDetector._parse_int(
|
||
headers.get("x-ratelimit-remaining-requests")
|
||
)
|
||
requests_reset = RateLimitDetector._parse_datetime(
|
||
headers.get("x-ratelimit-reset-requests")
|
||
)
|
||
|
||
# 判断限制类型
|
||
# 1. 明确的 RPM 限制
|
||
if requests_remaining is not None and requests_remaining == 0:
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=requests_limit,
|
||
remaining=requests_remaining,
|
||
reset_at=requests_reset,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 2. 并发限制判断(多条件策略)
|
||
# 判断条件(满足任一即可):
|
||
# A. 强判断:remaining > 0 且 retry_after <= 30
|
||
# B. 弱判断:只有 retry_after <= 5 且缺少 remaining 头
|
||
is_likely_concurrent = False
|
||
concurrent_reason = ""
|
||
|
||
if (
|
||
requests_remaining is not None
|
||
and requests_remaining > 0
|
||
and retry_after is not None
|
||
and retry_after <= 30
|
||
):
|
||
is_likely_concurrent = True
|
||
concurrent_reason = (
|
||
f"remaining={requests_remaining} > 0, retry_after={retry_after}s <= 30s"
|
||
)
|
||
elif requests_remaining is None and retry_after is not None and retry_after <= 5:
|
||
is_likely_concurrent = True
|
||
concurrent_reason = f"no remaining header, retry_after={retry_after}s <= 5s"
|
||
|
||
if is_likely_concurrent:
|
||
logger.info(f"检测到并发限制: {concurrent_reason}")
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.CONCURRENT,
|
||
retry_after=retry_after,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 3. 默认视为 RPM 限制(更保守的处理)
|
||
if retry_after is not None or requests_limit is not None:
|
||
logger.info(
|
||
f"无法明确区分限制类型,保守视为 RPM 限制: "
|
||
f"remaining={requests_remaining}, retry_after={retry_after}"
|
||
)
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=requests_limit,
|
||
remaining=requests_remaining,
|
||
reset_at=requests_reset,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 4. 完全没有信息,标记为未知
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.UNKNOWN,
|
||
retry_after=retry_after,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
@staticmethod
|
||
def _parse_generic_headers(
|
||
headers: dict[str, str],
|
||
current_usage: int | None = None,
|
||
) -> RateLimitInfo:
|
||
"""
|
||
解析通用的速率限制头
|
||
|
||
标准头部:
|
||
- retry-after: 60
|
||
- x-ratelimit-limit: 100
|
||
- x-ratelimit-remaining: 0
|
||
- x-ratelimit-reset: 1609459200
|
||
"""
|
||
retry_after = RateLimitDetector._parse_retry_after(headers)
|
||
|
||
limit_value = RateLimitDetector._parse_int(headers.get("x-ratelimit-limit"))
|
||
remaining = RateLimitDetector._parse_int(headers.get("x-ratelimit-remaining"))
|
||
|
||
# 1. 明确的 RPM 限制
|
||
if remaining is not None and remaining == 0:
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=limit_value,
|
||
remaining=remaining,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 2. 并发限制判断(多条件策略)
|
||
# 判断条件(满足任一即可):
|
||
# A. 强判断:remaining > 0 且 retry_after <= 30
|
||
# B. 弱判断:只有 retry_after <= 5 且缺少 remaining 头
|
||
is_likely_concurrent = False
|
||
concurrent_reason = ""
|
||
|
||
if (
|
||
remaining is not None
|
||
and remaining > 0
|
||
and retry_after is not None
|
||
and retry_after <= 30
|
||
):
|
||
is_likely_concurrent = True
|
||
concurrent_reason = f"remaining={remaining} > 0, retry_after={retry_after}s <= 30s"
|
||
elif remaining is None and retry_after is not None and retry_after <= 5:
|
||
is_likely_concurrent = True
|
||
concurrent_reason = f"no remaining header, retry_after={retry_after}s <= 5s"
|
||
|
||
if is_likely_concurrent:
|
||
logger.info(f"检测到并发限制: {concurrent_reason}")
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.CONCURRENT,
|
||
retry_after=retry_after,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 3. 默认视为 RPM 限制(更保守的处理)
|
||
if retry_after is not None or limit_value is not None:
|
||
logger.info(
|
||
f"无法明确区分限制类型,保守视为 RPM 限制: "
|
||
f"remaining={remaining}, retry_after={retry_after}"
|
||
)
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.RPM,
|
||
retry_after=retry_after,
|
||
limit_value=limit_value,
|
||
remaining=remaining,
|
||
current_usage=current_usage,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
# 4. 完全没有信息,标记为未知
|
||
return RateLimitInfo(
|
||
limit_type=RateLimitType.UNKNOWN,
|
||
retry_after=retry_after,
|
||
raw_headers=headers,
|
||
)
|
||
|
||
@staticmethod
|
||
def _parse_retry_after(headers: dict[str, str]) -> int | None:
|
||
"""解析 Retry-After 头"""
|
||
retry_after_str = headers.get("retry-after")
|
||
if not retry_after_str:
|
||
return None
|
||
|
||
try:
|
||
# 尝试解析为整数(秒数)
|
||
return int(retry_after_str)
|
||
except ValueError:
|
||
# 尝试解析为HTTP日期格式
|
||
try:
|
||
retry_date = datetime.strptime(retry_after_str, "%a, %d %b %Y %H:%M:%S %Z")
|
||
delta = retry_date - datetime.now(timezone.utc)
|
||
return max(int(delta.total_seconds()), 0)
|
||
except Exception:
|
||
return None
|
||
|
||
@staticmethod
|
||
def _parse_int(value: str | None) -> int | None:
|
||
"""安全解析整数"""
|
||
if not value:
|
||
return None
|
||
try:
|
||
return int(value)
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
@staticmethod
|
||
def _parse_datetime(value: str | None) -> datetime | None:
|
||
"""安全解析ISO 8601日期时间"""
|
||
if not value:
|
||
return None
|
||
try:
|
||
# 尝试解析 ISO 8601 格式
|
||
if value.endswith("Z"):
|
||
value = value[:-1] + "+00:00"
|
||
return datetime.fromisoformat(value)
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
# 便捷函数
|
||
def detect_rate_limit_type(
|
||
headers: dict[str, str],
|
||
provider_name: str = "unknown",
|
||
current_usage: int | None = None,
|
||
) -> RateLimitInfo:
|
||
"""
|
||
检测速率限制类型(便捷函数)
|
||
|
||
Args:
|
||
headers: 429响应头
|
||
provider_name: 提供商名称
|
||
current_usage: 当前使用量(RPM 计数)
|
||
|
||
Returns:
|
||
RateLimitInfo对象
|
||
"""
|
||
return RateLimitDetector.detect_from_headers(headers, provider_name, current_usage)
|