feat(channels): 源改为串行 + 按模型节流(照参考 model-gate.ts)

照参考的实测依据:这些平台的**并发配额按模型**分别计量,与"还剩多少 token"
无关(那边实测撞 429 时 token 还剩 299.4 万)。同账号下:

    GLM-5.3-Flash  605 次 200    0 次限流   ← 从未撞过
    GLM-5.3         74 次 200   21 次重试  6 次最终 429

两条措施缺一不可:
① **串行**(max_concurrent=1):并发发起必然浪费一个配额;
② **最小间隔**(rpm):串行只保证"不重叠"、不保证"有间隔" —— 连续放行的两次
   调用可能只隔几十毫秒,仍会撞并发窗口。

此前插件注册源一律 `max_concurrent = 2` 且**从不设 rpm** ⇒ 既会重叠又无间隔。
现全部改为 `max_concurrent = 1` + `rpm = 20`(默认 3 秒一个,可按平台覆盖)。

⚠️ 参考的另一条教训也记进注释了:闸门**只包 fetch,不包 captcha**。
那边第一版把 mint 与 fetch 一起放进闸门,mint 耗时从 200-500ms 暴涨到
2500-3100ms(因为 mint 被串行后变成"排在 N 个人后面再 mint")。
我们的 rpm 节流在内核层、只作用于上游 dispatch,天然不涉及这一点。

顺带:`list_sources` 补暴露 `rpm` —— 运维与判据都要能看到它。只看
max_concurrent 无法区分"串行但有间隔"与"串行且无间隔"(后者仍会撞窗口)。

判据 `TestChannelSourcesAreSerialWithThrottle`:源必须 max_concurrent=1 且 rpm>0。
回归:9 个包全绿。
This commit is contained in:
JianFeeeee
2026-10-14 16:00:34 +08:00
parent 5720e60fc9
commit 92c240cc25
3 changed files with 86 additions and 4 deletions

View File

@ -3050,7 +3050,19 @@ local function syncAccountSource(provider, accountId)
base_url = plat.base_url,
adapter = plat.adapter or "openai",
api_key = tok,
max_concurrent = 2,
-- ⚠️ max_concurrent=1 + rpm 节流,而不是 2。
--
-- 照参考 model-gate.ts 的实测依据:这些平台的**并发配额按模型**分别计量,
-- 与"还剩多少 token"无关(那边实测撞 429 时 token 还剩 299.4 万)。同账号下:
-- GLM-5.3-Flash 605 次 200 0 次限流 ← 从未撞过
-- GLM-5.3 74 次 200 21 次重试 6 次最终 429
-- 两条措施缺一不可:
-- ① **串行**:同一时刻只允许一个上游请求在飞(并发发起必然浪费一个配额)
-- ② **最小间隔**:串行只保证"不重叠",不保证"有间隔" —— 连续放行的两次
-- 调用可能只隔几十毫秒,仍会撞并发窗口。故用 rpm 让内核把它们隔开。
-- 这两条都是**客户端自保**(防患于未然),与"上游明确要求排队"是两回事。
max_concurrent = 1,
rpm = plat.rpm or 20, -- 默认 3 秒一个请求,可按平台覆盖
models = models,
}
-- 平台的 chat 端点常与 base_url 不是简单拼接(Gemini 走私有
@ -4897,7 +4909,9 @@ function plugin.complete_login(req)
base_url = plat.base_url,
adapter = "openai",
api_key = legacyTok,
max_concurrent = 2,
-- 同 syncAccountSource:串行 + 节流(见那里的长注释)
max_concurrent = 1,
rpm = plat.rpm or 20,
models = (function()
local ms = {}
local eff = effectiveModels(plat.id)
@ -5971,7 +5985,7 @@ function plugin.provider_set_enabled(req)
if legacy and legacyTok then
__llmsproxy_kernel.call("add_source", { source = {
name = "channels-" .. plat.id, base_url = plat.base_url, adapter = plat.adapter or "openai",
api_key = legacyTok, max_concurrent = 2,
api_key = legacyTok, max_concurrent = 1,
models = (function() local ms = {} local eff = effectiveModels(plat.id)
for i, m in ipairs(eff) do ms[i] = { id = m, kind = "chat", priority = 20 } end return ms end)(),
} })
@ -8726,7 +8740,7 @@ function plugin.__sync_source_probe(args)
end
local test_src = {
name = want, base_url = plat.base_url, adapter = plat.adapter or "openai",
api_key = tok, max_concurrent = 2, models = models,
api_key = tok, max_concurrent = 1, models = models,
}
if plat.endpoint then test_src.endpoint = plat.endpoint end
local rr, rerr = __llmsproxy_kernel.call("add_source", { source = test_src })