Repository navigation
feat!: 重写为 18 模型 / 6 provider,修掉 4 个「本地绿线上挂」的真 bug - #1
Merged
Merged
Conversation
这一版的重点不是加功能,是修掉上一版几个「本地测试全绿、线上必挂」
的问题,并把模型清单收敛到单一真相源。每条修复都有对应回归测试。
## 四个真 bug(都有复现证据)
1. **流式请求没有真的流式。** openai-compat 分支里 stream 被硬编码成
false,客户端请求 stream:true 时收到一个普通 JSON。聊天类客户端
(Cline / Cursor / 各种 Web UI)会一直等一个不会到来的 SSE,表现为
卡死。受影响的 18 个模型里有 13 个走这条路径。
2. **Gemini 完全没有流式。** 只实现了 generateContent,没有
streamGenerateContent。
3. **有效的 Qwen 账号被判定为不可用(最致命)。** 账号池判断 token
是否有效时读的是 acc.token,而账号存的是 access_token。这个分支
永远不会命中,于是每个请求都走「拿 refresh_token 去换」的路径:
一是白白多一次上游往返;二是 refresh 一旦失败(这个 grant 不是稳定
的公开接口,失败很常见),一个还有一小时寿命的有效 token 会被冷藏
一整天,用户看到 "no Qwen account available"。而 Qwen 是默认模型家族。
复现:token 剩余 3600 秒 → 返回 502,refresh 被调用 1 次,真实对话 0 次。
4. **README 声称的「跨 provider 自动故障转移」并不存在。** 代码里只有
Qwen 的账号轮换。现在真的实现了:按「同模型 → 同 provider 其它模型
→ 其它已配置 provider」构建候选链,只在 429/401/403/5xx 时往下走
(400 说明请求本身有问题,换 provider 也一样,不重试)。
另外修掉:
- 错误体是 {"error": "字符串"} 而非 OpenAI 的 {"error": {"message"}},
官方 SDK 解析时抛异常,用户看到的是 SDK 的报错而不是真实原因。
- Gemini 只取 messages 最后一条,多轮历史整段丢弃(Cline/Cursor 每轮
都带完整历史,等于每轮让模型失忆)。
- 上游中途断开时,SSE 缓冲区里已收到的内容被丢掉("说到一半突然没了")。
- 账号池「一次 429 封一整天」,改成按原因分级冷却。
## 新增
- providers.js:模型目录与 provider 注册表分离,18 模型 / 6 provider
(新增 Groq)。新增模型不再需要复制 base/type/auth 三份配置。
- scripts/check-docs.mjs:CI 强制 README 与代码的模型清单一致。
上一版 README 写 7 个模型、正文表格写 8+、package.json 写 5 个、
代码实际 8 个 —— 四份数字没一个对得上。
- 响应标注实际服务的模型:_proxy 字段 + X-IceProxy-Fallback 头。
- /v1/models 带 context_length / capabilities / available。
- 账号池并发刷新去重(KV 没有原子操作)。
## 测试
15 → 82 个单测,全部离线零依赖。并做了 10 项变异测试:把每个修复
逐个还原成 bug,确认对应测试真的会变红(避免橡皮图章)。
上一版的测试测的全是「配置项自洽」(如 base 是不是 https),
永远绿,而真实故障一条都没覆盖 —— 这一版按故障现象组织测试。
另用官方 openai-python SDK 对本地运行的 worker 跑了 14 项端到端
(真 HTTP、真 SSE 解析),全绿。
## 破坏性变更
- 模型 ID 有增删:gemini/gemini-1.5-flash、openrouter/auto、
qwen/vision-model 被替换成实际可用的 ID。README 里的
"Qwen3-Max / GLM-5.3 / Kimi-K3" 从来就不存在于代码里。
- 错误响应结构从字符串改成对象。
- deploy.sh 不再用 sed 改 wrangler.toml(旧正则静默失败,退出码
仍是 0,KV id 留在占位符上,每个 Qwen 请求都失败)。
- scripts/auth.js 改用 execFileSync,不拼 shell 字符串。
- package.json 标记 "type": "module",Node >= 20。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这一版的重点不是加功能,而是修掉上一版几个本地测试全绿、线上必挂的问题。
先说结论:上一版的 15 个测试测的全是「配置项自洽」(比如
base是不是https),所以永远绿;而真实发生的故障一条都没覆盖。这一版按故障现象组织测试,
并且每一条都做过变异测试(把修复还原成 bug,确认测试真的会红)。
🐞 四个真 bug,都有复现证据
1. 流式请求根本没有流式
openai-compat分支里stream被硬编码成false:客户端请求
stream: true时,上游收到的是非流式,我们回给它一个普通 JSON。聊天类客户端(Cline、Cursor、各种 Web UI)会一直等一个不会到来的 SSE —— 表现为卡死。
18 个模型里有 13 个走这条路径(GLM / Cerebras / Groq / OpenRouter 全部)。
2. Gemini 完全没有流式
只实现了
generateContent,没有streamGenerateContent。3. 有效的 Qwen 账号被判定为不可用 —— 最致命的一个
账号池判断「token 是否有效」时读的是
acc.token:但账号存进去的字段是
access_token。这个分支永远不会命中,于是每个请求都掉进「拿
refresh_token去换」的分支。两层后果:markFailed判成「今天废了」—— 一个还有一小时寿命的有效 token 会被冷藏一整天。
我构造了一个 token 剩余 3600 秒的账号:
而
qwen/qwen3-coder-flash是DEFAULT_MODEL,也就是默认路径全挂。4. README 声称的「跨 provider 故障转移」并不存在
文档写着 "Auto-failover — when one account is rate-limited, automatically switch to next",
但代码里只有 Qwen 的账号轮换,没有任何跨 provider 回退。
现在真的实现了。候选链按「同模型 → 同 provider 其它模型 → 其它已配置 provider」构建,
只在 429 / 401 / 403 / 5xx 时往下走 —— 400 说明请求本身有问题,
换个 provider 只会白烧另一家的额度,所以不重试。
🔧 另外还修掉
{"error": "unknown model: x"}(字符串),而官方 SDK 按
{"error": {"message": ...}}解析 —— 用户看到的是 SDK 自己的解析异常,而不是真实原因。
messages最后一条。Cline / Cursor每轮都带完整历史,等于每轮都让模型失忆。
read()抛错时直接向上抛,已经读到、还没交付的部分就没了。表现是「模型说到一半突然没了」,断点还是随机的。
认证失败几小时。
✨ 新增
src/providers.js:模型目录与 provider 注册表分离,18 模型 / 6 provider(新增 Groq)。加模型不用再复制
base/type/auth三份配置。scripts/check-docs.mjs+ CI 校验:强制 README 与代码的模型清单一致。上一版 README 标题写 7 个模型、正文表格写 8+、
package.json写 5 个、代码里实际 8 个 —— 四份数字,没有一个对得上。
_proxy字段 +X-IceProxy-Fallback头。/v1/models带context_length/capabilities/available。📊 验证
单测 15 → 82 条,全部离线、零依赖(
node:test)。变异测试:把每个修复逐个还原成 bug,确认对应测试真的会变红 —— 10/10 全部抓到。
没做过这一步的测试很可能是橡皮图章,所以这一步不能省:
stream硬编码access_token→token)[DONE]收尾官方 SDK 端到端(openai-python 2.26,真 HTTP + 真 SSE 解析,14/14 全绿):
models.list、流式/非流式、Gemini 流式、Qwen 账号池、跨 provider 回退、
错误映射(
BadRequestError/AuthenticationError)、多轮上下文。模型 ID 实测校验:OpenRouter 那 5 个免费模型是从
/v1/models抓出来的,不是照印象写的。第一版我凭印象写了
z-ai/glm-4.6:free等 5 个,实测全都不存在 ——这正是上一版 README 那个故障的同一类错误(宣传了不存在的模型),所以单独验了一遍。
gemini/gemini-1.5-flash、openrouter/auto、qwen/vision-model被替换成实际可用的 ID。README 里那批「Qwen3-Max / GLM-5.3 / Kimi-K3」从来就不存在于代码里,已删除。
deploy.sh不再用sed改wrangler.toml。旧正则匹配的是注释行# id = "your_kv_id",格式一变就静默失败(sed退出码仍是 0),KV id 留在占位符上,部署出来的 Worker 每个 Qwen 请求都失败。已改成解析后写回。
scripts/auth.js改用execFileSync,不再手工拼 shell 字符串(旧版对含引号的 JSON 做手工转义,既易错也是注入面)。
package.json标记"type": "module",engines.node >= 20。❓ 需要你确认的一件事
还想加哪家 provider? 现在 6 个是按「免费额度真实可用」挑的。
如果你有某家的 key(Mistral、Together、DeepSeek 直连之类),告诉我 ——
加一个 provider 现在是
PROVIDERS加一条 +MODELS加几行,再把 README 表格同步一下,
check-docs会盯着不让两边漂移。另外:这次没有真实调用任何受限上游(没有你的 key),所以
「协议形状」是用 mock 上游 + 官方 SDK 双向验的;真实上游的额度行为
我没法在这儿验,README 里也刻意不写具体额度数字 ——
那种数字写死了迟早变假信息。