dsh-connect-trae 2.5.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,123 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.1 (2026-10-05)
4
+
5
+ > **修复「Max 档随机丢失、同一模型上下文窗口在多次刷新间漂移」**([#23](https://github.com/dingminhua/dsh-connect-trae/issues/23),报告人 JiewiW)。
6
+ >
7
+ > 这是 **2.6.0 引入/放大的缺陷**:那一版把发现侧从 2 个货架扩到 7 个,却保留了「先到先得」去重——而同一个模型在多个货架里的**容量并不相同**,货架顺序又**每次调用都不同**。结果同一模型这次是 `116000`、下次是 `256000`、再下次才带 1M Max,用户配置的 1M 预算被随机忽略。
8
+
9
+ ### Bug Fixes
10
+
11
+ - **跨货架去重改为「保留容量最强的行」**(`TraeSoloRemoteCatalogClient.fetchModels`):
12
+ - **根因**:remote `/models` 按 function 分组,同一模型 id 出现在多个组里,各组报告不同容量。实测(本机 CN 凭证,2026-10-05)同一次响应里 `Doubao-Seed-2.1-Pro` 的各组值:`builder_v3` → `116000`/无 Max;`solo_work_remote` / `solo_work_lite` → `256000`/无 Max;`chat_v3` / `solo_agent` / `solo_agent_remote` → `256000`/**`1000000`**(`max_mode: true`)。
13
+ - **为什么旧写法必错**:原来的去重是 `seen` Set 的「第一次遇到就锁定」,而我当时的代码注释还写着「保持网关自己的组顺序稳定」——**那个假设是错的**:连打 5 次实测得到 **5 种不同的组顺序**。于是容量的发布值取决于哪一组恰好先回来。`applyContextBudgets` 只能在 `maxContextWindow` 存在时升窗,所以丢失该字段会让配置了 1M 的模型被静默压到 256K 或更低,DSH 因此在远早于用户设定的阈值处反复压缩。
14
+ - **修法**:按 ① 带 Max 档优先 ② 都有 Max 取更大 ③ 都无 Max 取更宽的 dev 窗口 的顺序择强;平手保留先到者(模型顺序仍由网关决定)。同一 id 的各行在 `multimodal` / `reasoning` / 积分倍率上完全一致(同日实测逐字段核对),因此按容量择强不会丢失能力信息。
15
+ - **实测对比**(本机 CN,连打 6 次实时目录,同一预算配置):
16
+ | | 修复前 | 修复后 |
17
+ | --- | --- | --- |
18
+ | `Doubao-Seed-2.1-Pro` 的取值 | `116000/无`、`256000/1000000`、`256000/无` **三种漂移** | 6/6 稳定 **`256000/1000000`** |
19
+ | 整目录带 Max 的模型数 | 11 → 26 **随机** | 6/6 稳定 **26** |
20
+
21
+ ### Tests
22
+
23
+ - **全仓 413 → 417**:新增 4 例(`preferStrongerRow` 的三条择强分支 + 平手保留先到;以及**同一组数据在全部 6 种货架排列下产出完全相同的目录**——这条直接锁住了「结果不得依赖网关顺序」)。
24
+ - **变异验证 3 次全部被抓**:改回「先到先得」→ 1 例失败;去掉 Max 档优先 → 2 例失败;把平手规则从 `>` 改成 `>=`(后到者胜)→ 1 例失败。
25
+ - 其中第 3 次最初**没被抓到**:我原来的平手用例只覆盖了「都无 Max」的情况,没覆盖「两个相等的 Max」。补齐用例后才抓住——这本身说明那条断言当时的覆盖是假的。
26
+
27
+ ### 致谢
28
+
29
+ 报告人 **JiewiW** 给出的定位(跨组容量不一致 + 组顺序不稳定)与实测数据表完全正确,并附了可直接套用的补丁思路;本版按其方向实现,并补上了「结果与顺序无关」的回归测试。
30
+
31
+ ## 2.7.0 (2026-10-02)
32
+
33
+ > **新增「一键测试启用模型」**:给每个已启用的模型发一条最小消息,看哪些**真的能应答**。
34
+ >
35
+ > 起因是 issue #19 留下的那个问题——目录里有一些模型拿不到调用 id,而**能不能调用取决于账号档位**,本地任何检查都判断不了(本机只有免费账号,测不出付费账号的真实现状)。既然无法代替用户判断,就把判断交给上游,让用户一键问一次。
36
+
37
+ ### Features
38
+
39
+ - **一键测试启用模型**(卡片模型区新增按钮):
40
+ - 对**已启用**的每个模型发一条最小的聊天请求(一句 `ping`、不带工具,花费降到最低),逐个给出结论:
41
+ | 结论 | 含义 |
42
+ | --- | --- |
43
+ | **可用** | 上游正常应答 |
44
+ | **需要更高档位** | `1005`:当前套餐不覆盖该模型(报告人 Pro 账号能用的模型,免费账号会看到这一条) |
45
+ | **上游不提供** | `4001` / `4011`:没有任何 SOLO function 服务这个 config_name |
46
+ | **被限流** | 上游限流,本次未测出 |
47
+ | **登录凭据被拒** | 凭据失效;整轮测试立即停止(否则后续每个模型都只会重复同一句) |
48
+ | **本次未测出** | 超时、连接中断、代理、5xx——**一律不算「不可用」** |
49
+ - **`本次未测出` 是一等公民**:把限流或断连显示成「不可用」,用户会去删掉一个其实能用的模型。只有上游明确的拒绝才算负面结论。
50
+ - **这是插件里唯一消耗额度的操作**(另一个是签到,改变的是账号状态)。因此:只由用户点击触发、没有定时也没有启动时自动跑;按钮带说明;一次测试发送的提示词固定为一句「ping」且不带工具。
51
+ - **逐个请求、串行执行**:Trae 对突发请求限流明显,并发探测会自己制造出「被限流/未测出」;每测完一个立刻更新该行徽标,所以不是空等一分钟。
52
+ - **结果只在会话内存里**:不写进设置——某个模型「上游不提供」是某一刻、某个账号的事实,持久化下来只会在上游改主意后继续误报。
53
+ - **路由守卫**:POST + 回环来源 + 区域参数;待测 id 必须存在于该区域的实时目录(不能拿任意的上游 config 名去消耗额度),数量上限 64。
54
+
55
+ ### Tests
56
+
57
+ - **全仓 395 → 413(+18)**:
58
+ - `tests/model-probe.spec.ts`(新增 16 例):分类规则的每一个分支,重点是「不许把不确定判成不可用」——超时、断连、5xx、无输出的 200 流全部保持 `unknown`;`1005` → `gated`、`4001/4011` → `unsupported`、401 → `auth`、429 → `rate_limited`;流中途抛出的拒绝**带错误码时仍要分类**(不能退回 unknown);串行顺序与「认证失败即停止」;探测请求体是最小的 `ping` 且不含 `tools`。
59
+ - `tests/web-status.spec.ts`(+2 例):新路由把 id 交给**该区域**的探测实现并回传结论;方法/来源/请求体不合法时**一次调用都不发出**(用探测调用次数断言,而不是只看状态码),以及超过上限的列表被拒。
60
+ - **变异验证 3 次全部被抓**:把「不确定」改成负面结论 → 1 例失败;去掉认证失败即停止 → 1 例失败;不识别 `1005` 门禁码 → 2 例失败。恢复后 46 文件 413 例全绿。
61
+ - **真机验证(本机免费账号,国际版)**:`gpt-5.4` / `kimi-k3` → 可用;`gpt-6-luna` → 需要更高档位(`1005`);`gpt-6-astra` / `gpt-5.6-sol` / `glm-5.2` → 上游不提供。三种结论都在真实链路上得到验证。
62
+
63
+ ## 2.6.0 (2026-10-02)
64
+
65
+ > **包含仍未发布的 2.5.1。** 2.5.1(补问 `chat_v3`,国际版目录 11 → 17)只打了 tag、未推 npm,因此没有用户拿到过它;2.6.0 在同一棵树上带上它。
66
+ >
67
+ > 本版做了一件**行为上的反转**:目录里拿不到调用 id 的模型**不再被隐藏,而是照常显示**,调用失败时给出可读的上游错误。起因是 issue #19 的讨论——可调用名单是**按账号档位**下发的,用免费账号测不到,不能推出付费账号用不了;**隐藏用户自己 IDE 里有的模型,比让他看到一条明确的错误更糟**。
68
+
69
+ ### Features
70
+
71
+ - **模型列表改为「全部呈现」:不再隐藏拿不到调用 id 的模型**(issue #19):
72
+ - **原行为**:`mergeTraeModelSources` 会剔除「目录里有、但任何 function 的可调用名单里都没有」的行(2026-08-30 的结论:这类模型发出去必然 `4001 param is invalid`,所以不该暴露)。
73
+ - **为什么反转**:那个结论把**账号范围的事实**当成了**普遍事实**。可调用名单按账号档位下发,本机免费账号问不到不等于付费账号没有——报告人是 Pro,他的账号能正常调用本机免费账号被门禁(`1005`)挡住的模型。插件隐藏了他 Trae IDE 里明明有的模型,这才是更糟的结果。
74
+ - **现在的行为**:这些行照常出现在列表与勾选界面里;因为没有 wire 目标,调用会走显示 id + 默认 function,上游拒绝时由 2.5.0 引入的错误翻译如实转述,用户看到的是:
75
+ ```
76
+ Trae does not serve this model under the SOLO function the request used
77
+ (config_name rejected) · Trae code 4001 · upstream: We're sorry, the param is invalid…
78
+ ```
79
+ 而不是「模型不见了」或一句像网络故障的提示。
80
+ - **实测(本机实时凭据 + 插件自身代码路径)**:国际版目录 **17 → 22**(新增 `gpt-6-astra`、`gpt-5.6-sol/terra/luna`、`glm-5.2` 这 5 个无调用目标的行);国内版目录 **19 → 30**。无调用目标的行会随上游开放 SOLO 通道而自动变得可调用——判据就是能不能 join 到调用 id,无需再改插件。
81
+
82
+ ### Bug Fixes
83
+
84
+ - **国内版目录补齐:读全 7 个货架,新增 11 个模型**(issue #19 跟进):
85
+ - 国内版的发现名单此前只有 `solo_agent_remote` + `solo_work_remote`,把 `solo_coder`(12 个)、`chat_v3`(18 个)、`solo_agent`(18 个)、`solo_work_lite`(15 个)等组全部漏读——与国际版此前那个缺口形状相同。现在两个区域使用同一份组名单。
86
+ - 调用名单同步补上 `solo_agent_remote` / `chat_v3` / `solo_coder`(都在最后,只补缺口):实测国内版新增可见 `Doubao-Seed-Code`、`Doubao-Seed-2.0-Code`、`glm-5.1`、`glm-5`、`glm-5v-turbo`、`DeepSeek-V4-Pro`、`DeepSeek-V4-Flash`、`kimi-k2.5`、`qwen-3.5`、`minimax-m2.7`、`qwen-3.6-plus` 等。
87
+ - **纠正一处我自己的测量错误**:2.5.1 曾记「CN 的 `solo_coder` 组里 3 个模型任何 function 都 4001,所以不能扩大 CN」——那是**探测脚本的 bug**:调用时用了展示名而不是该 function 返回的 `config_name`。用正确的 config_name 重测,`doubao-seed-2.0-code`、`deepseek-v4-pro`、`deepseek-v4-flash` 在 `solo_coder` / `chat_v3` 等下**全部正常出字**。(同一个坑在同一轮里踩了两次,见 `docs/RELEASE_EXPERIENCE.md` 的教训类型。)
88
+
89
+ ### Tests
90
+
91
+ - **全仓 395 → 395**(改写 4 条既有断言为新的行为契约):merge 保留无 wire 目标的行且不伪造 `wireConfigName` / `wireFunction`;国内版发现名单必须含 `solo_coder` / `chat_v3` / `solo_work_lite`;国内版调用名单的顺序与内容。
92
+ - **变异验证 3 次全部被抓**:把 merge 改回「丢弃无线行」→ 2 例失败;把国内版调用名单改回两项 → 1 例失败;把国内版发现名单改回两项 → 2 例失败。恢复后 45 文件 395 例全绿。
93
+
94
+ ### Docs
95
+
96
+ - README(中英)「暂不支持」一节改写:这类模型**会显示**,选中后得到明确的上游拒绝错误,并说明「按账号档位下发、免费账号测不到不等于付费不可用」的新策略。
97
+
98
+ ## 2.5.1 (2026-10-02)
99
+
100
+ > **补上一个 2.5.0 没修完的缺口**:国际版模型目录仍比应有的少。报告人升级 2.5.0 后反馈「pro 状态 PASS、调用 PASS,但模型数量还是没取全」——复测确认,并定位到插件**从未请求过的那个 function**。
101
+
102
+ ### Bug Fixes
103
+
104
+ - **国际版模型目录 11 → 17:`chat_v3` 是一个从未被请求的宽名单**(issue #19 跟进):
105
+ - **现象**:2.5.0 之后报告人仍看不到 `Dola-Seed-2.0-Code`、`kimi-k2.7-code`、`deepseek-v4-flash-0731` 等模型。
106
+ - **根因**:2.5.0 只扩大了**发现侧**(remote 目录读全部组),而**最终目录 = 发现 ∩ wire**(`mergeTraeModelSources` 会剔除 join 不到 `config_name` 的模型——这是防止「显示了但一点就 4001」的保护,本身正确)。问题是插件的 wire 名单只问过 `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`,而 **`chat_v3` 从未被请求**——它恰恰是名单最宽的一个:ai 区域 40 个 `config_name`(仅次于不可用于聊天的 `solo_agent` 的 43 个),并且列着一批别的 function 都没有的模型。
107
+ - **实测(本机实时凭据,AI 区域)**:把 `chat_v3` 加入 wire 名单(**放在最后**,因此只会补缺口、不改变任何既有模型的 function 归属)后,用插件自身代码路径(`TraeSoloRemoteCatalogClient` + `TraeSoloUpstreamClient` + `mergeTraeModelSources`)跑实时上游:发现 22 → wire 51 → **最终目录 11 → 17**。新增的 6 个全部挂在 `chat_v3`:`Dola-Seed-2.0-Code`、`deepseek-v4-flash-0731`、`kimi-k2.7-code`、`deepseek-v3.2`、`gemini-3-flash-premium`、`gemini_2.5_flash_premium`;每个都用插件自己的 `prepareSoloBody` 信封逐个验证**可正常出字**(同一模型在 `solo_agent_remote` 下回 `4001 param is invalid`)。
108
+ - **发现侧同步**:`deepseek-v3.2`、`gemini-3-flash-premium`、`gemini_2.5_flash_premium` 只出现在 `chat_v3` 组,因此 remote 目录也补问该组(否则它们根本不会被发现)。
109
+ - **仍然不显示的**:`gpt-6-astra`、`gpt-5.6-Sol/Terra/Luna`、`glm-5.2` —— 它们在**任何** function 的 wire 名单里都不存在,对 `llm_utils_chat` 每个通道都返回 `4011`/`4001`。它们属于 Trae IDE 的内置通道,不是插件能走的 SOLO 通道;merge 剔除它们是**正确行为**(宁可没有,也不放一个点了就报错的模型)。另外 `gpt-5.5` 现在可以显示了(它在 `solo_work_remote` 的 wire 名单里,此前只是被发现侧漏读)。
110
+ - **CN 区域故意未动**:CN 的 `solo_coder` 组确实还藏着模型(`glm-5`、`glm-5.1`、`qwen-3.5` 可调用),但它与另外三个「任何 function 都 4001」的模型(`doubao-seed-2.0-code`、`deepseek-v4-pro`、`deepseek-v4-flash`)**混在同一组**,而 merge 无法按 function 区分它们。扩大 CN 发现等于把它们一起暴露出来——正是这条 join 规则要防的事,因此留待逐个模型定出可用 function 后再做。
111
+
112
+ ### Tests
113
+
114
+ - **全仓 392 → 395**:新增「ai wire 名单把 `chat_v3` 排在最后(且断言它必须是最后一位,防止将来被挪到前面抢走既有模型的 config)」、「ai 发现名单请求 `chat_v3`」、以及一条**守护 CN 不被顺手扩大**的断言(CN 名单既不含 `chat_v3` 也不含 `solo_coder`)。
115
+ - **变异验证 3 次全部被抓**:把 `chat_v3` 挪到名单最前(优先级被夺)→ 3 例失败;从 ai 发现名单去掉 `chat_v3` → 2 例失败;顺手扩大 CN 发现名单 → 2 例失败。恢复后 45 文件 395 例全绿。
116
+
117
+ ### Docs
118
+
119
+ - `docs/MODEL_MANAGEMENT_DESIGN.md` 更正一处结论:2026-08-30 记的「`Doubao-Seed-Code` 不是任何 `config_name`、必须剔除」只对**当时问过的 function** 成立——它的 `config_name` 在 `chat_v3` 里且可正常调用。join 规则不变,变的是「有没有 wire」必须建立在**完整的 function 名单**上。
120
+
3
121
  ## 2.5.0 (2026-10-01)
4
122
 
5
123
  > **国际版(AI 区域)三条用户可见故障的修复**,全部来自 [issue #19](https://github.com/dingminhua/dsh-connect-trae/issues/19) 的报告与
package/README.en.md CHANGED
@@ -42,6 +42,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
42
42
  - **Vision follows Trae's own declaration** — when Trae marks a model `multimodal`, the plugin advertises it as such with no manual tick per model (issue #16). The rule is "**models you tick ∪ models Trae marks multimodal**": models Trae marks `false`, or does not mark at all, stay text-only and can still be turned on by hand. A row enabled automatically is shown ticked and read-only, with the reason. See "Image input" below.
43
43
  - **Account switching** — refreshes the token list and lets users select an account without storing tokens in DSH settings.
44
44
  - **Read-only usage and model management** — Work/general credits on CN accounts, subscription/trial status on international ones; enable Trae models freely. Read-only queries consume nothing.
45
+ - **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`/`4011`; no SOLO function serves it) / **throttled** / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
45
46
  - **Daily check-in claiming** — a one-click claim on the CN card (the button shows the daily reward, and reads "Claimed today" and disables itself once that account has been paid). This is the plugin's **only** operation that changes account state, and it runs only when you click. Trae allows **one check-in per device per day**: after switching accounts, if this machine's slot is already spent (by any account), the selected account cannot claim here that day — the card explains that rather than pretending the account was paid. The international side has no check-in activity, so no button is shown there.
46
47
  - **Secure loopback shim** — one random port + in-process random secret per region; the real Trae token is never handed to pi-ai.
47
48
 
@@ -86,7 +87,7 @@ Notes:
86
87
 
87
88
  This plugin serves models from Trae's **SOLO channel** (`DeepSeek-V4-Flash-Official`, `DeepSeek-V4-Pro-Official`, `GLM-5.3`, `GLM-5.2`, `Kimi-K3`, `MiniMax-M3`, `Qwen3.8-Max`, `Doubao-Seed-*`, …), for both the domestic and international sides.
88
89
 
89
- The following three models currently come from the **TraeCode (Trae IDE) channel** and have **not** been opened up to the SOLO channel, so this plugin does **not** support them yet:
90
+ The following three models currently come from the **TraeCode (Trae IDE) channel** and have **not** been opened up to the SOLO channel:
90
91
 
91
92
  | Model |
92
93
  | --- |
@@ -94,7 +95,16 @@ The following three models currently come from the **TraeCode (Trae IDE) channel
94
95
  | `kimi-k2.8-preview` |
95
96
  | `qwen3.8-flash` |
96
97
 
97
- These three are reachable only inside the Trae IDE client; there is no callable channel for them on the plugin side. **They can only be supported once Trae officially opens them up to the SOLO channel.**
98
+ They **do appear in the model list** (Trae's directory advertises them), but the plugin has no channel that can call them yet. Sending a message with one selected produces an **explicit error**, for example:
99
+
100
+ ```
101
+ Trae does not serve this model under the SOLO function the request used
102
+ (config_name rejected) · Trae code 4001 · upstream: We're sorry, the param is invalid…
103
+ ```
104
+
105
+ That is the upstream refusal stated plainly, rather than a silent failure or something that reads like a network fault. **Once Trae opens them up to the SOLO channel the plugin picks them up automatically** (the test is simply whether a callable id can be resolved) — no plugin upgrade needed.
106
+
107
+ > **Why they are listed now (policy from 2026-10-02)**: such rows — present in the directory but with no resolvable callable id — used to be **hidden outright**, on the grounds that showing one only produces an error. But the callable rosters are issued **per account tier**: not seeing a model on a free account does not prove a paying account cannot call it (the reporter in issue #19 is on Pro, and their account calls models this machine's free account gets gated on). **Hiding models the user's own Trae IDE lists is worse than showing an explicit error**, so the behaviour is now: list everything, report refusals honestly.
98
108
 
99
109
  > `deepseek-v4.1-flash` used to be listed here — `docs/DS41_CALLABILITY.md` (evidence gathered 2026-09-15) measured `4001 param is invalid` from all eight SOLO functions. **Trae has since opened it up to the SOLO channel**: it now ships in the live directory with `wireFunction: solo_work_remote` and calls succeed (verified against a live refresh). This page had not caught up, so it is removed from the table.
100
110
 
package/README.md CHANGED
@@ -42,6 +42,7 @@
42
42
  - **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
43
43
  - **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。
44
44
  - **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
45
+ - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`/`4011`,SOLO 通道没有这个模型)/ **被限流** / **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
45
46
  - **每日签到领取** —— 国内版卡片上一键领取当日签到积分(按钮显示每日奖励,该账号已领取则显示「今日已领取」并禁用);这是插件里**唯一会改变账号状态**的操作,由你主动点击触发。Trae 的签到按「**每台设备每天一次**」限制:切换账号后如果本机今天的名额已经用掉(无论是哪个账号用的),当前账号在本机当天无法再签,卡片会说明原因而不是谎报「已领取」。国际版没有签到活动,因此不显示该按钮。
46
47
  - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 Trae token 不交给 pi-ai。
47
48
 
@@ -86,7 +87,7 @@ DSH PiAiAdapter(每个 provider 一套)
86
87
 
87
88
  插件提供的是 Trae **SOLO 通道**的模型(`DeepSeek-V4-Flash-Official`、`DeepSeek-V4-Pro-Official`、`GLM-5.3`、`GLM-5.2`、`Kimi-K3`、`MiniMax-M3`、`Qwen3.8-Max`、`Doubao-Seed-*` 等),国内版与国际版均走这条通道。
88
89
 
89
- 以下 3 个模型目前来自 **TraeCode(Trae IDE)通道**,尚未开放到 SOLO 通道,因此本插件**暂不支持**:
90
+ 以下 3 个模型目前来自 **TraeCode(Trae IDE)通道**,尚未开放到 SOLO 通道:
90
91
 
91
92
  | 模型 |
92
93
  | --- |
@@ -94,7 +95,16 @@ DSH PiAiAdapter(每个 provider 一套)
94
95
  | `kimi-k2.8-preview` |
95
96
  | `qwen3.8-flash` |
96
97
 
97
- 这 3 个模型只在 Trae IDE 客户端内可用,插件侧没有可调用的通道。**需要等官方把它们开放到 SOLO 通道后,插件才能支持。**
98
+ 它们**会出现在模型列表里**(Trae 的目录列出了它们),但插件侧暂时没有可调用它们的通道。选中后发送消息会得到一条**明确的错误**,例如:
99
+
100
+ ```
101
+ Trae does not serve this model under the SOLO function the request used
102
+ (config_name rejected) · Trae code 4001 · upstream: We're sorry, the param is invalid…
103
+ ```
104
+
105
+ 即「上游不提供该模型」被如实说明,而不是静默失败或像网络故障。**官方把它们开放到 SOLO 通道后,插件会自动可用**(判据就是能不能拿到调用 id),无需升级插件。
106
+
107
+ > **为什么现在会显示(2026-10-02 起的策略)**:此前这类「目录里有、但拿不到调用 id」的模型会被**直接隐藏**,理由是「放出来只会报错」。但可调用名单是**按账号档位**下发的——在免费账号上测不到,不等于付费账号用不了(issue #19 的报告人就是 Pro:他的账号能调用本机免费账号被门禁挡住的模型)。**隐藏用户自己 Trae IDE 里有的模型,比让他看到一条明确的错误更糟**,所以改为:全部呈现 + 如实报错。
98
108
 
99
109
  > `deepseek-v4.1-flash` 曾在此列——`docs/DS41_CALLABILITY.md`(2026-09-15 取证)实测 8 个 SOLO function
100
110
  > 全部返回 `4001 param is invalid`。**上游后来把它开放到了 SOLO 通道**:当前实时目录里它带
package/lib/client.js CHANGED
@@ -11,6 +11,13 @@ window.__ModuleLoader__.load({
11
11
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
12
12
  /** Plugin-owned live model refresh endpoint. */
13
13
  const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
14
+ /**
15
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
16
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
17
+ * (minimal) chat call per model — so it is user-initiated only, and the card
18
+ * labels the button as consuming a small amount.
19
+ */
20
+ const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
14
21
  /** Plugin-owned local account rescan endpoint. */
15
22
  const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
16
23
  /**
@@ -254,6 +261,18 @@ window.__ModuleLoader__.load({
254
261
  .dsm-trae-model-copy{display:flex;align-items:baseline;gap:8px;min-width:0}
255
262
  .dsm-trae-model-name{display:inline-flex;align-items:baseline;gap:7px;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;font-weight:500;line-height:19px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
256
263
  .dsm-trae-model-name-rate{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:400;line-height:16px;flex:none}
264
+ /* The two head actions sit together; the probe one SPENDS quota, so it carries
265
+ the hint tooltip rather than looking like its read-only sibling. */
266
+ .dsm-trae-models-actions{display:flex;align-items:center;gap:8px;flex:none}
267
+ .dsm-trae-models-hint{margin:4px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
268
+ /* Verdict badge. Only a real refusal is coloured as a warning; an unknown
269
+ verdict stays neutral because an inconclusive probe is not evidence against
270
+ the model. */
271
+ .dsm-trae-model-probe{flex:none;border-radius:999px;padding:1px 7px;font-size:10px;font-weight:500;line-height:15px;white-space:nowrap}
272
+ .dsm-trae-model-probe-available{color:var(--dsw-alias-state-success-primary,#22a06b);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22a06b) 40%,transparent)}
273
+ .dsm-trae-model-probe-gated,.dsm-trae-model-probe-unsupported{color:var(--dsw-alias-state-warning-primary,#d98f20);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d98f20) 40%,transparent)}
274
+ .dsm-trae-model-probe-auth{color:var(--dsw-alias-state-error-primary,#d92d20);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 40%,transparent)}
275
+ .dsm-trae-model-probe-rate-limited,.dsm-trae-model-probe-unknown{color:var(--dsw-alias-label-tertiary,#999);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#999) 35%,transparent)}
257
276
  .dsm-trae-model-meta{display:flex;align-items:center;gap:7px 12px;flex-wrap:wrap;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
258
277
  .dsm-trae-model-options{display:flex;align-items:center;justify-content:flex-end;gap:12px;flex:none}
259
278
  .dsm-trae-model-image{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px;cursor:pointer}
@@ -388,6 +407,29 @@ window.__ModuleLoader__.load({
388
407
  label: paid ? "row.subscribed" : "row.noPackage"
389
408
  };
390
409
  }
410
+ /**
411
+ * The verdict badge for one model, or null when it has not been tested.
412
+ *
413
+ * `unknown` renders as inconclusive, never as a failure: a throttle or a dropped
414
+ * connection says nothing about the model, and painting it the same red as a
415
+ * real refusal is what makes users delete working models.
416
+ */
417
+ const PROBE_LABELS = {
418
+ available: "row.probeAvailable",
419
+ gated: "row.probeGated",
420
+ unsupported: "row.probeUnsupported",
421
+ rate_limited: "row.probeRateLimited",
422
+ auth: "row.probeAuth",
423
+ unknown: "row.probeUnknown"
424
+ };
425
+ function probeBadge(probe, t) {
426
+ if (probe === void 0) return null;
427
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
428
+ className: `dsm-trae-model-probe dsm-trae-model-probe-${probe.verdict}`,
429
+ title: probe.detail,
430
+ children: t(PROBE_LABELS[probe.verdict])
431
+ });
432
+ }
391
433
  function dotStyle(status) {
392
434
  return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
393
435
  }
@@ -409,6 +451,19 @@ window.__ModuleLoader__.load({
409
451
  }
410
452
  });
411
453
  const [busy, setBusy] = (0, react.useState)(false);
454
+ /**
455
+ * Availability verdicts from the one-click test, per region, keyed by model
456
+ * id. Session-only on purpose: a verdict is one moment on one account, and a
457
+ * persisted "not served" would keep claiming that long after upstream changed
458
+ * its mind.
459
+ */
460
+ const [probeByRegion, setProbeByRegion] = (0, react.useState)({
461
+ cn: {},
462
+ ai: {}
463
+ });
464
+ const [testing, setTesting] = (0, react.useState)(void 0);
465
+ const [testError, setTestError] = (0, react.useState)(void 0);
466
+ const [testSummary, setTestSummary] = (0, react.useState)(void 0);
412
467
  const [settingsRevision, setSettingsRevision] = (0, react.useState)(0);
413
468
  /** Per-region unsaved model edits; a draft on one tab is never dropped by
414
469
  * switching to the other tab, only by that tab's discard/save. */
@@ -637,6 +692,84 @@ window.__ModuleLoader__.load({
637
692
  if (mounted.current) setTogglingRegion(void 0);
638
693
  }
639
694
  };
695
+ /**
696
+ * One-click availability test for the ENABLED models of the active region.
697
+ *
698
+ * One model per request, deliberately: the host probe is sequential anyway
699
+ * (Trae throttles bursts), and a per-model round trip lets each row show its
700
+ * verdict as soon as it is known instead of the card sitting blank for a
701
+ * minute. User-started, and it stops on the first authentication failure —
702
+ * every later probe would only repeat that answer.
703
+ */
704
+ const testEnabledModels = async () => {
705
+ const ids = [...activeEnabledIds];
706
+ setTestError(void 0);
707
+ setTestSummary(void 0);
708
+ if (ids.length === 0) {
709
+ setTestError(t("row.modelsTestEmpty"));
710
+ return;
711
+ }
712
+ setTesting({
713
+ done: 0,
714
+ total: ids.length
715
+ });
716
+ const collected = {};
717
+ let available = 0;
718
+ let denied = false;
719
+ try {
720
+ for (const [index, id] of ids.entries()) {
721
+ if (!mounted.current) return;
722
+ const response = await fetch(withTraeRegion(TRAE_MODELS_TEST_PATH, activeRegion), {
723
+ method: "POST",
724
+ headers: {
725
+ accept: "application/json",
726
+ "content-type": "application/json"
727
+ },
728
+ credentials: "same-origin",
729
+ body: JSON.stringify({ ids: [id] })
730
+ });
731
+ const body = await response.json().catch(() => ({}));
732
+ if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
733
+ const verdict = body.results?.[0];
734
+ if (verdict !== void 0) {
735
+ collected[id] = {
736
+ verdict: verdict.verdict,
737
+ detail: verdict.detail
738
+ };
739
+ if (verdict.verdict === "available") available += 1;
740
+ setProbeByRegion((prev) => ({
741
+ ...prev,
742
+ [activeRegion]: {
743
+ ...prev[activeRegion],
744
+ [id]: {
745
+ verdict: verdict.verdict,
746
+ detail: verdict.detail
747
+ }
748
+ }
749
+ }));
750
+ if (verdict.verdict === "auth") {
751
+ denied = true;
752
+ break;
753
+ }
754
+ }
755
+ setTesting({
756
+ done: index + 1,
757
+ total: ids.length
758
+ });
759
+ }
760
+ if (mounted.current) {
761
+ setTestSummary(t("row.modelsTestDone", {
762
+ available,
763
+ total: Object.keys(collected).length
764
+ }));
765
+ if (denied) setTestError(t("row.probeAuth"));
766
+ }
767
+ } catch (error) {
768
+ if (mounted.current) setTestError(t("row.modelsTestError", { message: error instanceof Error ? error.message : t("row.requestFailed") }));
769
+ } finally {
770
+ if (mounted.current) setTesting(void 0);
771
+ }
772
+ };
640
773
  const refreshModels = async () => {
641
774
  setBusy(true);
642
775
  try {
@@ -1089,16 +1222,44 @@ window.__ModuleLoader__.load({
1089
1222
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1090
1223
  className: "dsm-trae-models-summary",
1091
1224
  children: t("row.modelsSummary", { count: activeEnabledIds.size })
1092
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1093
- type: "button",
1094
- className: "dsm-btn dsm-btn-outline",
1095
- disabled: busy,
1096
- onClick: () => {
1097
- refreshModels();
1098
- },
1099
- children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1225
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1226
+ className: "dsm-trae-models-actions",
1227
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1228
+ type: "button",
1229
+ className: "dsm-btn dsm-btn-outline",
1230
+ disabled: busy,
1231
+ onClick: () => {
1232
+ refreshModels();
1233
+ },
1234
+ children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1235
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1236
+ type: "button",
1237
+ className: "dsm-btn dsm-btn-outline",
1238
+ disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
1239
+ title: t("row.modelsTestHint"),
1240
+ onClick: () => {
1241
+ testEnabledModels();
1242
+ },
1243
+ children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
1244
+ done: testing.done,
1245
+ total: testing.total
1246
+ })
1247
+ })]
1100
1248
  })]
1101
1249
  }),
1250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1251
+ className: "dsm-trae-models-hint",
1252
+ children: t("row.modelsTestHint")
1253
+ }),
1254
+ testSummary === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1255
+ className: "dsm-trae-models-summary",
1256
+ children: testSummary
1257
+ }),
1258
+ testError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1259
+ className: "dsm-trae-usage-error",
1260
+ role: "alert",
1261
+ children: testError
1262
+ }),
1102
1263
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1103
1264
  className: "dsm-trae-model-list",
