Skip to content

About

TRAE 的 OpenAI 兼容反向代理:多账号轮转、自动签到、token 自动刷新,纯 Go 零依赖。

Resources

Stars

48 stars

Watchers

0 watching

Forks

Repository files navigation

Traework2API Panel

Traework2API Panel

把 Trae 免费对话通道包成 OpenAI / Anthropic 兼容 API 的多账号网关 · 附 Web 管理面板
三协议入站 · 多账号轮转 · 自动签到 · Token 保活 · 会话粘性 · 截断自动续写 · 断流四条防线 · 单二进制 · 零第三方依赖

Go API Deploy License


本项目是 Sliverkiss/traework2api 的延续分支。 源头仓库已下线、不再可访问,本仓接手后独立演进;对上游的同步止于其下线前最后一次可取的代码。 血统、致谢与只作参考的同类实现见 血统与致谢。

Important

本仓的源头 Sliverkiss/traework2api 已下线:原仓库连同代码、issue 与提交历史整体消失,不再可访问。本仓属于那次下线之后延续下来的分支,链路为 Sliverkiss/traework2api → 本仓,中间没有别的维护者,也不存在官方支持与后续保障。

本仓没有可跟随的基线仓,Trae 协议事实的来源必须说清:取自公开的同血统实现(见血统与致谢),并结合作者在自有设备、本人账号上对官方客户端的抓包分析印证。凡标注「实测」的结论都有对应的抓包或真号回执,未实测的会明写「未证实」(如国际版计费、请求体加密能否复刻)。

维护方式披露:本仓的 issue / PR 处置、代码审查、测试与修复实现由 Ekko Studio + Hermes Agent(AI 编码 agent)辅助完成,合并与发布决策由维护者本人作出。请知悉后再使用或参与贡献。

关于作者与维护节奏:本仓作者现实身份是一名在读大学生、摄影爱好者,也是玉林市公共汽车公司的公交迷。因家庭经济困难,周末需兼职于玉林市某公司从事相关工作,业余还有自己的兴趣与生活。这不代表作者有全部精力投入维护——本项目是兴趣驱动,更新随缘、响应不定,可能出现较长时间没有提交;也请不要把它当作有 SLA 的商业项目。作者没有跑路,只是精力有限,看到的问题会在有余力时处理。

尽管时间有限,作者仍保有一部分精力用于维护本项目——在自己的电脑上对相关请求做抓包分析(自有设备、本人账号),据此定位并修复接口变化。感谢源头仓作者与同血统实现的维护者,在协议事实无从考证时仍把这些结论公开出来;也感谢字节跳动 Trae 提供的相关服务。

风险自评:上游随时可能收紧导致通道不可用;因使用非官方客户端导致的账号风控 / 封禁,由使用者自行承担。

Warning

郑重提醒——这段话针对两类人:现实中与作者同校的使用者,以及拿本项目搭建对外中转站、做批量小号 / 售卖额度等恶意滥用的人。

作者无法追踪、也不会去追踪任何人的行为,不对任何人行使监控。但必须把话说清楚:上述用法严重违背上游项目作者与维护者的初衷,也是消耗本仓作者维护动力的最大来源——本项目免费、开源、由一名学生用课余时间维护,不是给任何人拿去转卖的资源。

如果哪一天这个项目确确实实让作者心力憔悴、不再有动力维护,本仓也会像上游那样停止维护,且不另行通知。上游的今天,可能就是本仓的明天,请自重。

同样的底线也适用于同校使用者:若有人出于一己私利,或以作者无法理解、也不知情的行为,把本项目的情况反映到学校信息中心、作者所在的二级学院,乃至学院院长与校方其他上级领导,藉此换取奖学金等个人好处,本仓也会同时停止维护。这不是赌气——接受核查、说明与后续处理所占用的时间与心力,已经超出本仓能承受的维护成本。

项目简介

Traework2API 是一个自托管的 OpenAI 兼容反向代理网关,把 Trae 的免费对话通道(llm_utils_chat + function=solo_work_lite)包装为统一服务。入站支持 chat_completions(POST /v1/chat/completions)、codex_responses(POST /v1/responses)与 anthropic_messages(POST /v1/messages,同样接受 POST /messages)。

  • 官方不提供 OpenAI 形态的开放 API,本项目通过浏览器登录换票(面板「添加账号」或 login.sh)获取账号凭证,在网关侧做 token 自动刷新、账号池调度与流量治理;
  • 面向个人多账号场景:积分加权选号、单号故障自动换号、冷却 / 熔断防止雪崩、会话粘性保证多轮上下文不跳号;
  • 对客户端暴露 chat_completions、codex_responses 与 anthropic_messages 三种协议,先归一成 chat 请求再走同一条链路(切号、冷却、会话粘性、提示词改写、出站改写),上游仍是 Trae chat SSE。

只引三个第三方库,全在出站伪装这一层:refraction-networking/utls(TLS 握手 ClientHello 指纹)、andybalholm/brotli 与 klauspost/compress/zstd(响应解压,utls 的传递依赖)。业务逻辑仍是纯标准库。

⚠️ 合规须知:本项目是非官方网关,使用 Trae 账号作为上游,仅限本人授权账号、本机 / 私有环境测试。详细边界见安全与合规。

核心能力

能力 说明
🔌 三种入站协议 POST /v1/chat/completions(OpenAI chat)、POST /v1/responses(Codex;wire_api="chat" 已在 Codex 0.84+ 移除,只认 Responses)、POST /v1/messages 与 POST /messages(Anthropic Messages,含 x-api-key 鉴权)。三者都先转成 chat 请求再走同一条链路,上游仍是 Trae chat SSE;另有 GET /v1/models
🔄 多账号池 积分加权挑选(闲置补偿 + 快过期优先)、在途限额、熔断 / 降权 / 计划冷却、401 连续 3 次才禁用(一次抖动不杀号)、请求级轮转
🛡️ 流内错误分类 HTTP 级与 event:error 共用一张表:1005 权益不足 → 12h 计划冷却;4008 / 限流文案 → 软冷却退避;401 / 429 / 404 / 5xx 各归各类;4001「model config is empty」一类模型 / 参数问题不罚号——它不是账号的问题,记账只会把好号推向熔断
✂️ 截断自动续写 SOLO 单次输出有硬上限(按变体不同:32000 / 16000),而 done 仍假报 finish_reason:"stop"。网关识别「本段撞了该模型的 cap 且是纯文本」后用「已输出内容作 assistant + 续写指令」补发同模型请求,帧接进同一条流:客户端只看到一个 stop、正文连续,usage 跨段求和
🧵 断流四条防线 首帧 1.5s 心跳(客户端 3~4s 会自己断连)、心跳不堵换号、已提交流的错误降级成协议内 error 帧、传输层断链先补 finish_reason=length 再上抛;另有「停在预告句上」的救援续写(等用户拍板时不救)与非流式同口径。判据与边界见下文
⏰ 定时任务 多时点自动签到(schedule.checkin_hours)、周期余额刷新、多时点无条件全账号刷新 token(不看剩余有效期,refreshToken 轮换落盘)、手动批量签到(signin.sh)与积分查询(credit.sh),各类独立开关
🧲 会话粘性 同一会话的多轮请求粘同一账号(命中上游 prompt 缓存),失败自动解绑
📊 可观测 按「时间片 × 账号 × 模型」记请求数 / 失败数 / token / 延迟的调用用量台账,面板可查;每请求一行表格日志(TTFB / token / 积分 / 出站形态)
💬 系统提示词改写 passthrough / custom / append 三态,出站前替换或追加
🔑 登录闭环 login.sh 自生成登录链接 → 浏览器登录 → 粘贴回调链接 → 换 token 落盘(面板「添加账号」同一流程;Trae 只认它自己的本机回调,见下)
🖥️ Web 管理面板 内嵌单页面板(明暗主题),账号运维 / 模型档位查询 / 在线改配置(热生效)/ 运行日志 / 用量台账,见 管理面板

快速开始

Docker

# 1. 准备凭证目录(放 trae-*.json)
mkdir -p auths data

# 2. 配置(Bearer 鉴权用的 api_key 在 config.json;面板「配置」页可看可改)
cp config.example.json config.json
# 编辑 config.json,给 api_key 填一个自己的随机密钥(留空 = 不鉴权,仅本机可访问)

# 3. 启动
docker compose up -d --build

# 4. 验证
curl http://127.0.0.1:7864/healthz          # → ok
curl http://127.0.0.1:7864/v1/models         # → 模型列表
curl http://127.0.0.1:7864/status            # → 账号状态

# 5. 对话
curl -X POST http://127.0.0.1:7864/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $KEY" \            # KEY=$(jq -r .api_key config.json),或在面板「配置」页复制
  -d '{"model":"glm-5.2","messages":[{"role":"user","content":"你好"}]}'

# Codex Responses(流式,Codex CLI 走这条)
curl -N http://127.0.0.1:7864/v1/responses \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"glm-5.2","input":"你好","stream":true}'

# Anthropic Messages(官方 SDK 用 x-api-key)
curl http://127.0.0.1:7864/v1/messages \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"model":"glm-5.2","max_tokens":64,"messages":[{"role":"user","content":"你好"}]}'

登录流程

Trae 登录页强制回调 127.0.0.1,但浏览器与服务器不需要同机: login.sh 自生成登录链接,登录成功后你把地址栏回调链接粘回去即可。 (同一件事在面板里也能做:点「登录账号」→ 粘贴回调 → 换票落盘,见「管理面板」。)

# 1. 在服务器(或任意机器)运行 login.sh
./login.sh
#    → 打印登录链接(带 127.0.0.1 回调 + 新的 machine/device id)

# 2. 用浏览器打开链接登录(手机号/验证码)
#    登录成功后浏览器跳到打不开的 127.0.0.1 地址

# 3. 复制浏览器地址栏的完整回调链接,粘贴到 login.sh
#    → 解析 refreshToken/userInfo → ExchangeToken 换 token → GetUserInfo 拿 uid
#    → 落盘 auths/trae-{uid}.json → 自动签到 + 查积分 → 重启容器加载新账号

