把 Qoder 的免费 Qwen3.8-Flash 接成自己的 OpenAI 端点
Qoder 是阿里做的一个 AI 编程 IDE,注册就送额度,其中 Qwen3.8-Flash 是免费档(内部倍率 price_factor: 0)。我想要的很简单:把它的模型接到我自己那套 OpenAI 兼容的工具链上,用标准 /v1/chat/completions 调,实现"零成本日常问答"。
这件事最后做成了,但过程中我犯了一个挺典型的错误,值得单独拎出来说。
一、先绕过去:驱动官方 CLI
Qoder 官方有个命令行工具 qodercli。第一版方案很直接——代理收到 OpenAI 请求,转成 prompt 喂给 CLI 子进程,把 CLI 的 stream-json 输出再翻译回 OpenAI 的 SSE 格式。
能跑,但有两个硬伤:
- 慢。 每次请求都要冷启动一个 Node 子进程,实测端到端 9.9 秒,其中 5 秒纯粹是进程启动。哪怕上游首字只要 600 毫秒。
- 拿不到 function calling。 CLI 本身就是个 agent,它的输出是"文本 + 自己执行工具",不是结构化的
tool_calls。接进任何需要工具编排的框架里,工具卡片、审批流程、diff 预览全都没有了。
当时我判断:这是协议限制,忍了。为了让 agent 类场景能用,我还专门做了个 qoder-agent 虚拟模型——把工具挂到 CLI 自己身上,让它自己干完活、只回文本。一个绕行方案,能work但很别扭。
二、白嫖的日常:两个区域加每日签到
在拆签名之前,还有些"薅羊毛基础设施"要先搭好。
Qoder 分国内版和国际版,是两套独立的账号体系、两套额度和两套接口域名。 国际版的额度通常更宽松(我这个号是 Pro Trial),国内版则经常有活动赠送。所以代理做成单端口双区域:默认走国际版,失败自动切国内版——对上层工具链来说只是一个 base_url。
认证链也不一样。 国内版是三层:auth.v1.dat 里的长期凭据换 dt- 开头的中期 token,再换 jt- 开头的 job token(24 小时有效)。国际版是 PAT(pt- 开头)通过 jobToken/exchange 换 job token。两边最后都要拿 job token 去换用户身份,才能拿到签名需要的 uid。
签到是真的能领钱。 每日签到端点会给 100 Credits 左右。一开始我判它无效——因为返回的是"活动已领取"之类的状态。后来发现少了一环:必须带设备身份头(Cosy-MachineToken 那套)。把设备指纹(从 uid 派生的 machineId / machineToken / machineType 三个值,而且每个账号是稳定派生的,不是随机的)补上之后,国际版的可见活动从 1 条变成 2 条,领取成功。
这块现在全自动:每天定时签到,签到结果推微信。余额分两个桶——user_quota 和 add_on_quota——加起来才是可用总额,只看一个会误判。
三、我判错的那堵墙
真正的问题在这儿。
Qoder 的模型接口不是普通 REST,它有一套叫 COSY 的签名协议:请求头里要带 Authorization: Bearer COSY.<base64>.<md5>,还有 Cosy-Key、Cosy-Date 一堆头。我一开始在 CLI 里翻,发现签名逻辑进了一个 WASM 模块,于是得出结论:
签名在 WASM 里,无法用 curl 复刻,只能驱动官方 CLI。
然后我就绕着这堵墙建了一整套架构——双区域兜底、CLI worker 进程池、agent 模式、工具白名单审计……全是在"我过不去这堵墙"这个前提下的补丁。
直到有天我把这句话跟一个开源项目对照了一下:qoder2api-hub(MIT)。人家纯 Python,直连上游,没有 WASM,没有子进程。
我错了。签名根本不在 WASM 里,它就是一个能直接实现的算法。我去 CLI 的 WASM 里找,是因为我想当然地认为"CLI 能发这个请求,那签名一定在 CLI 里"。
COSY 其实是四个动作
- 请求体编码:一个自定义 Base64 变体。标准表先按"尾段/中段/首段"三段轮转,再过一张换过的字母表,
=变成$。 - 会话密钥:随机生成 16 字节 AES key,用服务端硬编码的 1024 位 RSA 公钥 PKCS#1 v1.5 包裹,作为
cosy-key。 - 身份体:把 9 个字段(uid、name、user_type、token…)按键排序紧凑序列化,用
key == iv == 刚生成的 tempKey做 AES-128-CBC 加密,得到info。 - 签名:
md5(payload + "\n" + cosyKey + "\n" + 日期 + "\n" + 请求体 + "\n" + path),注意请求体是编码后的串,path 要去掉/algo前缀。
没了。Node 原生 crypto 模块,两百行。
我把 JS 实现和参考项目的 Python 实现并排跑同一批输入,自定义编码(含 padding 边界)、AES、指纹派生、Base64 往返——逐字节一致。不是"看起来能跑",是对得上。
十分钟的移植,替换掉了我之前所有的绕行设计。
四、两个静默出错的地方
接上直连之后,遇到了两个特别阴的坑,都属于"不报错、但结果是错的"。
坑一:模型 key 用展示名会被静默回落
上游路由模型靠的是内部 key,不是展示名:
| 展示名 | 真实 key |
|---|---|
| Qwen3.8-Flash | qfmodel |
| GLM-5.3 | gmodel |
| DeepSeek-V4-Pro | dmodel |
我一开始把展示名 Qwen3.8-Flash 直接当 key 传。服务端返回 HTTP 200 和一段完全正常的回答。 看起来一切正常。
后来我故意传了个乱串 totally_bogus_key_xyz——还是 200,还是正常回答。
那一刻我才意识到:它在静默回落到默认模型。也就是说在那之前,我以为我在调 Qwen,实际跑的可能根本是别的模型,而且没有任何提示。改用真 key 之后,三个模型才真正区分开:Qwen 自称通义千问,GLM 自称 Z.ai 出品,DeepSeek-Flash 乱报自己是 Claude。
正确的 key 从官方目录接口 /algo/api/v2/model/list 拉,我写了个刷新脚本自动维护映射。顺带发现我们国际版的模型表严重不全——静态表只有 3 个,官方目录实际 17 个。
坑二:请求体骨架不能精简
拿到真 key 之后,我用最小骨架请求,结果 400。
一开始以为是 key 的问题,二分删字段才发现:business 字段是必需的,删掉直接挂;而其他十几个字段(chat_context 里的一堆、is_reply、source、version…)删掉都不影响。
这纠正了我另一个错误认知。我之前一直觉得"骨架里大部分是被覆写的提示词和工具定义,可以裁掉"——协议字段和业务字段得分开看,业务字段能省,协议字段省了就是 400。
附加发现:GET 带 body
拉模型目录这个接口要求 GET 且携带请求体(body 要和签名一致),裸 GET 一律 403 Signature invalid。
问题是:Node 的 fetch 直接拒绝 GET+body,https.request 会静默丢弃 GET 的 body。最后只能用 tls 手写 HTTP/1.1 请求。
手写的时候又踩了个经典 bug:响应是 chunked 编码,chunk 长度是字节数,而响应体里有中文。我先 toString() 再按字符切片,分块边界全切错了,JSON 中间混进了 a1c\r\n 这种分块头,报 Bad control character in string literal。必须在 Buffer(字节)层面解析分块。
五、CLI 通道上那些更隐蔽的坑
既然要留着 CLI 当备路,就得把它调明白。这部分坑特别有代表性——它们全都不报错。
坑一:--allowed-tools 不缩小工具集。 我本想禁用 CLI 的工具(纯文本问答模式),于是照直觉用了 --allowed-tools 传一个空集。结果审计发现:实际可用工具 34 个,一个没少。真正的白名单参数是 --tools,--allowed-tools 只是"允许列表",不构成限制。也就是说我以为我关掉了工具执行能力,实际上它一直开着,能读写文件、能跑命令。在"我只想让它回答个问题"的场景里,这是个不小的意外。
坑二:--system-prompt 是替换不是追加。 我以为传进去是"补充说明",实际它整体替换了 CLI 的基础提示词。这会导致行为明显退化(甚至出现"好的,我来做"然后什么都不做的空转)。要追加得用 --append-system-prompt。而两个一起传时,是"替换 + 追加"依次生效——我写了个变量探针(让模型在响应里回显两个标记词)才确认这个执行顺序,不然光看文档会理解反。
坑三:--permission-mode 的参数值。 文档/提示里出现的是驼峰 bypassPermissions,但实际生效的写法是下划线 bypass_permissions。传错了不报错,只是不生效。
坑四:不加 --include-partial-messages 就没有流式。 这个参数官方文档里没有。不加的话,即使 --output-format stream-json,流事件数也是 0——你会以为"CLI 不支持流式",其实只是少了个开关。
这几个坑的共同点:参数错了不报错,只是静默地不生效或反向生效。 和前面模型 key 那件事是同一类问题。我的应对方式变成了固定的:任何"我已经关掉了 / 我限制住了"的判断,都要主动造一个越界输入去试。 试不出来,就是没关掉。
六、直连之后
同一请求、同一模型、流式对比:
| 通道 | 首字 | 端到端 |
|---|---|---|
| 直连 | 3.7s | 4.9s |
| CLI 子进程 | 4.9s | 9.9s |
端到端快了一倍,差的那 5 秒就是进程冷启动。CN 区域单请求实测可以到 645ms。
另外直连能拿到真实 usage(上游回传 prompt_tokens / completion_tokens / credits),CLI 路径拿不到,只能本地估算。
但我没有删掉 CLI 通道。 直连依赖签名算法和服务端公钥,官方哪天换协议就全线失效;而驱动官方 CLI 是官方组件自己签名,协议怎么改都不受影响。所以两者是主路 + 备路:直连优先,失败自动回落 CLI。我特意注入了故障验证过——强制直连超时后,日志打出 [direct] 失败(直连超时),回落 CLI,然后正常作答。
真 function calling
直连是标准 Chat 语义,tools 原样透传上游,就能拿到结构化的 tool_calls:
- 非流式:
finish_reason: "tool_calls",content: null,tool_calls: [{id, type, function:{name, arguments}}] - 流式:按 index 分片下发
delta.tool_calls,客户端自己拼装(实测 6 个分片拼出完整的get_weather({"city": "上海"}))
也就是说,之前那个 qoder-agent 虚拟模型的绕行方案可以退休了。接进任意 harness 都能正常走工具编排。
七、最后长成什么样
最终这个东西是一个单文件 Node 服务,跑在本地 8493 端口,对外就是一个标准 OpenAI 端点:
POST http://127.0.0.1:8493/v1/chat/completions
Authorization: Bearer <自己设的 key>
{"model": "Qwen3.8-Flash", "messages": [...], "tools": [...]}
内部是三层:签名层(COSY 那两百行)、通道层(直连优先 / CLI 兜底,两条路的返回值结构做成一致的,所以上层完全无感)、协议层(OpenAI 兼容,流式与非流式、tools 与 tool_calls、reasoning_content、usage 都按标准字段给)。
几个具体的工程决定:
- 不用配置文件,用环境变量。
QP_DIRECT=on/off/only一个变量就能切换通道策略,出问题时能秒回退到最稳的那条路。 /v1/models跟官方目录走,不写死。写死的静态表会悄悄过期,还会漏模型(我们国际版就漏了 14 个)。- 日志明确打印走了哪条通道。
[direct] INTL Qwen3.8-Flash ✓ 4388ms——出问题时第一眼看的就是这个。静默降级的教训太深,能打日志的地方绝不含糊。 - 凭据全部 gitignore。profile、token、API key 一律不进仓库,发布前又完整扫了一遍历史。
接进我自己的工具链只需要改一个 base_url 和一个 key,模型列表自动出现 21 个(20 个真实模型 + 1 个 agent 虚拟模型)。日常用下来成本是 0——免费的 Qwen3.8-Flash 打底,签到领的额度覆盖偶尔用到的高档模型。
八、三条经验
第一,报错之前先搜一圈。 我把"签名不可复刻"当成硬约束、围着它建了一整套架构,而社区三个月前就解了。我不是没能力解,是没去找。凡是"这做不到"的结论,都值得先花十分钟查一遍别人做到没有。
第二,区分"能力上限"和"路径上限"。 CLI 不支持 function calling,这是那条路径的限制,不是 Qoder 的限制。我把路径限制当成了能力限制,于是花了大量精力去做绕行方案,而不是换条路。
第三,最危险的不是报错,是静默降级。 用错的模型 key,服务端 200 + 正常回答,你完全不知道自己在跟谁说话。这类问题不会让程序崩溃,只会让你基于错误的前提做一堆正确的事。区分的方法只有一个:主动造一个非法输入,看它会不会报错。 不报错,就说明你的校验根本不存在。
本文对应的项目已开源(私有仓库):yuanguangshan/qoder-proxy。协议细节对齐自 qoder2api-hub(MIT)。