Files
ModelRouter/docs/channels-parity.md
JianFeeeee 3e94047889 docs(channels): 三线对齐表(登录/模型获取/接入模型服务)逐平台实录
按参考仓的命名规律(X-auth / X-product / X-adapter)建表,记录:
- 本轮修的四件事(codearts 登录、模型兜底表、chat 端点、gemini 私有协议)
- 三个**未闭环**项并写明理由(codearts 模型获取需 AK/SK 签名、
  trae 载荷需转 SOLO 格式、gemini 信封需字母序 + 身份五头)
- 明确不做的四项(gemini-sigstore 不适用、qoder-wasm/zcode-captcha/
  captcha 池属 B 类)
- 本机实测的 5 个坑
2026-10-13 17:04:09 +08:00

9.0 KiB
Raw Blame History

channels 插件与参考插件的三线对齐表

参考仓:Ebony-Vinyl/dsh-our-free-model(本地克隆 /tmp/pi-github-repos/Ebony-Vinyl/dsh-our-free-model),关键实现位于 vendor/channel-pack/src/(144 个 TS 文件)。

命名规律(照它逐厂商对齐的三条线):

后缀 职责 我们要对齐的东西
X-auth.ts / X-oauth.ts / login.ts 登录 登录流程、端点、参数、凭据形状
X-product.ts / models.ts 模型获取 远端端点、解析路径、兜底表
X-adapter.ts / llm-adapter.ts 接入模型服务 chat 端点、载荷转换、鉴权头

一、登录(line ①)

平台 参考流程 我们的 flow 状态
trae OAuth 17 参数 + 设备身份派生 callback ✅ 逐字节对齐(含 login_trace_id / machine_id / x_device_*)
minimax device-poll device-poll ✅
cline device-poll device-poll ✅
buddy state-poll(无回调) state-poll ✅
gemini OAuth + client_secret callback ✅(含 client_secret、localhost 主机、6 scope)
lobsterai 本地回调 + authCode 换 token callback ✅(含动态版本号、3 步活动领取)
opencode paste paste ✅(参考本身即粘贴凭据)
codearts PKCE + DPoP ES256 + STS 换 AK/SK callback ✅ 本轮修复(此前误标 paste)
qoder device-poll + 设备身份 device-poll ✅(含 Cosy-MachineToken/Type 成对)
zcode init-poll init-poll ✅
loomy 微信扫码 poll wechat-poll ✅
raccoon 二维码 poll qr-poll ✅

codearts 登录(本轮修复的重点)

此前标成 flow="paste"(手填凭据)是错的。参考 login.ts / oauth.ts 默认走新式 IAM OAuth:

  1. 起本地回调服务器,端口须 ≥10000(MIN_CALLBACK_PORT,低于此 portal 拒绝), 路径 /oauth/callback
  2. 打开 https://codearts.huaweicloud.com/portal/authorize,参数逐项: theme=2、locale=zh-cn、uri_scheme=codearts-agent、client_id=codearts-agent、 code_challenge_method=**SHA-256**(不是 RFC 的 S256 —— 错值会让 portal 回退旧 ticket 流程,看起来"也能用"但不是 OAuth)、ticket_id、 plugin-name=snap_AIIDE、plugin-version=5.2.0; 不得含 auth_callback_url(会被判异常并回退)
  3. 回调收到 code 后,带 DPoP 证明向 https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens 换 credentials{access_key_id, secret_access_key, security_token, expiration}
    • refresh_token
  4. 凭据持久化须含续期材料:refresh_token / code_verifier / dpop_private_key_jwk

DPoP 细节:ES256 JWS,header={alg,typ:dpop+jwt,jwk}、 payload={htm,htu,iat,jti};签名是 raw r||s 64 字节,而 openssl 输出 DER ——必须转换格式,否则服务端验签失败而字符串看起来完全正常。

终态判定只认 invalid_grant / ErrorCode 含 ExpiredRefreshToken; 不认 InvalidDPoPHeader(那是一次请求层面的拒绝:时钟偏差、proof 重放、 网关抖动都会触发,当终态会把完好账号一步标死)。

旧 ticket 流程(CODEARTS_LOGIN_BASE + /snap-manager/v1/login/ticket 轮询) 保留作回退。

二、模型获取(line ②)

远端优先、兜底补位的机制已具备(models_remote + effectiveModels)。 本轮把兜底表逐平台补成参考全量:

平台 参考来源 兜底数 状态
trae TRAE_FALLBACK_MODELS 28 可见 + 4 hidden ✅(hidden 单列 models_hidden)
minimax GET /mavis/api/v1/models?region=cn&buildEnv=prod 4 ✅
cline GET /api/v1/models + recommended-models 3 ✅
buddy (无独立表) 7 ✅
gemini — 2 ✅
lobsterai GET /api/models/available(须带 model_flag) 19 ✅
opencode GET /v1/models 14 ✅(含 models_free)
codearts opengw gateway/config + /v1/model/builtin 3 ⚠️ 见下
qoder /api/v2/model/list(需 WASM 签名) 19 ✅(含 CN 独有 q37fmodel/gm51model)
zcode GET /api/v1/client/configs 2 ✅
loomy GET /api/v1/models(取 type==='chat') 8 ✅
raccoon 远端 + 兜底 6 ✅

