mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
按发布纪律回流。这四笔原本**误提交在 main** 上(我的操作失误): 纪律要求修复先在 release/v2.0.x 提交、再回流 main。已迁移: ① release 上 cherry-pick 这四笔(干净落地); ② main reset 回 9bec158(那四笔未推送,无远端影响); ③ 内容对齐回 main —— 等价于只捡拾修复、不带版本号。 内容: - channels-qoder 适配器:10605(排队) 与 110(额度耗尽) 分开说,厂商码留在插件域 - channels-cline 适配器:解析英文句子里的等待时长(内核按额度耗尽冷却而非 30s 猛敲) - auto_claim:按 covers_today 记账(Qoder 10:00 刷新前不得算已处理)+ 间隔 2 小时 - isFree:改为按平台声明权威来源(cline 用远端 free[]/命名约定,opencode 用 cost) 校验:git diff release/v2.0.x = 0 行;go build 通过。
92 lines
4.2 KiB
Go
92 lines
4.2 KiB
Go
package core
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// Cline 的 429/402 话术与冷却时长(照参考 cline-rate-limit.ts)。
|
||
//
|
||
// ★ 这是**比参考更严重**的形态:参考退到"1 小时快照兜底",用户看到
|
||
// 「永远 60 分钟」(等满一小时再试仍被拒,且每失败一次就从当下重新计一小时
|
||
// ⇒ 无限循环)。而我们的内核把 429 当普通限流、用**固定 30 秒**冷却 ——
|
||
// 那会变成**每 30 秒重试一次、永远失败**,在猛敲上游。
|
||
//
|
||
// 抓包真值(参考实测,no-retry: true —— 没有任何 retry-after 头):
|
||
//
|
||
// HTTP 429
|
||
// {"error":{"code":"INFERENCE_CAP_ERROR",
|
||
// "message":"Error 429: Daily free limit reached on model
|
||
// deepseek/deepseek-v4-flash. Try again in 19h 39m"}}
|
||
func TestClineAdapterParsesProseWait(t *testing.T) {
|
||
c, _ := poolCore(t)
|
||
vm := c.VM()
|
||
|
||
// ① 当日免费额度用尽:必须被归一成**内核已认的中立短语**
|
||
//(否则内核按 30 秒短冷却处理 ⇒ 每 30 秒猛敲上游)
|
||
body := `{"error":{"code":"INFERENCE_CAP_ERROR","message":` +
|
||
`"Error 429: Daily free limit reached on model deepseek/deepseek-v4-flash. Try again in 19h 39m"}}`
|
||
out, ok, err := vm.TransformError("channels-cline", 429, body)
|
||
if err != nil || !ok {
|
||
t.Fatalf("transform_error: err=%v ok=%v", err, ok)
|
||
}
|
||
if !strings.Contains(out, "额度已用完") {
|
||
t.Errorf("必须归一成内核已认的「额度已用完」(否则不会被当成额度耗尽处理),实得 %q", out)
|
||
}
|
||
// 真实等待时长必须出现在话术里(19h39m ≈ 19 小时)
|
||
if !strings.Contains(out, "19") {
|
||
t.Errorf("必须把真实等待时长写进话术(用户才知道要等多久),实得 %q", out)
|
||
}
|
||
if !strings.Contains(out, "重试无用") {
|
||
t.Errorf("当日额度用尽要明确告知重试无用(否则用户以为等一会就好),实得 %q", out)
|
||
}
|
||
// ⚠️ 绝不能出现参考踩过的「1 小时」误导
|
||
if strings.Contains(out, "1 小时") && !strings.Contains(out, "19") {
|
||
t.Errorf("不得退回快照式的「1 小时」误导,实得 %q", out)
|
||
}
|
||
|
||
// ② 时长 token 必须**累加**(`1h 30m` = 90 分钟),不能只取第一个 ——
|
||
// 通过真实的 transform_error 路径验证(不自己重算解析器)。
|
||
acc, _, _ := vm.TransformError("channels-cline", 429,
|
||
`{"error":{"message":"Daily free limit reached. Try again in 1h 30m"}}`)
|
||
if !strings.Contains(acc, "1 小时 30 分") {
|
||
t.Errorf("`1h 30m` 必须累加成人话时长(1 小时 30 分),实得 %q", acc)
|
||
}
|
||
// 秒级也要能解析(否则秒级限流会退化成通用文案)
|
||
secs, _, _ := vm.TransformError("channels-cline", 429,
|
||
`{"error":{"message":"Rate limited. Try again in 45s"}}`)
|
||
if !strings.Contains(secs, "分") && !strings.Contains(secs, "秒") {
|
||
t.Errorf("秒级时长应被解析并说出,实得 %q", secs)
|
||
}
|
||
|
||
// ③ 402:等待不会恢复,必须说清楚要充值(不能只说"限流")
|
||
out402, _, _ := vm.TransformError("channels-cline", 402,
|
||
`{"error":{"message":"Insufficient credits"}}`)
|
||
if !strings.Contains(out402, "充值") {
|
||
t.Errorf("402 应明确提示需充值(等待无用),实得 %q", out402)
|
||
}
|
||
|
||
// ④ 普通 429 带时长:如实说,但不谎报成额度耗尽
|
||
out429, _, _ := vm.TransformError("channels-cline", 429,
|
||
`{"error":{"message":"Rate limited. Try again in 30s"}}`)
|
||
if strings.Contains(out429, "额度已用完") {
|
||
t.Errorf("普通限流不得谎报成额度耗尽(那会让内核长时间冷却一个健康源),实得 %q", out429)
|
||
}
|
||
if !strings.Contains(out429, "限流") {
|
||
t.Errorf("普通 429 应说成限流,实得 %q", out429)
|
||
}
|
||
|
||
// ⑤ 多种外壳都要认(`{error:"…"}` 字符串 / `{msg}` / 非 JSON 纯文本)
|
||
//(只认一种会在别的形态下静默丢掉整条信息)
|
||
for _, shell := range []string{
|
||
`{"error":"Daily free limit reached. Try again in 1h"}`,
|
||
`{"msg":"Daily free limit reached. Try again in 1h"}`,
|
||
`Daily free limit reached. Try again in 1h`,
|
||
} {
|
||
o, _, _ := vm.TransformError("channels-cline", 429, shell)
|
||
if !strings.Contains(o, "额度已用完") {
|
||
t.Errorf("外壳 %q 未被识别为额度耗尽,实得 %q", shell, o)
|
||
}
|
||
}
|
||
}
|