修复: 熔断耗尽渠道并统一额度阀
This commit is contained in:
parent
f805b00dd1
commit
efb2b3e781
@ -29,6 +29,12 @@ MINIMAX_MODELS = {"MiniMax-M3", "MiniMax-M2.7"} # 计入每窗预算的
|
|||||||
BUDGET_CHAIN = ["MiniMax-M3", "MiniMax-M2.7", "glm-5.2", "deepseek-v4-flash"] # 全局统一降级链
|
BUDGET_CHAIN = ["MiniMax-M3", "MiniMax-M2.7", "glm-5.2", "deepseek-v4-flash"] # 全局统一降级链
|
||||||
WINDOW_BUDGET_USD = 24.0 # 每窗 MiniMax 花费上限(创始人 2026-07-18 提额 $10→$24),超则切 glm-5.2→deepseek
|
WINDOW_BUDGET_USD = 24.0 # 每窗 MiniMax 花费上限(创始人 2026-07-18 提额 $10→$24),超则切 glm-5.2→deepseek
|
||||||
WINDOW_CALL_CAP = 6000 # 每窗全模型调用上限(创始人 2026-07-18 提额 4000→6000),达则自动睡到下一窗续跑
|
WINDOW_CALL_CAP = 6000 # 每窗全模型调用上限(创始人 2026-07-18 提额 4000→6000),达则自动睡到下一窗续跑
|
||||||
|
# 上游实测上限:请求前主动裁剪,避免依赖不同渠道含混甚至错误的 HTTP 400 文案再猜测重发。
|
||||||
|
# M3 / deepseek 未观察到该限制,故不在表内、不主动裁剪。
|
||||||
|
MODEL_MAX_TOKENS = {
|
||||||
|
"MiniMax-M2.7": 196608,
|
||||||
|
"glm-5.2": 12000,
|
||||||
|
}
|
||||||
# 费率兜底(model_ratio, completion_ratio, cache_ratio),与 New-API /api/pricing 一致(2026-07-16 快照)
|
# 费率兜底(model_ratio, completion_ratio, cache_ratio),与 New-API /api/pricing 一致(2026-07-16 快照)
|
||||||
PRICING_FALLBACK = {
|
PRICING_FALLBACK = {
|
||||||
"MiniMax-M3": (0.15, 4.0, 0.2),
|
"MiniMax-M3": (0.15, 4.0, 0.2),
|
||||||
@ -45,14 +51,21 @@ class SensitiveError(Exception):
|
|||||||
立即抛给上层走模型降级链(创始人 2026-07-14:M3→MiniMax-M2.7→deepseek-v4-flash)。"""
|
立即抛给上层走模型降级链(创始人 2026-07-14:M3→MiniMax-M2.7→deepseek-v4-flash)。"""
|
||||||
|
|
||||||
|
|
||||||
|
class PlanQuotaExhausted(Exception):
|
||||||
|
"""上游模型渠道的 Token Plan 已耗尽。
|
||||||
|
|
||||||
|
该错误在同一额度窗内重试不会恢复,必须立即交给治理层熔断当前模型;它与普通限流 429
|
||||||
|
不同,普通 429 仍保留指数退避重试。"""
|
||||||
|
|
||||||
|
|
||||||
def chat(prompt, model=DEFAULT_MODEL, max_tokens=512000, temperature=0.2,
|
def chat(prompt, model=DEFAULT_MODEL, max_tokens=512000, temperature=0.2,
|
||||||
retries=2, timeout=900, system=None, top_p=None):
|
retries=2, timeout=900, system=None, top_p=None):
|
||||||
"""单轮对话,返回 (content, usage)。网络错/5xx/429 指数退避重试。
|
"""单轮对话,返回 (content, usage)。网络错/5xx/普通 429 指数退避重试。
|
||||||
|
|
||||||
content 已剥离 <think>…</think>(推理模型可能把思考混进正文)。
|
content 已剥离 <think>…</think>(推理模型可能把思考混进正文)。
|
||||||
system:身份段与任务材料分离(角色遵从更稳、身份段利于上游缓存)。
|
system:身份段与任务材料分离(角色遵从更稳、身份段利于上游缓存)。
|
||||||
top_p:随 temperature 分化实验用(M 家族官方推荐 1.0/0.95,eval A/B 后定版)。
|
top_p:随 temperature 分化实验用(M 家族官方推荐 1.0/0.95,eval A/B 后定版)。
|
||||||
max_tokens 默认 512000(创始人 2026-07-15 拍板:不设人为输出限制)。
|
max_tokens 默认 512000;仅对有实测硬上限的 M2.7/GLM 请求前主动裁剪。
|
||||||
预扣费机制备忘:New-API 按 max_tokens 预扣(512k 预扣 $0.15375/次,网关已验证接受该值;
|
预扣费机制备忘:New-API 按 max_tokens 预扣(512k 预扣 $0.15375/次,网关已验证接受该值;
|
||||||
结算按实际用量,余额充足时预扣不产生额外成本)——**余额须 ≥ 并发路数 × $0.154**,
|
结算按实际用量,余额充足时预扣不产生额外成本)——**余额须 ≥ 并发路数 × $0.154**,
|
||||||
否则触发 403「预扣费额度失败」(2026-07-15 余额见底实测坐实此机制)。
|
否则触发 403「预扣费额度失败」(2026-07-15 余额见底实测坐实此机制)。
|
||||||
@ -61,10 +74,15 @@ def chat(prompt, model=DEFAULT_MODEL, max_tokens=512000, temperature=0.2,
|
|||||||
s.trust_env = False # 本机代理 env 会劫持内网直连
|
s.trust_env = False # 本机代理 env 会劫持内网直连
|
||||||
messages = ([{"role": "system", "content": system}] if system else []) \
|
messages = ([{"role": "system", "content": system}] if system else []) \
|
||||||
+ [{"role": "user", "content": prompt}]
|
+ [{"role": "user", "content": prompt}]
|
||||||
|
model_cap = MODEL_MAX_TOKENS.get(model)
|
||||||
|
effective_max_tokens = min(max_tokens, model_cap) if model_cap is not None else max_tokens
|
||||||
|
if effective_max_tokens != max_tokens:
|
||||||
|
print(f"[llm] {model} max_tokens={max_tokens} 主动裁为模型上限 {effective_max_tokens}",
|
||||||
|
file=sys.stderr)
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"max_tokens": max_tokens,
|
"max_tokens": effective_max_tokens,
|
||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
}
|
}
|
||||||
if top_p is not None:
|
if top_p is not None:
|
||||||
@ -76,17 +94,9 @@ def chat(prompt, model=DEFAULT_MODEL, max_tokens=512000, temperature=0.2,
|
|||||||
r = s.post(f"{BASE}/v1/chat/completions",
|
r = s.post(f"{BASE}/v1/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {TOKEN}"},
|
headers={"Authorization": f"Bearer {TOKEN}"},
|
||||||
json=payload, timeout=timeout)
|
json=payload, timeout=timeout)
|
||||||
# 各模型 max_tokens 上限不一(实测 M3 收 512k、M2.7 上限 196608 报 400):
|
# Token Plan 耗尽不是瞬时限流:同模型退避只会白等 8/16 秒,立即交治理层按窗熔断。
|
||||||
# 撞上限自适应降档重发,保证降级链换模型时不被参数掀翻。
|
if r.status_code == 429 and "Token Plan 用量上限" in r.text:
|
||||||
# 必须在敏感检查之前处理——重发后的响应仍要完整走敏感/429/5xx 判定链
|
raise PlanQuotaExhausted(f"Token Plan 已耗尽 HTTP 429: {r.text[:200]}")
|
||||||
if r.status_code == 400 and "max tokens" in r.text.lower():
|
|
||||||
cur = payload.get("max_tokens", 0)
|
|
||||||
payload["max_tokens"] = 196608 if cur > 196608 else 12000
|
|
||||||
print(f"[llm] {model} max_tokens={cur} 超模型上限,降为 {payload['max_tokens']} 重发",
|
|
||||||
file=sys.stderr)
|
|
||||||
r = s.post(f"{BASE}/v1/chat/completions",
|
|
||||||
headers={"Authorization": f"Bearer {TOKEN}"},
|
|
||||||
json=payload, timeout=timeout)
|
|
||||||
# 内容安全拦截:同模型退避重试必再敏感,立即抛 SensitiveError 交上层
|
# 内容安全拦截:同模型退避重试必再敏感,立即抛 SensitiveError 交上层
|
||||||
# 降级换模型,不在此浪费退避(否则一敏感章空烧 3 次,实测占放量请求 23%)
|
# 降级换模型,不在此浪费退避(否则一敏感章空烧 3 次,实测占放量请求 23%)
|
||||||
if r.status_code >= 500 and "sensitive" in r.text.lower():
|
if r.status_code >= 500 and "sensitive" in r.text.lower():
|
||||||
@ -148,6 +158,16 @@ def extract_json(text):
|
|||||||
# 全窗调用达 6000 次自动睡到下一窗续跑——降级策略只此一处,调用方只管拿结果。
|
# 全窗调用达 6000 次自动睡到下一窗续跑——降级策略只此一处,调用方只管拿结果。
|
||||||
|
|
||||||
_PRICING_CACHE = None
|
_PRICING_CACHE = None
|
||||||
|
# 仅保存当前额度窗内已确认 Token Plan 耗尽的模型。进程重启会自然重探;跨窗也会清空重探。
|
||||||
|
_PLAN_QUOTA_OPEN = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_quota_open_models(wk):
|
||||||
|
"""返回当前窗已熔断模型集合,并清除其他窗口的陈旧状态。"""
|
||||||
|
stale = [key for key in _PLAN_QUOTA_OPEN if key != wk]
|
||||||
|
for key in stale:
|
||||||
|
del _PLAN_QUOTA_OPEN[key]
|
||||||
|
return _PLAN_QUOTA_OPEN.setdefault(wk, set())
|
||||||
|
|
||||||
|
|
||||||
def get_pricing():
|
def get_pricing():
|
||||||
@ -267,7 +287,7 @@ def chat_governed(prompt, model=DEFAULT_MODEL, system=None, max_tokens=512000,
|
|||||||
策略(每次调用前):
|
策略(每次调用前):
|
||||||
1) 读本窗账本;本窗 total_calls ≥ WINDOW_CALL_CAP → 打日志、睡到下一窗边界(不持DB连接)、重读续跑;
|
1) 读本窗账本;本窗 total_calls ≥ WINDOW_CALL_CAP → 打日志、睡到下一窗边界(不持DB连接)、重读续跑;
|
||||||
2) 本窗 minimax_usd ≥ WINDOW_BUDGET_USD → 降级链去掉 MiniMax 前缀(只剩 glm-5.2→deepseek),否则用全链;
|
2) 本窗 minimax_usd ≥ WINDOW_BUDGET_USD → 降级链去掉 MiniMax 前缀(只剩 glm-5.2→deepseek),否则用全链;
|
||||||
3) 沿链逐个模型调 chat():撞 SensitiveError 或 RuntimeError(重试耗尽/不可用) → 换下一个;成功即止;
|
3) 跳过本窗已确认 Token Plan 耗尽的模型;其余模型沿链调用,敏感/不可用时换下一个;成功即止;
|
||||||
4) 成功后 _bump_window(本窗, MiniMax模型才计成本否则0),返回三元组;全链失败返回 (None,None,None)。"""
|
4) 成功后 _bump_window(本窗, MiniMax模型才计成本否则0),返回三元组;全链失败返回 (None,None,None)。"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
while True:
|
while True:
|
||||||
@ -291,11 +311,21 @@ def chat_governed(prompt, model=DEFAULT_MODEL, system=None, max_tokens=512000,
|
|||||||
f"本窗改用非 MiniMax 链 {chain}", file=sys.stderr)
|
f"本窗改用非 MiniMax 链 {chain}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
chain = list(BUDGET_CHAIN)
|
chain = list(BUDGET_CHAIN)
|
||||||
|
plan_quota_open = _plan_quota_open_models(wk)
|
||||||
|
skipped = [m for m in chain if m in plan_quota_open]
|
||||||
|
if skipped:
|
||||||
|
print(f"[llm] 本窗 {wk} 跳过 Token Plan 已耗尽模型 {skipped}", file=sys.stderr)
|
||||||
|
chain = [m for m in chain if m not in plan_quota_open]
|
||||||
# 3) 沿链逐个模型调用;撞敏感/不可用换下一个
|
# 3) 沿链逐个模型调用;撞敏感/不可用换下一个
|
||||||
for m in chain:
|
for m in chain:
|
||||||
try:
|
try:
|
||||||
content, usage = chat(prompt, model=m, system=system, max_tokens=max_tokens,
|
content, usage = chat(prompt, model=m, system=system, max_tokens=max_tokens,
|
||||||
temperature=temperature, top_p=top_p)
|
temperature=temperature, top_p=top_p)
|
||||||
|
except PlanQuotaExhausted as e:
|
||||||
|
plan_quota_open.add(m)
|
||||||
|
print(f"[llm] 治理链 {m} Token Plan 本窗耗尽,立即熔断并降级下一个:{str(e)[:80]}",
|
||||||
|
file=sys.stderr)
|
||||||
|
continue
|
||||||
except (SensitiveError, RuntimeError) as e:
|
except (SensitiveError, RuntimeError) as e:
|
||||||
print(f"[llm] 治理链 {m} 失败({type(e).__name__}: {str(e)[:80]}),降级下一个",
|
print(f"[llm] 治理链 {m} 失败({type(e).__name__}: {str(e)[:80]}),降级下一个",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
不碰真 DB、不发真请求、不做任何真实 LLM 调用。直接 `.venv/bin/python test_quota.py` 跑(无需 pytest)。
|
不碰真 DB、不发真请求、不做任何真实 LLM 调用。直接 `.venv/bin/python test_quota.py` 跑(无需 pytest)。
|
||||||
|
|
||||||
覆盖:window_key 归窗 / seconds_to_next_window 边界 / cost_usd 计价(含缓存折扣) /
|
覆盖:window_key 归窗 / seconds_to_next_window 边界 / cost_usd 计价(含缓存折扣) /
|
||||||
chat_governed 六条路由(首选成功、$24 预算耗尽切链、敏感换模型、不可用换模型、全链失败、6000 次达上限睡窗)。"""
|
模型 max_tokens 主动上限 / Plan 429 不退避与按窗熔断 / 普通 429 重试 /
|
||||||
|
chat_governed 六条既有路由(首选成功、预算降级、敏感换模型、不可用换模型、全链失败、调用上限睡窗)。"""
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
@ -34,8 +35,9 @@ def _install(now, reads, chat, sleep=None):
|
|||||||
return (add, 1) # 返回值 chat_governed 不消费,占位即可
|
return (add, 1) # 返回值 chat_governed 不消费,占位即可
|
||||||
llm._bump_window = fake_bump
|
llm._bump_window = fake_bump
|
||||||
llm.chat = chat
|
llm.chat = chat
|
||||||
|
llm._PLAN_QUOTA_OPEN.clear()
|
||||||
# 只替换 time.sleep(chat 被打桩后 chat_governed 路径不再用 time.time),不污染真 time 模块
|
# 只替换 time.sleep(chat 被打桩后 chat_governed 路径不再用 time.time),不污染真 time 模块
|
||||||
llm.time = types.SimpleNamespace(sleep=(sleep if sleep else (lambda s: None)))
|
llm.time = types.SimpleNamespace(sleep=(sleep if sleep else (lambda s: None)), time=lambda: 0)
|
||||||
return bumps
|
return bumps
|
||||||
|
|
||||||
|
|
||||||
@ -84,6 +86,129 @@ def test_current_quota_policy():
|
|||||||
assert llm.WINDOW_CALL_CAP == 6000, "当前每窗总调用上限必须是 6000"
|
assert llm.WINDOW_CALL_CAP == 6000, "当前每窗总调用上限必须是 6000"
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeResponse:
|
||||||
|
"""只实现 chat() 所需的最小 requests.Response 接口。"""
|
||||||
|
|
||||||
|
def __init__(self, status_code, text="", data=None):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = text
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
if self.status_code >= 400:
|
||||||
|
raise llm.requests.HTTPError(f"HTTP {self.status_code}: {self.text}")
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
|
def _success_response():
|
||||||
|
return _FakeResponse(200, data={
|
||||||
|
"choices": [{"message": {"content": "ok"}, "finish_reason": "stop"}],
|
||||||
|
"usage": dict(_USAGE),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _run_chat_with_responses(model, responses, max_tokens=512000, retries=2):
|
||||||
|
"""替换 HTTP 会话并执行一次 chat,返回(结果、请求 payload、sleep 记录)。"""
|
||||||
|
posts = []
|
||||||
|
sleeps = []
|
||||||
|
queue = list(responses)
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
trust_env = True
|
||||||
|
|
||||||
|
def post(self, url, headers=None, json=None, timeout=None):
|
||||||
|
posts.append(dict(json))
|
||||||
|
return queue.pop(0)
|
||||||
|
|
||||||
|
old_session, old_time = llm.requests.Session, llm.time
|
||||||
|
try:
|
||||||
|
llm.requests.Session = FakeSession
|
||||||
|
llm.time = types.SimpleNamespace(time=lambda: 0, sleep=lambda secs: sleeps.append(secs))
|
||||||
|
result = llm.chat("p", model=model, max_tokens=max_tokens, retries=retries)
|
||||||
|
return result, posts, sleeps
|
||||||
|
finally:
|
||||||
|
llm.requests.Session, llm.time = old_session, old_time
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_429_does_not_retry():
|
||||||
|
"""Token Plan 429 必须首响应立即抛出,不进入 8/16 秒退避。"""
|
||||||
|
body = '{"error":{"message":"已达到 Token Plan 用量上限:请升级套餐"}}'
|
||||||
|
posts = []
|
||||||
|
sleeps = []
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
trust_env = True
|
||||||
|
|
||||||
|
def post(self, url, headers=None, json=None, timeout=None):
|
||||||
|
posts.append(dict(json))
|
||||||
|
return _FakeResponse(429, text=body)
|
||||||
|
|
||||||
|
old_session, old_time = llm.requests.Session, llm.time
|
||||||
|
try:
|
||||||
|
llm.requests.Session = FakeSession
|
||||||
|
llm.time = types.SimpleNamespace(time=lambda: 0, sleep=lambda secs: sleeps.append(secs))
|
||||||
|
try:
|
||||||
|
llm.chat("p", model="MiniMax-M3", retries=2)
|
||||||
|
raise AssertionError("Plan 429 应抛 PlanQuotaExhausted")
|
||||||
|
except llm.PlanQuotaExhausted:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
llm.requests.Session, llm.time = old_session, old_time
|
||||||
|
assert len(posts) == 1, f"Plan 429 不应重试,实发 {len(posts)} 次"
|
||||||
|
assert sleeps == [], f"Plan 429 不应退避,实睡 {sleeps}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_regular_429_still_retries():
|
||||||
|
"""普通速率限制 429 仍按原策略退避重试,不能误触发 Plan 熔断。"""
|
||||||
|
result, posts, sleeps = _run_chat_with_responses(
|
||||||
|
"MiniMax-M3",
|
||||||
|
[_FakeResponse(429, text='{"error":{"message":"rate limit"}}'), _success_response()],
|
||||||
|
retries=2,
|
||||||
|
)
|
||||||
|
assert result[0] == "ok", "普通 429 后应重试成功"
|
||||||
|
assert len(posts) == 2, f"普通 429 应重试一次,实发 {len(posts)} 次"
|
||||||
|
assert sleeps == [8], f"第一次普通 429 应退避 8 秒,实得 {sleeps}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_max_tokens_caps():
|
||||||
|
"""只主动裁 M2.7/GLM;M3/deepseek 保留调用方请求值。"""
|
||||||
|
cases = [
|
||||||
|
("MiniMax-M2.7", 196608),
|
||||||
|
("glm-5.2", 12000),
|
||||||
|
("MiniMax-M3", 512000),
|
||||||
|
("deepseek-v4-flash", 512000),
|
||||||
|
]
|
||||||
|
for model, expected in cases:
|
||||||
|
_, posts, _ = _run_chat_with_responses(model, [_success_response()])
|
||||||
|
assert posts[0]["max_tokens"] == expected, \
|
||||||
|
f"{model} max_tokens 应为 {expected},实得 {posts[0]['max_tokens']}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_quota_breaker_same_window_and_recovery():
|
||||||
|
"""M3/M2.7 同窗各探一次后跳过;跨窗清空熔断并从 M3 重探。"""
|
||||||
|
clock = {"t": datetime(2026, 7, 16, 12, 0)}
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def chat(prompt, model=None, **kw):
|
||||||
|
calls.append((llm.window_key(clock["t"]), model))
|
||||||
|
if llm.window_key(clock["t"]) == "2026-07-16T10" and model in llm.MINIMAX_MODELS:
|
||||||
|
raise llm.PlanQuotaExhausted("plan exhausted")
|
||||||
|
return "ok", dict(_USAGE)
|
||||||
|
|
||||||
|
_install(lambda: clock["t"], lambda wk: (0.0, 0), chat)
|
||||||
|
assert llm.chat_governed("first")[2] == "glm-5.2"
|
||||||
|
assert llm.chat_governed("second")[2] == "glm-5.2"
|
||||||
|
old_window_models = [m for wk, m in calls if wk == "2026-07-16T10"]
|
||||||
|
assert old_window_models == ["MiniMax-M3", "MiniMax-M2.7", "glm-5.2", "glm-5.2"], \
|
||||||
|
f"同窗 MiniMax 应各探一次后跳过,实得 {old_window_models}"
|
||||||
|
|
||||||
|
clock["t"] = datetime(2026, 7, 16, 15, 0)
|
||||||
|
assert llm.chat_governed("third")[2] == "MiniMax-M3"
|
||||||
|
assert calls[-1] == ("2026-07-16T15", "MiniMax-M3"), "跨窗后应清空熔断并重探 M3"
|
||||||
|
|
||||||
|
|
||||||
def test_route_first_success():
|
def test_route_first_success():
|
||||||
"""①usd=0/calls=0:链首 M3 成功 → 返回 MiniMax-M3、记账 usd>0、bump 一次到本窗。"""
|
"""①usd=0/calls=0:链首 M3 成功 → 返回 MiniMax-M3、记账 usd>0、bump 一次到本窗。"""
|
||||||
calls = []
|
calls = []
|
||||||
@ -186,6 +311,8 @@ def test_route_call_cap_sleep():
|
|||||||
def main():
|
def main():
|
||||||
tests = [test_window_key, test_seconds_to_next_window, test_cost_usd,
|
tests = [test_window_key, test_seconds_to_next_window, test_cost_usd,
|
||||||
test_current_quota_policy,
|
test_current_quota_policy,
|
||||||
|
test_plan_429_does_not_retry, test_regular_429_still_retries,
|
||||||
|
test_model_max_tokens_caps, test_plan_quota_breaker_same_window_and_recovery,
|
||||||
test_route_first_success, test_route_budget_exhausted,
|
test_route_first_success, test_route_budget_exhausted,
|
||||||
test_route_sensitive_then_next, test_route_runtime_then_next,
|
test_route_sensitive_then_next, test_route_runtime_then_next,
|
||||||
test_route_all_fail, test_route_call_cap_sleep]
|
test_route_all_fail, test_route_call_cap_sleep]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user