ClientID 与账号强绑定,别改(2026-10-15 实测):refresh token 是按 ClientID 铸的, 换票时两者必须同档,跨档一律 10101 无效参数(2×2 全交叉矩阵在两个 host 上、正序倒序各跑一遍, 结果一致;BoundDeviceID 也分档)。本仓所有账号都是 en1oxy7wnw8j9n(对话档)铸的, 所以 upstream.ClientID / login.sh 的 CLIENT_ID / 登录页 client_id 三处必须保持一致, 任意一处漂到 IDE 档的 ono9krqynydwx5 都会让全部已有账号换票失败。 该档位本身健在:GetPCAuthCode 用对话档与 IDE 档都返回 200,只有伪造的 client_id 才 400 Invalid client.(对照有区分力)。

本机运行(非 Docker)

# 依赖 Go 1.22+
go build -o tw2api ./cmd/server
./tw2api          # 监听 :7864,auths/ 目录读取凭证;密钥在 config.json 的 api_key

config.json(参考 config.example.json)首次运行自动生成,字段分组与 workbuddy2api-panel 对齐(同段同键,两个面板可以对着读):

段 作用
listen / api_key / auth_dir / state_file 服务与凭证位置
default_model 请求缺 model 时的默认模型(Trae 扩展,wb 无此键)
cooldown plan_credit(1005 硬冷却,Trae 扩展)、soft_rate + soft_rate_max(限流指数退避封顶)
schedule checkin_hours / keepalive_hours 时点数组 + *_enabled 开关,balance_refresh_* 周期刷余额
upstream 超时三元组(短请求 / 首字节 / 流内空闲)+ user_agent / client_version 覆盖
pool 在途限额、熔断、降权、闲置补偿、快过期窗口
session_sticky 会话粘性开关 / TTL / GC 周期
prompt 系统提示词改写模式与文件

全部项可用 env 覆盖:TW2A_LISTEN / TW2A_AUTH_DIR / TW2A_STATE_FILE / TW2A_DEFAULT_MODEL / TW2A_PLAN_CREDIT / TW2A_SOFT_RATE / TW2A_SOFT_RATE_MAX / TW2A_CHECKIN_HOURS / TW2A_KEEPALIVE_HOURS / TW2A_TIMEOUT_SECONDS / TW2A_HEADER_TIMEOUT_SECONDS / TW2A_IDLE_TIMEOUT_SECONDS / TW2A_USER_AGENT / TW2A_CLIENT_VERSION / TW2A_MAX_IN_FLIGHT / TW2A_EXPIRING_SOON / TW2A_PROMPT_MODE / TW2A_PROMPT_FILE。非空才覆盖。api_key 不在 env 覆盖之列: 它只来自 config.json,单一来源——面板「配置」页显示的就是文件里的原值,改完当场生效 (不像其它 env 那样存在"界面显示 A、实际用 B"的错位)。首次运行生成的 config.json 里 api_key 是随机 sk-…(默认监听 0.0.0.0,空 key 等于裸暴露),启动日志会打印一次。 已存在的配置不会被改写(O_EXCL),老配置里空着就继续空着,改文件或在面板里填都行。

与 workbuddy2api 的配置面差异(有差异的地方都是"Trae 上游没有这个能力"): schedule.travel/activity/blackcat_*(活动任务)、global(国际域路由)、upstash (共享状态)、features.sanitize_blacklist_fingerprints、pool.max_in_flight_global、 pool.cost_explore_interval(成本分层探索)、upstream.device_token*/cli_version 未提供—— 写成空壳字段只会让人以为改了有效果。

发版

版本号是上海时间,形如 v2026.9.28.1530(2026.9.28 日期,月日不补零;1530 是 15:30, 时不补零、分两位、不带秒)。发版就是打标签并推送,工作流看到 v* 才建 Release:

VERSION=$(TZ=Asia/Shanghai date +%Y.%-m.%-d.%-H%M)
git tag "v$VERSION" && git push origin "v$VERSION"
  • go-binaries.yml:push main / PR → go test ./... + linux(amd64/arm64)、windows(amd64) 交叉编译,产物只进 artifact;打 v* tag → 同一个版本号注入 -X main.appVersion=(面板 「概览」显示它),打包成 traework2api-panel-v<版本>-<系统>-<架构>.zip|tar.gz(含 config.example.json 与 README)挂到 Release,另附 checksums.txt。
  • docker-ghcr.yml:多架构(amd64/arm64)镜像推 ghcr.io/misakano7545/traework2api-panel, main 推 latest / main / sha-xxxxxx,PR 只构建不推送。
  • 源码里 appVersion 默认 dev(本地 go build 与 Docker 镜像里都是 dev),只有 CI 发版会 注入真实版本号;不要手改它。

运维

./signin.sh             # 批量签到(全账号,自动 refresh 过期 token)
./credit.sh             # 积分日报(美化)
./credit.sh -json       # 积分日报(JSON)
./credit.sh <uid>       # 指定账号

积分口径:池与面板的「剩余/总额」都来自 ide_user_ent_usage(user_entitlement_pack_list[] 里 quota.credits_limit>0 的包求和,usage.credits_amount 为已用)。这条通路上没有独立的 Trae 额度字段: 2026-10-11 抓整条对话流实测,事件只有 done / extra_info / metadata / output / timing_cost / token_usage, 没有第三方笔记记的 notify_usage,整条流里 credit/billing/quota/remain 字段 0 条 —— 复跑 TW2A_PROBE_CHAT=1 go test ./internal/upstream -run TestProbeLiveBilling -v。 该笔记另称 ent usage 聚合的包含 work 包(即「显示 remain=2000 其实是 work_credits」):本仓未复核 (接口只给汇总,./credit.sh 拿不到包级数值),所以别据此改选号口径。

agent 域名是上游下发的(换域名时先看这里)

上游会换 agent 域名,而且面板看不出征兆:官方客户端从不写死具体后端,它请求一个逻辑主机名 (国服 trae-api-cn.mchost.guru,国际 coresg-normal.trae.ai),由 TTNet 的 dispatcher 按 TNC 下发的 域名表在传输层改写成 api<N>-normal[-<region>].mchost.guru 再发出去。所以同一个客户端,referer 里写着旧域名,实际打的是新域名 —— 2026-10-15 国服那轮改动就是这么发生的(trae-api-cn → api5-normal,客户端 referer 至今还是旧名)。

本仓是纯 Go HTTP、不走 TTNet,默认自己把那表拉回来解析(internal/upstream/domains.go):

  1. GET https://tnc0-aliec2.zijieapi.com/get_domains/v5/(国服)/ https://tnc-boot.trae.ai/...(国际) —— 无需鉴权。注意 cronet_version / ttnet_version 两个参数不能省,省了返回 0 条规则。
  2. 从 data.ttnet_dispatch_actions[].param.host_group[] 里挑出候选:国服 = 无区域段 (api5-normal.mchost.guru),国际 = 带区域段(api22-normal-alisg.mchost.guru)。 同表的 trae-api-cn.mchost.guru 之类不是分片后端,会被排除。
  3. 逐个探活(POST /api/ide/v1/get_detail_param 发最小报文,拿到任何 HTTP 响应即算活 —— 401 也 证明路由存在),取第一个可用的;全部失败就沿用现有值,最终回落 constants.go 的常量。 发现机制自己坏掉不影响出流。

配置与开关:

键 默认 说明
upstream.host_discovery true 关掉 = 只用 agent_host / 内置常量
upstream.host_discovery_hours 6 刷新周期。改了需重启(ticker 启动时定)
upstream.agent_host "" 显式钉死国服基址,非空则优先于自动发现(热生效)
upstream.agent_host_intl "" 同上,国际版

排查时先看两个地方:

# 1) 面板首屏 JSON(/panel/api/overview)——agent_host_source 是 auto/内置,看它是不是「自动发现」
curl -s http://127.0.0.1:7864/panel/api/overview | python3 -c 'import json,sys;d=json.load(sys.stdin);print({k:v for k,v in d.items() if "agent_host" in k})'

# 2) 启动日志:每次发现成功/失败/换域名都有一行 [domains]
grep '\[domains\]' <面板日志>

agent_host_source 长期是「内置常量/显式配置」而不是「自动发现」,或者日志里反复出现 「域名表获取失败」,说明 TNC 接口或 boot 主机也变了 —— 那时候要改的是 domains.go 里的 tncBootHosts / domainQuery,而不是 constants.go 的常量。

已实测(2026-10-15,两区真 token):两条域名下的 agent 基址同时可用,模型表逐字一致, 所以自动发现是「跟着上游走」而不是「不换就坏」;换域名那次也是旧域名仍返回 401 而非 404 (属于遗留入口,给运维留了观察窗口)。

Max 上下文(1M 档)

客户端模型选择器里每个模型都有一个「Max 上下文」开关(关着时窗口 116k/200k/256k,开着 1M)。 面板把这条通道做出来了,默认关闭——与客户端一致(客户端开着时也会提示「更多积分消耗」)。

怎么开

两种写法等价,任选:

# 1) 请求体加一个字段(OpenAI 客户端忽略未知字段,不会报错)
curl -s http://127.0.0.1:7864/v1/chat/completions -H 'Content-Type: application/json' -d '{
  "model": "cn:glm-5.2", "max_context": true,
  "messages": [{"role":"user","content":"hi"}]}'

# 2) 模型名带上游内部后缀(客户端原生写法)
curl ... -d '{"model":"cn:glm-5.2__max","messages":[...]}'

/v1/models 里每个模型会如实带上可用性,客户端可据此决定要不要开:

字段 含义
context_length 默认档窗口(__dev 实际请求口径)
context_length_max 上游声明的 Max 口径
max_available true = 这个模型真能开 Max(有 __max 变体且窗口确实变大)
max_model 出站要用的变体名(形如 glm-5.2__max)

当前国服 32 个模型里 20 个 max_available=true。Doubao-Seed-2.1-Turbo 这类上游给了 __max 名字但窗口没变大的,报 false —— 开了也不增长,报 true 就是骗客户端。

为什么要换 function

Max 变体只在部分上游面上声明了更大的窗口(2026-10-15 全表实测 782 条明细):

function 面 带 __max 条目 其中窗口更大 默认档 dev
solo_work_lite(面板默认面) 12 0 200000
solo_coder 36 0 200000
chat_v3(Max 面,默认) 20 20 116000
solo_agent / _lite / _remote 54/39/31 44/36/36 200000

所以开 Max 时出站 function 会换到 chat_v3(upstream.max_function 可改,热生效)。 换面不影响可调用性 —— 本仓 2026-10-04 已实测 function 不是约束(4 模型 × 4 轴 16/16 全 200)。

