Skip to content

Commit c7847b6

Browse files
committed
fix(engine): 并发冷却封顶按渠道分档 + Qoder 拒绝 developer 角色归一
- raccoon 的 429 `user_model_rate_limited` 是账号×模型的分钟级频率窗口,不是 CodeArts 那种「在途满了」:156 次 429 到同凭证下次成功的间隔中位数 1522s, 仅 22/156 在 5min 内恢复。CONCURRENCY 封顶改为按渠道分档——raccoon 30min、 其余仍 5min;首枪 60s 与同链翻倍不变,整组同秒 429 的 burst 封顶同样分档 - Qoder 出站报文把 `role:"developer"` 归一为 `system`:上游以 `unknown variant 'developer'` 400 拒绝(同分钟 CodeArts 也拒),语义等价无损 - TECHNICAL / README / README.en / PROPOSAL 同步冷却口径与报文形状说明
1 parent 8373234 commit c7847b6

11 files changed

Lines changed: 130 additions & 30 deletions

File tree

‎PROPOSAL.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
- **凭据是网页会话 JWT**:浏览器 cookie `xhx_token`(`iss=xiaohuanxiong.com/iam`,约 30 天)直接当 `Authorization: Bearer`。没有官方 API Key,也没有可用的 OAuth 回调(桌面端只注册 `gateway.qwenwork.cn` 那一类服务端回调),故**只能手工导入**:管理台「JSON 导入」粘贴 `{"access_token":"<cookie 值>"}`(也接受裸 JWT / 整段 cookie)。到期需重新登录取值。
9494
- **未知模型会静默回落默认模型**(实测 `definitely-not-a-model` → 200 + `model=raccoon-090dd3`):provider 发请求前按目录校验,流首帧再比对上游回显的 `model`,不一致即失败——宁可这次报错,也不让客户端「问 A 答 B」。
9595
- **计费按请求**:每笔 LLM 调用记 `points=-1`,与 token 量无关(4K 与 205K token 同价);每日 00:00 发 300 积分,**当天登录过才延期到 7 天**(未登录当日 24:00 清零,实测心跳不算登录)。`Quota.remaining`/`total` = 可用积分,账本记 `credits`。目录的 `billing_effective_multiplier` 直接当 `credit_rate`(0 = 限免,调度器优先选)。
96-
- **并发上限约 5**(6 并发第 6 个 `429 user_model_rate_limited`):pacer 用 `RACCOON_MAX_CONCURRENCY`(默认 2)+ `CONCURRENCY` 固定短冷却;不是账号故障。
96+
- **并发上限约 5**(6 并发第 6 个 `429 user_model_rate_limited`):pacer 用 `RACCOON_MAX_CONCURRENCY`(默认 2)+ `CONCURRENCY` 模型级冷却(首枪 60s、同链翻倍、封顶 30min——实测恢复中位数 25min,5min 封顶等于每 5min 白撞一波);不是账号故障。
9797
- **登录轨道不实现**:没有 `start_auth`/`poll_auth`/`complete_callback`——写了也只是永远不被调用的死代码(与 Zen/Kilo 同一分工)。
9898
- **签到=页面访问 + 流水核对**:上游没有领取接口,`checkin` = `user_info`(页面级访问,触发延期/账单物化)→ 摸余额 → 读流水;只认流水(同 CodeArts「claim 成功但 confirm 失败按失败报」的纪律,发放规则见上条)。今天没有新发放但仍有未过期 `daily_grant` 视为已签;凌晨 00:00~01:30 只封账到 01:30,避免 00:02 误报。流水用上游服务端过滤 **`type=income`**(只留 `daily_grant` / `reward_grant`)——`type=all` 里消费流水挤占条数窗口,活跃账号的每日发放会看不到(2026-11-10 实测排到第 62 条,60 条窗口内为空,误报「尚未发放」)。**换新 token(重新登录)会立刻补发当天 300**(`created_at` 是登录后几秒、`grant_at` 回填 00:00);只拿旧 token 访问 `user_info` / `home` / 心跳不触发,故缺当日发放的号只能人工重登。
9999
- **活跃上报=页面访问 + 设备心跳**:`GET /api/web/auth/v1/user_info` 后 `POST /api/web/auth/v1/devices_current_heartbeat`;缺 `x-client-device-id` 等头族回 `100002 params_invalid_error`;device_id 由 uid 稳定派生(随机值会被当成新设备)。心跳只刷 `last_seen_at`,不算登录。
@@ -152,7 +152,7 @@ Provider 只管上游私有部分(发请求、解析事件、分类错误、
152152
4. **到期额度两级字典序**:先比主窗口(`QUOTA_EXPIRY_WINDOW_SECONDS`,默认 36h)内将过期的额度,打平再比次窗口(`QUOTA_EXPIRY_SECONDARY_WINDOW_SECONDS`,默认 7 天)。
153153
5. **健康度三态**:`known(0-100)` > `unknown` > `exhausted`;打平比剩余积分,再按 `credential_id` 稳定。
154154

155-
**冷却分两层的原因**:`429 + 6004` 是「此模型在当前账号用超」,`400/404 + 11102` 是「当前账号没此模型」;一律记账号级 → 一次模型级限流踢掉整号。故账号级写 `credentials.cooling_until`(带来源 `cooling_kind`;人工 `revive` 默认保留上游点名的限流/额度类,`force` 才清),模型级另建 `credential_model_cooldowns`;账号级冷却出现时清空该凭证模型级条目,防「切模型」绕过。业务码只认 `"code": N` 键值形态。时长:`PLAN` 12h、`SOFT` 60s、`OTHER` 连续 3 次→10m、`MODEL` 600s 起翻倍封顶 2h、`BLOCKED` 6h 起封顶 24h、`CONCURRENCY` 固定 60s、`CREDIT` 到次日 04:00、`RESTRICTED` 2h 起按命中次数翻倍(封顶 7 天;同渠道窗口内第二个不同凭证再中招 → 该渠道整体 24h,见 §2.1 `11140`)。**传输层失败归 `SOFT`**:`httpx.TransportError`、`ssl.SSLError`、`UpstreamTransportError` 统一在 `executor._classify` 兜住(异常可从 `__aenter__`、`aiter_bytes()`、写请求体多处抛出,逐点包装必漏),不泛化到 `OSError` / `ConnectionError`。`ssl.SSLError` 必须单列:anyio 只把 `SSLEOFError` 转 `BrokenResourceError`,其余 TLS 中断(`record layer failure` 等)原样上抛 → 不列就逃到 `stream_guarded` 变 `internal_error`:不换号、不冷却、统计零痕迹。**上游重置时刻可覆盖常量**:`Retry-After`(整秒/HTTP-date)或 `Try again in 17h 59m` 经 `parse_retry_after` 解析后按作用域写入,双端钳制 `[now + 常量, now + 24h]`。**头缺失看响应体**:CB 国际站 `429+6004` 的恢复时刻只在正文(见 §2.1),由 `parse_reset_at` 解析;头优先于正文,`UTC±N` 偏移缺失则放弃。不解析正文 → 固定 600s 后重试再撞 429(实测同一号一天被爆 42 次)。
155+
**冷却分两层的原因**:`429 + 6004` 是「此模型在当前账号用超」,`400/404 + 11102` 是「当前账号没此模型」;一律记账号级 → 一次模型级限流踢掉整号。故账号级写 `credentials.cooling_until`(带来源 `cooling_kind`;人工 `revive` 默认保留上游点名的限流/额度类,`force` 才清),模型级另建 `credential_model_cooldowns`;账号级冷却出现时清空该凭证模型级条目,防「切模型」绕过。业务码只认 `"code": N` 键值形态。时长:`PLAN` 12h、`SOFT` 60s、`OTHER` 连续 3 次→10m、`MODEL` 600s 起翻倍封顶 2h、`BLOCKED` 6h 起封顶 24h、`CONCURRENCY` 首枪 60s 且同一命中链才翻倍(封顶分档:默认 5min、raccoon 30min)、`CREDIT` 到次日 04:00、`RESTRICTED` 2h 起按命中次数翻倍(封顶 7 天;同渠道窗口内第二个不同凭证再中招 → 该渠道整体 24h,见 §2.1 `11140`)。**传输层失败归 `SOFT`**:`httpx.TransportError`、`ssl.SSLError`、`UpstreamTransportError` 统一在 `executor._classify` 兜住(异常可从 `__aenter__`、`aiter_bytes()`、写请求体多处抛出,逐点包装必漏),不泛化到 `OSError` / `ConnectionError`。`ssl.SSLError` 必须单列:anyio 只把 `SSLEOFError` 转 `BrokenResourceError`,其余 TLS 中断(`record layer failure` 等)原样上抛 → 不列就逃到 `stream_guarded` 变 `internal_error`:不换号、不冷却、统计零痕迹。**上游重置时刻可覆盖常量**:`Retry-After`(整秒/HTTP-date)或 `Try again in 17h 59m` 经 `parse_retry_after` 解析后按作用域写入,双端钳制 `[now + 常量, now + 24h]`。**头缺失看响应体**:CB 国际站 `429+6004` 的恢复时刻只在正文(见 §2.1),由 `parse_reset_at` 解析;头优先于正文,`UTC±N` 偏移缺失则放弃。不解析正文 → 固定 600s 后重试再撞 429(实测同一号一天被爆 42 次)。
156156

157157
**「窗口内积分总量」的原因**:实测 CB 额度是几十个各自独立到期的小包(每日 100 积分 × N),只存一个日期没有区分度。四个取舍:① 统计窗口内到期积分总量,账号间才可比;② 落库到期阶梯 `[(到期 epoch, 包剩余积分)]` 而非预计算数字;③ 过滤必须 `end > now` 且排除已用完的包;④ 两级窗口——只比 36h 会大量账号同为 0。**展示与调度指标分开存**:`quota_packages`(含包名/用量)与 `quota_expiry_ladder`(只有 `[到期, 剩余]`)。
158158

‎README.en.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ Free models at [Kilo](https://kilo.ai) (`api.kilo.ai/api/gateway`), no login. St
209209

210210
**Keepalive frames and queueing**: the upstream sends keepalive frames with `body:"null"` (outer status 200) mid-stream → skipped, never interrupting output; "queued" arrives as a 403 whose queue marker hides inside a **serialized string** → classified as QUEUE, waits the upstream's named seconds and reopens on the same credential instead of hard-disabling it. "Daily billing count exhausted" (403 `code 110`, `Billing daily count exceeded`) counts as quota → cools until 04:00 next day, likewise never hard-disabling (same predicate for HTTP and in-stream envelopes).
211211

212+
**Message shape**: an assistant `content: null` that carries `tool_calls` is normalized to an empty string; `role:"developer"` is normalized to `system` (the upstream rejects the variant — `unknown variant 'developer'`; CodeArts rejects the same body the same minute). Both rewrites happen on the outbound copy and are lossless.
213+
212214
### CodeArts (`codearts`)
213215

214216
[Huawei Cloud CodeArts](https://codearts.huaweicloud.com), real account: *Log in to CodeArts*, authorize, then paste the **unreachable local URL** back; server exchanges it for STS credentials (AK/SK + security_token + refresh_token + DPoP key) and stores them encrypted.
@@ -274,7 +276,7 @@ The upstream is plain OpenAI protocol, with three quirks:
274276

275277
**Activity reporting**: `POST /api/web/auth/v1/devices_current_heartbeat` (the web client's real device heartbeat); with the `x-client-*` header family it returns 200 and is idempotent, refreshing `last_login_at`. Off by default; shares `ACTIVITY_REPORT_ENABLED` / `ACTIVITY_REPORT_HOUR` with CodeBuddy. Device id is derived stably from uid (a random one is treated as a new device).
276278

277-
**Concurrency cap ~5** (the 6th concurrent request returns `429 user_model_rate_limited`) → `RACCOON_MAX_CONCURRENCY` defaults to 2, with its own pacer.
279+
**Concurrency cap ~5** (the 6th concurrent request returns `429 user_model_rate_limited`) → `RACCOON_MAX_CONCURRENCY` defaults to 2, with its own pacer. That 429 is really a per-account-per-model rate window (measured recovery median 25 min), so the `CONCURRENCY` cooldown starts at 60s, doubles along a hit chain, and caps at 30 min.
278280

279281
### Lingxi (`lingxi`)
280282

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ curl http://127.0.0.1:8000/v1/chat/completions \
213213

214214
**保活帧与排队**:上游在流中发 `body:"null"` 的保活帧(外层 200)→ 跳过、不打断输出;「排队中」包成 403 且排队标记藏在**序列化字符串**里 → 归 QUEUE 等上游点名的秒数后在原号重开,不硬禁用凭证。「每日计费次数用尽」(403 `code 110`,`Billing daily count exceeded`)→ 归额度类,冷却到次日 04:00,同样不硬禁用(HTTP 与流内信封同一判据)。
215215

216+
**报文形状**:带 `tool_calls` 的 assistant `content: null` 归一为空串;`role:"developer"` 归一为 `system`(上游不认这个变体,实测 400 `unknown variant 'developer'`,同分钟 codearts 亦拒)。两者都是出站副本上的无损改写。
217+
216218
### CodeArts(`codearts`)
217219

218220
[华为云 CodeArts](https://codearts.huaweicloud.com),需真实账号:点「登录 CodeArts」→ 授权后浏览器跳到打不开的本地地址 → **整条链接**粘回页面换 STS 凭证(AK/SK + security_token + refresh_token + DPoP 私钥),加密入库。
@@ -280,7 +282,7 @@ curl http://127.0.0.1:8000/v1/chat/completions \
280282

281283
**活跃上报**:`POST /api/web/auth/v1/devices_current_heartbeat`(网页端真实设备心跳),补 `x-client-*` 头族即 200、幂等,`last_login_at` 随之刷新。默认关闭,与 CodeBuddy 共用 `ACTIVITY_REPORT_ENABLED` / `ACTIVITY_REPORT_HOUR`。设备 id 由 uid 稳定派生(随机值会被上游当成新设备)。
282284

283-
**并发上限约 5**(第 6 个回 `429 user_model_rate_limited`)→ `RACCOON_MAX_CONCURRENCY` 默认 2,独立节流器。
285+
**并发上限约 5**(第 6 个回 `429 user_model_rate_limited`)→ `RACCOON_MAX_CONCURRENCY` 默认 2,独立节流器。该 429 实为账号×模型的频率窗口(实测恢复中位数 25min),冷却按 `CONCURRENCY` 首枪 60s、同链翻倍、封顶 30min。
284286

285287
### 灵犀(`lingxi`)
286288

0 commit comments

Comments
 (0)