trae 的 isHidden 语义:browser_use_subagent / explore_sub_agent_v13 / explore_sub_agent_v2 / summary 是内部子 agent,不进用户可选列表。 model_list 已按此过滤(models_hidden)。

opencode 的 isFree:参考明确标注 7 个模型匿名可用、其余需付费 key。 此前 model_list 把 isFree 硬编码为 false,等于把官方免费模型标成付费。

codearts 模型获取(未闭环)

参考 models.ts 从两个端点拉:

  • OPENGW_GATEWAY_CONFIG_URL(opengw.developer.huaweicloud.com/api/v1/gateway/config) → result.models,这是 benefit(免费额度)模型权威来源
  • SNAP_MODEL_BUILTIN_URL(/v1/model/builtin)→ builtinModels,常规模型

两者都要 AK/SK 的 SDK-HMAC-SHA256 签名。另有 normalizeModelId (去尾部 -NNNN 日期版本)与 isCodeArtsBenefitModel(决定 chat 是否带 maas_type: benefit,不带则 404 model is not registered)。 这部分尚未实现——我们的 codearts 目前只有 3 个硬编码模型名。

三、接入模型服务(line ③)

本轮发现的最严重问题:源注册时 base_url + adapter.endpoint 拼出的 chat URL 全部缺路径前缀,11 个平台里 11 个都不对。已逐个修正:

平台 正确 chat 端点 修正前
trae …/api/agent/v3/llm_utils_chat …/chat/completions
cline api.cline.bot/api/v1/chat/completions api.cline.bot/chat/completions
lobsterai …/api/proxy/v1/chat/completions …/chat/completions
qoder api2-v2.qoder.sh/model/v1/chat/completions …/chat/completions
buddy copilot.tencent.com/v2/chat/completions …/chat/completions
codearts …/api/v2/chat/completions …/chat/completions
opencode opencode.ai/zen/v1/chat/completions opencode.ai/zen/chat/completions
raccoon …/api/web/llm/v2/chat/completions …/chat/completions
loomy loomy.xunfei.cn/chat/completions loomy.xunfei.cn
zcode zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages zcode.z.ai/v1/messages
gemini /v1internal:generateContent(私有动词) /v1beta/models/{model}:generateContent(公开,凭据不匹配)

未闭环:trae 的载荷转换

trae 不是 OpenAI 兼容。参考 trae.ts:30 注释: 「载荷必须转换:OpenAI 的 {model, messages, tools, tool_choice, stream} 需要 映射为 SOLO 格式(function、config_name 等字段),不能透传」; model 还要映射到 config_name(查远端模型列表)。

我们有一个 internal/lua/adapters/trae.lua,但它描述的是 trae-local-api(本地 OpenAI 兼容代理,127.0.0.1:19900),不做 SOLO 转换(grep config_name = 0 命中),且平台声明用的是 adapter = "openai"。 所以 trae 的端点现在对了,载荷仍然错。这是一块独立性工作。

未闭环:gemini 的私有协议信封

端点是 /v1internal: 了,但参考 gemini.ts 还有三件必须保留的细节:

  1. 身份五头逐字写死、不许随机(且不带 x-goog-api-key / x-goog-api-client)
  2. 流式请求刻意不带 Accept 头
  3. 信封逐层字母序序列化(Go 的 encoding/json 对 map 自动字母序,TS 的 JSON.stringify 按插入序 —— 不排序字节就与上游不同)

四、明确不做的(附理由)

参考模块 行数 为什么不做
gemini-sigstore.ts 268 我们推理走无状态适配器,插件不参与推理 → 插件侧缓存会成为死代码 ⛔
qoder-wasm.ts 1442 需随插件分发 298KB wasm 二进制;B 类,待立项
zcode-captcha.ts 2212 阿里云滑块,需真实浏览器/验证码链路;B 类
captcha-*.ts 1223 同上

五、本机实测踩到的坑(都已修)

  1. xxd 不存在 —— command -v xxd 为空,用它取十六进制会静默得到空串。 改用 od -An -tx1。
  2. openssl ec -text 会截断 32 字节 priv(实测只给 30 字节)→ d 必须用 openssl asn1parse 的 [HEX DUMP];tail -c 32 拿到的是 SEC1 结构里 公钥的 y,不是 d。
  3. Lua local 前向引用:local function 只从定义处向后可见。hexToBin / b64url / b64urlRaw 被 CodeArts 的 PKCE/DPoP 调用,必须移到 CodeArts 块 之前(本文件累计踩过 6 次)。
  4. min_port 参数读到 argv[2] 与 out_path 冲突 —— 回调脚本的第 5 个 参数才是 minPort。
  5. Lua 双引号串里的 \1 是转义坑(sed 's/…/…/\1/' 会报 invalid escape sequence)→ 改用 awk -F': ' '{print $NF}'。