想要「200k 默认档 + 1M Max」的话把 max_function 设成 solo_agent 或 solo_agent_lite。

上游没给 Max 时

如实忽略并留一行 WARN: [max] <模型> 没有更大的 Max 窗口…,不换 model、不换 function、不报错, 请求照常成功。宁可少一个开关,不硬发一个上游认不出的名字。

一个坑:变体名必须从上游表里读,不能拼

model = config_name + "__max" 是错的。全表 417 条带后缀的变体名里 99 条不满足这个规律:

search_agent_qwen_fast → search_agent_model__max   (多个 config 共用一个变体名)
glm-5.2_advisor_doubao → glm-5.2__dev             (advisor 槽位复用基础模型的变体名)
custom_model_kimi      → custom_model_placeholder__dev

拼出来的名字上游认不出,失败形态是「静默按 dev 行为跑」—— 客户端以为开了 1M,实际还是 116k。 代码只存读到的原值(upstream.variantNames),取不到就是空串,绝不猜。 回归测试用的是真抓包夹具 internal/upstream/testdata/batch_max_variants.json。

已实测 / 未实测(如实分列)

已实测(2026-10-15,真 token):

  • 出站报文:config_name 留裸名、model 填变体名、function 换 chat_v3,日志有 [max] 行。
  • 面 × 变体 6 格矩阵(solo_work_lite/chat_v3 × __dev/__max/裸名)全部 200 且正文正确。
  • 面板端到端 13 次调用(默认档 6 次 / Max 档 7 次,含带与不带 max_tokens),除下述两次异常外全部正常。

未实测:1M 窗口本身没有被验证。 上游表里 context_window_tokens.max = 1000000 是声明, 客户端侧看不到实际生效窗口。要证实得发一段超过默认档(116000 token)的输入,约需 100 积分 —— 没做,别把声明当实测。

观察到的异常(未复现,留档):首轮面板 E2E 里两次 Max 调用出现一次英文胡话、一次正文为空 且烧掉 16000 个推理 token(正好是 Max 档输出上限)。随后按同样条件重跑 3 次 dev + 3 次 max 全部正常, 2×2 上游矩阵也全正常,复现不出来。原因未明,可能是上游瞬时状态或满预算的思考跑飞,继续观察。

专家模型(AdvisorTool)

社区流传「TRAE 里有个强制开启、没有开关的 AdvisorTool,不管你用什么模型都会把方案送去给一个 不可见的专家模型,积分并入本次任务且查不到」。2026-10-23 用真号(CN,20 个官方模型)实测, 结论对一半:

  • 专家配置确实存在,且按「function 面 × 模型」挂在模型表里:model_extra_config.advisor_tool 带 advisor_config_name / advisor_model / max_uses_per_request=3 / trim_system_prompt / trim_tool_results。所以「专家是哪个模型」其实在接口里看得到(面板不显示而已)。

  • 不是「不管你用什么模型」:面板默认面 solo_work_lite 的 20 个官方模型一个都不带; chat_v3 21 个里 1 个(Doubao-Seed-Code);solo_agent 21 个里 9 个(DeepSeek 三兄弟、 MiniMax-M3 两兄弟、qwen3.8-flash / qwen3.8-max / qwen-3.7-plus、Doubao-Seed-Code); solo_coder 12 个都不带。

  • 带配置 ≠ 会跑:裸 API 请求(只有 messages + tools)的 prompt_tokens 只有 36~41, 流里没有任何 advisor 事件,provider_model_name 也只有主模型一个 —— 服务端不会自己把方案 送去给专家,也没有注入那段 advisor_tool_policy 策略文本。

  • 真正的触发条件是客户端把 AdvisorTool 声明成普通 function:同一句「上次方案不行,我很烦」, 不声明 → 0 次调用;声明 → 必然调用(14~25 帧 tool_calls,带 question 参数)。 只给策略文本、不声明工具,模型只能嘴上提它,调不出来。

  • 积分:专家确实另算,而且贵一个量级(2026-10-23 实测)。用额度接口 user_entitlement_pack_list[].usage.credits_amount(浮点、45 秒内结算)做前后差, 两臂各 4 次、prompt 都约 12.5k token:

    臂 模型(倍率) prompt completion 扣积分 单次
    A 主模型 deepseek-v4.1-flash(0.06) 12916 323 0.5684 0.142
    B 专家槽 glm-5.2_advisor_default(0.78) 12356 1498 3.8024 0.951

    同样输入量下专家一次 ≈ 主模型一次的 6.7 倍(token 单价 13 倍,专家输出也更长)。 按官方单价复核:专家 12356×216/1e6 + 1498×756/1e6 = 3.80,与实测分毫不差;主模型实测比 价目表估的高 43%(缓存口径偏),说明价目表对 GLM 准、对 DeepSeek 偏低。 真实会话里专家收到的是整段裁剪后的历史(几万 token 很常见),再乘「每请求最多 3 次」, 所以「消耗非常大」这个方向是对的——30k token 的方案转发一次约 6.5 积分。

  • 结算不慢,是显示粒度粗:credits_amount 是浮点、45 秒内就更新;面板按整数显示, 几十 token 的小请求差值被取整吃掉,才显得「延迟十几分钟」。别拿整数位当计费依据。

面板侧因此做两件事(都只影响出站,不改客户端):

  1. *_advisor* 槽位(glm-5.2_advisor_doubao 这类「基础模型 + 专家」绑定槽)不进官方模型表, 国际版按「有显示名」放行也挡(upstream.isAdvisorSlot);
  2. 客户端若声明了名为 AdvisorTool 的工具,出站前剔掉并留一行 WARN (upstream.normalizeTools / isAdvisorToolDecl)——专家是模型只能「叫得到才算」的工具, 不让它存在就等于硬拦截。

复跑取证(测试的工作目录是包目录,默认读 ../../auths,别传 ./auths):

TW2A_PROBE=1 go test ./internal/upstream -run 'TestAdvisor' -v        # 只读 dump + 4 次短请求
TW2A_PROBE=1 TW2A_CREDIT=1 go test ./internal/upstream -run TestAdvisorCredit -v  # 花钱,约 4.4 积分

保活(防掉线)

schedule.keepalive_hours(默认 [3])到点对全账号无条件刷一遍 token,不看剩余有效期: access token 活 14 天,只在「过期前 24h」刷等于 refresh token 链十天半个月没人碰,上游一旦让它过期 就只能重新登录(掉线)。每天摸一次,链不会冷。刷新 token 不吃积分,只换凭证并原子落盘 auths/。 面板「全部保活」= 同一件事的手动入口(POST /panel/api/keepalive)。

session 失效(上游 1001 / 20101,refresh token 也救不回来)连续 3 次才永久禁用:一次 401 多是 上游抖动,见到就杀号会让可用账号凭空下线。期间任何一次刷新成功都清零(ClearSessionDead), 误判的号自己回来;真被判死的号用面板「启用」(POST /panel/api/accounts/{uid}/enable)清掉禁用标记、 计数与冷却直接复池,不必再「移除 + 重登」。计数与禁用状态都落 state_file。

管理面板

浏览器打开 http://127.0.0.1:7864/panel/。页面和 app.js 不用密钥;/panel/api/* 与 /v1 共用 config.json 里的 api_key(「配置」页可见可改,保存后两边同时换新)。没设密钥时这些接口只接受本机连接。

「模型」页按上游 get_detail_param 原值列出模型的 capability(model_capability)与思考 相关字段(model_extra_config 里的 Thinking.Type、reasoning_effort_config 原文),不做解释和换算。

三种入站协议在 handler 层先归一成一份 chat 请求(responses.go / messages.go),再进上面这条 管线,所以切号/冷却/会话粘性/台账对三条路一样生效。codex_responses 不保存 previous_response_id / store(Codex 每轮自带全量 input);anthropic_messages 不回传 thinking(上游没有 signature)。缓存命中各按客户端口径回:responses 出 input_tokens_details.cached_tokens,messages 出 cache_read_input_tokens(都取自 Trae usage.cache_read_input_tokens,没有就不写,不拿 0 冒充"没命中")。 客户端自己带的 reasoning_effort 之类字段是原样透传上游的,不认的也一个不吞(PrepareBody 只改 stream/function/config_name/model/tools)。

Codex 工具契约(/v1/responses,2026-10-08 实测):Codex 的 wire 契约跟 OpenAI chat 不是一回事, 网关侧补齐三件——① type:"custom"(freeform,如 apply_patch)降级成单 input 参数的 function 送出, 回程还原成 custom_tool_call + response.custom_tool_call_input.delta/done,载荷严格解包(多键/非字符串/ 半截 JSON 一律按不可执行下发 status:"incomplete",避免客户端拿脏载荷去改文件);② type:"namespace" 工具组(Codex 0.160 的 multi_agent_v1 就是这种)展开成扁平 function 出站,回程补回 namespace,组内/ 与顶层重名时加稳定后缀(read_file_2)并双向映射,客户端按自己声明的名字回传结果能对上;③ 无 call_id 的 function_call_output(别的任务的回执)与 agent_message 注入成用户指令,不伪装成工具结果(空 tool_call_id 到上游只会撞配对失败);带图时整段保留 parts。实测:Codex 0.160 一次 apply_patch 建文件 全程走通,multi_agent_v1.list_threads 调用回程带 namespace。

usage 口径:input_tokens/input_tokens_details.cached_tokens 按各自协议出(Anthropic 口径的 input_tokens 不含缓存读/写,扣掉 cache_read_/cache_creation_input_tokens;Responses 出 cached_tokens);上游没报就不写,不拿 0 冒充「没命中」。

截断自动续写(internal/upstream/continue.go,2026-11-10 真号实测):SOLO 单次输出有硬上限, 且按变体不同(cn:deepseek-v4.1-flash 恰在 completion_tokens=32000 被切、glm-5.3__max 在 16000), 而 done 仍报 finish_reason:"stop" —— 截断信号被上游伪装,客户端(Codex 这类不带输出限额的)写大文件 会在半途被截且毫无提示。网关在 SOLO 帧层识别「本段撞了该模型的 cap 且是纯文本」后用「已输出内容作 assistant + 续写指令」补发同模型请求,后续帧接进同一条流:客户端只看到一个 stop、正文连续。

