Files
Aether/_deprecated_py_src/services/rate_limit/detector.py

425 lines
15 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
速率限制检测器 - 解析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,
2025-12-10 20:52:44 +08:00
):
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],
2025-12-10 20:52:44 +08:00
provider_name: str = "unknown",
current_usage: int | None = None,
2025-12-10 20:52:44 +08:00
) -> RateLimitInfo:
"""
从响应头中检测速率限制类型
Args:
headers: 429响应的HTTP头
provider_name: 提供商名称用于选择解析策略
current_usage: 当前使用量RPM 计数用于启发式判断是否为并发限制
2025-12-10 20:52:44 +08:00
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)
2025-12-10 20:52:44 +08:00
elif "openai" in provider_name.lower():
return RateLimitDetector._parse_openai_headers(headers_lower, current_usage)
2025-12-10 20:52:44 +08:00
else:
return RateLimitDetector._parse_generic_headers(headers_lower, current_usage)
2025-12-10 20:52:44 +08:00
@staticmethod
def _parse_anthropic_headers(
headers: dict[str, str],
current_usage: int | None = None,
2025-12-10 20:52:44 +08:00
) -> 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 <= 30Provider 明确告知还有配额但需要等待)
# B. 弱判断:只有 retry_after <= 5 且缺少 remaining 头(短等待时间是并发限制的典型特征)
#
# 选择保守的 retry_after 阈值:
# - 强判断用 30 秒(有 remaining 头时)
# - 弱判断用 5 秒(无 remaining 头时,更保守)
is_likely_concurrent = False
concurrent_reason = ""
# 条件 Aremaining > 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"
2025-12-10 20:52:44 +08:00
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:
2025-12-10 20:52:44 +08:00
logger.info(
f"无法明确区分限制类型,保守视为 RPM 限制: "
2025-12-10 20:52:44 +08:00
f"remaining={requests_remaining}, retry_after={retry_after}"
)
return RateLimitInfo(
limit_type=RateLimitType.RPM,
2025-12-10 20:52:44 +08:00
retry_after=retry_after,
limit_value=requests_limit,
remaining=requests_remaining,
reset_at=requests_reset,
current_usage=current_usage,
2025-12-10 20:52:44 +08:00
raw_headers=headers,
)
# 4. 完全没有信息,标记为未知
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
) -> 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"
2025-12-10 20:52:44 +08:00
if is_likely_concurrent:
logger.info(f"检测到并发限制: {concurrent_reason}")
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
raw_headers=headers,
)
# 4. 完全没有信息,标记为未知
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
) -> 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"
2025-12-10 20:52:44 +08:00
if is_likely_concurrent:
logger.info(f"检测到并发限制: {concurrent_reason}")
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
raw_headers=headers,
)
# 4. 完全没有信息,标记为未知
2025-12-10 20:52:44 +08:00
return RateLimitInfo(
limit_type=RateLimitType.UNKNOWN,
retry_after=retry_after,
raw_headers=headers,
)
@staticmethod
def _parse_retry_after(headers: dict[str, str]) -> int | None:
2025-12-10 20:52:44 +08:00
"""解析 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:
2025-12-10 20:52:44 +08:00
"""安全解析整数"""
if not value:
return None
try:
return int(value)
except (ValueError, TypeError):
return None
@staticmethod
def _parse_datetime(value: str | None) -> datetime | None:
2025-12-10 20:52:44 +08:00
"""安全解析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],
2025-12-10 20:52:44 +08:00
provider_name: str = "unknown",
current_usage: int | None = None,
2025-12-10 20:52:44 +08:00
) -> RateLimitInfo:
"""
检测速率限制类型便捷函数
Args:
headers: 429响应头
provider_name: 提供商名称
current_usage: 当前使用量RPM 计数
2025-12-10 20:52:44 +08:00
Returns:
RateLimitInfo对象
"""
return RateLimitDetector.detect_from_headers(headers, provider_name, current_usage)