mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
照参考的实测依据:这些平台的**并发配额按模型**分别计量,与"还剩多少 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 个包全绿。
64 lines
2.0 KiB
Go
64 lines
2.0 KiB
Go
package core
|
||
|
||
import (
|
||
"testing"
|
||
)
|
||
|
||
// 源注册必须**串行 + 节流**,而不是并发 2、无间隔。
|
||
//
|
||
// 照参考 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** ⇒ 既会重叠又无间隔。
|
||
func TestChannelSourcesAreSerialWithThrottle(t *testing.T) {
|
||
c, ps := poolCore(t)
|
||
m, err := ps.Call("channels", "account_add", map[string]interface{}{
|
||
"platform": "buddy", "account_id": "gated",
|
||
"credential": `{"access_token":"TOK"}`,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("account_add: %v", err)
|
||
}
|
||
j, _ := m.(map[string]interface{})["json"].(map[string]interface{})
|
||
if j == nil || j["error"] != nil {
|
||
t.Fatalf("account_add 失败: %#v", m)
|
||
}
|
||
aid, _ := j["account_id"].(string)
|
||
|
||
srcs := listSourcesViaKernel(t, c)
|
||
sm, ok := srcHasAccount(srcs, aid)
|
||
if !ok {
|
||
t.Fatal("账号未出源")
|
||
}
|
||
// ① 串行
|
||
if mc, _ := toFloat(sm["max_concurrent"]); mc != 1 {
|
||
t.Errorf("源必须 max_concurrent=1(并发发起必然浪费一个配额),实得 %v", sm["max_concurrent"])
|
||
}
|
||
// ② 节流(rpm>0 才有间隔)
|
||
if rpm, _ := toFloat(sm["rpm"]); rpm <= 0 {
|
||
t.Errorf("源必须设 rpm>0(串行不保证间隔),实得 %v", sm["rpm"])
|
||
}
|
||
}
|
||
|
||
func toFloat(v interface{}) (float64, bool) {
|
||
switch x := v.(type) {
|
||
case float64:
|
||
return x, true
|
||
case int:
|
||
return float64(x), true
|
||
case int64:
|
||
return float64(x), true
|
||
}
|
||
return 0, false
|
||
}
|