实测(同一句「输出 6000 行 print」,不带 max_tokens):段 1 completion=32000(撞上限)→ 自动续写 1 次 → 回给客户端 completion=44206(32000+12206,跨段求和)、prompt=29182(末段值,不求和:续写段的 prompt 天然含整段上下文,求和会虚高 N 倍让客户端提前全量压缩)、total=73388 自洽;正文 1..6000 全在, 只有一个 finish_reason:"stop",请求记 success、扣 5.77 积分(对照组:同一提示改 4000 行时 completion=28384 没撞上限,不触发续写)。同批 /v1/responses 跑 6000 行只有一个 response.completed (output_tokens=42618),不会被误判成 incomplete。

第二种形态是上游干脆不发 done 就干净 EOF:solosse 的幂等兜底会给它补一个 [DONE],responses 适配器见 sawDone 就报 completed —— 半个回答被当成完整答复。这里改用同一份续写体盲重试一次 (幂等,不重复输出),仍失败就补一条 finish_reason:"length",让 responses 报 incomplete、 messages 报 stop_reason:max_tokens(这两个映射本来就有,缺的只是「这段没正常收尾」这个事实)。

第三种形态是传输层断链(连接被重置、空闲看门狗掐断、读超时):正文写到一半断,客户端此前只会 看到「流停在那」——既没有 finish_reason 也没有 [DONE]。现在按「写过几个字节」分流:写过内容就 先在 solosse 里补一帧 finish_reason:"length" + [DONE] 再上抛(错误照常上抛,记账仍记失败—— 「这段没正常收尾」与「这次请求失败」是两件事,都别说谎);一个字节都没写过则是换号(handler 看 w 的已开口标记决定,见下节),客户端根本不知道换过号。

边界:工具调用分片不续(残缺 JSON 拼接比不续更糟),只补截断终态;客户端显式 max_tokens < cap 时不续 (尊重意图);段数默认 3 段、限额 ≥ cap 时按 ceil(限额/cap) 扩容、硬顶 4 段(每段都重发上下文,输入 token 近二次增长)。未照搬参考实现的两块:思考吃光预算后的「关思考续写」(SOLO 没有可靠的关思考开关, 见下文全档实测)与累计 token 上限按档位下限 ×4(本仓没有档位下限概念)。

副作用留档:上游不认客户端的小 max_tokens(实测 max_tokens=300 仍产出 32000 token 才停), 所以「客户端只要 300」这件事网关无法替上游兑现,只保证不再追加续写。

断流的另外四条防线

(solosse.go / narration.go / internal/server/ssecommit.go)这四条治的是 「没有任何一层报错、连接却半途没了」——日志里既没有 length 也没有断流,续写/盲重试一次都不会触发:

  1. 首帧心跳分两档:等上游第一个数据帧的空窗里每 1.5s 写一条 : keepalive,首帧之后回到 15s。 原来只有 15s 单档,比客户端的耐心大一个数量级(同类客户端 Codex/ZCode 线上实测四次「零内容 + client_gone」全落在 3.46~4.09s),那段静默里一个字节都不写,客户端就自己关连接。两档节拍是常量, 唯一调整入口是 upstream.SetKeepaliveIntervals(head, idle)(idle<=0 关闭)——不进配置:这是个 一调错就全线断流的旋钮,不面板化。
  2. 心跳不堵换号:换号的判据从「写过任何字节(含注释帧)」改成「写过数据帧」(chunkWritten)。 注释帧只提交响应头,客户端还没拿到内容,此时换号后第二段的帧接在同一条 200 上、客户端无感—— 空流重试与首帧错误否决因此都还在,不会被 1.5s 心跳吃掉。
  3. 已提交流的错误降级(ssecommit.go):心跳提交 200 之后若全部号都失败,旧行为是往 SSE 流里追加 一段裸 JSON(实测形态:: keepalive ×6 紧跟 {"error":{...}}),客户端当成脏帧忽略或解析报错—— 看起来同样是「莫名断流」。现在已开口就只写协议内 event: error + data: [DONE](OpenAI 对象信封, SDK 能抛 APIError),未开口仍是真实状态码 + JSON。
  4. 预告句救援(narration.go):finish_reason=stop、零工具调用、零报错,正文却停在「……:」这种 预告上。五层判据同时成立才救:① stop 且正文非空、无工具分片/error 帧、非读错误;② 请求体声明了工具 且历史里有工具轮(角色扮演客户端的冒号收尾是正常文风);③ 尾部是未说完形态(冒号/逗号/破折号/ 省略号/左括号/未闭合代码块/无句末标点),文本化的工具调用标记按完整性分流(完整块不救,客户端 会执行它;截断块必须救);④ 标点型尾部受 400B 长度闸门,结构上没写完的不受限;⑤ 每请求一次。 额外的安全闸:正文里出现「请确认/等你确认/要不要我…」或以问号收尾时不救——那类收尾是模型在等 用户拍板,补一句「继续」会拿到工具调用并被采纳,等于替用户按下发送键(参考实现线上发生过两例)。 日志留 WARN: [narration] … 尾 60B=…,先攒样本再决定要不要扩判据。

非流式也接续写器:attempt 的非流式分支同样是 NewContinueReader → Aggregate。此前只有流式那侧 有三种救援,stream:false 的请求撞 cap 或上游不发 done 时,Aggregate 的 finish_reason 初值 "stop" 会原样写回——半个回答被当成完整答复(/v1/responses、/v1/messages 的非流式请求都走这条路)。

只观测未救援的一类:「尾巴看着说完了、其实该动手」(参考实现的 toolonly.go)。判据 = stop + 正文非空且 <400B + 尾部是句末标点(悬空尾部归上面的预告句救援)+ 不在等用户拍板 + 请求体声明了工具、 历史里有工具轮、且最后一条消息就是工具结果。命中只打一行 WARN: [toolonly] … 尾 60B=… → 候选(仅观测,不救援),不改任何行为。 没直接抄救援的理由有账可算:那边的采纳条件是「续写段必须回完整 tool_calls,否则整段丢弃」,等于 每命中一次都要先缓冲一整段上游输出(多打一次上游;本仓实测 14k prompt 的一次请求 0.39 积分、 70k prompt 1.97 积分)。首个真号样本(09:43:19,342 token prompt)是一句完整结论——按采纳逻辑会白打 一次上游并可能续出废话。攒够样本比例再决定开不开。

工具结果里的图片(/v1/responses 的 function_call_output、/v1/messages 的 tool_result): 图不能留在 tool 消息里(Chat 的 tool 角色只接受文本,带 image_url 会被 OpenAI 直接 400), 也不能整段拍平成 JSON 文本(base64 按正文分词 ≈ 字符数×0.69,一张 400KB 截图十几万 token,而且模型 看不到图)——所以拆成「文本进 tool 消息 + 图片攒到本轮工具结果全部落地之后补一条 user 消息」。 不能插在中间:一轮里多条工具结果是连续的 tool 消息,中间插任何消息都会让后面的 tool_call_id 失去应答(严格上游判 11148),并行工具调用必然踩中。只有图片时 tool 消息留占位文案指路。

上游承载不了的工具一律留痕剔除:tools[] 里没有 function 键的(Codex 每轮都发 type:"web_search"、Anthropic 的 server tool)在上游只承载函数工具,剔除时按名字记一条 WARN: [upstream] 剔除 N 个…,并连带删掉点名了它们的 tool_choice(名字对不上,上游只当参数非法)。 静默丢会让客户端以为自己能联网搜索,而日志里一个字都没有。

与参考实现对拍(internal/upstream/vectors_check_test.go,TW2A_VECTORS=<dir> 才跑、只报告不判红;答案卷来自 另一套独立实现 shadyrispy/cpa-multi-plugins 的 plugins/trae,只当对拍基线,不进仓):chat 路 18 个 伪装头键集逐键一致(已固化成 TestSOLOHeadersKeySet),出站 body 与聚合口径除我们有意的分歧 (不发 config_name、max_tokens 上限、未知字段透传、空流报错好换号)外全对。

对拍查出的 4 条 classify 差距已修(client.go 的 Classify 现在与 event:error 共用 soloSpecificKind 那张具体判据表,README 早先写的「HTTP 级与流内共用一张表」名副其实): 400/4001、413、400 prompt is too long 都归调用方问题不罚号(与 4026 同一类,见 context_overflow_test.go),403/4008 归软冷却。探针按效果比对(参考的 model_unavailable/input_too_large 等于本仓的 ErrNone),跑完只剩 2 条,都是有意为之: ① 参考实现把 4008 当 plan_limit(12h 硬冷却),本仓按软冷却 —— 4008/quota 是可自愈的限流 而不是权益不足,判据钉在 TestClassify;② {"error":"This input is too large"} 参考归 client(降权),本仓归不罚号 —— 调用方发太大,罚号没有意义(他们自己的标记表漏了这句)。 传输层信号仍优先于 body 里的业务码(500 里带 quota 文案按服务端故障算)。

全模型全档实测(不发 + 10 档 × 3 轮,取 reasoning_tokens 中位数,max_tokens=2000):

模型 不发 none minimal lowest low medium high max xhigh ultra highest 档间极差 档内波动 ρ(档位序)
Doubao-Seed-Evolving 128 77 149 105 113 68 128 111 124 119 120 81 ±58 +0.28
Doubao-Seed-2.1-Pro 109 117 140 73 108 78 223 173 64 78 70 159 ±132 -0.40
Doubao-Seed-2.1-Turbo 160 137 186 136 118 114 120 181 166 140 180 72 ±63 +0.22
step-5-preview 0 0 0 0 0 0 0 0 0 0 0 0 ±0 —
glm-5.3 123 139 112 148 136 121 83 124 123 124 123 65 ±72 -0.26
glm-5.2 356 340 321 340 349 326 337 346 340 325 351 30 ±54 +0.25
deepseek-v4.1-flash 77 74 58 55 58 68 80 72 64 61 67 25 ±17 +0.14
DeepSeek-V4-Flash-Official 67 78 94 78 82 83 78 77 25 84 81 69 ±30 -0.14
DeepSeek-V4-Pro-Official 115 112 133 109 100 108 120 117 104 92 109 41 ±22 -0.40
kimi-k3 80 157 113 120 137 175 158 126 104 167 160 71 ±78 +0.31
kimi-k2.7-code 114 116 80 79 107 140 106 84 100 103 86 61 ±53 -0.09
kimi-k2.6 164 197 203 148 156 177 152 141 201 146 160 62 ±50 -0.36
minimax-m3 82 110 120 103 136 132 54 80 86 83 90 82 ±42 -0.58
qwen3.8-max 109 104 108 133 97 110 100 99 109 110 114 36 ±23 +0.25
qwen-3.7-plus 143 142 152 144 143 158 144 143 144 147 138 20 ±20 -0.12

