把 Trae 免费对话通道包成 OpenAI / Anthropic 兼容 API 的多账号网关 · 附 Web 管理面板
三协议入站 · 多账号轮转 · 自动签到 · Token 保活 · 会话粘性 · 截断自动续写 · 断流四条防线 · 单二进制 · 零第三方依赖
本项目是 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 管理面板 | 内嵌单页面板(明暗主题),账号运维 / 模型档位查询 / 在线改配置(热生效)/ 运行日志 / 用量台账,见 管理面板 |
# 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.(对照有区分力)。
# 依赖 Go 1.22+
go build -o tw2api ./cmd/server
./tw2api # 监听 :7864,auths/ 目录读取凭证;密钥在 config.json 的 api_keyconfig.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 域名,而且面板看不出征兆:官方客户端从不写死具体后端,它请求一个逻辑主机名
(国服 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):
GET https://tnc0-aliec2.zijieapi.com/get_domains/v5/(国服)/https://tnc-boot.trae.ai/...(国际) —— 无需鉴权。注意cronet_version/ttnet_version两个参数不能省,省了返回 0 条规则。- 从
data.ttnet_dispatch_actions[].param.host_group[]里挑出候选:国服 = 无区域段 (api5-normal.mchost.guru),国际 = 带区域段(api22-normal-alisg.mchost.guru)。 同表的trae-api-cn.mchost.guru之类不是分片后端,会被排除。 - 逐个探活(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 上下文」开关(关着时窗口 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 就是骗客户端。
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。
如实忽略并留一行 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 上游矩阵也全正常,复现不出来。原因未明,可能是上游瞬时状态或满预算的思考跑飞,继续观察。
社区流传「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_v321 个里 1 个(Doubao-Seed-Code);solo_agent21 个里 9 个(DeepSeek 三兄弟、 MiniMax-M3 两兄弟、qwen3.8-flash / qwen3.8-max / qwen-3.7-plus、Doubao-Seed-Code);solo_coder12 个都不带。 -
带配置 ≠ 会跑:裸 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 的小请求差值被取整吃掉,才显得「延迟十几分钟」。别拿整数位当计费依据。
面板侧因此做两件事(都只影响出站,不改客户端):
*_advisor*槽位(glm-5.2_advisor_doubao这类「基础模型 + 专家」绑定槽)不进官方模型表, 国际版按「有显示名」放行也挡(upstream.isAdvisorSlot);- 客户端若声明了名为
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.5s 写一条
: keepalive,首帧之后回到 15s。 原来只有 15s 单档,比客户端的耐心大一个数量级(同类客户端 Codex/ZCode 线上实测四次「零内容 + client_gone」全落在 3.46~4.09s),那段静默里一个字节都不写,客户端就自己关连接。两档节拍是常量, 唯一调整入口是upstream.SetKeepaliveIntervals(head, idle)(idle<=0关闭)——不进配置:这是个 一调错就全线断流的旋钮,不面板化。 - 心跳不堵换号:换号的判据从「写过任何字节(含注释帧)」改成「写过数据帧」(
chunkWritten)。 注释帧只提交响应头,客户端还没拿到内容,此时换号后第二段的帧接在同一条 200 上、客户端无感—— 空流重试与首帧错误否决因此都还在,不会被 1.5s 心跳吃掉。 - 已提交流的错误降级(
ssecommit.go):心跳提交 200 之后若全部号都失败,旧行为是往 SSE 流里追加 一段裸 JSON(实测形态:: keepalive×6 紧跟{"error":{...}}),客户端当成脏帧忽略或解析报错—— 看起来同样是「莫名断流」。现在已开口就只写协议内event: error+data: [DONE](OpenAI 对象信封, SDK 能抛 APIError),未开口仍是真实状态码 + JSON。 - 预告句救援(
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会拦住改动)。
把两版客户端装在同一台机上,用本地 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)。
第一轮只对齐了「发什么头」,第二轮把抓包按链路拆开做集合差集,发现少的是两层:
报文形态(大小写 / 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。这条差异无法用「头对齐」消除, 只能实现那套加密 —— 属于另一件事。- 请求体加密:同上,是最硬的一条差异(见本文前面的结论)。
请求头是 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)。
客户端 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 快照/字节码里,要么做了字符串混淆运行时解密。 继续挖只有两条路:
- 动态调试(Frida hook
BRs或 native 导出函数、x64dbg 跟llm_utils_chat的 构造栈),能拿到算法与密钥;但密钥大概率是设备绑定 + 服务端下发,换号就不通用。 - 改 mitm 不截断重抓,先确认完整密文的长度关系(是否 = 明文 + 固定开销), 再判断是不是标准 AEAD。
优先级判断:低。 上游目前接受明文 body(README 前面记过 wirecheck 实测 200, 本仓真账号也一直用明文跑通),所以这条差异只影响「包像不像」,不影响可用性。 真正的风险是上游某天把校验从「可选」切成「强制」—— 那是服务端开关,不是本仓能提前 解决的问题;真到那天,上面的动态调试也得重做一遍。
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 字节原样保留。
中间连着踩了两个坑,都记在这里免得重来:
- 把 padding 当成 Tengine 的雷。加 padding 后
api.trae.cn报handshake failure, 一度判成「阿里 Tengine 不认 padding」并把默认档退回chrome_133。实际元凶是specWithALPN里对 utls 预置档(ID 档)也做了 KeyShare 重建,把 spec 改坏 —— 修掉之后 padding 和无 padding 都能握。 - 拿单次握手当结论。这台网关在连续握手时会抖,同一条 spec 前一秒成功后一秒
handshake failure。TestAhaStabilityTraeCN连打 6 次统计:aha 带 padding 6/6、 去 padding 6/6、chrome_1336/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两个带 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),这条能出正文,也是唯一能证明「会话真的产出内容」的路径。
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,与抓包里的目标域一致。
上面的指纹是拿 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 构造有问题,别直接上生产。
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 秒防抖落盘)。
国内版与国际版是同一套 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 里「积分合计」只算积分号、国际额度单列一张卡、到期分布图只画积分(国际号单列计数并注明)。
没验证成功,如实记着。 用面板打了 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)只读参考,不取代码。
MIT,见 LICENSE。