1104
1265
  children: visibleModels.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1118,10 +1279,14 @@ window.__ModuleLoader__.load({
1118
1279
  className: "dsm-trae-model-copy",
1119
1280
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1120
1281
  className: "dsm-trae-model-name",
1121
- children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1122
- className: "dsm-trae-model-name-rate",
1123
- children: ["· x", model.creditMultiplier.toFixed(2)]
1124
- })]
1282
+ children: [
1283
+ model.name,
1284
+ model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1285
+ className: "dsm-trae-model-name-rate",
1286
+ children: ["· x", model.creditMultiplier.toFixed(2)]
1287
+ }),
1288
+ probeBadge(probeByRegion[activeRegion][model.id], t)
1289
+ ]
1125
1290
  })
1126
1291
  })]
1127
1292
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1316,6 +1481,18 @@ window.__ModuleLoader__.load({
1316
1481
  "row.modelsSummary": "{count} enabled",
1317
1482
  "row.modelsRefresh": "Refresh from Trae",
1318
1483
  "row.modelsRefreshing": "Refreshing models…",
1484
+ "row.modelsTestEnabled": "Test enabled models",
1485
+ "row.modelsTesting": "Testing {done}/{total}…",
1486
+ "row.modelsTestHint": "Sends one minimal message per enabled model to see which ones actually answer. Costs a small amount of quota.",
1487
+ "row.modelsTestEmpty": "Enable at least one model first.",
1488
+ "row.modelsTestError": "Test failed: {message}",
1489
+ "row.modelsTestDone": "{available} of {total} answered.",
1490
+ "row.probeAvailable": "works",
1491
+ "row.probeGated": "needs a higher plan",
1492
+ "row.probeUnsupported": "not served",
1493
+ "row.probeRateLimited": "throttled",
1494
+ "row.probeAuth": "sign-in rejected",
1495
+ "row.probeUnknown": "not determined",
1319
1496
  "row.modelEnabled": "Enabled",
1320
1497
  "row.discard": "Discard changes",
1321
1498
  "row.save": "Save",
@@ -1396,6 +1573,18 @@ window.__ModuleLoader__.load({
1396
1573
  "row.modelsSummary": "已启用 {count} 个",
1397
1574
  "row.modelsRefresh": "从 Trae 刷新",
1398
1575
  "row.modelsRefreshing": "正在刷新模型…",
1576
+ "row.modelsTestEnabled": "一键测试启用模型",
1577
+ "row.modelsTesting": "正在测试 {done}/{total}…",
1578
+ "row.modelsTestHint": "给每个已启用模型发一条最小消息,看哪些真的能应答。会消耗少量额度。",
1579
+ "row.modelsTestEmpty": "请先启用至少一个模型。",
1580
+ "row.modelsTestError": "测试失败:{message}",
1581
+ "row.modelsTestDone": "{total} 个里有 {available} 个正常应答。",
1582
+ "row.probeAvailable": "可用",
1583
+ "row.probeGated": "需要更高档位",
1584
+ "row.probeUnsupported": "上游不提供",
1585
+ "row.probeRateLimited": "被限流",
1586
+ "row.probeAuth": "登录凭据被拒",
1587
+ "row.probeUnknown": "本次未测出",
1399
1588
  "row.modelEnabled": "启用模型",
1400
1589
  "row.discard": "放弃修改",
1401
1590
  "row.save": "保存",
package/lib/index.d.ts CHANGED
@@ -225,12 +225,22 @@ interface TraeWireModel {
225
225
  * multiplier, reasoning and multimodal flags. `wire` (from `get_detail_param`)
226
226
  * supplies the real `llm_utils_chat` `config_name` — the only id the chat
227
227
  * endpoint actually accepts — plus the authoritative post-discount credit
228
- * multiplier when `display_contact_config` carries one. A remote row is only
229
- * callable when it maps to a wire `config_name`, so a remote row with no wire
230
- * match is DROPPED (it would otherwise be sent as an invalid `config_name` and
231
- * rejected with 4001 "param is invalid"). Verified 2026-08-30: the Remote
232
- * directory advertises `Doubao-Seed-Code` and `glm-5.3`, neither of which is a
233
- * current `config_name`; both fail every request, so they must not be exposed.
228
+ * multiplier when `display_contact_config` carries one.
229
+ *
230
+ * A remote row with no wire match is STILL KEPT (decision 2026-10-02, issue
231
+ * #19). It previously was dropped, on the reasoning that a row with no
232
+ * `config_name` is sent as an invalid one and rejected with 4001. That
233
+ * reasoning holds only for the accounts and functions we can measure, and
234
+ * measuring is the problem: whether a roster contains a model is ACCOUNT-
235
+ * SCOPED, so "no wire entry on a free account" cannot prove a paying account
236
+ * cannot call it. Dropping on that evidence silently denied those users models
237
+ * their own Trae IDE offers. The row therefore ships, and its call fails
238
+ * honestly: the bridge now translates the in-stream refusal into a readable
239
+ * error (`4001` → "Trae does not serve this model under the SOLO function the
240
+ * request used", `4011` → "Trae refused this model under the SOLO function the
241
+ * request used"), so a user who picks an unusable model is told why instead of
242
+ * seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
243
+ * which later turned out to be callable under `chat_v3`.
234
244
  *
235
245
  * Credit multiplier precedence: the wire's `display_contact_config` rate wins
236
246
  * whenever present (it is the post-discount figure the Trae IDE renders — the
@@ -1505,6 +1515,13 @@ declare class TraeUsageClient {
1505
1515
  //#region src/status-paths.d.ts
1506
1516
  /** Plugin-owned usage endpoint consumed by its browser half. */
1507
1517
  declare const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
1518
+ /**
1519
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
1520
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
1521
+ * (minimal) chat call per model — so it is user-initiated only, and the card
1522
+ * labels the button as consuming a small amount.
1523
+ */
1524
+ declare const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
1508
1525
  /**
1509
1526
  * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
1510
1527
  * route in this plugin that changes upstream account state.
@@ -1561,6 +1578,34 @@ interface TraeWebModel {
1561
1578
  * directory did not declare it, which is not a capability claim either way.
1562
1579
  */
1563
1580
  multimodal?: boolean;
1581
+ /**
1582
+ * The availability verdict from the one-click test, when this card session
1583
+ * has run one. Absent = not tested. Deliberately NOT persisted with the
1584
+ * settings: it describes one moment on one account, and a stale verdict shown
1585
+ * as current is worse than a blank field.
1586
+ */
1587
+ probe?: TraeModelProbeVerdict;
1588
+ /** Upstream's own words for the verdict, when it gave any. */
1589
+ probeDetail?: string;
1590
+ }
1591
+ /**
1592
+ * One model's availability verdict from the one-click test.
1593
+ *
1594
+ * `unknown` is a first-class outcome, not a failure: a timeout, a proxy drop or
1595
+ * a throttle says nothing about the model, and reporting it as unusable would
1596
+ * make the user delete a model that works. Only an upstream refusal is negative.
1597
+ */
1598
+ type TraeModelProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
1599
+ /** What the probe route answers with. */
1600
+ interface TraeModelsTestResponse {
1601
+ /** One entry per model tested, in the order they were sent. */
1602
+ results: {
1603
+ id: string;
1604
+ verdict: TraeModelProbeVerdict;
1605
+ detail: string;
1606
+ }[];
1607
+ /** Models skipped because the region has no live catalog row for them. */
1608
+ skipped?: string[];
1564
1609
  }
1565
1610
  /**
1566
1611
  * Daily check-in state rendered below the credit stats.
@@ -1784,6 +1829,18 @@ interface TraeUsageRouteOptions {
1784
1829
  regionEnabled(region: TraeRegion): boolean;
1785
1830
  /** Re-read one region's live directory from the upstream. */
1786
1831
  discoverModels?(region: TraeRegion, signal?: AbortSignal): Promise<readonly TraeModelInfo[]>;
1832
+ /**
1833
+ * Test the given model ids for availability, one real (minimal) chat call
1834
+ * each. Present only when the region's chat path is wired.
1835
+ */
1836
+ probeModels?(region: TraeRegion, ids: readonly string[]): Promise<{
1837
+ results: {
1838
+ id: string;
1839
+ verdict: string;
1840
+ detail: string;
1841
+ }[];
1842
+ skipped?: string[];
1843
+ }>;
1787
1844
  /** Raw-Chat capability state of the requested region. */
1788
1845
  rawDiagnostic?(region: TraeRegion): TraeRawDiagnostic;
1789
1846
  }
@@ -1798,6 +1855,64 @@ declare function traeWebUsage(deps: TraeUsageRouteOptions, region: TraeRegion):
1798
1855
  /** Mount the GET usage route on an optional webServer context. */
1799
1856
  declare function registerTraeUsageRoute(ctx: Context, deps: TraeUsageRouteOptions): void;
1800
1857
  //#endregion
1858
+ //#region src/model-probe.d.ts
1859
+ /** How one model answered. */
1860
+ type TraeProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
1861
+ interface TraeProbeResult {
1862
+ /** The model id as the card knows it. */
1863
+ id: string;
1864
+ verdict: TraeProbeVerdict;
1865
+ /** Upstream's own words (already translated), or a short note. */
1866
+ detail: string;
1867
+ }
1868
+ /**
1869
+ * How long one probe may take. Trae answers a one-word prompt in a few seconds;
1870
+ * beyond this the answer is not worth waiting for and is reported as unknown.
1871
+ */
1872
+ declare const TRAE_PROBE_TIMEOUT_MS = 30000;
1873
+ /**
1874
+ * Classify a failure that arrived as a thrown stream error rather than a
1875
+ * returned result.
1876
+ *
1877
+ * The bridge errors the stream (instead of returning a result) when a refusal
1878
+ * interrupts an answer that had already started; its message is the same
1879
+ * translated text, so a named code is still decisive. Anything without a
1880
+ * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
1881
+ */
1882
+ declare function classifyProbeError(message: string): {
1883
+ verdict: TraeProbeVerdict;
1884
+ detail: string;
1885
+ };
1886
+ /**
1887
+ * Classify a completed upstream call.
1888
+ *
1889
+ * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
1890
+ * arrived inside a 200 body has already been turned into a real error result
1891
+ * (and its code translated into `message`) by the bridge — this function only
1892
+ * has to read it back.
1893
+ */
1894
+ declare function classifyProbeResult(result: TraeChatResult, outputSeen: boolean): {
1895
+ verdict: TraeProbeVerdict;
1896
+ detail: string;
1897
+ };
1898
+ /** The minimal chat call one probe makes. */
1899
+ declare function probeBody(model: string): string;
1900
+ /**
1901
+ * Probe one model through the bridge the plugin actually chats with, so the
1902
+ * recorded verdict describes the user's real path (same wire resolution, same
1903
+ * function stamping, same error translation).
1904
+ */
1905
+ declare function probeOneModel(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, id: string, timeoutMs?: number): Promise<TraeProbeResult>;
1906
+ /**
1907
+ * Probe a list of models ONE AT A TIME.
1908
+ *
1909
+ * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
1910
+ * would manufacture the very `rate_limited`/`unknown` verdicts this feature
1911
+ * must avoid. An authentication failure stops the run — every later probe would
1912
+ * fail the same way, and spending calls to learn that again is waste.
1913
+ */
1914
+ declare function probeModelsSequentially(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, ids: readonly string[], onResult?: (result: TraeProbeResult, index: number) => void): Promise<TraeProbeResult[]>;
1915
+ //#endregion
1801
1916
  //#region src/index.d.ts
1802
1917
  declare const name = "dsh-connect-trae";
1803
1918
  declare const inject: string[];
@@ -1920,4 +2035,4 @@ declare function asVolatile<S>(schema: S): S;
1920
2035
  declare const Config: z<Config>;
1921
2036
  declare function apply(ctx: Context, config: Config): void;
1922
2037
  //#endregion
1923
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeObservedModelConfig, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
2038
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/lib/index.js CHANGED
@@ -185,12 +185,22 @@ function displayKey(name) {
185
185
  * multiplier, reasoning and multimodal flags. `wire` (from `get_detail_param`)
186
186
  * supplies the real `llm_utils_chat` `config_name` — the only id the chat
187
187
  * endpoint actually accepts — plus the authoritative post-discount credit
188
- * multiplier when `display_contact_config` carries one. A remote row is only
189
- * callable when it maps to a wire `config_name`, so a remote row with no wire
190
- * match is DROPPED (it would otherwise be sent as an invalid `config_name` and
191
- * rejected with 4001 "param is invalid"). Verified 2026-08-30: the Remote
192
- * directory advertises `Doubao-Seed-Code` and `glm-5.3`, neither of which is a
193
- * current `config_name`; both fail every request, so they must not be exposed.
188
+ * multiplier when `display_contact_config` carries one.
189
+ *
190
+ * A remote row with no wire match is STILL KEPT (decision 2026-10-02, issue
191
+ * #19). It previously was dropped, on the reasoning that a row with no
192
+ * `config_name` is sent as an invalid one and rejected with 4001. That
193
+ * reasoning holds only for the accounts and functions we can measure, and
194
+ * measuring is the problem: whether a roster contains a model is ACCOUNT-
195
+ * SCOPED, so "no wire entry on a free account" cannot prove a paying account
196
+ * cannot call it. Dropping on that evidence silently denied those users models
197
+ * their own Trae IDE offers. The row therefore ships, and its call fails
198
+ * honestly: the bridge now translates the in-stream refusal into a readable
199
+ * error (`4001` → "Trae does not serve this model under the SOLO function the
200
+ * request used", `4011` → "Trae refused this model under the SOLO function the
201
+ * request used"), so a user who picks an unusable model is told why instead of
202
+ * seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
203
+ * which later turned out to be callable under `chat_v3`.
194
204
  *
195
205
  * Credit multiplier precedence: the wire's `display_contact_config` rate wins
196
206
  * whenever present (it is the post-discount figure the Trae IDE renders — the
@@ -217,8 +227,7 @@ function mergeTraeModelSources(remote, wire) {
217
227
  const result = [];
218
228
  for (const model of remote) {
219
229
  const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
220
- if (wireModel === void 0) continue;
221
- const creditMultiplier = wireModel.creditMultiplier ?? model.creditMultiplier;
230
+ const creditMultiplier = wireModel?.creditMultiplier ?? model.creditMultiplier;
222
231
  result.push({
223
232
  id: model.id,
224
233
  name: model.name,
@@ -232,8 +241,8 @@ function mergeTraeModelSources(remote, wire) {
232
241
  reasoning: model.reasoning,
233
242
  reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
234
243
  },
235
- ...wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
236
- ...wireModel.function === void 0 ? {} : { wireFunction: wireModel.function }
244
+ ...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
245
+ ...wireModel?.function === void 0 ? {} : { wireFunction: wireModel.function }
237
246
  });
238
247
  }
239
248
  return result;
@@ -1969,13 +1978,50 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
1969
1978
  * (it answers `minimax-m3`, `gemini-3.1-pro`, … that no `solo_work_*` lists)
1970
1979
  * and therefore takes the last slot — after the two IDE functions that serve
1971
1980
  * their shared configs without the Remote plan gate.
1981
+ *
1982
+ * `chat_v3` is the widest chat roster of all (measured 2026-10-01 on live
1983
+ * credentials, issue #19 follow-up): its `get_detail_param` answers 40
1984
+ * config_names on `ai` — more than any other function except `solo_agent` (43,
1985
+ * which cannot serve chat). It lists models NO other chat function names, and
1986
+ * it is joined LAST so it only fills gaps: a config two functions list keeps
1987
+ * the earlier function, so no precedence is disturbed. The ai models it unlocks
1988
+ * were each verified callable end-to-end through the plugin's own
1989
+ * `prepareSoloBody` envelope:
1990
+ *
1991
+ * ai (+6): kimi-k2.7-code, deepseek-v4-flash-0731, deepseek-v3.2,
1992
+ * gemini-3-flash-premium, gemini_2.5_flash_premium, Dola-Seed-2.0-Code
1993
+ *
1994
+ * CN joins `chat_v3` and `solo_coder` too (2026-10-02). Both are callable there
1995
+ * — verified per model through `prepareSoloBody`, calling each model under a
1996
+ * function whose roster names it: `glm-5` / `glm-5.1` / `qwen-3.5` and
1997
+ * `deepseek-v4-pro` / `deepseek-v4-flash` under `solo_coder`, and
1998
+ * `doubao-seed-2.0-code` under `solo_coder` / `solo_work_remote` /
1999
+ * `solo_work_lite`. An earlier note here claimed those three answered `4001`
2000
+ * "under every function" — that was a measurement error, not an upstream
2001
+ * refusal: the probe had called them by display id while the chat endpoint needs
2002
+ * the config_name its owning function returned.
2003
+ *
2004
+ * This also corrects an older note that `Doubao-Seed-Code` "has no config_name
2005
+ * and must stay hidden" (docs/MODEL_MANAGEMENT_DESIGN.md): it does have one,
2006
+ * under `chat_v3`.
2007
+ *
2008
+ * A model in NO chat function's roster is still left to the merge: it ships
2009
+ * with no wire target and its call fails with a readable upstream message
2010
+ * rather than being hidden (see mergeTraeModelSources).
1972
2011
  */
1973
2012
  const TRAE_DIRECTORY_FUNCTIONS = {
1974
- cn: ["solo_work_remote", TRAE_SOLO_FUNCTION],
2013
+ cn: [
2014
+ "solo_work_remote",
2015
+ TRAE_SOLO_FUNCTION,
2016
+ "solo_agent_remote",
2017
+ "chat_v3",
2018
+ "solo_coder"
2019
+ ],
1975
2020
  ai: [
1976
2021
  "solo_work_remote",
1977
2022
  TRAE_SOLO_FUNCTION,
1978
- "solo_agent_remote"
2023
+ "solo_agent_remote",
2024
+ "chat_v3"
1979
2025
  ]
1980
2026
  };
1981
2027
  const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
@@ -2763,20 +2809,75 @@ const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
2763
2809
  * groups are therefore requested, and every returned group is unioned.
2764
2810
  *
2765
2811
  * Asking for a function the gateway does not serve is harmless: unknown names
2766
- * are ignored rather than rejected (`solo_work_lite` returns no group, and the
2767
- * request still answers HTTP 200). The CN list is left as it was — the CN
2768
- * rosters were not re-measured here, and the union keeps whichever groups come
2769
- * back.
2812
+ * are ignored rather than rejected (`solo_work_remote` returns no group on
2813
+ * `ai`, and the request still answers HTTP 200).
2814
+ *
2815
+ * `chat_v3` was added on 2026-10-01 (issue #19 follow-up) because it carries
2816
+ * three ai-region models that appear in NO other group — `deepseek-v3.2`,
2817
+ * `gemini-3-flash-premium`, `gemini_2.5_flash_premium` — and those three are
2818
+ * callable through it (see TRAE_DIRECTORY_FUNCTIONS in solo.ts).
2819
+ *
2820
+ * Both regions now ask the SAME group list (2026-10-02). CN previously asked
2821
+ * only `solo_agent_remote` + `solo_work_remote`, which left its `solo_coder`
2822
+ * group (12 models) unread — the same shape of miss as the ai one. Every group
2823
+ * below was measured to exist on both gateways; a region that does not serve a
2824
+ * name simply returns no group for it, which the union tolerates. Reading costs
2825
+ * one query and the merge no longer discards rows for lacking a wire match (see
2826
+ * mergeTraeModelSources), so an unread group is a model silently missing from
2827
+ * the user's own list — the failure this list exists to prevent.
2828
+ *
2829
+ * `builder_v3` is included for the same reason even though neither region's
2830
+ * current wire lists name it: its rows are real directory entries, and whether
2831
+ * a paying account can call one is not something this client can determine.
2770
2832
  */
2771
2833
  const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
2772
- cn: ["solo_agent_remote", "solo_work_remote"],
2834
+ cn: [
2835
+ "solo_agent",
2836
+ "solo_agent_remote",
2837
+ "solo_work_remote",
2838
+ "solo_work_lite",
2839
+ "chat_v3",
2840
+ "solo_coder",
2841
+ "builder_v3"
2842
+ ],
2773
2843
  ai: [
2774
2844
  "solo_agent",
2775
2845
  "solo_agent_remote",
2776
- "solo_work_remote"
2846
+ "solo_work_remote",
2847
+ "solo_work_lite",
2848
+ "chat_v3",
2849
+ "solo_coder",
2850
+ "builder_v3"
2777
2851
  ]
2778
2852
  };
2779
2853
  /**
2854
+ * Of two rows for the SAME model id, the one that advertises the stronger
2855
+ * capacity (issue #23).
2856
+ *
2857
+ * The gateway groups its directory by function and reports a model's capacity
2858
+ * per group, with the group order varying between calls. Keeping the strongest
2859
+ * row makes the published window deterministic:
2860
+ *
2861
+ * 1. a row with a Max tier beats one without (`maxContextWindow` is only set
2862
+ * when the group reported `max_mode: true` with a positive max);
2863
+ * 2. two Max rows: the larger max wins;
2864
+ * 3. no Max on either side: the wider dev window wins.
2865
+ *
2866
+ * Ties keep the incumbent, so the first row seen still decides equal cases and
2867
+ * the model order stays the one the gateway sent.
2868
+ */
2869
+ function preferStrongerRow(candidate, incumbent) {
2870
+ if (incumbent === void 0) return candidate;
2871
+ const candidateMax = candidate.maxContextWindow;
2872
+ const incumbentMax = incumbent.maxContextWindow;
2873
+ if (candidateMax !== void 0 || incumbentMax !== void 0) {
2874
+ if (incumbentMax === void 0) return candidate;
2875
+ if (candidateMax === void 0) return incumbent;
2876
+ return candidateMax > incumbentMax ? candidate : incumbent;
2877
+ }
2878
+ return (candidate.contextWindow ?? 0) > (incumbent.contextWindow ?? 0) ? candidate : incumbent;
2879
+ }
2880
+ /**
2780
2881
  * Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
2781
2882
  * CN locale headers; the international directory lives on the shared
2782
2883
  * `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
@@ -2833,14 +2934,13 @@ var TraeSoloRemoteCatalogClient = class {
2833
2934
  });
2834
2935
  if (!response.ok) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
2835
2936
  const groups = (await response.json()).data?.list ?? [];
2836
- const seen = /* @__PURE__ */ new Set();
2837
- const models = [];
2937
+ const byId = /* @__PURE__ */ new Map();
2838
2938
  for (const group of groups) for (const raw of group.models ?? []) {
2839
2939
  const model = parseTraeRemoteModel(raw);
2840
- if (model === void 0 || seen.has(model.id)) continue;
2841
- seen.add(model.id);
2842
- models.push(model);
2940
+ if (model === void 0) continue;
2941
+ byId.set(model.id, preferStrongerRow(model, byId.get(model.id)));
2843
2942
  }
2943
+ const models = [...byId.values()];
2844
2944
  if (models.length === 0) throw new Error("SOLO remote models response contained no models");
2845
2945
  return models;
2846
2946
  }
@@ -3381,6 +3481,165 @@ var TraeDelegatingUpstreamClient = class {
3381
3481
  }
3382
3482
  };
3383
3483
  //#endregion
3484
+ //#region src/model-probe.ts
3485
+ /**
3486
+ * How long one probe may take. Trae answers a one-word prompt in a few seconds;
3487
+ * beyond this the answer is not worth waiting for and is reported as unknown.
3488
+ */
3489
+ const TRAE_PROBE_TIMEOUT_MS = 3e4;
3490
+ /** Upstream error codes the probe recognises, and what they mean. */
3491
+ const GATE_CODES = /* @__PURE__ */ new Set([1005]);
3492
+ const UNSUPPORTED_CODES = /* @__PURE__ */ new Set([4001, 4011]);
3493
+ /** Pull `Trae code N` out of an already-translated failure message. */
3494
+ function codeOf(message) {
3495
+ const match = /Trae code (\d+)/u.exec(message);
3496
+ return match === null ? void 0 : Number(match[1]);
3497
+ }
3498
+ /**
3499
+ * Classify a failure that arrived as a thrown stream error rather than a
3500
+ * returned result.
3501
+ *
3502
+ * The bridge errors the stream (instead of returning a result) when a refusal
3503
+ * interrupts an answer that had already started; its message is the same
3504
+ * translated text, so a named code is still decisive. Anything without a
3505
+ * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
3506
+ */
3507
+ function classifyProbeError(message) {
3508
+ if (codeOf(message) !== void 0) {
3509
+ const classified = classifyProbeResult({
3510
+ ok: false,
3511
+ status: 0,
3512
+ kind: "client",
3513
+ message
3514
+ }, false);
3515
+ if (classified.verdict !== "unknown") return classified;
3516
+ }
3517
+ return {
3518
+ verdict: "unknown",
3519
+ detail: message === "" ? "the stream failed without a message" : `stream failed: ${message}`
3520
+ };
3521
+ }
3522
+ /**
3523
+ * Classify a completed upstream call.
3524
+ *
3525
+ * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
3526
+ * arrived inside a 200 body has already been turned into a real error result
3527
+ * (and its code translated into `message`) by the bridge — this function only
3528
+ * has to read it back.
3529
+ */
3530
+ function classifyProbeResult(result, outputSeen) {
3531
+ if (result.ok) return outputSeen ? {
3532
+ verdict: "available",
3533
+ detail: "upstream answered"
3534
+ } : {
3535
+ verdict: "unknown",
3536
+ detail: "the stream ended without any output"
3537
+ };
3538
+ const message = result.message.trim() === "" ? `HTTP ${result.status}` : result.message;
3539
+ if (result.kind === "authentication" || result.status === 401 || result.status === 403) return {
3540
+ verdict: "auth",
3541
+ detail: message
3542
+ };
3543
+ if (result.kind === "soft_rate" || result.status === 429) return {
3544
+ verdict: "rate_limited",
3545
+ detail: message
3546
+ };
3547
+ const code = codeOf(message);
3548
+ if (code !== void 0 && GATE_CODES.has(code)) return {
3549
+ verdict: "gated",
3550
+ detail: message
3551
+ };
3552
+ if (code !== void 0 && UNSUPPORTED_CODES.has(code)) return {
3553
+ verdict: "unsupported",
3554
+ detail: message
3555
+ };
3556
+ if (result.kind === "client" && result.status >= 400 && result.status < 500) return {
3557
+ verdict: "unsupported",
3558
+ detail: message
3559
+ };
3560
+ return {
3561
+ verdict: "unknown",
3562
+ detail: message
3563
+ };
3564
+ }
3565
+ /** Read a probe response without letting a large or endless body stall. */
3566
+ async function readProbeOutput(response) {
3567
+ try {
3568
+ const text = await response.text();
3569
+ return { output: /"content"\s*:\s*"[^"]/u.test(text) || /"tool_calls"/u.test(text) };
3570
+ } catch (error) {
3571
+ return {
3572
+ output: false,
3573
+ error: error instanceof Error ? error : new Error(String(error))
3574
+ };
3575
+ }
3576
+ }
3577
+ /** The minimal chat call one probe makes. */
3578
+ function probeBody(model) {
3579
+ return JSON.stringify({
3580
+ model,
3581
+ messages: [{
3582
+ role: "user",
3583
+ content: "ping"
3584
+ }]
3585
+ });
3586
+ }
3587
+ /**
3588
+ * Probe one model through the bridge the plugin actually chats with, so the
3589
+ * recorded verdict describes the user's real path (same wire resolution, same
3590
+ * function stamping, same error translation).
3591
+ */
3592
+ async function probeOneModel(chat, id, timeoutMs = TRAE_PROBE_TIMEOUT_MS) {
3593
+ const controller = new AbortController();
3594
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3595
+ try {
3596
+ const result = await chat(probeBody(id), controller.signal);
3597
+ if (!result.ok) return {
3598
+ id,
3599
+ ...classifyProbeResult(result, false)
3600
+ };
3601
+ const { output, error } = await readProbeOutput(result.response);
3602
+ if (error !== void 0) return {
3603
+ id,
3604
+ ...classifyProbeError(error.message)
3605
+ };
3606
+ return {
3607
+ id,
3608
+ ...classifyProbeResult(result, output)
3609
+ };
3610
+ } catch (error) {
3611
+ if (controller.signal.aborted) return {
3612
+ id,
3613
+ verdict: "unknown",
3614
+ detail: `no answer within ${Math.round(timeoutMs / 1e3)}s`
3615
+ };
3616
+ return {
3617
+ id,
3618
+ ...classifyProbeError(error instanceof Error ? error.message : String(error))
3619
+ };
3620
+ } finally {
3621
+ clearTimeout(timer);
3622
+ }
3623
+ }
3624
+ /**
3625
+ * Probe a list of models ONE AT A TIME.
3626
+ *
3627
+ * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
3628
+ * would manufacture the very `rate_limited`/`unknown` verdicts this feature
3629
+ * must avoid. An authentication failure stops the run — every later probe would
3630
+ * fail the same way, and spending calls to learn that again is waste.
3631
+ */
3632
+ async function probeModelsSequentially(chat, ids, onResult) {
3633
+ const results = [];
3634
+ for (const [index, id] of ids.entries()) {
3635
+ const result = await probeOneModel(chat, id);
3636
+ results.push(result);
3637
+ onResult?.(result, index);
3638
+ if (result.verdict === "auth") break;
3639
+ }
3640
+ return results;
3641
+ }
3642
+ //#endregion
3384
3643
  //#region src/usage.ts
3385
3644
  /**
3386
3645
  * Read-only Trae usage/credits client.
@@ -3644,6 +3903,13 @@ var TraeUsageClient = class {
3644
3903
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
3645
3904
  /** Plugin-owned live model refresh endpoint. */
3646
3905
  const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
3906
+ /**
3907
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
3908
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
3909
+ * (minimal) chat call per model — so it is user-initiated only, and the card
3910
+ * labels the button as consuming a small amount.
3911
+ */
3912
+ const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
3647
3913
  /** Plugin-owned local account rescan endpoint. */
3648
3914
  const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
3649
3915
  /**
@@ -3808,6 +4074,39 @@ function json(res, status, body) {
3808
4074
  });
3809
4075
  res.end(payload);
3810
4076
  }
4077
+ /**
4078
+ * Read the ids to test from a POST body.
4079
+ *
4080
+ * Only ids present in the region's live catalog are accepted downstream (see
4081
+ * index.ts), so this reader only has to produce a bounded, string-only list.
4082
+ */
4083
+ async function readModelTestBody(req) {
4084
+ const chunks = [];
4085
+ let size = 0;
4086
+ for await (const chunk of req) {
4087
+ const buffer = chunk;
4088
+ size += buffer.byteLength;
4089
+ if (size > 65536) throw new Error("request body too large");
4090
+ chunks.push(buffer);
4091
+ }
4092
+ const text = Buffer.concat(chunks).toString("utf8").trim();
4093
+ if (text === "") return [];
4094
+ let parsed;
4095
+ try {
4096
+ parsed = JSON.parse(text);
4097
+ } catch {
4098
+ throw new Error("invalid JSON body");
4099
+ }
4100
+ const raw = typeof parsed === "object" && parsed !== null ? parsed["ids"] : void 0;
4101
+ if (!Array.isArray(raw)) throw new Error("body must carry an ids array");
4102
+ const ids = [];
4103
+ for (const value of raw) {
4104
+ if (typeof value !== "string") continue;
4105
+ const id = value.trim();
4106
+ if (id !== "" && !ids.includes(id)) ids.push(id);
4107
+ }
4108
+ return ids;
4109
+ }
3811
4110
  /** Loopback browser origins only; other devices are refused until trusted origins exist. */
3812
4111
  function loopbackOrigin(req) {
3813
4112
  const origin = req.headers.origin;
@@ -3990,6 +4289,40 @@ function registerTraeUsageRoute(ctx, deps) {
3990
4289
  }
3991
4290
  });
3992
4291
  /**
4292
+ * One-click availability test for the enabled models.
4293
+ *
4294
+ * This ROUTE COSTS QUOTA: each tested model gets a real, minimal chat call.
4295
+ * Every other plugin surface is read-only, so the guards here are tighter
4296
+ * than a GET's — POST only, loopback origin only, and the id list is taken
4297
+ * from the request body and validated against the region's live catalog, so
4298
+ * a caller cannot spend calls on arbitrary upstream config names. The count
4299
+ * is capped for the same reason.
4300
+ */
4301
+ const disposeModelsTest = ctx.webServer.register({
4302
+ kind: "exact",
4303
+ path: TRAE_MODELS_TEST_PATH,
4304
+ handler: async (req, res) => {
4305
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4306
+ if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
4307
+ if (deps.probeModels === void 0) return json(res, 503, { error: "model test unavailable" });
4308
+ const region = requestRegion(req, res);
4309
+ if (region === void 0) return;
4310
+ let ids;
4311
+ try {
4312
+ ids = await readModelTestBody(req);
4313
+ } catch (error) {
4314
+ return json(res, 400, { error: safeMessage(error) });
4315
+ }
4316
+ if (ids.length === 0) return json(res, 400, { error: "no models to test" });
4317
+ if (ids.length > 64) return json(res, 400, { error: `too many models (limit ${String(64)})` });
4318
+ try {
4319
+ json(res, 200, await deps.probeModels(region, ids));
4320
+ } catch (error) {
4321
+ json(res, 500, { error: safeMessage(error) });
4322
+ }
4323
+ }
4324
+ });
4325
+ /**
3993
4326
  * Daily check-in claim — the ONLY route in this plugin that mutates
3994
4327
  * upstream account state, so it is guarded more tightly than its siblings:
3995
4328
  * POST only, loopback origin only, and the status read runs FIRST so a day
@@ -4055,6 +4388,7 @@ function registerTraeUsageRoute(ctx, deps) {
4055
4388
  return () => {
4056
4389
  disposeCheckin();
4057
4390
  disposeRefresh();
4391
+ disposeModelsTest();
4058
4392
  disposeAccounts();
4059
4393
  disposeUsage();
4060
4394
  };
@@ -4572,6 +4906,32 @@ function apply(ctx, config) {
4572
4906
  }
4573
4907
  return merged;
4574
4908
  },
4909
+ /**
4910
+ * One-click availability test. The ids come from the card, so each one is
4911
+ * checked against THIS region's live catalog first: a caller must not be
4912
+ * able to spend quota probing arbitrary upstream config names, and a row the
4913
+ * region does not know is reported as skipped rather than silently dropped.
4914
+ *
4915
+ * The probe goes through `upstream` — the same bridge real chats use — so
4916
+ * wire resolution, function stamping and error translation all match what
4917
+ * the user gets when they actually send a message.
4918
+ */
4919
+ probeModels: async (ids) => {
4920
+ const known = /* @__PURE__ */ new Set();
4921
+ for (const model of [...catalog.current(), ...displayModels(current(), region)]) {
4922
+ known.add(model.id.trim().toLowerCase());
4923
+ known.add(model.name.trim().toLowerCase());
4924
+ }
4925
+ const testable = [];
4926
+ const skipped = [];
4927
+ for (const id of ids) if (known.has(id.trim().toLowerCase())) testable.push(id);
4928
+ else skipped.push(id);
4929
+ const results = await probeModelsSequentially((body, signal) => upstream.chatStream(body, signal), testable);
4930
+ return skipped.length === 0 ? { results } : {
4931
+ results,
4932
+ skipped
4933
+ };
4934
+ },
4575
4935
  invalidateAdapter: () => {},
4576
4936
  rawDiagnostic: () => rawDiagnostic()
4577
4937
  };
@@ -4583,6 +4943,7 @@ function apply(ctx, config) {
4583
4943
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
4584
4944
  regionEnabled: (region) => regionEnabled(current(), region),
4585
4945
  discoverModels: (region, signal) => stacks[region].discoverModels(signal),
4946
+ probeModels: (region, ids) => stacks[region].probeModels(ids),
4586
4947
  rawDiagnostic: (region) => stacks[region].rawDiagnostic()
4587
4948
  }));
4588
4949
  /**
@@ -4727,4 +5088,4 @@ function apply(ctx, config) {
4727
5088
  });
4728
5089
  }
4729
5090
  //#endregion
4730
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
5091
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
- "version": "2.5.0",
5
+ "version": "2.7.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",