档间极差 = 各档中位数的极差;档内波动 = 同一档 3 轮的平均极差;ρ(档位序) = 中位数与档位序号的 Spearman 相关系数(真按档位调档应该接近 +1)。14 个会思考的模型没有一个呈单调关系:ρ 落在 −0.58~+0.31, 档间极差基本没超过档内波动,连 none/minimal 也关不掉思考(Thinking.Type=enabled 的 kimi-k2.6 亦然)——上游不按这个字段调档,静默忽略;非法值、错类型、未知键都不会让请求失败。 step-5-preview 那一行全 0 是模型特性不是失败:它 33 发全程 reasoning_tokens=0(completion 80~109 正常出答案)。 本轮 495 发(15 模型 × 11 列 × 3 轮,版本码 20260811)零失败。

kimi-k3 的开关是「客户端版本码」,不是账号档位(实测 2026-09-27):上游按 X-Ide-Version-Code 下发不同的模型表与门控,X-Ide-Version 字符串本身不影响结果(0.1.43/0.1.50/1.0.0 同码同表; 2026-10-11 复核补测 0.1.52/0.1.61 仍逐行一致)。

版本码 模型表 k3 的 access.identity_list 结果
20260716(旧) 39 条 [2,3,100] 免费档被挡,请求回 1005 plan:2
20260811 42 条 [0,5,1,2,3,100] 免费档可用,实测出流正常
20260924(当前,见 constants.go) 48 条 同上 与 20260811 的模型名逐字相同,只是表变长(实测 2026-10-30)
20260814 / 20260821 / 20260927 / 20261231 42 条 [0,5,1,2,3,100] 与 20260811 逐字一致(含故意超前的未来码)
  • 可见集合在 20260811 换过一茬:多出 kimi-k3 / glm-5.3 / step-5-preview / deepseek-v4.1-flash,少掉 glm-5 / DeepSeek-V4-Flash / DeepSeek-V4-Pro(后三个在新版客户端里 已 is_invisible_to_user)。可见集合本身还会随灰度漂移(同一版本码下 qwen* 时有时无), 所以动态拉取才是权威,静态表只是拉不到时的兜底快照。
  • 官方版本号在更新日志里:https://docs.trae.cn/work_changelog(Trae 最新 v0.1.52, 2026-08-21 那批;IDE 侧另见 ide_changelog)。客户端自带的更新接口 log.snssdk.com/service/2/app_alert_check/ 实测只回 {"message":"success"},取不到版本。
  • 冒充最新版能请求到模型,但没有收益:用 0.1.52 / 20260821 的身份发 kimi-k3 拿到 HTTP 200 出流,可模型表与门控跟 20260811 逐字一致;上游也不校验版本码(1.0.0 / 20261231 照收)。constants.go 现在跟官方客户端实测值走(3.4.11 / 20260924,2026-10-30 抓包), 理由只有一个:身份指纹一致性,不是功能需要。
  • 复跑矩阵(只读,6 个请求 + 一发 k3): TW2A_PROBE_VERCODE=1 go test ./internal/upstream -run TestProbeLiveVersionCode -v —— 哪天阵容出现 +/− 变化,才需要动 constants.go(TestWorkChannelIdentityFrozen 会拦住改动)。

出站请求头:以客户端抓包为准(2026-10-30)

把两版客户端装在同一台机上,用本地 MITM(自签 CA + 系统代理)拦 Agent 模式的真请求, 逐项对拍后改了 headers.go / identity.go / constants.go:

