Skip to content

Commit eccfd7a

Browse files
committed
fix(kilo): 上游 429/5xx 判模型级冷却,不再冷却整条渠道
上游 429 报错点名具体模型(OpenRouter 共享池转发,limit_source: upstream_provider_shared_pool),且同一时刻其他免费模型仍 200;429 消退后 同一模型转 503 no endpoints available,同样模型级。原判账号级 SOFT(429) 与 OTHER(5xx 累计 3 次→10m)会因单虚拟凭证把整条 kilo 渠道冷却,客户端 收到 all credentials unavailable。改判 ErrKind.MODEL(只冷却 (凭证, 模型)), 对齐 CB/TRAE 的 429+6004 → MODEL 口径。 - events.py: classify_status/classify_error_code 的 429 与 502/503/504 → MODEL - tests/test_kilo.py: 断言更新 + 新增端到端回归(只锁触发模型、账号不冷却) - PROPOSAL Q46 / README / TECHNICAL §3.2+§3.15 同步
1 parent d8990d0 commit eccfd7a

7 files changed

Lines changed: 123 additions & 37 deletions

File tree

‎PROPOSAL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
| Q43 | macOS 部署模板去本地路径(占位符 + 安装脚本渲染) | `deploy/launchd/com.coding2api.plist` 与 `deploy/newsyslog/coding2api.conf` 原把开发机家目录绝对路径与用户名(`<user>:staff` 属主)写死在仓库里——克隆到别处不可用,还泄露本机目录结构。launchd 与 newsyslog **都不展开 `$HOME` / 环境变量**,路径必须写死,无法像 systemd 模板那样用约定路径,所以改成模板占位符 `__PROJECT_ROOT__`(路径)+ `__LOG_OWNER__`(属主),由 `scripts/install-launchd.sh`(新增,渲染 plist → `~/Library/LaunchAgents` → bootout/bootstrap,bootout 异步需重试)与 `scripts/install-newsyslog.sh`(改为渲染后写入 `/etc/newsyslog.d/`)在安装时替换成本机实际值。`test_deployment_assets.py` 锁三条不变量:全仓库无个人家目录路径、模板含占位符、安装脚本渲染占位符;newsyslog 与 launchd 模板的路径一致性改为**模板对模板**比对(渲染后仍校验本机已装 plist)。systemd / logrotate 的 `/opt/coding2api`、`/var/log/coding2api` 是约定部署路径非个人路径,保留不动。无 schema / 配置变更 |
5252
| Q44 | Zen 独立聊天节流(不再与 CB/TRAE 共享 pacer) | 原先 zen 的 `ZenProvider(pacer=chat_pacer)` 与 CodeBuddy/TRAE 共用同一个全局 pacer(`codebuddy_chat_min_interval`,默认 5s,min=max=5 → 固定 5s)。该 pacer 的存在理由是避开 CB 11128 / TRAE 流内错误的**账号级频率风控**,而 zen 是匿名免费层、无账号、无此类约束。共享的后果是**自伤式延迟**:任何 CB/TRAE 请求刚发出,紧随的 zen 请求就要在 pacer 里空等满 5s 才打上游;单一用户连发或 IDE 并发多个 zen 请求时,第 2、3 个请求 TTFB 实测 +5s、+10s(并发 3 个 zen:9.2s / 13.5s / 18.1s,去掉节流后应基本齐平)。实测确认**不是网络问题**:首 token 直连与走本机代理(127.0.0.1:7897)互有胜负、无稳定收益(`GET /models` 直连 0.29s vs 代理 0.60s;chat TTFB 直连 ≈ 代理),故不引入代理。改为 zen 用独立 `Pacer`,新增热更项 `ZEN_CHAT_MIN_INTERVAL`(默认 **0** = 不节流);仍保留可调旋钮,若上游日后对匿名层限流可调大。CB/TRAE 继续共享原 pacer,互不影响。无 schema 变更 |
5353
| Q45 | 聊天节流按凭证分桶并允许桶内并发(同渠道同模型并发不再串行台阶) | Q44 给 zen 拆了独立 pacer 后,CB/TRAE 的 `chat_pacer` 仍是**一把全局 `asyncio.Lock` + 单个 `_last_started`**:任何两个请求(哪怕不同账号、不同模型)都串行排队,后到者按 `interval - elapsed` 补足等待。实测 3 个并发 CB 请求 TTFB ≈ 1.55 / 6.71 / 11.47s(正好 +5s、+10s 台阶);把间隔热更为 0 后 6 并发 TTFB ≈ 1.48–1.84s、总 1.84s → 延迟完全来自节流排队而非上游。**关键**:并发请求常被会话粘性/健康度排序收敛到**同一个凭证**(DB 里 6 条并发全部命中 `cred_75e8edcf`),所以只按凭证分桶、桶内继续排队并不能解决,必须同时允许桶内并发。改为 `Pacer(min, max, *, allow_concurrent=False)`:`allow_concurrent=True`(仅聊天 pacer)时按桶(渠道前缀 + 凭证身份摘要)维护**在途计数**——同桶已有在途请求则新请求**立即放行**,只有桶空闲、且距上次请求开始不足最小间隔时才补足等待(即只有「上一请求已结束、紧接着又来一个」的顺序连发才节流)。请求结束由 provider `stream_chat` 的 `finally` 调 `pacer.release(key)` 归还名额(async generator 被提前关闭时依赖 asyncio 的 asyncgen finalize,延迟归还只会让节流略松、不会误排队)。`allow_concurrent=False`(后台任务 pacer)保持原严格串行语义不变。桶键用 `stable_key(provider, identity)`:CB 取 `account_uid or user_id or bearer_token`、TRAE 取 `uid or access_token`、zen 用渠道常量;`identity` 缺失回落该渠道单桶。CB/TRAE 仍共享同一 pacer 实例,但桶键带渠道前缀 + 身份摘要,彼此不互堵;`CODEBUDDY_CHAT_MIN_INTERVAL` 语义从「跨渠道全局间隔」变为「同渠道同凭证的顺序连发间隔」(默认 5s 不变)。无 schema 变更 |
54-
| Q46 | Kilo Gateway 免费层(第四渠道 `kilo`) | 把 [Kilo Gateway](https://kilo.ai)(`api.kilo.ai/api/gateway`)免费层接成第四个 provider(`KNOWN_PROVIDERS` 加 `"kilo"`,**无 schema 变更**)。**协议是标准 OpenAI 兼容**(`/chat/completions` + `/models`),既无私有信封也无门禁伪装——与 Zen 的关键差异正在此:Zen 要伪造 UA/session/tools 并过滤伪工具调用,Kilo 完全不需要。**免费模型有权威标记**:`/models` 每个条目带 `isFree` 布尔(实测 2026-09-29 共 395 个模型、17 个 `isFree=true`,含 `kilo-auto/free`、`stealth/space-bunny-alpha`、`openrouter/free` 等无 `:free` 后缀者),据此**直接过滤**免费集——**不做探活**(与 Zen 相反):探活会真发一次推理、白耗本就极小的免费配额(网关级约 200 req/h/IP),且结果随上游免费池波动不稳定,`isFree` 已足够权威。免费模型显式标 **x0 倍率**(`credit_rate=0.0`);`name`/`context_length`/`top_provider.max_completion_tokens`/`supported_parameters`(含 `tools`)/`architecture.input_modalities`(含 `image`)透传为中立 `Model` 元数据。思考字段是 **`delta.reasoning`**(**不是** Zen 的 `reasoning_content`)。**凭证模型用虚拟凭证行**(同 Zen):Kilo 无凭证/无额度接口,池里种一条空凭证复用现有调度/冷却/统计(`probe_quota` 恒 `probe_failed=True` → health NULL「未知」,**不是耗尽**);删除后重启复活,也可在凭证页「登录渠道账号」点「添加 Kilo Gateway」立即补回,永久停用请用「暂停」。**错误分类**(同 Zen 口径):401→`INVALID`(无凭证,401 只表示该模型需要付费 key/BYOK,避免强制付费模型硬禁用整条渠道)、429→`SOFT`(软冷却换模型)、400/404/422→`INVALID`、403→`REQUEST`。限流交引擎软冷却处理,**本包不自建熔断**——上游 429 报错自报限额来自 OpenRouter 共享池(`limit_source: openrouter_shared_capacity`),证实免费池实为 OpenRouter 转发。新增 `KILO_API_ENDPOINT` / `KILO_ALLOWED_ENDPOINTS`(端点白名单,Kilo 不带真实 Token)/ `KILO_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 0 = 不节流,与 zen / CB / TRAE 互不排队) |
54+
| Q46 | Kilo Gateway 免费层(第四渠道 `kilo`) | 把 [Kilo Gateway](https://kilo.ai)(`api.kilo.ai/api/gateway`)免费层接成第四个 provider(`KNOWN_PROVIDERS` 加 `"kilo"`,**无 schema 变更**)。**协议是标准 OpenAI 兼容**(`/chat/completions` + `/models`),既无私有信封也无门禁伪装——与 Zen 的关键差异正在此:Zen 要伪造 UA/session/tools 并过滤伪工具调用,Kilo 完全不需要。**免费模型有权威标记**:`/models` 每个条目带 `isFree` 布尔(实测 2026-09-29 共 395 个模型、17 个 `isFree=true`,含 `kilo-auto/free`、`stealth/space-bunny-alpha`、`openrouter/free` 等无 `:free` 后缀者),据此**直接过滤**免费集——**不做探活**(与 Zen 相反):探活会真发一次推理、白耗本就极小的免费配额(网关级约 200 req/h/IP),且结果随上游免费池波动不稳定,`isFree` 已足够权威。免费模型显式标 **x0 倍率**(`credit_rate=0.0`);`name`/`context_length`/`top_provider.max_completion_tokens`/`supported_parameters`(含 `tools`)/`architecture.input_modalities`(含 `image`)透传为中立 `Model` 元数据。思考字段是 **`delta.reasoning`**(**不是** Zen 的 `reasoning_content`)。**凭证模型用虚拟凭证行**(同 Zen):Kilo 无凭证/无额度接口,池里种一条空凭证复用现有调度/冷却/统计(`probe_quota` 恒 `probe_failed=True` → health NULL「未知」,**不是耗尽**);删除后重启复活,也可在凭证页「登录渠道账号」点「添加 Kilo Gateway」立即补回,永久停用请用「暂停」。**错误分类**:401→`INVALID`(无凭证,401 只表示该模型需要付费 key/BYOK,避免强制付费模型硬禁用整条渠道)、400/404/422→`INVALID`、403→`REQUEST`、**429 与 502/503/504→`MODEL`(模型级冷却)**。429 与上游 5xx 归模型级而非账号级:实测(2026-09-30)429 报错点名具体模型(`<model> is temporarily rate-limited upstream`,`limit_source: upstream_provider_shared_pool`),429 消退后同一模型转 503 `no endpoints available`,两种情况下**同一时刻其他免费模型仍 200**——免费池实为 OpenRouter 共享池转发,拥塞/端点缺失按模型隔离,归账号级会因单模型问题把整条 kilo 渠道冷却(429→60s;5xx 累计 3 次→10m;单虚拟凭证下均即 `all credentials unavailable`)。对齐 CB/TRAE 的 `429+6004 → MODEL` 口径。限流交引擎处理,**本包不自建熔断**。新增 `KILO_API_ENDPOINT` / `KILO_ALLOWED_ENDPOINTS`(端点白名单,Kilo 不带真实 Token)/ `KILO_CHAT_MIN_INTERVAL`(热更项,独立 pacer、默认 0 = 不节流,与 zen / CB / TRAE 互不排队) |
5555

5656
## 2. 目标与非目标
5757

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ curl http://127.0.0.1:8000/v1/chat/completions \
148148

149149
- **无凭证**:Kilo 不需要 Token。凭证池里那条 `Kilo Gateway` 是虚拟占位行(让调度 / 冷却 / 统计照常工作),额度列显示「免费层(无额度接口)」。它**删除后重启会复活**,也可在「凭证管理 → 登录渠道账号」点「添加 Kilo Gateway」立即补回——要永久停用请点「暂停」,不要删除。暂停它也会让 kilo 模型暂时从模型列表消失。
150150
- **无探活**:免费集完全由上游 `isFree` 决定,上游增删免费模型自动跟随,不维护静态白名单。
151-
- **额度与限流**:免费层额度很小(网关级约 200 请求/小时/IP),且免费池实为 OpenRouter 免费池的转发(上游 429 报错原文含 `limit_source: openrouter_shared_capacity`),会随上游池波动。上游 429 由引擎按软冷却自动换模型重试;本服务不自建熔断。
151+
- **额度与限流**:免费层额度很小(网关级约 200 请求/小时/IP),且免费池实为 OpenRouter 免费池的转发(上游 429 报错原文含 `limit_source: upstream_provider_shared_pool` 并**点名具体模型**),会随上游池波动。上游 **429 与 502/503/504 都是模型级**:只冷却被点名的那个模型(其余免费模型照常可用),冷却期内再请求该模型返回 `no_healthy_credential`;本服务不自建熔断。
152152
- **上游会改**:端点用 `KILO_API_ENDPOINT`(须在 `KILO_ALLOWED_ENDPOINTS` 内)。付费模型即使强制 `模型@kilo` 也只会报「不可用」,不会拖垮渠道。
153153
- **无额度接口**:健康度恒为「未探测到额度」(未知 ≠ 耗尽)。
154154

‎TECHNICAL.md‎

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -205,21 +205,23 @@ class ErrKind(StrEnum):
205205
(见 §6.1),不碰账号级 `cooling_until`,因此同账号的其他模型仍可选。反之账号级冷却
206206
出现时会清空该凭证的模型级条目——否则「切模型」能绕过账号级限流。
207207

208-
| 上游信号 | CB | TRAE | ErrKind |
209-
|---|---|---|---|
210-
| 权益耗尽 | `code=1005`/plan 相关 | `"code":1005` | PLAN |
211-
| 余额不足 | 402 / `code=14018` | 402 / `code=14018` | CREDIT |
212-
| 模型级限流 | 429 + `code=6004` | 429 + `code=6004` | MODEL |
213-
| 该账号无此模型 | 400/404 + `code=11102` | 400/404 + `code=11102` | BLOCKED |
214-
| 请求级错误 | 400 + 11101/`Unmarshal chat params failed`/11115/11135 | — | REQUEST |
215-
| 限流 | 429(无 6004) | 429(无 6004) | SOFT |
216-
| 不存在 | 404 | 404 | SOFT |
217-
| 会话失效 | 401/403 | 401 | DEAD |
218-
| 服务端错误 | 5xx | 5xx | OTHER |
219-
| 请求自身无效 | 400 | 400(含 4001) | INVALID |
220-
| 流内错误事件 | SSE error 事件 | `event:error` 业务码 | 同上映射(单一来源:provider 解析时写 `Event.error_kind`,executor 直接消费;缺失回落 OTHER) |
221-
222-
> **zen 的 401 例外**:Zen 无凭证,`401 Missing API key.` 只说明该模型需要付费 key,故 zen 的 `classify_status` / `classify_error_code` 把 401 归 `INVALID` 而非 `DEAD`(见 §3.14),避免一次强制付费模型请求把整条免费渠道硬禁用。
208+
| 上游信号 | CB | TRAE | Kilo | ErrKind |
209+
|---|---|---|---|---|
210+
| 权益耗尽 | `code=1005`/plan 相关 | `"code":1005` | — | PLAN |
211+
| 余额不足 | 402 / `code=14018` | 402 / `code=14018` | — | CREDIT |
212+
| 模型级限流 | 429 + `code=6004` | 429 + `code=6004` | 429 / 502/503/504(点名模型) | MODEL |
213+
| 该账号无此模型 | 400/404 + `code=11102` | 400/404 + `code=11102` | — | BLOCKED |
214+
| 请求级错误 | 400 + 11101/`Unmarshal chat params failed`/11115/11135 | — | 403 | REQUEST |
215+
| 限流 | 429(无 6004) | 429(无 6004) | — | SOFT |
216+
| 不存在 | 404 | 404 | — | SOFT |
217+
| 会话失效 | 401/403 | 401 | — | DEAD |
218+
| 服务端错误 | 5xx | 5xx | 500 | OTHER |
219+
| 请求自身无效 | 400 | 400(含 4001) | 400/404/422 | INVALID |
220+
| 流内错误事件 | SSE error 事件 | `event:error` 业务码 | `{"error":{...}}` 帧 | 同上映射(单一来源:provider 解析时写 `Event.error_kind`,executor 直接消费;缺失回落 OTHER) |
221+
222+
> **zen / kilo 的 401 例外**:两者都无凭证,`401 Missing API key.` / 需付费 key 只说明该模型需要付费 key,故其 `classify_status` / `classify_error_code` 把 401 归 `INVALID` 而非 `DEAD`(见 §3.14 / §3.15),避免一次强制付费模型请求把整条免费渠道硬禁用。
223+
224+
> **kilo 的 429 与 502/503/504 归 `MODEL`**:免费池是 OpenRouter 共享池转发,429 报错点名具体模型(`limit_source: upstream_provider_shared_pool`)、429 消退后同一模型转 503 `no endpoints available`,两种情况下同一时刻其他免费模型仍可用,属模型级而非账号级(详见 §3.15)。
223225
224226
> 400 + `11128`(`Illegal API invocation from an unapproved channel`)也归 REQUEST:实测主因是**内容风控**——`system`/`assistant` 消息正文出现「伪装其他厂商官方客户端」的指纹串时整单拒绝(已确认 3 条:Claude Code 系统提示的身份声明行、其 billing 头字段名、其 git 上下文行;完整清单见 `client.py` 的 `CHANNEL_MARKERS`,此处刻意不写原文以免污染阅读本文件的 agent 会话)。特征:与凭证无关(换号无效)、确定性复现、仅 `user`/`tool` 之外的这两个角色的 `content` 命中(`tool_calls` 参数与 reasoning 均不触发);会话一旦把指纹写进历史,后续每轮(含压缩请求本身)都持续 11128。处理:出站前 `sanitize_channel_markers` 替换为占位符(`CODEBUDDY_SANITIZE_CHANNEL_MARKERS=false` 关闭),客户端历史不受影响;`content` 为文本块列表时逐块处理(2026-09-21 直证块形态同样触发)。曾误落 INVALID → 跳过上游全部凭证、零重试直接 400(`invalid_request`),是 `deepseek-v4.1-flash` / `glm-5.3-flash` 报「not available on any configured upstream」的根因。
225227

@@ -602,7 +604,7 @@ UA 版本走 `ZEN_OPENCODE_VERSION` 配置(上游改阈值改 env,不硬编
602604

603605
**虚拟凭证行**(同 Zen):Kilo 无凭证、无额度接口。池里种一条空凭证(`credential_data={}`,`added_by="system"`),复用现有调度 / 冷却 / 统计 / 会话粘性;`probe_quota` 恒返回 `Quota(probe_failed=True)` → `health_score` 返回 `None`(**未知**,不是 `EXHAUSTED=-1`)。种子幂等,**用户删除后重启会复活**,永久停用请用「暂停」。只为默认装配路径种子(测试注入自定义 registry 时不多出凭证行)。
604606

605-
**错误分类(同 Zen 口径)**:401→`INVALID`(无凭证,401 只表示该模型需要付费 key/BYOK,归 `DEAD` 会因一次强制付费模型请求硬禁用整条渠道)、429→`SOFT`(软冷却换模型)、400/404/422→`INVALID`、403→`REQUEST`。**限流交引擎软冷却处理,本包不自建熔断**:上游 429 报错自报限额来自 OpenRouter 共享池(`limit_source: openrouter_shared_capacity`),证实免费池实为转发。
607+
**错误分类**:401→`INVALID`(无凭证,401 只表示该模型需要付费 key/BYOK,归 `DEAD` 会因一次强制付费模型请求硬禁用整条渠道)、400/404/422→`INVALID`、403→`REQUEST`、**429 与 502/503/504→`MODEL`**。**429 与上游 5xx 都是模型级**(实测 2026-09-30):429 报错点名具体模型(`<model> is temporarily rate-limited upstream`,`limit_source: upstream_provider_shared_pool`),429 消退后同一模型转 503 `no endpoints available`,两种情况下**同一时刻其他免费模型仍 200**——免费池是 OpenRouter 共享池转发,拥塞/端点缺失按模型隔离。故归模型级冷却(对齐 CB/TRAE 的 `429+6004 → MODEL`),只锁触发模型、不连累整条 kilo 渠道;**限流交引擎处理,本包不自建熔断**。曾误判账号级 `SOFT`(429)与 `OTHER`(5xx,累计 3 次→10m):单虚拟凭证下都会把整条渠道冷却,客户端收到 `all credentials unavailable`。
606608

607609
**独立 pacer(同 Zen)**:`KILO_CHAT_MIN_INTERVAL`(热更项,默认 0 = 不节流)走独立 `Pacer`,与 zen / CB / TRAE 互不排队;`stream_chat` 在 `finally` 里 `pacer.release("kilo")` 归还并发名额。
608610

‎src/provider/kilo/__init__.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
1313
实测(2026-09-29):免费模型匿名可用(无需 key);上游限流按 **200 请求/小时/IP**
1414
(网关级),且免费池实为 OpenRouter 免费池的转发(429 报错原文含
15-
`limit_source: openrouter_shared_capacity`),故免费模型会随 OpenRouter 池
16-
波动——上游 429 由引擎按 `SOFT` 软冷却触发换模型,本包不自建熔断。
15+
`limit_source: upstream_provider_shared_pool`),故免费模型会随 OpenRouter 池
16+
波动——上游 429 与 502/503/504 归 **模型级**冷却(实测都点名具体模型、且
17+
同一时刻其他免费模型仍可用),只锁触发模型、不连累整条渠道;本包不自建熔断。
1718
"""

‎src/provider/kilo/client.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
`isFree` 决定,上游增删自动跟随。
1212
1313
链路层:`/api/gateway/models` 与 `/api/gateway/chat/completions`,匿名即可
14-
(免费模型无需 Authorization)。上游在 429 报错里自报限额来自 OpenRouter
15-
共享池(`limit_source: openrouter_shared_capacity`),故限流按不可控处理,
16-
交给引擎软冷却换模型,本包不自建熔断。
14+
(免费模型无需 Authorization)。上游 429 自报限额来自 OpenRouter 共享池
15+
(`limit_source: upstream_provider_shared_pool`)并**点名具体模型**,429 消退
16+
后同一模型还会转 503 `no endpoints available`;两种情况下实测同一时刻其他
17+
免费模型仍 200——故 429 与 502/503/504 都归模型级冷却,只锁触发模型,交给
18+
引擎按 `MODEL` 处理,本包不自建熔断。
1719
"""
1820

1921
from __future__ import annotations

0 commit comments

Comments
 (0)