起因
Trae 桌面端登录后能用一批模型,但只能在它自己的界面里用。我想在别的工具里也调这些模型——不想再单独买 API key,也不想把凭据复制来复制去。
于是就有了 trae-proxy:一个纯 Node、零第三方依赖的本地代理,把 Trae 桌面端已登录的模型暴露成标准的 OpenAI 兼容 API。任何支持 OpenAI 协议的客户端、IDE 插件、SDK、CLI,填上 baseURL + apiKey 就能用。
国内版(cn) http://127.0.0.1:39303/v1
国际版(ai) http://127.0.0.1:39304/v1
一个进程同时服务两个区域,各自用独立的持久 bearer key 做本地鉴权。
整体思路
Trae 桌面端的登录态是加密存在本地 storage.json 里的,对话走的是私有 SSE 事件协议。所以代理要做三件事:
- 离线解密本地登录态。只用
node:crypto:硬编码盐 + AES-128-CBC + SHA-512 KDF,不需要安装 Trae 之外的任何东西。 - 复刻客户端请求。设备指纹头(machineId / deviceId / appVersion 等)、
Cloud-IDE-JWT鉴权、SOLO 通道的请求体格式。 - 桥接 SSE。把 Trae 私有事件流转成标准
/v1/chat/completions增量,包括tool_calls和usage。
有一个刻意的设计:token 刷新只在进程内存里做,绝不写回桌面端文件、不落地副本。代理只读登录态,也不提供账号切换——避免把「读」变成「改」。
运行门槛也压到了最低:Node 22.19+ / 24+,靠原生类型擦除直接跑 TypeScript,不用构建、不用 npm install。
踩过的四个坑
这部分才是真正费时间的地方。
一、上游悄悄加了两个必填字段
某天开始,所有对话请求都失败,但表现非常具有误导性:
- 公开网关:HTTP 200,只回一个 error 事件,
4011 requests have exceeded the rate limit(看起来像被限流) - 企业网关:HTTP 200,
4001 the param is invalid(看起来像参数写错)
实际原因是上游协议更新,请求体新增了 request_id / session_id 两个必填字段,老客户端不传就被拒。而且服务端用 HTTP 200 + error 事件来报错,状态码完全帮不上忙。
修法很简单——逐请求生成两个 UUID(去掉横杠)。但从「限流」这个误导性错误码反推到「缺字段」,绕了不少路。
二、企业版账号必须走企业网关
我用的不是私有部署,是购买的企业版 SaaS 套餐。这类账号的 storage.json 里会有一个 iCubeHostInfo:
"iCubeHostInfo": {
"consoleHost": "https://console.enterprise.trae.cn",
"apiHost": "https://console.enterprise.trae.cn"
}
企业版账号在公开网关上会被拒(就是上面那个假的 4011)。所以代理的做法是:
每个请求实时读取当前登录态的
iCubeHostInfo,自动选路——探测到企业网关就整体切过去(对话和模型目录都是),普通账号回落公开网关。
好处是切换账号不用重启进程。这也是为什么文档里要同时说明普通版和企业版都支持。
三、模型目录是分层的,不是一个接口
Trae 的模型不是从单一接口拿的,而是分散在多个 directory function 下。代理的策略是按优先级 union 所有能拉到的结果,first-wins——同一个模型出现在多个 function 里时,第一个命中的决定调用时该带哪个 function 字段。
因此一个很实际的问题:CN 区最初漏了 solo_agent 这个目录,导致客户端里有、代理里没有几个模型。把 solo_agent 追加到目录列表末尾(借 first-wins 保证不影响已有模型)后,一次性多出 4 个可用模型。
四、客户端能选的模型,服务端不一定有
这是最反直觉的一个。
现象:DeepSeek-V4.1-Flash 在 Trae 客户端里能正常选择、能正常生成,但走代理转发就稳定报 4001 the param is invalid,其他模型全部正常。
排查过程(穷举):
- 换
function:solo_work_lite/solo_work_remote/solo_agent/chat_v3/builder_v3/builder—— 全部 4001 - 换网关:企业网关目录 42 个模型、公开网关目录 73 个模型 —— 两个都没有这个模型
- 换版本号 /
agent_type/mode_type组合 —— 目录数量不变 - 换模型名写法:
__dev、__max、全小写、-Official—— 全部 4001
最后换到客户端 builder 用的 create_agent_task 通道,服务端终于给出了直白的错误:
config item is empty for config opt:
{"Function":"solo_agent","ConfigName":"DeepSeek-V4.1-Flash",...}
服务端配置表里就没有这个模型。
那客户端为什么能用?因为 Trae 桌面端的 native 二进制内置了一份 fallback 模型表。客户端本地缓存里确实有 DeepSeek-V4.1-Flash,但它缺少服务端下发时才有的 encrypted_model_params 字段——说明它来自本地内置表,而不是网关下发。客户端自己也清楚这一点,日志里会回退:
legacy recent selection unresolved before fallback
{"legacyModelKey":"1_-_DeepSeek-V4.1-Flash","fallbackApplied":true}
结论:这是客户端「内置了模型」和「网关服务端还没开通」之间的时间差。代理侧无能为力,只能等后台开通。
一点反思
这个项目里最花时间的不是写代码,而是从误导性的错误码里反推真实原因。HTTP 200 + 业务错误事件、假的「限流」、客户端有服务端没有的模型,这几件事凑在一起,每一步都能把人引到错误的方向。
我的经验是:当同一个请求「状态码正常但内容不对」时,不要相信错误信息本身,要去构造对照实验——固定其他变量只换一个,看哪个变量真正改变了结果。上面那条 config item is empty 就是这么逼出来的。
参考与许可
这个项目是站在别人肩膀上的。实现思路和部分代码参考了:
版权与署名见仓库的 LICENSE 与 THIRD_PARTY_NOTICES.md。
Github: https://github.com/anghunk/trae-proxy
提醒:本项目只适用于自己已登录的账号,请遵守 Trae 的服务条款,不要用于绕过计费或多人共享。
评论