子舒的博客
子舒
子舒
@zishu

把 Trae 桌面端的模型接进任意 OpenAI 客户端

0 评论
把 Trae 桌面端的模型变成任意 OpenAI 客户端都能调的本地 API,顺带记录四个误导性报错的排查过程。

起因

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 事件协议。所以代理要做三件事:

  1. 离线解密本地登录态。只用 node:crypto:硬编码盐 + AES-128-CBC + SHA-512 KDF,不需要安装 Trae 之外的任何东西。
  2. 复刻客户端请求。设备指纹头(machineId / deviceId / appVersion 等)、Cloud-IDE-JWT 鉴权、SOLO 通道的请求体格式。
  3. 桥接 SSE。把 Trae 私有事件流转成标准 /v1/chat/completions 增量,包括 tool_callsusage

有一个刻意的设计:token 刷新只在进程内存里做,绝不写回桌面端文件、不落地副本。代理只读登录态,也不提供账号切换——避免把「读」变成「改」。

运行门槛也压到了最低:Node 22.19+ / 24+,靠原生类型擦除直接跑 TypeScript,不用构建、不用 npm install

踩过的四个坑

这部分才是真正费时间的地方。

一、上游悄悄加了两个必填字段

某天开始,所有对话请求都失败,但表现非常具有误导性:

  • 公开网关:HTTP 200,只回一个 error 事件,4011 requests have exceeded the rate limit(看起来像被限流)
  • 企业网关:HTTP 2004001 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,其他模型全部正常。

排查过程(穷举):

  • functionsolo_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 就是这么逼出来的。

参考与许可

这个项目是站在别人肩膀上的。实现思路和部分代码参考了:

版权与署名见仓库的 LICENSETHIRD_PARTY_NOTICES.md

Github: https://github.com/anghunk/trae-proxy

提醒:本项目只适用于自己已登录的账号,请遵守 Trae 的服务条款,不要用于绕过计费或多人共享。

评论