- 在 ChatService.completions() 中实现跨 Token 重试循环,最多尝试 3 个 不同 Token,遇到 429 时标记当前 Token 为 COOLING 并切换下一个 - TokenPool.select() 和 TokenManager.get_token() 增加 exclude 参数, 支持排除已失败的 Token - 新增 TokenManager.mark_rate_limited() 方法,将 429 的 Token 配额 归零并标记 COOLING,等待 Scheduler 刷新恢复 - 内层 retry_on_status 不再重试 429,避免对同一耗尽 Token 做无意义重试 - 循环耗尽后兜底抛出异常,避免隐式返回 None
97 lines
2.7 KiB
Python
97 lines
2.7 KiB
Python
"""Token 池管理"""
|
||
|
||
import random
|
||
from typing import Dict, List, Optional, Iterator
|
||
|
||
from app.services.token.models import TokenInfo, TokenStatus, TokenPoolStats
|
||
|
||
|
||
class TokenPool:
|
||
"""Token 池(管理一组 Token)"""
|
||
|
||
def __init__(self, name: str):
|
||
self.name = name
|
||
self._tokens: Dict[str, TokenInfo] = {}
|
||
|
||
def add(self, token: TokenInfo):
|
||
"""添加 Token"""
|
||
self._tokens[token.token] = token
|
||
|
||
def remove(self, token_str: str) -> bool:
|
||
"""删除 Token"""
|
||
if token_str in self._tokens:
|
||
del self._tokens[token_str]
|
||
return True
|
||
return False
|
||
|
||
def get(self, token_str: str) -> Optional[TokenInfo]:
|
||
"""获取 Token"""
|
||
return self._tokens.get(token_str)
|
||
|
||
def select(self, exclude: set = None) -> Optional[TokenInfo]:
|
||
"""
|
||
选择一个可用 Token
|
||
策略:
|
||
1. 选择 active 状态且有配额的 token
|
||
2. 优先选择剩余额度最多的
|
||
3. 如果额度相同,随机选择(避免并发冲突)
|
||
"""
|
||
# 选择 token
|
||
available = [
|
||
t
|
||
for t in self._tokens.values()
|
||
if t.status == TokenStatus.ACTIVE and t.quota > 0
|
||
and (not exclude or t.token not in exclude)
|
||
]
|
||
|
||
if not available:
|
||
return None
|
||
|
||
# 找到最大额度
|
||
max_quota = max(t.quota for t in available)
|
||
|
||
# 筛选最大额度
|
||
candidates = [t for t in available if t.quota == max_quota]
|
||
|
||
# 随机选择
|
||
return random.choice(candidates)
|
||
|
||
def count(self) -> int:
|
||
"""Token 数量"""
|
||
return len(self._tokens)
|
||
|
||
def list(self) -> List[TokenInfo]:
|
||
"""获取所有 Token"""
|
||
return list(self._tokens.values())
|
||
|
||
def get_stats(self) -> TokenPoolStats:
|
||
"""获取池统计信息"""
|
||
stats = TokenPoolStats(total=len(self._tokens))
|
||
|
||
for token in self._tokens.values():
|
||
stats.total_quota += token.quota
|
||
|
||
if token.status == TokenStatus.ACTIVE:
|
||
stats.active += 1
|
||
elif token.status == TokenStatus.DISABLED:
|
||
stats.disabled += 1
|
||
elif token.status == TokenStatus.EXPIRED:
|
||
stats.expired += 1
|
||
elif token.status == TokenStatus.COOLING:
|
||
stats.cooling += 1
|
||
|
||
if stats.total > 0:
|
||
stats.avg_quota = stats.total_quota / stats.total
|
||
|
||
return stats
|
||
|
||
def _rebuild_index(self):
|
||
"""重建索引(预留接口,用于加载时调用)"""
|
||
pass
|
||
|
||
def __iter__(self) -> Iterator[TokenInfo]:
|
||
return iter(self._tokens.values())
|
||
|
||
|
||
__all__ = ["TokenPool"]
|