面 客户端实测 旧实现
User-Agent Mozilla/5.0 … TraeCN/1.107.1 Chrome/142.0.7444.235 Electron/39.2.7 Safari/537.36(INTL 品牌段是 Trae/) Trae/0.1.50
Accept */*(流靠 body stream:true) text/event-stream
x-app-version 国内 3.4.11;国际字面量 default 恒 default
x-ide-version 3.4.11 / 3.6.4(= product.json appVersion) 0.1.50 / 1.0.2
x-cloudide-token / x-uid 不发 发了
aha 五件套 x-harness:1 / x-agent-host-type:0 / x-bridge-transport:aha / x-lgw-req-sdk-type:3 / package-type 缺
ug 族 国内 stable_cn + x-lscbd-* + x-user-region:CN + X-SS-DP:787976;国际 stable_i18n + X-SS-DP:677332(无 lscbd / region) 缺
trace x-custom-trace-id + traceparent(00-) + x-flow-traceparent(04-) + x-request-id: req_<uuid> 缺

坑(花了一次联调才定位):x-request-pin 与 x-requested-at 是客户端加密载荷的 伴随头 —— 客户端发 llm_utils_chat 时 body 是 X-Medusa/X-Helios 包着的 base64 密文。 本仓发明文 body,只要带上这两个里任意一个,上游立刻回 400 {"error":"x-request-pin or x-requested-at..."}(逐头二分实测)。所以这两个头 必须不发;x-request-id / x-ahanet-timeout 单独带没事。

明文 body 本身不受影响:客户端风格头 + 明文 JSON 打 api5-normal.mchost.guru 实测 200 出 SSE 正文 (go test -tags wirecheck ./internal/upstream -run TestWireCheckLive -v,需 TW2A_CAP 指向抓包 jsonl)。

出站报文形态:第二轮对拍(抓包 vs 代码逐头差集)

第一轮只对齐了「发什么头」,第二轮把抓包按链路拆开做集合差集,发现少的是两层: 报文形态(大小写 / HTTP 版本 / 编码协商)和地区分支。口径全部来自 .reverse/cap_cn_full.jsonl 与 .reverse/capture_intl_agent.jsonl。

面 客户端实测 本轮改动
Referer 逻辑域名 + 同 path:CN https://trae-api-cn.mchost.guru/…、INTL https://coresg-normal.trae.ai/…。不是传输域名 api5-normal(.alisg).mchost.guru 此前零实现 → soloReferer + RefererBaseCN/Intl
sec-fetch-* chat 与模型表都发 原先只有 UgHeaders 有
app-version 独立头:CN 3.4.11、INTL 3.6.4。与 x-app-version 是两个头(INTL 的 x-app-version 反而是 default) 补
x-tt-trace-id 00-<32hex>-<16hex>-01,第二段 = 第一段前 16 位,末 4 位恒 ffff 补(soloTtTraceID)
Connection keep-alive 补;同时关掉 HTTP/2(h2 拒绝 Connection 头,客户端本来就是 h1)
头名大小写 只有 Host / Connection / Content-Length / Referer / X-SS-DP / X-Helios / X-Medusa / X-Neptune 首字母大写,其余全小写 Go 的 Header.Set 会把 x-os-version 规范成 X-Os-Version、X-SS-DP 成 X-Ss-Dp → Transport 层改写副本
request-traffic-type 只有国际发,CN 的 chat 与模型表都没有 改成仅 INTL
traceparent CN 有;INTL 只有 x-flow-traceparent 改成仅 CN
x-harness CN 有,INTL 无 改成仅 CN
x-trae-request-id INTL chat 独有(uuidv4) 补
Accept-Encoding gzip, deflate, br, zstd 四值完整复刻(解压见下)

Accept-Encoding 的坑:net/http 的透明解压在 Accept-Encoding 非空时会关掉 (transport.go 里 requestedGzip 只在 header 为空时置真)。所以自己写这个头就必须 自己解压,否则读到的是压缩字节 —— wire.go 的 decodeBody 接管了全部四种编码: gzip/deflate 走标准库,br 走 andybalholm/brotli,zstd 走 klauspost/compress/zstd。 后两个是 utls 的传递依赖,本来就在树里,补齐四值不再额外增重。

头顺序:已对齐(order.go)。客户端是 aha 的固定顺序,而 net/http 的 Header.writeSubset 按 key 字母序写、不受调用方控制。做法是在 net.Conn 层再包一层: 判断这次 Write 是不是一整段报文头,是就按客户端顺序重排后写出 —— Transport 完全 不知情,连接池 / 超时 / chunked / SSE 流仍是标准库在管。

只处理「一次 Write 里就有完整头」的情况,头被拆开时退回字母序:跨 Write 缓冲要跟着 请求边界重置,判断错一次就会把上个请求的 body 拼进新头里(实测第二次请求直接 400)。 宁可少对齐一层,也不能发坏包。端到端有 TestWireTransportHeaderOrderEndToEnd 钉住 —— 它在 TLS listener 侧读明文头,核对顺序与头名大小写。

故意没做的两条:

  • X-Medusa / X-Helios / x-request-pin / x-requested-at:客户端这几条伴随 的是加密 body,本仓发明文,带了反而 400。这条差异无法用「头对齐」消除, 只能实现那套加密 —— 属于另一件事。
  • 请求体加密:同上,是最硬的一条差异(见本文前面的结论)。

TLS 指纹:从脚本栈换成浏览器栈

请求头是 HTTP 层的,TLS 握手在它之前就完成了 —— 光对齐 header,在做过 JA4 的 风控眼里还是脚本。crypto/tls 的 ClientHello 是「单 supported_group(x25519) / 无 GREASE / 10 个扩展」;浏览器系(BoringSSL)是「多套件 / 多 group / 带 GREASE / 十几个扩展含 ALPS·ECH」。同一台机打同一目标,两个 JA3:

crypto/tls       771,49195-49199-…,0-11-65281-23-18-5-10-13-50-43-51,4587,0
utls Chrome_133  771,43690-4865-…,10794-35-23-13-65037-5-43-27-16-…,4588,0

internal/upstream/tls.go 用 refraction-networking/utls 接管握手。默认档是 aha (官方客户端 aha/TTNet 的真实形状,按抓包复刻,见上一节),其余可选 chrome_133 / chrome_131 / chrome_120 / go。配置项 upstream.tls_fingerprint (环境变量 TW2A_TLS_FINGERPRINT)热生效;go = 回退原生握手,用于对照。

aha 档已经是逐项复刻(套件 / GREASE / groups / sigalgs / 扩展集合 / padding 都按 抓包对齐,见上一节)。chrome_* 那几档只是备选与对照 —— 它们比真客户端多出整个 PQ key share 与 ECH,在 JA3/JA4 上是对不上的。

ALPN 是必须收窄的:浏览器默认 offer h2, http/1.1,而我们已经关掉 h2 (客户端是 h1 + Connection: keep-alive)。真协商到 h2 就会用 h1 报文去说 h2, 直接坏掉 —— 所以 ALPN 只发 http/1.1。代价是 JA4 的 ALPN 段与真浏览器不同, 这是「能跑」和「完全一致」之间必须选一个。

接线上的两个坑:

  • 用 DialTLSContext 接管后 http.Transport 不再看 TLSClientConfig, InsecureSkipVerify 等要自己从里面继承,否则自签环境直接验签失败。
  • setHeaderTimeout 原先断言 *http.Transport 改字段,包了 headerCaseTransport 之后会静默失效 —— 改成走接口(SetResponseHeaderTimeout)。

请求体加密:静态侦察结论(2026-11-01)

客户端 llm_utils_chat 的 body 是 base64 包着的二进制,配套 X-Medusa / X-Helios / X-Neptune / x-request-pin / x-requested-at 五件套;本仓发明文 JSON。这是握手与头 全部对齐之后剩下的唯一实质差异,所以专门查了一遍能不能复刻。

查了什么

位置 结果
Trae CN.exe 的 strings(22MB) medusa / helios / neptune 零命中
aha_net.dll / sscronet.dll / metasecml.dll 同样零命中
resources/app/out/**(86MB JS) 零命中
libckg.dll 命中 medusa/helios,但那是代码知识图谱服务(50000 端口、sqlite_vec),字符串巧合
JS 侧加密库 有 sm4:{encrypt:(i,e,t)=>BRs(i,e,1,t)} 实现,但没有 sm4.encrypt 调用点
native sm4 符号 Trae CN.exe 扫到 43 处,逐条看上下文全是二进制噪声(�Sm4),不是符号

抓包 body 的形态(CN 首条,body_len=3220 完整):

  • base64 解码后 2414 字节,高熵,不是 JSON、不是 gzip/zstd(无 1f8b / 28b5 魔数)
  • 2414 mod 16 = 14 —— 不是裸块加密密文;像「nonce + 流加密」或「压缩后再加密」
  • 另外两条抓包(22408 / 155160)是截断的:.reverse/mitm.py 只记 body[:4000], 解码后都是 3000 字节,拿不到完整密文做密码分析

结论:静态不可复刻。

头名和算法常量都不在明处 —— 要么在 V8 快照/字节码里,要么做了字符串混淆运行时解密。 继续挖只有两条路:

  1. 动态调试(Frida hook BRs 或 native 导出函数、x64dbg 跟 llm_utils_chat 的 构造栈),能拿到算法与密钥;但密钥大概率是设备绑定 + 服务端下发,换号就不通用。
  2. 改 mitm 不截断重抓,先确认完整密文的长度关系(是否 = 明文 + 固定开销), 再判断是不是标准 AEAD。

优先级判断:低。 上游目前接受明文 body(README 前面记过 wirecheck 实测 200, 本仓真账号也一直用明文跑通),所以这条差异只影响「包像不像」,不影响可用性。 真正的风险是上游某天把校验从「可选」切成「强制」—— 那是服务端开关,不是本仓能提前 解决的问题;真到那天,上面的动态调试也得重做一遍。

抓到了官方客户端自己的 ClientHello(aha 档)

tools/hellocap 起裸 TCP 桩 + 系统代理,启动本机 Trae CN(explorer.exe 拉起, 直接 Start-Process 会在 V8 isolate 阶段静默退出),抓到它发给 api.trae.com.cn 的真实握手(517 字节)。它是 aha/TTNet 原生网络栈发的,不是 Chromium:

套件 supported_groups 签名算法 备注
aha / TTNet(本仓 aha 档) 15 29 / 23 / 24 8 无 PQ、带 17513(ALPS)、padding
Chrome 133(utls 预置) 15 56026 / 4588 / 29 / 23 / 24 7 带 PQ、ECH、17613
本机 Chromium 进程(对照) 15 4588 / 29 / 23 / 24 12 带 PQ、ECH、17613、私有 51764

结论:用 Chrome 指纹冒充这个客户端,在 JA3/JA4 上是对不上的 —— 多出整个 PQ key share 和 ECH,groups 集合也不同。internal/upstream/tls_aha.go 按抓包复刻了真实形状。

默认档就是 aha(2026-11-01 定),四个生产主机(含 api.trae.cn)实测全通, padding 按抓包的 220 字节原样保留。

中间连着踩了两个坑,都记在这里免得重来:

  1. 把 padding 当成 Tengine 的雷。加 padding 后 api.trae.cn 报 handshake failure, 一度判成「阿里 Tengine 不认 padding」并把默认档退回 chrome_133。实际元凶是 specWithALPN 里对 utls 预置档(ID 档)也做了 KeyShare 重建,把 spec 改坏 —— 修掉之后 padding 和无 padding 都能握。
  2. 拿单次握手当结论。这台网关在连续握手时会抖,同一条 spec 前一秒成功后一秒 handshake failure。TestAhaStabilityTraeCN 连打 6 次统计:aha 带 padding 6/6、 去 padding 6/6、chrome_133 6/6 —— 三档都稳,之前的「失败」是抖动。

真账号实测(默认档 aha):CN 主链路出正文 18.8KB / 业务码 0,模型表 29/31 条, ug 族只读业务(付费身份 / 额度 / 积分包)全 200。

要回退或对照:

TW2A_TLS_FINGERPRINT=chrome_133   # 或 aha / chrome_131 / chrome_120 / go

真账号实测(默认档):CN 主链路出正文 21.4KB / 业务码 0,模型表 29 条, ug 族只读业务(付费身份 / 额度 / 积分包)全 200。

要回退或对照:

TW2A_TLS_FINGERPRINT=chrome_133   # 或 aha / chrome_131 / chrome_120 / go

重抓(客户端版本变了就重来一次):

go build -o hellocap.exe ./tools/hellocap
./hellocap.exe -addr 127.0.0.1:8443 -out ./cap -sni "trae,mchost" -timeout 120s
# 另开一个:把系统代理指到 127.0.0.1:8443,然后 explorer.exe 拉起 Trae CN.exe
# 抓到后 cap/spec-*.go.txt 直接就是可编译的 utls spec

真实上游实测(2026-11-01)

两个带 build tag 的探针,常规 go test ./... 不会跑:

go test -tags tlsprobe ./internal/upstream -run TestLiveTLSProbe -v       # 只握手,不吃额度
go test -tags liveauth ./internal/upstream -run TestLiveAccountModels -v  # 真票据拉模型表
TW2A_LIVE_CHAT=1 go test -tags liveauth ./internal/upstream \
  -run TestLiveAccountIntlSlot -v                                         # 会吃额度

握手矩阵(四个生产主机 × 两种指纹,全部成功):

主机 crypto/tls utls chrome_133
api5-normal.mchost.guru(CN agent) 189ms · ALPN 空 13ms · ALPN http/1.1
api.trae.cn(CN ug) 122ms · ALPN 空 10ms · ALPN http/1.1
api5-normal-alisg.mchost.guru(INTL agent) 155ms · ALPN 空 11ms · ALPN http/1.1
growsg-normal.trae.ai(INTL ug) 1.17s · ALPN 空 1.73s · ALPN http/1.1

两侧协商结果都是 TLS 1.3(0x0304)+ TLS_AES_256_GCM_SHA384(0x1302)。 Go 侧 JA3 恒为 f27d2fb55f81674260957aae4330985b(13 套件 / 单 group 4587 / 无 GREASE), utls 侧带 GREASE 与 17 个扩展。ALPN 那列是「Go 根本不 offer」对「我们 offer 且协商到 h1」, 不是失败。

真票据业务矩阵(票据从 .reverse/ 抓包里捞,真客户端出站的那张):

区 指纹 模型表 会话
CN chrome_133 29 条 / 0.63s 出正文 27.6KB / 3.4s
CN go 29 条 / 0.56s 出正文 16.5KB / 2.1s
INTL chrome_133 31 条 / 5.7s 直发 1005;走槽位出正文 5658B
INTL go 31 条 / 5.7s 同上

两种指纹的业务结果一致(模型表条数相同、会话都出正文;正文长度每次不同属正常)—— 换握手层不影响功能,只是把指纹从脚本栈挪到浏览器栈。

4011 的真因是我们自己:6a2e99d 把出站头名统一改成客户端抓包里的小写写法, user-agent 也在内,结果国内通道对所有 CN chat 请求回 4011「exceeded the rate limit」 (国际通道不受影响)。2026-11-01 单变量实测:全小写 4011×3 / 不动 200×3 / 只小写 user-agent 4011×3 / 只改回 User-Agent 200×3。06856c0 把它加回 clientUpperHeaders 并加了 TestWireUserAgentStaysTitleCase 钉住。

这件事的教训写在 wire.go 的注释里:抓包与放行口径不一致时以放行为准。 客户端抓包里确实是 user-agent 小写,但上游按精确 key 匹配,小写进不了放行分支。 同一个 bug 也解释了为什么模型表 / 签到 / 额度三个接口一直是 200 —— 只有 chat 通道按这个头分支。

INTL 的 1005 {plan:4} 是付费档门控,绕法是走 custom_model_gpt-5 槽位 (openai/gpt-5.6-sol),这条能出正文,也是唯一能证明「会话真的产出内容」的路径。

真实 Chromium 客户端的 ClientHello:133 已经够近(2026-11-01)

tools/hellocap 抓了一条本机 Chromium 系客户端的握手(SNI web.tabbit.com), 拿它跟 utls.HelloChrome_133 逐项对:

面 结果
cipher suites(15 个,去 GREASE) 顺序与集合完全一致
supported_groups(4 个) 完全一致:4588 / 29 / 23 / 24(X25519MLKEM768 + X25519 + P-256 + P-384)
extensions(去 GREASE) 133 的 16 个是子集,只差一个 51764(0xca34,私有扩展)
GREASE / shuffle 两侧都有;该客户端还开了 extension permutation(三条抓包扩展顺序全不同)

这推翻了此前「133 顶替 142 差很多」的判断。 Chromium 124 → 142 之间 ClientHello 的骨架没变,差的只是私有扩展与扩展顺序随机化。所以:

  • 不需要再为「拿到 142 的 spec」专门抓包;
  • 真要补,只差 51764 这一个扩展,且它是该客户端的私有扩展,Trae 未必发 —— 加错了反而是新的指纹差异,所以没有内置。

tools/hellocap 的两条更正(都是实测踩出来的):

  • JA4 的 c 段要求 extensions 与 sigalgs 排序后再哈希,b 段保持原顺序。 不排序的话,开了 extension permutation 的客户端每次算出不同的 c 段。
  • 生成的 spec 里 ECH(65037) 默认注释掉:它的 config 带 HPKE 公钥,原样搬等于 每次握手发同一把公钥,比不发更糟。key_share 同理,Data 留空让 utls 现生成。

抓 Trae 本体的限制:Trae CN.exe 在非交互 shell 里起不来(debug.log 只写到 SetPartitionAllocOomCallback 就断,进程随即消失),必须由桌面会话手动启动。 抓包桩本身没问题 —— 抓到的那条就是同机 Chromium 系的真实握手。

顺带从 debug.log 的 AppManifest 里确认了两件头相关的事: ahaNet.ttnet_params.bypass_proxy=false、access_policy.domain_white_list 覆盖 *.mchost.guru / *.trae.cn / *.trae.com.cn,与抓包里的目标域一致。

抓真实 ClientHello:tools/hellocap

上面的指纹是拿 utls 的 HelloChrome_133 顶的,而客户端 UA 是 Chrome/142。要真正复刻 142 的握手,得先有它的 ClientHello 字节 —— 抓这个不需要证书、不需要解密,握手第一步 就能断开。

# 1) 编译并起桩(默认监听 127.0.0.1:8443)
go build -o hellocap ./tools/hellocap
./hellocap -addr 127.0.0.1:8443 -out ./cap-out -once -timeout 30s

# 2) 把系统代理指到 127.0.0.1:8443,然后启动 Trae 客户端、随便点一次对话
# 3) 桩会把首条 ClientHello 落四样东西:
#      hello-1.bin     原始字节
#      hello-1.hex      hex(肉眼核对用)
#      hello-1.json     解析结果(套件 / 扩展 / group / sigalg / key_share)
#      spec-1.go.txt    可直接编译的 utls ClientHelloSpec

桩同时支持两种入口:走 HTTP 代理的 CONNECT host:443(先回 200 再读 TLS)和透明代理/ 直连(第一个字节就是 0x16)。抓到就关连接,客户端会报握手失败 —— 那是预期的。

输出里会带一份与 HelloChrome_133 的结构 diff(套件顺序、扩展顺序与集合、groups、 sigalgs、key_share 组、ALPN、两个 JA3/JA4),差在哪一眼能看到。

三个踩过的坑(都写进代码注释了):

  • supported_versions 的前缀是 1 字节长度,不是 groups/sigalgs 那种 2 字节。 读错会把 0x0304 变成 0x0403。
  • JA4 的三处计数(版本、套件数、扩展数)都要先滤掉 GREASE。Chrome 系会在 supported_versions 里塞 0xbaba,不过滤的话最大值变成 47770,版本直接算成 00。
  • 生成的 spec 里 KeyShareExtension 的 Data 必须留空 —— utls 会为每个 group 现生成密钥。把抓到的公钥钉进去,等于每次握手都发同一把,比不伪装还糟。

生成完怎么用:把 spec-1.go.txt 里的 capturedSpec 贴进 internal/upstream/tls.go, 加一个档位,然后跑 TestBuiltSpecStillHandshakes(它会用这份 spec 真握手一次)—— 红了就是 spec 构造有问题,别直接上生产。

ug 族(签到 / 积分 / 额度)也分地区

UgHeaders 原先对国际账号发的是国内那套头,2026-10-30 补上了分叉。两区实测差异:

面 国内(api.trae.cn,v2 路径) 国际(growsg-normal.trae.ai,v1 路径)
package-type stable_cn stable_i18n
app-version 3.4.11 3.6.4
user-agent VSCode 1.107.1 (Trae CN) VSCode 1.107.1 (Trae)
x-user-region CN 客户端发 SG,但那是账号所在区(US/SG/…),本仓无数据源 → 不发
x-lscbd-aid / -platform 787976 / windows 不发
store-country-code(-src) 不发 客户端发 kr / uid,同样是账号属性 → 不发
x-market-user-id / x-device-id 按 UID 派生的伪身份(两区一致,不动) 同左

关键实测:两区 ug 接口都不按这些头分支 —— 拿国内头打国际网关、拿国际头打国内网关 都是 200 且响应逐字一致。所以分叉是为了身份一致性,不是功能必需;缺 x-user-region / store-country-code 也不影响鉴权(发错了才是明确的错误信号)。

国内版 UA 里那个 (TRAE SOLO CN) 是唯一故意没动的旧值:客户端现在是 (Trae CN), 但 9074 是设备维度的风控、claim 又是写操作,没有可靠复现就不碰。实测 checkin/status 两个 UA 都 200 且响应一致,说明鉴权层不按 UA 分支;真要改,先拿一个号验 claim。

双区联调:

TW2A_CAP=<CN抓包jsonl> TW2A_CAP_INTL=<国际抓包jsonl> \
  go test -tags wirecheck ./internal/upstream -run TestUgHeadersLive -v

实测输出(2026-10-30):

INTL ent_usage v1: status=200 code=0 dollar=true 套餐数=1
CN  checkin status: status=200 {"checked_in":true,"code":0,"credits":100,...,"message":"success"}

UgHostIntl 保留 api-sg-central.trae.ai(与登录回调的 ApiHost 同域);客户端自己打的是 growsg-normal.trae.ai,两台实测都 200 且响应一致,所以没有硬切。

实测输出(2026-09-27;可见 是 13 还是 15 随 qwen* 灰度浮动):

===== 版本码矩阵(get_detail_param, function=solo_work_lite),账号 uid=253358232317424
  ver / code          表   可见  k3 门控                          阵容变化
  0.1.43 / 20260716    39   13  identity_list":[2,3,100]}        旧档
  0.1.48 / 20260811    42   13  identity_list":[0,5,1,2,3,100]} +[deepseek-v4.1-flash glm-5.3 step-5-preview]
                                                              −[DeepSeek-V4-Flash DeepSeek-V4-Pro glm-5]
  0.1.51 / 20260814    42   13  identity_list":[0,5,1,2,3,100]} (与上一档逐字一致)
  0.1.52 / 20260821    42   13  identity_list":[0,5,1,2,3,100]} (与上一档逐字一致)
  0.1.60 / 20260927    42   13  identity_list":[0,5,1,2,3,100]} (与上一档逐字一致)
  1.0.0  / 20261231    42   13  identity_list":[0,5,1,2,3,100]} (与上一档逐字一致)

===== 用最新档身份(0.1.52 / 20260821)真发 kimi-k3
  HTTP 200,流开头: event:metadata data:{"session_id":"e3841837-…","prompt_cache_pool_record":null}

复跑:TW2A_PROBE_CHAT=1 go test ./internal/upstream -run TestProbeLiveEffortAB -v(吃额度;档位清单 TW2A_PROBE_EFFORTS=、模型 TW2A_PROBE_MODEL=、样本 TW2A_PROBE_ROUNDS=、上限 TW2A_PROBE_MAXTOKENS=)。 换模型即换 TW2A_PROBE_MODEL=;矩阵是一条 shell 循环每个模型跑一次,逐模型累积日志。

可签到、刷新剩余积分、全部保活(立刻全账号刷 token,不吃积分)、禁用/启用、解除冷却、移除, 以及粘贴登录回调后立刻写入 auths/trae-{uid}.json 并进池,不用重启。

「登录账号」拿到的授权链接里,auth_callback_url 恒为 http://127.0.0.1:18080/authorize,写死不做配置项: 实测 Trae 只认它自己 IDE 的这一条,换任何别的地址(面板自己的路径、别的端口、公网隧道域名)授权页 直接「登录失败 / 网络错误,请刷新页面重试」,登录都进行不下去。所以回跳必然落在浏览器那台机器的 127.0.0.1:18080 上(那里没人监听,页面打不开是正常的),只能把地址栏整段粘回面板换票。 想免粘贴就得在浏览器那台机器上跑个监听 18080 的本机中继(面板在服务器上时它够不着服务器的回环地址)。

保存配置后立即生效:冷却/熔断/降权/在途/闲置/快过期、会话粘性开关与 TTL、提示词模式与文件、 默认模型、超时与出站身份。需重启:listen、auth_dir、state_file、整个 schedule (时点与开关)、session_sticky.gc_interval——面板保存时会明确列出,不会假装已生效。

「用量」视图:按窗口(24h/72h/7 天/30 天/全部)汇总请求数、失败数、输入/输出 token、 平均延迟与吐字速率,可按模型、按账号、按时序下钻。失败尝试也计入请求数——重试放大正是 靠这一列才看得见。数据落在 data/usage.json(与 state_file 同目录,30 秒防抖落盘)。

国际版(Trae 国际)

国内版与国际版是同一套 API 的两套域 + 两套身份版本组,所以入站协议、SSE 事件、function=solo_work_lite 全都不变,只有域和版本号不同(实测 2026-09-28,见下表)。账号按 auth.domain 自动分地区(auth.Realm()), 不需要开关:

面 国内版 国际版
agent host(对话 / 模型表) api5-normal.mchost.guru(2026-10-15 起;旧的 trae-api-cn.mchost.guru 仍活但已是遗留入口) coresg-normal.trae.ai(2026-10-30 抓包更正;客户端逻辑域名,传输层 dispatch 到 api5-normal-alisg.mchost.guru。旧的 a0ai-api-sg.byteintlapi.com 仍活但已不是客户端在用的那台)
OAuth host api.trae.com.cn 登录回调里的 host 参数(本次实测 api-sg-central.trae.ai)
登录页 www.trae.cn/authorization www.trae.ai/authorization(同参数集,x_app_version=1.0.2)
模型名 glm-5.2 / kimi-k2.6 … gpt-5.2 / gpt-5.4 / kimi-k3(gpt-6-sol/gpt-6-luna 见下)
签到 / 积分 /trae/api/v2/ug/* 签到没有(两个国际域上 404,国际账号跳过签到);额度接口有,见下

国际版是美元计费,不是「免费无限」

2026-10-15 更正:本节此前写「国际版没有积分接口、跳过余额刷新」,是错的。 真相是国际账号有额度接口,只是口径与国内不是同一套——拿积分字段去读它,读出来是 0, 于是面板显示「无额度」,看起来像接口坏了。

上游在同一族接口上按账号返回两套计费体系,靠顶层 is_dollar_usage_billing 区分 (实测:国内 false + is_credits_billing:true;国际 true):

国内版 国际版
接口 api.trae.cn/trae/api/v2/pay/ide_user_ent_usage ug-normal.trae.ai(或账号 api_host)/trae/api/v1/pay/ide_user_ent_usage
额度字段 quota.credits_limit quota.basic_usage_limit
已用字段 usage.credits_amount usage.basic_usage_amount
计量单位 积分 美元
免费档 有积分的几条,加总即余额 Free plan:basic_usage_limit=1 → $1 免费额度

实测国际 Free plan 的完整额度报文(ug-normal.trae.ai,2026-10-15):

{"is_dollar_usage_billing": true,
 "user_entitlement_pack_list": [{
   "display_desc": "Free plan",
   "entitlement_base_info": {"quota": {"basic_usage_limit": 1, "credits_limit": 0,
                                       "advanced_model_request_limit": 1000,
                                       "premium_model_fast_request_limit": 10,
                                       "premium_model_slow_request_limit": 50}},
   "usage": {"basic_usage_amount": 0.00059, "credits_amount": 0}}]}

⇒ 免费档不是「无限免费」:$1 额度,且另有请求数上限(高级模型 1000 次、 高级模型快速档 10 次 / 慢速档 50 次)。面板按美元显示这一列:

额度列(国际号)  $0.99941 / $1        已用 $0.00059 · 剩 99.9%
额度列(国内号)  5466 / 5900

两个口径不可换算,所以面板任何地方都不把它们相加:账号表分列、 KPI 里「积分合计」只算积分号、国际额度单列一张卡、到期分布图只画积分(国际号单列计数并注明)。

未证实:面板转发的调用会不会吃掉这 $1

没验证成功,如实记着。 用面板打了 4 次国际模型调用(共约 2300 token)之后:

  • basic_usage_amount 仍是 0.00059,没动;
  • 上游逐会话用量接口 POST /trae/api/v1/pay/query_user_usage_group_by_session (usage_type:[5,6],近 7 天)只返回 1 条 —— 是用户自己客户端 19:56 那次 (DeepSeek-V4.1-Flash、mode:"Max"、in 13370 / out 8、$0.00005), 面板的调用一条都没登记。

可能的原因(都还没证):免费档按请求数而不是美元扣;或该台账只记客户端直连的用量; 或结算有较长延迟。在弄清之前,不要把 $1 当成面板用量的硬约束, 也别假设面板能把国际号用光。要确认只能在更长时间尺度上再对一次(面板跑一整天后比金额)。

顺带:那份逐会话台账带 mode/use_max_mode 字段,是 Max 模式被上游真实计量的独立佐证。

口径键名只准有一个(踩过的坑)

账号级口径在两个接口上必须用同一个键名(现统一为 credit_unit)。 踩过的坑:/panel/api/overview 发 credit_unit、/panel/api/packages 曾发 unit, 前端 isUSD() 只认前者 → 在「积分构成」视图里美元号被判成积分号,整张卡显示 0 (0 · 共 0 · 1 个包 · 占最高 0%),不报错、不写日志、单测全绿 —— 因为单测喂的账号对象带的是 credit_unit,只有真浏览器渲染才看得出来。 回归网:internal/panel/packages_unit_test.go::TestPackagesUnitKeyMatchesOverview。

gpt-6-sol / gpt-6-luna(官方门控 + 旁路):这两个在模型表里带 display_contact_config.access.identity_list=[4,1,2,3],免费档(identity 0/5)不在内,直接发回 1005 {"plan":4}(要付费档;与客户端版本码无关,试过 6 个版本码门控一字不变)。但同一批模型在租户 自定义槽位里能走:config_name=custom_model_gpt-5 + model=openai/gpt-5.6-sol|luna 实测出正文, 代理已给这两个名字挂了旁路(intlSlotRoutes),所以 intl:gpt-6-sol 直接可用。 槽位是租户配置,换账号可能没有——没有就照常报上游错误。其余 custom_model_* 槽位(gemini / kimi / minimax / claude 系列)在这台账号上全回 4023,是租户没配那些 provider,不是模型不存在。

work.trae.ai 网页端列出的 11 个名字不在同一通道上,出站 function 现在跟着模型走 (fnByModel,客户端不用关心):

网页端名字 内部模型 / 通道 状态
Trae Auto Model auto → 该地区旗舰(国际 gpt-5.2) 可用
GPT-6-Sol · GPT-6-Luna gpt-6-sol / gpt-6-luna(工作通道,走槽位旁路) 可用
GPT-5.4 · GPT-5.2 · Kimi-K3 同名,工作通道(solo_work_lite) 可用
Gemini-3.1-Pro-Preview gemini-3.1-pro,coder 通道(solo_coder) 通道已接;本账号受上游速率/额度限制,忙时报 4011
Gemini-3-Flash-Preview gemini-3-flash-solo,coder 通道 同上
MiniMax-M2.7 minimax-m2.7,coder 通道 同上
Kimi-K2.5 kimi-k2.5,coder 通道 同上
MiniMax-M3 这张账号的两张表里都没有(网页端列着,API 侧不下发) 照实 400

coder 表怎么来的:同一台 agent 主机、同一个 /api/agent/v3/llm_utils_chat,只把 body 里的 function 换成 solo_coder。网页端(work.trae.ai)自己的模型接口 GET /api/remote/v1/models 用同一个票据就能读,列出的 function=solo_coder 组正是那 6 个名字 (/api/remote/v1/* 是它完整的会话/沙箱 API,反代只用到 /models 做对照,没接那套)。

加账号:面板「添加账号」里选「国际版」(链接自动切 www.trae.ai),或 REALM=intl ./login.sh。 回调链接里自带 host,换票与落盘按它写 domain/apiHost,不用手填;换票后 apiHost 也会随 token 轮换一起用。

调模型:模型名带地区前缀,/v1/models 列出的 id 就带(cn:glm-5.2、intl:gpt-5.2),原样回传即可; 不带前缀按国内版走(老客户端零变化),但名字在模型表里属于国际版的(如 gpt-5.2)会自动落到国际账号。 选号按前缀过滤——国际模型只发国际号,反之亦然,所以国内号不会被 intl: 请求误伤。

别名层(客户端自带模型名):Claude Code / Cursor / Cline 把 claude-*、gpt-4o 写死在配置里, 这里照社区现成实现的做法把它们落到真模型上(internal/server/model_alias.go,别名表是唯一改动点; 口径参考 ZedeX/trae-local-api 的 model-config.json 与 linqiu919/trae2api 的 convertModelName): claude-*(含各种日期后缀)→ 该地区旗舰;gpt-4o / gpt-4o-mini / gpt-4.1 / gpt-3.5-turbo → 国内 glm-5.2、国际 gpt-5.2。未知名字仍然 400,不会静默塞默认模型。

国际版的 X-App-Version-Code 必须非空:缺了上游直接 4001 missing required parameter(实测), 代码里由 identFor() 按地区给。用量台账暂不区分地区(workbuddy2api-panel 有,按需再加一列)。

目录结构

cmd/server/       HTTP 服务(config + main)
cmd/signin/       批量签到工具
cmd/credit/       积分查询工具
assets/           README 图标等静态配图
internal/auth/    auth 文件解析/原子写回
internal/upstream/ Trae 上游客户端 + SSE 转换
internal/pool/    账号池(冷却/禁用/积分)
internal/scheduler/ 定时签到 + token 保活
internal/server/  三种入站协议(chat / responses / messages)+ 共用请求链路
internal/panel/   /panel/ 管理页(go:embed)
internal/session/ 会话粘性(会话 → 账号 绑定表)
internal/prompt/  系统提示词(内置默认 + 文件覆盖 + 改写)
internal/usage/   调用用量台账(分桶 + 持久化)
login.sh / signin.sh / credit.sh  运维脚本
auths/            (gitignored)trae-*.json 凭证
data/             (gitignored)state.json 池状态、usage.json 用量台账

脱敏说明

  • 任何真实 token/key 一律 ******** 或 env 引用,绝不落盘 git。
  • auths/、data/、config.json、.env、*.key、*.pem 全部 gitignored。
  • docs/ 不参与上传(gitignore + dockerignore)。
  • 验证类文档(VERIFICATION.md 等)不入库。
  • 日志/状态输出只显示 UID/Nickname/积分,不打印 token。
  • 抓包夹具(internal/upstream/testdata/)是上游响应报文原文:入库前把 uid / 订单号 / 设备号这类 12 位以上数字统一掩成 1000…0,计费与时间字段原样保留, 见 ent_usage_unit_test.go、domains_test.go、max_variant_test.go。

安全与合规

  • 非官方、未获授权:本文与上游服务商无任何关系,仅用于本人授权账号在本机 / 私有环境下的协议互操作。
  • 凭证:auths/*.json 以 0600 明文保存,目录不得进入备份、同步盘或共享范围;仓库不含任何账号、token 或密钥。
  • 鉴权:api_key(或 api_keys)为空时 /v1/* 与 /panel/api/* 不鉴权,仅适合 127.0.0.1 或可信内网;公网部署必须设密钥并前置 HTTPS 反代与访问限流。
  • 合规边界:不得用于搭建对外中转站、共享他人账号、批量注册小号或售卖额度;作者不承担使用后果。

血统与致谢

  • 本仓 fork 自 Sliverkiss/traework2api(原始仓库已下线)。 同血统的兄弟实现另见 JeffHu0912/trae2api 与 connectedGraph/trae2api-web(均 MIT)—— 模型表、版本码、 错误码这些实测结论与它们的笔记互为参照;对方实现更小或更旧,未直接合并其代码。
  • 只当对拍基线、不进仓的参考实现:shadyrispy/cpa-multi-plugins(答案卷)、240xu/trae2api-more、 star620/TraeTools、trae-workbuddy-switch。
  • 没有 license 的项目(如 wefewe/trae2api)只读参考,不取代码。

License

MIT,见 LICENSE。

About

TRAE 的 OpenAI 兼容反向代理:多账号轮转、自动签到、token 自动刷新,纯 Go 零依赖。

Resources

Stars

48 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages