deepseek-harness-trae
fleey/deepseek-harness-trae
仅凭一个个人访问令牌(PAT)把 Trae 企业版模型接入 DeepSeek Harness,直连 llm_raw_chat,不需要 trae2api 或 traecli 进程;启动时从上游拉取模型目录,并显示个人额度包与企业席位的剩余额度。
安装
dsh plugin --profile web add github:fleey/deepseek-harness-traeREADME
dsh-trae2api
一个 DeepSeek Harness(dsh)插件:在 Desktop / Web / CLI 里直接用 Trae 企业版的模型。 只需要填一个 Trae 个人访问令牌(PAT),其它什么都不用配。
v0.3 起插件自己直连 Trae 的 llm_raw_chat 接口(和 trae2api 的 direct 模式同一套协议),
不再需要 trae2api 进程、不需要 Python、不需要 traecli。
插件相关功能界面
当选择trae模式是聊天支持显示trae额度
模型配置只需要添加traecli令牌继续使用,配置上企业额度可实时读取企业额度信息,支持tob与toc额度显示(toc无需登录企业账号即可获取额度)
它做了什么
-
用你填的 PAT 向
https://api.enterprise.trae.cn/cloudide/api/v3/trae/oauth/ExchangeToken换一个 Cloud-IDE JWT(和traecli login --with-trae-pat同一套流程),快过期时自动再换。 -
注册一个叫 Trae 的模型来源,直接走
/api/ide/v2/llm_raw_chat:流式正文、流式 thinking、 function calling、token 用量都支持。 -
模型目录是问来的,不是写死的。Trae 会随时下线模型(2026-10-16 就一次下掉了四个), 写死的目录过期后那些模型在选择器里还在,一发就是
4023 the model is unknown。所以每次 插件启动(重载插件 = 重新拉一次)按这个顺序取:- 上游
POST /api/ide/v1/batch_get_detail_param—— traecli 自己也走这个,请求体就是 traecli 二进制里内置的那份。它是所有事实的权威:有哪些模型、两个后端 key、两个上下文 窗口、每个变体的输出上限、Max 是不是真的、能不能吃图、思考档位及默认档。 - traecli 缓存
~/.trae/cli/models_cache.json—— 不走网络,但只有 traecli 跑过才会更新; 它的input_modalities对每个模型都写["text","image"](Codex fork 的常量,不是能力), 所以能不能吃图这一项取插件内置表里的标记。 - 插件内置表(
src/trae/catalog.ts,20 个模型)—— 兜底,保证断网也有选择器。 已于 2026-10-16 和上游对齐过。
拿到的目录里:14 个模型直接按 1M 上下文(Max)提供,不再额外列小窗口的版本;剩下 6 个 用各自的标准窗口(Doubao 系 184K/200K,其余 200K)。思考档位在对话框模型菜单里选, 词表是
light / high / extra_high(界面上的 轻 / 高 / 极高)。想看插件现在用的是哪一份:
GET /api/dsh-trae2api/catalog会返回模型列表、来源 (upstream/cache/builtin)和取到的时间;加?refresh当场重新问一次上游。 刷新失败不会清空目录,继续用上一次拿到的那份。 - 上游
-
内置了 trae2api 里那几道兜底:工具调用历史修复(避免
4027)、把模型写成文本的<invoke>工具调用捞回来、重复塌缩截断、空回合重试、上游错误码翻译。
安装到 Desktop
-
打开一次 DeepSeek Harness Desktop(初始化
desktopprofile),然后完全退出。 -
终端里(
dsh命令可在 Desktop 菜单「管理 dsh 命令…」注册):dsh plugin --profile desktop add /Users/leeyf/Desktop/trae/dsh-plugin也可以在侧栏 插件 → 添加插件 里填这个目录的绝对路径。
-
重新打开 Desktop,去 设置 → 模型 → Trae,把 PAT 填进去、保存。 卡片底部的 连接状态 会显示是否连上、有哪些模型。
然后在对话框右下角的模型菜单里选 Trae 分组下的模型,思考档位在同一个菜单里切换。 选中 Trae 模型时,模型选择器旁边还会出现一个剩余额度小徽标(详见下文「额度显示」)。
卸载:dsh plugin --profile desktop remove dsh-trae2api。
用 tgz 安装(发给别人)
dsh-trae2api-0.4.0.tgz 是预构建好的包,不需要构建权限:
dsh plugin --profile desktop add ./dsh-trae2api-0.4.0.tgz
装完同样去 设置 → 模型 → Trae 填 PAT。tgz 方式下插件自带全部代码,和项目目录无关。
PAT 从哪来
在 Trae 企业版控制台生成个人访问令牌(Personal Access Token)。它只保存在 dsh 的凭据库
(~/.dsh/.credentials.yaml,凭据名默认 TRAE_PAT),不会写进任何配置文件,保存后即时生效。
如果你手上已经有一个现成的 Cloud-IDE JWT(例如
~/.trae/cli/auth.json里的access_token), 也可以直接填进 PAT 框——插件识别出是 JWT 就原样使用,过期后需要你自己更新。
配置项
界面里只有 PAT 一个字段。其余都是写死的默认值,只有在 Trae 以后改了接口、而插件还没更新时,
才需要在 profile 的 cordis.patch.yml 里覆盖 id: trae2api 这一行:
| 字段 | 默认 | 说明 |
|---|---|---|
patEnv | TRAE_PAT | PAT 在凭据库里的名字 |
host | https://api.enterprise.trae.cn | Trae 接口地址 |
clientId | app_EMoamEEZ73f0CkXaXp7hrann | PAT 兑换用的 OAuth client id(traecli 的) |
appId / ideVersionCode / clientVersion | 20260908 那一代 | 身份头,和模型名后缀(__dev)配对 |
catalogSource | auto | 模型目录来源:auto = 上游 → traecli 缓存 → 内置;也可锁成 upstream / cache / builtin |
modelsCacheFile | 空 | traecli 模型缓存路径;空 = ~/.trae/cli/models_cache.json |
consoleEmailEnv | TRAE_CONSOLE_EMAIL | 控制台邮箱在凭据库里的引用名(卡片写这里) |
consoleEmail | 空 | 控制台邮箱的配置兜底(凭据库里没有时才用) |
consolePasswordEnv | TRAE_CONSOLE_PASSWORD | 控制台密码在凭据库里的引用名 |
consoleHost | https://console.enterprise.trae.cn | 企业控制台地址 |
readTimeoutMs | 300000 | 上游两次数据之间的最长间隔 |
emptyRetries | 1 | 空回合/瞬时故障的额外重试次数 |
loopGuardChars | 600 | 重复塌缩截断的触发长度;0 关闭 |
suggestions | true | 回答完后在输入框里给出下一句建议(Tab 填入);false 关闭 |
suggestModel | 空 | 生成建议用的模型;空 = 用刚回答的那个模型 |
suggestTimeoutMs | 20000 | 生成一条建议的最长等待 |
reportCue | true | 把建议的「显示 / 采纳」上报到 Trae 的 Cue 统计(见下文);false 关闭 |
cueUserId | 空 | 覆盖上报用的 Trae user id;空 = 环境变量 TRAE_USER_ID,再空 = 自动从 GetUserInfo 取 |
⚠️ 模型名后缀(
__dev)和ideVersionCode(20260908)是一对,必须同代。Trae 升级后若 大量模型报4023/4001,说明身份头过时了,需要更新插件(或临时改这几个字段)。 单个模型报4023基本只有一种情况:目录没刷新到。重载一次插件,或GET /api/dsh-trae2api/catalog?refresh;catalogSource: builtin锁死在内置表时也会这样, 那就换回auto。2026-10-16 被下线的是 Qwen3.7-Plus、GLM-5V-Turbo、Kimi-K2.7-Code、 DeepSeek-V4-Pro(DeepSeek-V4-Pro 正式版还在),上游把它们标成is_invisible_to_user。
额度显示
插件会向 Trae 查询剩余额度,显示在两处:
- 设置 → 模型 → Trae 卡片里有一块「剩余额度」:显示每个额度包的已用/剩余/到期, 总量、美元计费标记,以及一个「刷新」按钮。
- 对话框右下角(模型选择器旁边)有一个小徽标,选中的是 Trae 模型时显示剩余百分比, 点击即刷新;其它来源的模型选中时不显示。
实现方式:插件在宿主端通过登录态隔离开的 /api/dsh-trae2api/quota 路由供 UI 查询,
带一分钟缓存,?refresh 强制刷新。后端流程是先向企业 API 的
/cloudide/api/v3/trae/CheckLogin 问 GetAIPayHost:true 拿到真正的支付网关地址,
再查 v1(企业,basic_usage_limit)→ v2(个人,credits_limit)两条
/pay/ide_user_ent_usage 路径,哪条先通用哪条,两边字段都兼容。limit: -1 按不限量处理;
额度包 basic_usage_limit: 0(免费档)显示为「当前账号没有额度包」。
空额度包列表不再当成结论:某个地址返回 0 个额度包时会继续问剩下的地址,只有全都没有额度包
才显示「当前账号没有额度包」,并附上回答这句话的 host + path。
企业版(ToB)席位:走控制台账号
用企业版 PAT 实测过(2026-10-13):IDE 那套额度接口是 CN 个人版的,拿不到企业席位的额度。
CheckLogin不给AIPayHost(Host: ""),所以一定走兜底api.trae.cn;api.enterprise.trae.cn上两条路径都是 openresty 的裸 404,网关层就没这条路由;api.trae.cn两条都 200,但只返回这个 user_id 的个人免费档:product_type: 0、display_desc: "免费"、entitlement_id: "free_utc…",所有数字额度字段都是 0 (v2连数字字段都不返回,只剩enable_solo_*布尔开关)。
席位的真实用量记在企业租户上(JWT 里的 tob_tenant_id、tenant_id: "tob_online"),
只有企业控制台的接口给得出来(src/trae/tob.ts):
| 接口 | 给什么 |
|---|---|
POST /trae/gtm/tob/api/v1/config/get_personal_quota | user_model_quota.seat_pool_currency_quota(席位额度)、paygo_currency_quota(超额/按量) |
POST /trae/gtm/tob/api/v1/config/get_user_model_usage | user_usage_list[].in_seat_detail.chat_usage_details[]:每个模型的 total_token_usage 与 total_cost_currency;model_usage_detail_list[] 是按量那侧 |
seat_total_cost_currency / extra_total_cost_currency 实测恒为 null,所以已用量是把
每个模型的 total_cost_currency 相加得出的。
认证只有一条路:这两个接口只认控制台的会话 cookie X-Cloudide-Tob-Session。Cloud-IDE JWT
一律回 30011 请先登录,account/check_token 换不到会话,在控制台域名上做 ExchangeToken
也不 Set-Cookie。所以插件用控制台自己的密码登录(POST /trae/gtm/tob/api/v1/account/login,
{Email, Password})拿会话,会话过期自动重登一次。SSO 登录的租户用不了这条路。
界面上因此分两种账号:
- JWT 带企业租户(
tob_tenant_id)→ Trae 卡片多出「企业控制台账号」一栏(邮箱 + 密码)。 邮箱和密码都写进 dsh 凭据库(默认引用名TRAE_CONSOLE_EMAIL/TRAE_CONSOLE_PASSWORD), 配置文件一个字都不碰——保存这对账号因此不依赖配置可写;consoleEmail配置项只作兜底。 凭据写失败时,卡片直接把宿主返回的原因显示在字段下面,而不是只说「保存失败」。填好后额度块显示「席位额度 / 超额(按量)」两个包、总剩余百分比,外加一个可展开的 按模型用量(花费 + token 数)。没填时显示「企业版席位:填下面的控制台账号才能看到额度」。 - 个人版账号 → 完全走原来的
ide_user_ent_usage,不显示控制台登录栏。
要重新验证或找新接口:
TRAE_PAT=… TRAE_CONSOLE_EMAIL=… TRAE_CONSOLE_PASSWORD=… python3 -I tools/probe_quota.py
(把 IDE 两条路径在各个地址、两套请求头下的原始响应,以及控制台的两条接口全打出来。)
模型目录那两条接口同样有个探针,拿 ~/.trae/cli/auth.json 里的会话直接问:
python3 -I tools/probe_catalog.py
输入建议(Tab 填入)
Trae 模型回答完一轮后,输入框为空时会用灰字显示一条猜你下一句要说的话(取代原来的占位提示),
右边带一个 Tab 小标记:
- 按 Tab:把建议填进输入框(可撤销),可以接着改,再回车发送。
- 直接打字:建议消失,这一轮不再出现。
- 发送新消息 / 模型开始工作:建议清掉,等下一轮回答完再出。
实现方式:宿主端的 adapter 在一轮主循环请求(带工具的那种)以纯文本回答收尾时,用同一个模型、
最低思考档位、不带工具、独立的上游 session 发一个很小的补全请求——只喂最近 3 条用户消息和这轮
的最终回答——让它以用户口吻写一句话。结果按会话存在内存里,经 /api/dsh-trae2api/suggestion
(同样走登录态隔离的 Connection 路由)给浏览器端;浏览器端在会话由运行转为空闲时去取,
生成还没完成时最多等 20 秒。标题生成、上下文压缩这类不带工具的旁路请求不会触发建议。
每轮回答会多花一次很小的请求额度。不想要可以在
cordis.patch.yml里给id: trae2api设suggestions: false,或用suggestModel指定一个更便宜的模型。输入框目前没有给插件留「灰字建议」的接口,这里是借
conversation.input.overlay插槽找到 输入卡片、藏起原占位文字、在同一位置放一个同样式的元素,并在卡片上以捕获阶段拦截 Tab。 以后 DSH 改了输入框结构,找不到占位元素时这个功能只会静默不显示,不影响正常输入。
计入 Trae 的 Cue 统计(默认开启)
默认无需任何配置:建议真的显示出来时报一条 code_gen_shown,真的按了 Tab
时报一条 code_gen_accept,走 Trae 补全客户端用的同一个业务上报接口:
POST {host}/api/ide/v1/report/clients (multipart,字段 body = JSON 字符串)
Authorization: Cloud-IDE-JWT <和 llm_raw_chat 同一个 token>
X-App-Id: <appId>
- 同一条建议的 shown / accept 共用一个
resp_id(每条建议生成时的 UUID),conversation_id是这条会话发给llm_raw_chat的session_id。 - 每条建议 shown、accept 各最多一次,去重和「显示了多久」(
suggestion_visible_duration)都在 宿主端算,浏览器只说「显示了 / 采纳了哪一条」;过期的建议 id 一律不报。 user_id是 IDE 登录态里的账号 id(客户端User.idOrDefault),JWT 里没有。插件和 IDE 登录时 一样调POST {consoleHost}/cloudide/api/v3/trae/GetUserInfo(请求头x-cloudide-token,空 body), 取Data.UserInfo.UserID,每个 JWT 只问一次。cueUserId/TRAE_USER_ID只是覆盖这个 id; 不要填租户 id(TenantID)或 TEA 的 device id / 匿名 uuid。- 同一个回答里的
Data.PrivacyMode打开时,什么都不报(和 IDE 的隐私模式闸门一致),配了cueUserId也一样。GetUserInfo 失败时这一条不报,只在日志里记一行。 code_gen_shown的 payload 只带resp_id/type/conversation_id,字段是按 accept 的结构推的, 未对照真实客户端抓包。
不想上报(例如把 tgz 发给别人、对方不希望计入)时,在 profile 的 cordis.patch.yml 里关掉:
- id: trae2api
config:
reportCue: false
注意:这些是聊天里的「下一句建议」,按
code_gen_*报上去后会和真正的代码补全混在一起计数, 企业控制台里的 Cue 用量 / 采纳率会因此变化。
已知限制
- 图片仅限多模态模型与 user 消息:图片以
data:URI 内联发给目录标记multimodal: true的模型(20 个里 15 个,GLM-5.3/GLM-5.2/DeepSeek 全系/MiniMax-M2.7 除外);纯文本模型与 tool 结果里的图片仍是占位文本(上游接口只在 user 消息接受image_url,发给纯文本模型会被1210拒绝还伪装成限流)。读取失败的单张图片降级为 占位文本而不是让整轮失败。图片按每边 1024px / 2MiB 预算由附件服务缩放。 - 这是 Trae 未公开的内部接口,后端改协议插件就会断——和 trae2api 的 direct 模式同样的前提。
- Max 的实际效果未经证实:trae2api 实测这条私有接口对
context_window_size和__maxkey 没有可观测差异(两者extra_info.model相同)。路由是真的(15 个模型的4023就是靠它修好的), 但「真能吃下 1M」没被实测过——目录里标着 Max 的那 14 个模型 dsh 会按 1M 规划上下文,万一后端 不认,很长的会话可能在上游被截断。真遇到了把catalogSource锁成builtin,再改src/trae/catalog.ts里那个模型的max。
开发
npm install # 仅用于类型检查 / 重新构建
npm run typecheck
npm run build # 产出 lib/index.js(宿主端)与 lib/client.js(浏览器端,无需编译)
npm run build:test # 产出 lib/testing.mjs,供单元测试导入内部纯函数
npm test # 目录、消息转换、SSE 解码、重复检测、文本工具调用、PAT/JWT 解析等
源码:
src/index.ts—— 宿主端:注册模型来源和 Models 页的 Trae 行。src/adapter.ts—— dshLlmAdapter,组装 llm_raw_chat 请求、解流、重组工具调用、兜底。src/trae/auth.ts—— PAT → Cloud-IDE JWT 兑换与缓存。src/trae/catalog.ts—— 目录的数据结构,外加断网兜底用的 20 个模型(其中 14 个走 Max)。src/trae/catalog-source.ts—— 运行时目录:上游batch_get_detail_param与 traecli 缓存的 解析、三级兜底、每次插件启动刷新一次的TraeCatalog。src/trae/wire.ts—— 消息转换、SSE、错误分类、重复检测、文本工具调用嗅探。src/trae/quota.ts—— 额度查询:CheckLogin 发现支付网关、v1/v2 兜底、宽容解析、企业席位判定。src/trae/tob.ts—— 企业版额度:控制台密码登录 + 个人配额/按模型用量两条接口。client/client.js—— 浏览器端:Models 页和插件页里的 PAT 卡片、额度块、输入框额度徽标。
已在 @deepseek-ai/dsh@0.2.0-rc.2 的 Web 界面里验证:Trae 卡片只有 PAT 一个字段,未填时
连接状态显示明确提示。24 项单元测试覆盖协议转换、各项兜底、额度解析与路由注册,以及出货
bundle 里每个模型的上下文窗口和思考档位。真实 PAT 兑换、模型调用与额度查询已在
api.enterprise.trae.cn 环境用真实 PAT 验证通过。