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,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str],
|
2025-12-10 20:52:44 +08:00
|
|
|
|
provider_name: str = "unknown",
|
2026-01-30 03:10:21 +08:00
|
|
|
|
current_usage: int | None = None,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> RateLimitInfo:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从响应头中检测速率限制类型
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
headers: 429响应的HTTP头
|
|
|
|
|
|
provider_name: 提供商名称(用于选择解析策略)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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():
|
2026-01-10 18:43:53 +08:00
|
|
|
|
return RateLimitDetector._parse_anthropic_headers(headers_lower, current_usage)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
elif "openai" in provider_name.lower():
|
2026-01-10 18:43:53 +08:00
|
|
|
|
return RateLimitDetector._parse_openai_headers(headers_lower, current_usage)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
else:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
return RateLimitDetector._parse_generic_headers(headers_lower, current_usage)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _parse_anthropic_headers(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 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
|
2026-02-01 17:28:00 +08:00
|
|
|
|
concurrent_reason = (
|
|
|
|
|
|
f"remaining={requests_remaining} > 0, retry_after={retry_after}s <= 30s"
|
|
|
|
|
|
)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
|
|
|
|
|
|
# 条件 B:无 remaining 头但 retry_after 很短(<= 5 秒)
|
2026-02-01 17:28:00 +08:00
|
|
|
|
elif requests_remaining is None and retry_after is not None and retry_after <= 5:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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(
|
2026-01-10 18:43:53 +08:00
|
|
|
|
f"无法明确区分限制类型,保守视为 RPM 限制: "
|
2025-12-10 20:52:44 +08:00
|
|
|
|
f"remaining={requests_remaining}, retry_after={retry_after}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return RateLimitInfo(
|
2026-01-10 18:43:53 +08:00
|
|
|
|
limit_type=RateLimitType.RPM,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
retry_after=retry_after,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 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
|
2026-02-01 17:28:00 +08:00
|
|
|
|
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:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
logger.info(f"检测到并发限制: {concurrent_reason}")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return RateLimitInfo(
|
|
|
|
|
|
limit_type=RateLimitType.CONCURRENT,
|
|
|
|
|
|
retry_after=retry_after,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 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"
|
2026-02-01 17:28:00 +08:00
|
|
|
|
elif remaining is None and retry_after is not None and retry_after <= 5:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
logger.info(f"检测到并发限制: {concurrent_reason}")
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return RateLimitInfo(
|
|
|
|
|
|
limit_type=RateLimitType.CONCURRENT,
|
|
|
|
|
|
retry_after=retry_after,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
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,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# 4. 完全没有信息,标记为未知
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return RateLimitInfo(
|
|
|
|
|
|
limit_type=RateLimitType.UNKNOWN,
|
|
|
|
|
|
retry_after=retry_after,
|
|
|
|
|
|
raw_headers=headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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
|
2026-01-30 03:10:21 +08:00
|
|
|
|
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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str],
|
2025-12-10 20:52:44 +08:00
|
|
|
|
provider_name: str = "unknown",
|
2026-01-30 03:10:21 +08:00
|
|
|
|
current_usage: int | None = None,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
) -> RateLimitInfo:
|
|
|
|
|
|
"""
|
|
|
|
|
|
检测速率限制类型(便捷函数)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
headers: 429响应头
|
|
|
|
|
|
provider_name: 提供商名称
|
2026-01-10 18:43:53 +08:00
|
|
|
|
current_usage: 当前使用量(RPM 计数)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
RateLimitInfo对象
|
|
|
|
|
|
"""
|
2026-01-10 18:43:53 +08:00
|
|
|
|
return RateLimitDetector.detect_from_headers(headers, provider_name, current_usage)
|