dsh-connect-trae 4.4.7 → 4.5.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 +108 -0
- package/README.en.md +18 -1
- package/README.md +18 -1
- package/lib/client.js +114 -33
- package/lib/index.d.ts +85 -3
- package/lib/index.js +225 -11
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,113 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.1 (2026-10-25)
|
|
4
|
+
|
|
5
|
+
> **只在租户池上架的模型不再隐形。** 插件的模型目录只问了 `solo.trae.cn` 一处,
|
|
6
|
+
> 而它并不发布租户池的完整名单——`DeepSeek-V4.1-Flash-1017` 就在其中。这一版把
|
|
7
|
+
> `console.enterprise.trae.cn` 的目录**并入**主目录,两个池的模型都保留。
|
|
8
|
+
>
|
|
9
|
+
> 成因不是合并层丢行,而是**读错了 host**:目录基址循环在第一个答非空的 host 就
|
|
10
|
+
> `return`,而主目录总会答——所以只在租户池上架的模型,即便拿着租户凭据也永远看不到
|
|
11
|
+
> (#46)。
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
|
|
15
|
+
- **模型目录并入租户池,只在租户侧上架的模型不再隐形**(#46)。
|
|
16
|
+
- **实测**(2026-10-24,真实 CN 租户凭据):`solo.trae.cn` 答 36 个 id、**没有**
|
|
17
|
+
`DeepSeek-V4.1-Flash-1017`;`console.enterprise.trae.cn` 答 24 个、**有**。
|
|
18
|
+
4.4.7 与 4.5.0 表现一致,所以升级修不了。
|
|
19
|
+
- **并集而非替换**:两侧没有一方包含另一方——主目录的 36 个里有 **19** 个不在租户
|
|
20
|
+
应答中,其中就包括 `deepseek-v4.1-flash`。换 host 等于用 1 个模型换掉 19 个。
|
|
21
|
+
- **也不能做成兜底候选**:基址循环在第一个答非空的 host 就返回,而主目录总会答,
|
|
22
|
+
候选恰好在唯一会出错的情形下被跳过。因此额外 host 单独读取后再并入
|
|
23
|
+
(`TraeSoloRemoteCatalogOptions.unionBaseUrls`)。
|
|
24
|
+
- **冲突保留主目录的拼写**,按不区分大小写比较。这里不能用 `preferStrongerRow`
|
|
25
|
+
打擂台:`deriveCatalog` 用 `enabled.has(model.id)` **精确匹配**,改名会让用户
|
|
26
|
+
已存进 `enabledModelIds` 的那一项静默失配、模型从选择里消失。
|
|
27
|
+
- **逐 host 尽力而为**:传输失败、非 2xx、响应体读不出、应答为空,都记日志后跳过,
|
|
28
|
+
只有调用方 abort 才向上抛。某个额外 host 坏掉,损失的是它的模型,而不是整份目录。
|
|
29
|
+
- **门控按凭据、不按面板**:只登录租户账号的机器,在 Trae(消费端)面板下
|
|
30
|
+
`store.resolve()` 依然会返回租户凭据(`offered()` 是「藏光了就回退」),所以按
|
|
31
|
+
面板判会漏掉这类用户。现在判「解析出的凭据 `tenantId` 非空」,个人凭据在发出请求
|
|
32
|
+
前就跳过。
|
|
33
|
+
- **`unionRegion` 挡住跨区域请求**:`DSH_TRAE_TENANT_IN_CONSUMER` 是环境变量、不分
|
|
34
|
+
区域,原先国际版面板也会挂上 CN 租户 host(用 SG 凭据实测确实发出、拿 401 后被
|
|
35
|
+
跳过)。现在区域不符即跳过。
|
|
36
|
+
- **额外 host 有负缓存**:`discoverModels()` 不传 signal、超时回落到 30 秒,原先企业
|
|
37
|
+
host 不可达时每次刷新都要多等。失败后与主 host 同款冷却 5 分钟。
|
|
38
|
+
- **它不是别名**:用插件自己的 bridge 驱动真实网关,两个 id 派往不同后端
|
|
39
|
+
(`DeepSeek-V4.1-Flash-1017` → `deepseek-v4.1-flash-economic-ark`,
|
|
40
|
+
`DeepSeek-V4.1-Flash` → `deepseek-v4.1-flash-ark`)。
|
|
41
|
+
- 测试:`tests/solo-remote.spec.ts` 新增 8 例,覆盖并集、仅大小写不同的冲突、
|
|
42
|
+
额外 host 不可达/被拒/应答为空、个人凭据跳过、跨区域跳过、冷却、已经是主 host 时
|
|
43
|
+
不重复请求,以及未配置 `unionBaseUrls` 时行为与改动前完全一致。
|
|
44
|
+
- 贡献者:[@yuantao313](https://github.com/yuantao313)(PR #59)。
|
|
45
|
+
|
|
46
|
+
## 4.5.0 (2026-10-24)
|
|
47
|
+
|
|
48
|
+
> **宽窗模型多一个 500K 上下文中间档。** Trae 每个模型公布两个窗口——默认(dev)档和
|
|
49
|
+
> Max 档——从 200K 直接跳到 1M 是 5 倍,而 1M 的计费并不是每个会话都用得上。这一版在两者
|
|
50
|
+
> 之间加了一个本地的半步。
|
|
51
|
+
>
|
|
52
|
+
> 500K **不是上游公布的窗口**,所以它有门槛、也有校验:只有当模型的 Max 真的够宽、500K
|
|
53
|
+
> 又确实落在它的两个窗口之间时才出现;落盘校验用同一条规则,旧值撑不破模型的真实窗口。
|
|
54
|
+
|
|
55
|
+
### 新增
|
|
56
|
+
|
|
57
|
+
- **宽窗模型多一个 500K 上下文中间档(国内版 / 国际版都有)**。
|
|
58
|
+
- Trae 每个模型公布两个窗口:默认(dev,实测 200K / 256K / 272K 三种)与 Max
|
|
59
|
+
(实测 1M,MiMo 为 981504)。从默认直接跳到 Max 是 5 倍,而 1M 的计费并不是每个
|
|
60
|
+
会话都用得上——所以加一个 500K 的半步。
|
|
61
|
+
- **500K 不是上游公布的窗口**,是本地的选择,因此有门槛:只有当该模型的
|
|
62
|
+
`maxContextWindow >= 500000`、且 500K 严格宽于它的 dev 窗、又严格窄于它的 Max
|
|
63
|
+
时才出现(`contextBudgetOptions`)。dev 本身已是 500K、或 Max 正好 500K 时不会
|
|
64
|
+
出现重复选项。
|
|
65
|
+
- **落盘校验用的是同一条规则**:`applyContextBudgets` 只接受该模型可选窗口里的值,
|
|
66
|
+
所以旧配置里留下的 500K 在某模型 Max 被上游调小之后会安静失效、退回 dev 档,
|
|
67
|
+
而不是把模型撑到它没有的窗口。
|
|
68
|
+
- 卡片每行因此可能是 1 / 2 / 3 个单选(无 Max 的行仍是一个只读选项);新增整区按钮
|
|
69
|
+
「整区切到 500K」,只作用于 Max ≥ 500K 的行,写入值与逐行单选完全一致。
|
|
70
|
+
- 测试:`tests/context-budget.spec.ts`(9 例,含「Max 不足 500K 不出现」「Max 正好
|
|
71
|
+
500K 不重复」「Max 调小后旧 500K 失效」)与 `tests/card-context-500k.spec.tsx`
|
|
72
|
+
(3 例,含「只给合格行第三个单选」「整区按钮只写合格行」「逐行与整区落盘一致」)。
|
|
73
|
+
两组均做过变异验证:去掉 500K 档 → 5 例红;放宽接受规则 → 3 例红。
|
|
74
|
+
|
|
75
|
+
### 修复
|
|
76
|
+
|
|
77
|
+
- **发布核验不再重建工作树,宿主不再被核验动作掀翻**(`fix(scripts)`)。
|
|
78
|
+
- **事故**(2026-10-24 14:40:08 / 14:41:01 / 14:42:16):桌面版渲染进程连崩三次
|
|
79
|
+
(`EXC_BREAKPOINT`/`SIGTRAP`),应用白屏,最后要人工重启。取证在
|
|
80
|
+
`~/Library/Logs/DiagnosticReports/DeepSeek Harness Helper (Renderer)-*.ips`。
|
|
81
|
+
- **成因**:开发者常态是把 bundle 以**软链**装进 profile
|
|
82
|
+
(`node_modules/dsh-connect-trae -> <本仓库>`),于是运行中的 DSH 宿主服务的正是本仓库
|
|
83
|
+
`lib/` 的字节;而 `scripts/verify-release.mjs` 的第 4 步(确定性重建比对)原本
|
|
84
|
+
**在工作树里**跑 `npm run build`——等于当着活着的宿主改写它正在服务的客户端 bundle,
|
|
85
|
+
宿主重新 import,渲染进程崩。
|
|
86
|
+
- **修法**:重建改到**临时 worktree**(`git worktree add --detach <tag>` + 软链
|
|
87
|
+
`node_modules`)里做,工作树的 `lib/` 一个字节都不动;`git worktree remove` 在收尾时
|
|
88
|
+
清理。用 worktree 而不是 `git archive | tar -x`,是因为测试机上的 PATH 上有**假的
|
|
89
|
+
`tar`**(发布测试用它冒充 registry),走 tar 会解出错的树。
|
|
90
|
+
- **守卫**:`tests/verify-release.spec.ts` 新增一例「工作树不被重建」。变异验证:把重建
|
|
91
|
+
改回 `cwd: repo` → 该例立刻变红(fixture 的 build 会真的写 `lib/`)。
|
|
92
|
+
- **构建前会提醒「这次会改写正在被宿主服务的字节」**(`chore(scripts)`)。
|
|
93
|
+
新增 `scripts/warn-live-host.mjs`,接到 `prebuild` 钩子:检查「有没有 profile 软链到本仓库」
|
|
94
|
+
且「有没有活着的宿主进程」,两者同时成立就打印警告。**它从不失败构建**——热更新的正当
|
|
95
|
+
流程必须还能走;崩了重启应用即可。
|
|
96
|
+
|
|
97
|
+
### 文档
|
|
98
|
+
|
|
99
|
+
- `DEVELOPMENT.md` 新增首节「危险动作:不要在宿主运行时重建 `lib/`」:讲清软链装法、
|
|
100
|
+
事故时间线与三种场景下该怎么做(不开了随便构建 / 只验证发布用 `verify:release` /
|
|
101
|
+
要热更新就接受代价 / 发版前先关宿主)。
|
|
102
|
+
- `RELEASING.md` 第 8 步补一句:第 4 步的重建在临时 worktree 里做,理由与对策指向上面那节。
|
|
103
|
+
- **在核心位置写明「不做插件之间的联动」这条边界**。
|
|
104
|
+
- `README.md` 开头加一条醒目的边界声明,并新增 `## 与其它插件的关系` 一节(英文版同步):
|
|
105
|
+
不共享状态、不做跨插件的账号/额度调度、不协商槽位、不共享开关,以及立这条边的理由。
|
|
106
|
+
- `src/index.ts` 的**模块头**加 SCOPE BOUNDARY 注释:本模块只与 DSH Host 和 Trae 上游
|
|
107
|
+
两方对话,不读写任何其它插件的文件或状态。
|
|
108
|
+
- 补一条区分:同一族插件之间**互相学习**(workbuddy 的教训被写进本仓库的注释与测试),
|
|
109
|
+
但**不互相调用**——学习是编译期的事,联动是运行时的事。
|
|
110
|
+
|
|
3
111
|
## 4.4.7 (2026-10-24)
|
|
4
112
|
|
|
5
113
|
> **4.4.6 把「读不出来的账号副本」改名留住了,但这件事一直是静默的——现在卡片会把它点名,
|
package/README.en.md
CHANGED
|
@@ -30,6 +30,8 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
30
30
|
|
|
31
31
|
> Apart from the daily check-in claim, every query this plugin makes is read-only and consumes nothing; the claim only happens when you click the button.
|
|
32
32
|
|
|
33
|
+
> **Boundary: this plugin does not interoperate with any other plugin.** Accounts, credentials, the model directory, the account pool and the settings namespace belong to this plugin alone; a `trae` failure never borrows another plugin's account or credit, and vice versa. Why, and what that rules out, is in [Relationship to other plugins](#relationship-to-other-plugins).
|
|
34
|
+
|
|
33
35
|
## Features
|
|
34
36
|
|
|
35
37
|
- **Three parallel providers** — the CN side registers as the `trae` provider (e.g. `DeepSeek-V4-Flash`, `DeepSeek-V4-Pro`), the enterprise side as `trae-enterprise`, and the international side as `trae-global` (the Gemini/GPT/MiniMax roster); **all three rosters appear in the DSH model picker simultaneously**: different sessions can each pick a side without interfering.
|
|
@@ -82,6 +84,19 @@ Usage overview hits the read-only `https://api.trae.cn/trae/api/v2/pay/*` and `/
|
|
|
82
84
|
|
|
83
85
|
> See `docs/IMPLEMENTATION_PLAN.md`, `docs/SOLO_ROUTE_DECISION.md`, `docs/USAGE_API_RESEARCH.md`.
|
|
84
86
|
|
|
87
|
+
## Relationship to other plugins
|
|
88
|
+
|
|
89
|
+
**This plugin does not interoperate with any other plugin.** That is a boundary, not a slogan:
|
|
90
|
+
|
|
91
|
+
- **No shared state.** Accounts, credentials, the model directory, the account pool, access tokens and the device id belong to this plugin alone; it reads and writes only its own files (`$DSH_HOME/.trae-auth.*.json`, `.trae-auth.deleted.*.json`, `.trae-pool.*.json`) and its own settings namespace — it does **not** read, write, or supply data to any other plugin. It does edit the Trae desktop app's own sign-in file, but that is another **application**, not another plugin.
|
|
92
|
+
- **No cross-plugin account or credit scheduling.** The pool ranks and switches only among this plugin's own accounts; a `trae` failure is a failure — it never borrows another connector plugin's account, credit or channel, and never hands a request to another plugin to retry. The reverse holds too: no plugin can send requests through this one.
|
|
93
|
+
- **No slot negotiation.** Whether the card and the composer readout appear is decided by **whether this plugin's provider is selected** (read from DSH's own `modelSelection` projection), not by plugin-to-plugin messages allocating space. Two connector plugins installed together coexist because each appears only for its own provider — that is **non-interference**, not collaboration.
|
|
94
|
+
- **No shared switches.** Turning one panel off withdraws only this plugin's provider and requests; it never changes another plugin's provider, models, binding or settings.
|
|
95
|
+
|
|
96
|
+
**Why the boundary exists.** Once plugins depend on each other, any upgrade, uninstall or version-line change silently alters the behaviour of its neighbours — and DSH's load order, parallel versions and user-authored profiles make that coupling impossible to express reliably. Holding the boundary keeps "who is broken" answerable with **one** plugin instead of a chain.
|
|
97
|
+
|
|
98
|
+
> Sibling plugins **learn from** each other (lessons from `dsh-connect-workbuddy` are quoted in this repository's comments and tests) but never **call** each other — learning is a compile-time affair, interoperation is a runtime one, and the two must stay apart.
|
|
99
|
+
|
|
85
100
|
## Image input
|
|
86
101
|
|
|
87
102
|
The plugin declares whether a model accepts images, and DSH uses that to decide whether an attachment is **sent as-is** or **degraded into a text description**. That makes the declaration a hard switch, not a cosmetic label.
|
|
@@ -134,7 +149,9 @@ To use these two models today, use the **Trae IDE itself**.
|
|
|
134
149
|
|
|
135
150
|
The "最大输出" (max output) on the model card comes from `model_detail_list[].max_tokens` in Trae's `get_detail_param` — upstream **does** publish it (live-verified: 80/80 CN configs carry it). As of 4.0.1 the value travels through the catalog merge all the way to the card, `/models/refresh` and DSH model registration; before that the merge step dropped the field, so the whole region rendered blank (issue #42 — full diagnosis in `docs/ISSUE42_DIAGNOSIS.md`). **Rows that are still blank are being honest**: models the directory lists but the SOLO roster has no config for (the three TraeCode rows above) have no candidate value to show — we prefer a blank over an invented cap. The value is metadata only; the plugin never puts `max_tokens` into request bodies (the server decides the real cap).
|
|
136
151
|
|
|
137
|
-
Context windows are a per-model choice between Trae's default (dev) window and its Max window, stored per panel as `contextBudgets`.
|
|
152
|
+
Context windows are a per-model choice between Trae's default (dev) window and its Max window — **and, as of 4.4.8, a 500K middle step for wide-window models** — stored per panel as `contextBudgets`. 500K is **not** an upstream-advertised window; it is a local half-way point. Dev windows are typically 200K/256K/272K and Max is typically 1M, so going straight from default to Max is a 5× jump, and Max-tier billing is not what every session needs. It is therefore **conditional**: the option only appears when that model's Max is at least 500K, and 500K is genuinely wider than its dev window and narrower than its Max. `applyContextBudgets` validates against the **same rule** (`contextBudgetOptions`), so a stored value that is not one of that model's selectable windows always falls back to the dev window — it can never push a model past a window it does not have (if upstream later shrinks a model's Max to 256K, a stale 500K quietly stops applying instead of overflowing).
|
|
153
|
+
|
|
154
|
+
For a whole-region switch, the model section has three buttons: "**Switch all to 500K**" (writes 500K for every row whose Max is at least 500K, leaves the rest alone), "**Switch all to Max**" (writes each Max-capable row's `maxContextWindow`, leaves no-Max rows and existing manual budgets alone) and "**Restore default context**" (clears the region's budget dict). All act on the draft and take effect on Save, producing exactly the same stored shape as the per-row radios (issue #42 side request — 18 Max rows in CN used to mean 18 clicks).
|
|
138
155
|
|
|
139
156
|
## Install
|
|
140
157
|
|
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
|
|
31
31
|
> 除每日签到领取外,插件的所有查询都是只读的、不消耗额度;签到领取也只在你点击按钮时才发生。
|
|
32
32
|
|
|
33
|
+
> **边界:本插件不与任何其它插件联动。** 账号、凭据、模型目录、账号池与设置命名空间都只属于本插件;`trae` 失败不会借用别的插件的账号或额度,反之亦然。理由与具体含义见[与其它插件的关系](#与其它插件的关系)。
|
|
34
|
+
|
|
33
35
|
## 功能特性
|
|
34
36
|
|
|
35
37
|
- **三供应商并行接入** —— 国内版注册为 DSH 的 `trae` provider(`DeepSeek-V4-Flash`、`DeepSeek-V4-Pro` 等),企业版注册为 `trae-enterprise`,国际版注册为 `trae-global`(Gemini / GPT / MiniMax 阵容),**三边模型同时出现在 DSH 模型选择器里**:不同会话可以各选一边,互不干扰。
|
|
@@ -87,6 +89,19 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
87
89
|
|
|
88
90
|
> 详见 `docs/IMPLEMENTATION_PLAN.md`、`docs/SOLO_ROUTE_DECISION.md`、`docs/USAGE_API_RESEARCH.md`。
|
|
89
91
|
|
|
92
|
+
## 与其它插件的关系
|
|
93
|
+
|
|
94
|
+
**本插件不与任何其它插件联动。** 这是一条边界,不是一句口号:
|
|
95
|
+
|
|
96
|
+
- **不共享状态。** 账号、凭据、模型目录、账号池、访问令牌、设备号都只属于本插件;插件只读写自己的文件(`$DSH_HOME/.trae-auth.*.json`、`.trae-auth.deleted.*.json`、`.trae-pool.*.json`)与自己的设置命名空间,**不读、不写、也不为任何别的插件供数**。它确实会编辑 Trae 桌面应用自己的登录文件,但那是另一个**应用**,不是另一个插件。
|
|
97
|
+
- **不做跨插件的账号或额度调度。** 账号池只在本插件自己的账号之间排序与换号;`trae` 失败就是失败,不会去「借」另一个连接器插件的账号、额度或通道,也不会把请求转交给别的插件重试。反方向同样成立:没有任何插件能通过本插件代发请求。
|
|
98
|
+
- **不协商槽位。** 卡片与 composer 读数是否出现,由**本插件的 provider 是否被选中**决定(读 DSH 自己的 `modelSelection` 投影),不靠插件间通信分配位置。两个连接器插件同时安装时,它们靠「各自只在自己的 provider 被选中时出现」共存——这是**互不干扰**,不是协作。
|
|
99
|
+
- **不共享开关。** 关掉本插件的某个面板,只撤下本插件的 provider 与请求,不会改动另一个插件的供应商、模型、绑定或设置。
|
|
100
|
+
|
|
101
|
+
**为什么立这条边界。** 插件之间一旦互相依赖,任何一个插件的升级、卸载或版本线变化,都会悄悄改变另一个插件的行为;而 DSH 的加载顺序、多版本并存与用户自定义 profile 让这种耦合无法可靠表达。立住这条边界,「出问题的是谁」永远可以定位到**一个**插件,而不是一串。
|
|
102
|
+
|
|
103
|
+
> 同一族插件之间**互相学习**(`dsh-connect-workbuddy` 的教训被写进本仓库的注释与测试),但**不互相调用**——学习是编译期的事,联动是运行时的事,这两件事必须分开。
|
|
104
|
+
|
|
90
105
|
## 图片输入(识图)
|
|
91
106
|
|
|
92
107
|
插件对外声明「这个模型是否接受图片」,DSH 据此决定附图是**原样发出**还是**降级成文字描述**。所以这条声明是硬开关,不是一个无关紧要的标记。
|
|
@@ -151,7 +166,9 @@ Trae does not serve this model under the SOLO function the request used
|
|
|
151
166
|
|
|
152
167
|
模型卡上的「最大输出」来自 Trae `get_detail_param` 的 `model_detail_list[].max_tokens` —— 上游**是**发布这个值的(CN 实测 80/80 config 都带)。4.0.1 起它随目录合并一路带到卡片、`/models/refresh` 与 DSH 模型注册;此前合并层漏传了这个字段,整区模型全部显示空白(issue #42,诊断见 `docs/ISSUE42_DIAGNOSIS.md`)。**仍然空白的行是诚实的**:目录里有、但 SOLO 名单里没有对应 config 的模型(上面那 3 个 TraeCode 通道行)拿不到任何候选值,宁可空白也不编一个上限。这个值是元数据,插件不把它写进请求体(输出上限由 Trae 服务端决定)。
|
|
153
168
|
|
|
154
|
-
上下文窗口在 Trae 公布的默认(dev)档与 Max
|
|
169
|
+
上下文窗口在 Trae 公布的默认(dev)档与 Max 档之间选,**4.4.8 起对宽窗模型多一个 500K 中间档**,按面板逐模型存成 `contextBudgets`。500K **不是**上游公布的窗口,是本地的半步:dev 常见 200K/256K/272K,Max 常见 1M,从默认直接跳到 Max 是 5 倍,而 1M 的计费并不是每个会话都用得上。所以它是**有门槛的**:只有当该模型的 Max ≥ 500K、且 500K 确实比 dev 宽、又确实比 Max 窄时才出现——`applyContextBudgets` 用**同一条规则**(`contextBudgetOptions`)校验,落盘值不是该模型可选窗口就一律退回 dev 档,绝不会把模型撑到它没有的窗口(比如某模型后来被上游把 Max 调到 256K,旧的 500K 会安静地失效而不是溢出)。
|
|
170
|
+
|
|
171
|
+
想整区一次切换:模型管理区有「**整区切到 500K**」(给本区所有 Max ≥ 500K 的行写入 500K,其余不动)、「**整区切到 Max**」(给每个带 Max 档的行写入其 `maxContextWindow`,没有 Max 档的行不动)和「**全部恢复默认上下文**」(清空本区预算)三个按钮,作用在草稿上、按「保存」生效,与逐行单选落盘的结果完全一致(issue #42 附带需求——CN 有 18 个 Max 模型,以前要点 18 次)。
|
|
155
172
|
|
|
156
173
|
## 安装
|
|
157
174
|
|
package/lib/client.js
CHANGED
|
@@ -411,6 +411,55 @@ window.__ModuleLoader__.load({
|
|
|
411
411
|
return tier !== void 0 && tier !== "" && !/^free$/i.test(tier);
|
|
412
412
|
}
|
|
413
413
|
//#endregion
|
|
414
|
+
//#region src/context-budget.ts
|
|
415
|
+
/**
|
|
416
|
+
* Context-budget arithmetic shared by the HOST (which applies a saved budget to
|
|
417
|
+
* the catalog DSH is told about) and the CLIENT card (which renders the radios).
|
|
418
|
+
*
|
|
419
|
+
* It lives in its own module because the client bundle cannot import
|
|
420
|
+
* `catalog.ts` (host-only) and the two sides must agree EXACTLY on which
|
|
421
|
+
* windows are selectable: a card that offers a window the host then refuses
|
|
422
|
+
* would be a control that silently does nothing.
|
|
423
|
+
*/
|
|
424
|
+
/**
|
|
425
|
+
* The one intermediate budget the card offers, in tokens: 500K.
|
|
426
|
+
*
|
|
427
|
+
* WHY IT EXISTS: Trae advertises a dev window (typically 200K/256K/272K) and a
|
|
428
|
+
* Max window (typically 1M). Switching dev→Max is a 5× jump for a user who
|
|
429
|
+
* just wants more room without paying Max-tier pricing for the whole session.
|
|
430
|
+
* 500K is the half-way point, and it is only ever offered for a model whose
|
|
431
|
+
* Max window is at least that wide.
|
|
432
|
+
*
|
|
433
|
+
* WHY A CONSTANT AND NOT A PER-MODEL VALUE: this is a LOCAL preference that
|
|
434
|
+
* DSH uses to decide when to compress; it is not a Trae-advertised window. The
|
|
435
|
+
* value must be sane for every model that can select it, so eligibility is
|
|
436
|
+
* gated on `maxContextWindow >= 500000` rather than inventing a window per
|
|
437
|
+
* model. `applyContextBudgets` additionally refuses any budget wider than what
|
|
438
|
+
* the model advertises, so a stale saved value cannot overflow a shrunken Max.
|
|
439
|
+
*/
|
|
440
|
+
const TRAE_CONTEXT_BUDGET_MID = 5e5;
|
|
441
|
+
/**
|
|
442
|
+
* The selectable windows for one model, smallest first: dev, then 500K (only
|
|
443
|
+
* when it is a genuine middle step), then Max. Duplicates are dropped so a
|
|
444
|
+
* model whose dev window is already 500K offers it once.
|
|
445
|
+
*
|
|
446
|
+
* A model with no Max window yields exactly one option — its dev window — which
|
|
447
|
+
* is what makes the card's radios read-only for such models.
|
|
448
|
+
*/
|
|
449
|
+
function contextBudgetOptions(model) {
|
|
450
|
+
const dev = model.contextWindow;
|
|
451
|
+
const max = model.maxContextWindow;
|
|
452
|
+
const options = [];
|
|
453
|
+
const push = (value) => {
|
|
454
|
+
if (value === void 0 || value < 1) return;
|
|
455
|
+
if (!options.includes(value)) options.push(value);
|
|
456
|
+
};
|
|
457
|
+
push(dev);
|
|
458
|
+
if (max !== void 0 && max >= 5e5 && (dev === void 0 || 5e5 > dev)) push(TRAE_CONTEXT_BUDGET_MID);
|
|
459
|
+
push(max);
|
|
460
|
+
return options.sort((left, right) => left - right);
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
414
463
|
//#region src/model-order.ts
|
|
415
464
|
/**
|
|
416
465
|
* Wire name of the default SOLO chat function.
|
|
@@ -4969,6 +5018,27 @@ window.__ModuleLoader__.load({
|
|
|
4969
5018
|
});
|
|
4970
5019
|
};
|
|
4971
5020
|
/**
|
|
5021
|
+
* Bulk 500K switch: every model that offers the middle step goes to it, and
|
|
5022
|
+
* every other model is left exactly as configured.
|
|
5023
|
+
*
|
|
5024
|
+
* The reporter-shaped case this serves: a region can hold 19 models with a
|
|
5025
|
+
* 1M Max, and picking the half-way window one radio at a time is 19 clicks.
|
|
5026
|
+
* It writes the literal {@link TRAE_CONTEXT_BUDGET_MID} — the same value the
|
|
5027
|
+
* per-row radio writes — so bulk and per-row land on identical saved state,
|
|
5028
|
+
* and `applyContextBudgets` re-validates it against each model's windows on
|
|
5029
|
+
* every read.
|
|
5030
|
+
*/
|
|
5031
|
+
const setAllContextMid = () => {
|
|
5032
|
+
editDraft((current) => {
|
|
5033
|
+
const next = { ...current.contextBudgets };
|
|
5034
|
+
for (const model of current.models) if (contextBudgetOptions(model).includes(5e5)) next[model.id] = TRAE_CONTEXT_BUDGET_MID;
|
|
5035
|
+
return {
|
|
5036
|
+
...current,
|
|
5037
|
+
contextBudgets: next
|
|
5038
|
+
};
|
|
5039
|
+
});
|
|
5040
|
+
};
|
|
5041
|
+
/**
|
|
4972
5042
|
* 「恢复默认上下文」 clears every override in this region's draft. Empty
|
|
4973
5043
|
* dict = every row falls back to its default window (the same state as a
|
|
4974
5044
|
* region that was never configured), and clearing is what makes the bulk
|
|
@@ -5586,26 +5656,22 @@ window.__ModuleLoader__.load({
|
|
|
5586
5656
|
toggleImage(model.id);
|
|
5587
5657
|
}
|
|
5588
5658
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
|
|
5589
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5659
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
5590
5660
|
className: "dsm-trae-context-budget",
|
|
5591
5661
|
"aria-label": t("row.contextBudget"),
|
|
5592
|
-
children:
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
onChange: () => {
|
|
5606
|
-
setContextBudget(model.id, model.maxContextWindow);
|
|
5607
|
-
}
|
|
5608
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.maxContextWindow ?? model.contextWindow, t("row.modelUnknown")) })] })]
|
|
5662
|
+
children: contextBudgetOptions(model).map((option) => {
|
|
5663
|
+
const selectable = model.maxContextWindow !== void 0;
|
|
5664
|
+
const checked = (activeContextBudgets[model.id] ?? model.contextWindow) === option;
|
|
5665
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5666
|
+
type: "radio",
|
|
5667
|
+
name: `context-${model.id}`,
|
|
5668
|
+
checked,
|
|
5669
|
+
disabled: !selectable || settingsScope?.getSnapshot().writable !== true || saving,
|
|
5670
|
+
onChange: () => {
|
|
5671
|
+
setContextBudget(model.id, option);
|
|
5672
|
+
}
|
|
5673
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(option, t("row.modelUnknown")) })] }, option);
|
|
5674
|
+
})
|
|
5609
5675
|
})]
|
|
5610
5676
|
})]
|
|
5611
5677
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5633,21 +5699,32 @@ window.__ModuleLoader__.load({
|
|
|
5633
5699
|
}),
|
|
5634
5700
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5635
5701
|
className: "dsm-trae-context-bulk",
|
|
5636
|
-
children: [
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5702
|
+
children: [
|
|
5703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5704
|
+
type: "button",
|
|
5705
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5706
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || !visibleModels.some((model) => contextBudgetOptions(model).includes(5e5)),
|
|
5707
|
+
title: t("row.contextAllMidHint"),
|
|
5708
|
+
onClick: setAllContextMid,
|
|
5709
|
+
children: t("row.contextAllMid")
|
|
5710
|
+
}),
|
|
5711
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5712
|
+
type: "button",
|
|
5713
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5714
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || !visibleModels.some((model) => model.maxContextWindow !== void 0),
|
|
5715
|
+
title: t("row.contextAllMaxHint"),
|
|
5716
|
+
onClick: setAllContextMax,
|
|
5717
|
+
children: t("row.contextAllMax")
|
|
5718
|
+
}),
|
|
5719
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5720
|
+
type: "button",
|
|
5721
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5722
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || Object.keys(activeContextBudgets).length === 0,
|
|
5723
|
+
title: t("row.contextAllDefaultHint"),
|
|
5724
|
+
onClick: resetAllContextBudgets,
|
|
5725
|
+
children: t("row.contextAllDefault")
|
|
5726
|
+
})
|
|
5727
|
+
]
|
|
5651
5728
|
}),
|
|
5652
5729
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5653
5730
|
className: "dsm-trae-model-capability-note",
|
|
@@ -6794,6 +6871,8 @@ window.__ModuleLoader__.load({
|
|
|
6794
6871
|
"row.expiryEarliest": "Earliest: {time}",
|
|
6795
6872
|
"row.settingsDirty": "Unsaved changes",
|
|
6796
6873
|
"row.modelsTestEmpty": "Enable at least one model first.",
|
|
6874
|
+
"row.contextAllMid": "Switch all to 500K",
|
|
6875
|
+
"row.contextAllMidHint": "Sets the DSH context budget of every model in this region whose Max window is at least 500K to the 500K middle step, leaving the rest alone. Half-way between the default window and Max — for when 1M is more than you need. Still a draft — press Save to apply.",
|
|
6797
6876
|
"row.contextAllMax": "Switch all to Max",
|
|
6798
6877
|
"row.contextAllMaxHint": "Sets the DSH context budget of every model in this region that has a Max window to its Max window at once. Still a draft — press Save to apply. Models without a Max window are left alone.",
|
|
6799
6878
|
"row.contextAllDefault": "Restore default context",
|
|
@@ -7171,6 +7250,8 @@ window.__ModuleLoader__.load({
|
|
|
7171
7250
|
"row.expiryEarliest": "最早到期:{time}",
|
|
7172
7251
|
"row.settingsDirty": "有未保存的修改",
|
|
7173
7252
|
"row.modelsTestEmpty": "请先启用至少一个模型。",
|
|
7253
|
+
"row.contextAllMid": "整区切到 500K",
|
|
7254
|
+
"row.contextAllMidHint": "把本区所有 Max 档不小于 500K 的模型的 DSH 上下文预算一次性设为 500K 中间档,其余模型不动。这是默认窗口与 Max 之间的半步——觉得 1M 用不上、又嫌默认太小时用。这只是草稿——要按「保存」才生效。",
|
|
7174
7255
|
"row.contextAllMax": "整区切到 Max",
|
|
7175
7256
|
"row.contextAllMaxHint": "把本区所有带 Max 档的模型的 DSH 上下文预算一次性设为各自的 Max 档。这只是草稿——要按「保存」才生效。没有 Max 档的模型不会被改动。",
|
|
7176
7257
|
"row.contextAllDefault": "全部恢复默认上下文",
|
package/lib/index.d.ts
CHANGED
|
@@ -481,9 +481,13 @@ declare function withChatCallableFunctions(rows: readonly TraeModelInfo[], chatF
|
|
|
481
481
|
/** Local DSH context budget per model; a value may only select an advertised window. */
|
|
482
482
|
type TraeContextBudget = number;
|
|
483
483
|
/**
|
|
484
|
-
* Apply the saved local budget. Trae advertises
|
|
485
|
-
*
|
|
486
|
-
*
|
|
484
|
+
* Apply the saved local budget. Trae advertises a dev window and a Max window
|
|
485
|
+
* per model, and the card may also offer the 500K middle step (see
|
|
486
|
+
* {@link contextBudgetOptions}), so a budget may only switch a model to a
|
|
487
|
+
* window that model actually offers — never to a fabricated number, and never
|
|
488
|
+
* to something wider than its Max (a saved 500K for a model later refreshed
|
|
489
|
+
* down to a 256K Max must fall back, not overflow). Everything else keeps the
|
|
490
|
+
* dev window.
|
|
487
491
|
*/
|
|
488
492
|
declare function applyContextBudgets(catalog: readonly TraeModelInfo[], budgets?: Readonly<Record<string, TraeContextBudget | undefined>>): TraeModelInfo[];
|
|
489
493
|
/** Convert Trae metadata into catalog rows; image input is decided by {@link applyImageSelection}. */
|
|
@@ -3482,6 +3486,51 @@ interface TraeSoloRemoteCatalogOptions {
|
|
|
3482
3486
|
* of official-host-first.
|
|
3483
3487
|
*/
|
|
3484
3488
|
log?: (message: string, detail?: unknown) => void;
|
|
3489
|
+
/**
|
|
3490
|
+
* Extra directory hosts whose rows are UNIONED into the primary answer.
|
|
3491
|
+
*
|
|
3492
|
+
* The base loop below stops at the FIRST host that answers non-empty. That is
|
|
3493
|
+
* the right rule for a FALLBACK chain (one host, mirrored), but it cannot
|
|
3494
|
+
* express "these hosts serve DIFFERENT pools" — and the primary host
|
|
3495
|
+
* answering successfully is exactly the case where another host's models
|
|
3496
|
+
* stay invisible. So these hosts are read SEPARATELY and merged, never added
|
|
3497
|
+
* as another candidate base: a candidate is skipped precisely when the
|
|
3498
|
+
* primary succeeds, which is always in the scenario that fails today.
|
|
3499
|
+
*
|
|
3500
|
+
* Measured 2026-10-24 on a live CN tenant credential:
|
|
3501
|
+
* `solo.trae.cn` (the primary) answered 36 ids and does NOT list
|
|
3502
|
+
* `DeepSeek-V4.1-Flash-1017`, while `console.enterprise.trae.cn` answered 24
|
|
3503
|
+
* and DOES list it (issue #46). Neither host is a superset of the other: 19
|
|
3504
|
+
* of the primary's ids are absent from the tenant answer, so REPLACING the
|
|
3505
|
+
* primary would trade one missing model for nineteen — a union is the only
|
|
3506
|
+
* shape that loses nothing.
|
|
3507
|
+
*
|
|
3508
|
+
* The primary answer wins every id collision, compared case-insensitively, and
|
|
3509
|
+
* a colliding extra row is dropped rather than capacity-competed: a
|
|
3510
|
+
* `preferStrongerRow` contest could rename a model the user has already saved
|
|
3511
|
+
* in `enabledModelIds` (`deriveCatalog` matches those ids EXACTLY), silently
|
|
3512
|
+
* dropping the model out of the selection.
|
|
3513
|
+
*
|
|
3514
|
+
* An extra host is asked ONLY when the credential being used can call that
|
|
3515
|
+
* pool at all (`region` must match the host, and a tenant host additionally
|
|
3516
|
+
* requires a tenant credential). The gate lives here rather than at the
|
|
3517
|
+
* construction site because the deciding fact — "is this credential a tenant
|
|
3518
|
+
* one?" — only exists once `credential()` has resolved, which is per refresh.
|
|
3519
|
+
* Gating on the PANEL instead missed the case that matters: a consumer store
|
|
3520
|
+
* does not hard-exclude tenant accounts (`auth.ts`'s `offered()` backs off
|
|
3521
|
+
* when its filter would hide everything), so a machine whose only login is a
|
|
3522
|
+
* tenant account resolves a tenant credential under the Trae panel too.
|
|
3523
|
+
*/
|
|
3524
|
+
unionBaseUrls?: readonly string[];
|
|
3525
|
+
/**
|
|
3526
|
+
* Region an extra host serves. An `ai` credential never asks a `cn` host.
|
|
3527
|
+
*
|
|
3528
|
+
* `unionBaseUrls` used to be ungated by region, so a `DSH_TRAE_TENANT_IN_CONSUMER`
|
|
3529
|
+
* machine on the international panel issued the CN tenant request on every
|
|
3530
|
+
* refresh — a 401 the union skipped, but one wasted request and one warn log
|
|
3531
|
+
* per refresh (measured with an SG credential 2026-10-24).
|
|
3532
|
+
*/
|
|
3533
|
+
unionRegion?: TraeRegion;
|
|
3485
3534
|
}
|
|
3486
3535
|
/**
|
|
3487
3536
|
* Read-only model catalog client for the SOLO Web API.
|
|
@@ -3522,9 +3571,42 @@ declare class TraeSoloRemoteCatalogClient {
|
|
|
3522
3571
|
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
3523
3572
|
*/
|
|
3524
3573
|
private static readonly OFFICIAL_RETRY_MS;
|
|
3574
|
+
/**
|
|
3575
|
+
* When each extra union host may be asked again after it failed, as epoch
|
|
3576
|
+
* milliseconds. Absent = ask it on the next refresh.
|
|
3577
|
+
*
|
|
3578
|
+
* The official host has {@link OFFICIAL_RETRY_MS} because a blocked network
|
|
3579
|
+
* would otherwise pay its failing connect on every refresh; an extra host has
|
|
3580
|
+
* the same problem and, since discovery (`discoverModels()`) passes no signal,
|
|
3581
|
+
* the same 30 s ceiling. A tenant host that is unreachable from this network
|
|
3582
|
+
* would therefore stall every refresh for up to 30 s. Same cooldown, same
|
|
3583
|
+
* reasoning — an unhelpful host is skipped for a while instead of retried
|
|
3584
|
+
* immediately.
|
|
3585
|
+
*/
|
|
3586
|
+
private readonly extraRetryAtMs;
|
|
3587
|
+
private readonly unionBaseUrls;
|
|
3588
|
+
private readonly unionRegion;
|
|
3525
3589
|
constructor(options: TraeSoloRemoteCatalogOptions);
|
|
3526
3590
|
private headers;
|
|
3527
3591
|
fetchModels(signal?: AbortSignal): Promise<TraeDiscoveredModel[]>;
|
|
3592
|
+
/**
|
|
3593
|
+
* Union the extra directory hosts' rows into the primary answer (issue #46).
|
|
3594
|
+
*
|
|
3595
|
+
* The base loop above returns on the FIRST host that answers non-empty, which
|
|
3596
|
+
* is correct for a FALLBACK chain but cannot express "these hosts serve
|
|
3597
|
+
* different pools" — the primary host answering is exactly the case where the
|
|
3598
|
+
* extra host's models would stay invisible forever. Hence a separate read and
|
|
3599
|
+
* a merge, not another candidate base (see `unionBaseUrls`).
|
|
3600
|
+
*
|
|
3601
|
+
* Every extra host is BEST-EFFORT: transport failure, non-2xx, an unreadable
|
|
3602
|
+
* body and an empty answer are each logged and skipped. Only an aborted
|
|
3603
|
+
* `signal` propagates — that is the caller cancelling, not a host being
|
|
3604
|
+
* unhelpful. Failing here must never fail a refresh the primary host already
|
|
3605
|
+
* satisfied, which is why a broken extra host costs models, not the catalog.
|
|
3606
|
+
* A failure also puts that host on {@link extraRetryAtMs} so a network that
|
|
3607
|
+
* cannot reach it stops paying its connect timeout on every refresh.
|
|
3608
|
+
*/
|
|
3609
|
+
private unionWithExtraHosts;
|
|
3528
3610
|
/**
|
|
3529
3611
|
* Bases to try for one refresh, in order.
|
|
3530
3612
|
*
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,55 @@ import { execFile } from "node:child_process";
|
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
import { Readable } from "node:stream";
|
|
18
|
+
//#region src/context-budget.ts
|
|
19
|
+
/**
|
|
20
|
+
* Context-budget arithmetic shared by the HOST (which applies a saved budget to
|
|
21
|
+
* the catalog DSH is told about) and the CLIENT card (which renders the radios).
|
|
22
|
+
*
|
|
23
|
+
* It lives in its own module because the client bundle cannot import
|
|
24
|
+
* `catalog.ts` (host-only) and the two sides must agree EXACTLY on which
|
|
25
|
+
* windows are selectable: a card that offers a window the host then refuses
|
|
26
|
+
* would be a control that silently does nothing.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The one intermediate budget the card offers, in tokens: 500K.
|
|
30
|
+
*
|
|
31
|
+
* WHY IT EXISTS: Trae advertises a dev window (typically 200K/256K/272K) and a
|
|
32
|
+
* Max window (typically 1M). Switching dev→Max is a 5× jump for a user who
|
|
33
|
+
* just wants more room without paying Max-tier pricing for the whole session.
|
|
34
|
+
* 500K is the half-way point, and it is only ever offered for a model whose
|
|
35
|
+
* Max window is at least that wide.
|
|
36
|
+
*
|
|
37
|
+
* WHY A CONSTANT AND NOT A PER-MODEL VALUE: this is a LOCAL preference that
|
|
38
|
+
* DSH uses to decide when to compress; it is not a Trae-advertised window. The
|
|
39
|
+
* value must be sane for every model that can select it, so eligibility is
|
|
40
|
+
* gated on `maxContextWindow >= 500000` rather than inventing a window per
|
|
41
|
+
* model. `applyContextBudgets` additionally refuses any budget wider than what
|
|
42
|
+
* the model advertises, so a stale saved value cannot overflow a shrunken Max.
|
|
43
|
+
*/
|
|
44
|
+
const TRAE_CONTEXT_BUDGET_MID = 5e5;
|
|
45
|
+
/**
|
|
46
|
+
* The selectable windows for one model, smallest first: dev, then 500K (only
|
|
47
|
+
* when it is a genuine middle step), then Max. Duplicates are dropped so a
|
|
48
|
+
* model whose dev window is already 500K offers it once.
|
|
49
|
+
*
|
|
50
|
+
* A model with no Max window yields exactly one option — its dev window — which
|
|
51
|
+
* is what makes the card's radios read-only for such models.
|
|
52
|
+
*/
|
|
53
|
+
function contextBudgetOptions(model) {
|
|
54
|
+
const dev = model.contextWindow;
|
|
55
|
+
const max = model.maxContextWindow;
|
|
56
|
+
const options = [];
|
|
57
|
+
const push = (value) => {
|
|
58
|
+
if (value === void 0 || value < 1) return;
|
|
59
|
+
if (!options.includes(value)) options.push(value);
|
|
60
|
+
};
|
|
61
|
+
push(dev);
|
|
62
|
+
if (max !== void 0 && max >= 5e5 && (dev === void 0 || 5e5 > dev)) push(TRAE_CONTEXT_BUDGET_MID);
|
|
63
|
+
push(max);
|
|
64
|
+
return options.sort((left, right) => left - right);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
18
67
|
//#region src/catalog.ts
|
|
19
68
|
/**
|
|
20
69
|
* Bootstrap catalog: identity only where current Trae metadata has not been
|
|
@@ -425,15 +474,22 @@ function withChatCallableFunctions(rows, chatFunctions) {
|
|
|
425
474
|
});
|
|
426
475
|
}
|
|
427
476
|
/**
|
|
428
|
-
* Apply the saved local budget. Trae advertises
|
|
429
|
-
*
|
|
430
|
-
*
|
|
477
|
+
* Apply the saved local budget. Trae advertises a dev window and a Max window
|
|
478
|
+
* per model, and the card may also offer the 500K middle step (see
|
|
479
|
+
* {@link contextBudgetOptions}), so a budget may only switch a model to a
|
|
480
|
+
* window that model actually offers — never to a fabricated number, and never
|
|
481
|
+
* to something wider than its Max (a saved 500K for a model later refreshed
|
|
482
|
+
* down to a 256K Max must fall back, not overflow). Everything else keeps the
|
|
483
|
+
* dev window.
|
|
431
484
|
*/
|
|
432
485
|
function applyContextBudgets(catalog, budgets = {}) {
|
|
433
|
-
return catalog.map((model) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
486
|
+
return catalog.map((model) => {
|
|
487
|
+
const budget = budgets[model.id];
|
|
488
|
+
return {
|
|
489
|
+
...model,
|
|
490
|
+
...budget !== void 0 && contextBudgetOptions(model).includes(budget) ? { contextWindow: budget } : {}
|
|
491
|
+
};
|
|
492
|
+
});
|
|
437
493
|
}
|
|
438
494
|
/** Convert Trae metadata into catalog rows; image input is decided by {@link applyImageSelection}. */
|
|
439
495
|
function discoveredCatalog(models) {
|
|
@@ -5969,6 +6025,17 @@ function traeSoloRemoteFallbackBases(region, officialBase) {
|
|
|
5969
6025
|
return backupBase === officialBase ? [] : [backupBase];
|
|
5970
6026
|
}
|
|
5971
6027
|
/**
|
|
6028
|
+
* Case-folded identity for a directory id.
|
|
6029
|
+
*
|
|
6030
|
+
* A local copy of `catalog.ts`'s private `displayKey`: the two hosts of the
|
|
6031
|
+
* union spell the same model differently (they disagree on at least 3 ids), and
|
|
6032
|
+
* ids that differ only by case MUST be treated as the same model — otherwise a
|
|
6033
|
+
* union would list one model twice under two spellings.
|
|
6034
|
+
*/
|
|
6035
|
+
function directoryKey(id) {
|
|
6036
|
+
return id.trim().toLowerCase();
|
|
6037
|
+
}
|
|
6038
|
+
/**
|
|
5972
6039
|
* Of two rows for the SAME model id, the one the catalog should keep.
|
|
5973
6040
|
*
|
|
5974
6041
|
* The gateway groups its directory by function and reports a model's capacity
|
|
@@ -6107,10 +6174,27 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6107
6174
|
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
6108
6175
|
*/
|
|
6109
6176
|
static OFFICIAL_RETRY_MS = 3e5;
|
|
6177
|
+
/**
|
|
6178
|
+
* When each extra union host may be asked again after it failed, as epoch
|
|
6179
|
+
* milliseconds. Absent = ask it on the next refresh.
|
|
6180
|
+
*
|
|
6181
|
+
* The official host has {@link OFFICIAL_RETRY_MS} because a blocked network
|
|
6182
|
+
* would otherwise pay its failing connect on every refresh; an extra host has
|
|
6183
|
+
* the same problem and, since discovery (`discoverModels()`) passes no signal,
|
|
6184
|
+
* the same 30 s ceiling. A tenant host that is unreachable from this network
|
|
6185
|
+
* would therefore stall every refresh for up to 30 s. Same cooldown, same
|
|
6186
|
+
* reasoning — an unhelpful host is skipped for a while instead of retried
|
|
6187
|
+
* immediately.
|
|
6188
|
+
*/
|
|
6189
|
+
extraRetryAtMs = /* @__PURE__ */ new Map();
|
|
6190
|
+
unionBaseUrls;
|
|
6191
|
+
unionRegion;
|
|
6110
6192
|
constructor(options) {
|
|
6111
6193
|
this.options = options;
|
|
6112
6194
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
6113
6195
|
this.baseUrl = options.baseUrl;
|
|
6196
|
+
this.unionBaseUrls = options.unionBaseUrls ?? [];
|
|
6197
|
+
this.unionRegion = options.unionRegion;
|
|
6114
6198
|
}
|
|
6115
6199
|
async headers(region) {
|
|
6116
6200
|
const credential = await this.options.credential();
|
|
@@ -6126,7 +6210,8 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6126
6210
|
};
|
|
6127
6211
|
}
|
|
6128
6212
|
async fetchModels(signal) {
|
|
6129
|
-
const
|
|
6213
|
+
const credential = await this.options.credential();
|
|
6214
|
+
const region = regionOfCredential(credential);
|
|
6130
6215
|
const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
6131
6216
|
const headers = await this.headers(region);
|
|
6132
6217
|
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
@@ -6161,12 +6246,88 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6161
6246
|
if (base !== officialBase) this.options.log?.(this.lastSuccessfulBase === base ? `model directory served by the remembered fallback host ${base}; the official host (${officialBase}) is re-probed every ${TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS / 6e4} minutes` : `model directory served by fallback host ${base}; the official host (${officialBase}) failed this refresh`, { officialBase });
|
|
6162
6247
|
else this.officialRetryAtMs = 0;
|
|
6163
6248
|
this.lastSuccessfulBase = base;
|
|
6164
|
-
return models;
|
|
6249
|
+
return await this.unionWithExtraHosts(region, credential, headers, models, signal);
|
|
6165
6250
|
}
|
|
6166
6251
|
if (emptyError !== void 0) throw emptyError;
|
|
6167
6252
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
6168
6253
|
}
|
|
6169
6254
|
/**
|
|
6255
|
+
* Union the extra directory hosts' rows into the primary answer (issue #46).
|
|
6256
|
+
*
|
|
6257
|
+
* The base loop above returns on the FIRST host that answers non-empty, which
|
|
6258
|
+
* is correct for a FALLBACK chain but cannot express "these hosts serve
|
|
6259
|
+
* different pools" — the primary host answering is exactly the case where the
|
|
6260
|
+
* extra host's models would stay invisible forever. Hence a separate read and
|
|
6261
|
+
* a merge, not another candidate base (see `unionBaseUrls`).
|
|
6262
|
+
*
|
|
6263
|
+
* Every extra host is BEST-EFFORT: transport failure, non-2xx, an unreadable
|
|
6264
|
+
* body and an empty answer are each logged and skipped. Only an aborted
|
|
6265
|
+
* `signal` propagates — that is the caller cancelling, not a host being
|
|
6266
|
+
* unhelpful. Failing here must never fail a refresh the primary host already
|
|
6267
|
+
* satisfied, which is why a broken extra host costs models, not the catalog.
|
|
6268
|
+
* A failure also puts that host on {@link extraRetryAtMs} so a network that
|
|
6269
|
+
* cannot reach it stops paying its connect timeout on every refresh.
|
|
6270
|
+
*/
|
|
6271
|
+
async unionWithExtraHosts(region, credential, headers, primary, signal) {
|
|
6272
|
+
if (this.unionBaseUrls.length === 0) return primary;
|
|
6273
|
+
if (this.unionRegion !== void 0 && this.unionRegion !== region) return primary;
|
|
6274
|
+
if (!isEnterpriseAccount(credential)) return primary;
|
|
6275
|
+
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
6276
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6277
|
+
for (const model of primary) byKey.set(directoryKey(model.id), model);
|
|
6278
|
+
for (const base of this.unionBaseUrls) {
|
|
6279
|
+
if (base === this.lastSuccessfulBase) continue;
|
|
6280
|
+
if ((this.extraRetryAtMs.get(base) ?? 0) > Date.now()) continue;
|
|
6281
|
+
let response;
|
|
6282
|
+
try {
|
|
6283
|
+
response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
|
|
6284
|
+
headers,
|
|
6285
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
6286
|
+
});
|
|
6287
|
+
} catch (error) {
|
|
6288
|
+
if (signal?.aborted) throw error;
|
|
6289
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6290
|
+
this.options.log?.(`model directory union host ${base} was unreachable; its models are missing from this refresh`, {
|
|
6291
|
+
base,
|
|
6292
|
+
error: String(error)
|
|
6293
|
+
});
|
|
6294
|
+
continue;
|
|
6295
|
+
}
|
|
6296
|
+
if (!response.ok) {
|
|
6297
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6298
|
+
this.options.log?.(`model directory union host ${base} answered HTTP ${response.status}; its models are missing from this refresh`, { base });
|
|
6299
|
+
continue;
|
|
6300
|
+
}
|
|
6301
|
+
let extra;
|
|
6302
|
+
try {
|
|
6303
|
+
const json = await response.json();
|
|
6304
|
+
extra = this.collectModels(region, json.data?.list ?? []);
|
|
6305
|
+
} catch (error) {
|
|
6306
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6307
|
+
this.options.log?.(`model directory union host ${base} answered a body that could not be read; its models are missing from this refresh`, {
|
|
6308
|
+
base,
|
|
6309
|
+
error: String(error)
|
|
6310
|
+
});
|
|
6311
|
+
continue;
|
|
6312
|
+
}
|
|
6313
|
+
this.extraRetryAtMs.delete(base);
|
|
6314
|
+
let added = 0;
|
|
6315
|
+
for (const model of extra) {
|
|
6316
|
+
const key = directoryKey(model.id);
|
|
6317
|
+
if (byKey.has(key)) continue;
|
|
6318
|
+
byKey.set(key, model);
|
|
6319
|
+
added += 1;
|
|
6320
|
+
}
|
|
6321
|
+
const collided = extra.length - added;
|
|
6322
|
+
this.options.log?.(`model directory union host ${base} contributed ${added} model(s) the primary directory did not list${collided > 0 ? ` (${collided} collided and kept the primary spelling)` : ""}`, {
|
|
6323
|
+
base,
|
|
6324
|
+
served: extra.length,
|
|
6325
|
+
added
|
|
6326
|
+
});
|
|
6327
|
+
}
|
|
6328
|
+
return [...byKey.values()];
|
|
6329
|
+
}
|
|
6330
|
+
/**
|
|
6170
6331
|
* Bases to try for one refresh, in order.
|
|
6171
6332
|
*
|
|
6172
6333
|
* While the official host is in its post-failure cooldown the backup hosts go
|
|
@@ -9974,7 +10135,57 @@ function apply(ctx, config) {
|
|
|
9974
10135
|
});
|
|
9975
10136
|
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
9976
10137
|
credential: () => store.resolve(),
|
|
9977
|
-
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
10138
|
+
log: (message, detail) => ctx.logger.warn(message, detail),
|
|
10139
|
+
/**
|
|
10140
|
+
* The TENANT directory, UNIONED into the consumer one (issue #46).
|
|
10141
|
+
*
|
|
10142
|
+
* This is the "reads both pools" half of the routing note below, which
|
|
10143
|
+
* was intent without an implementation: only the consumer host was ever
|
|
10144
|
+
* asked. The `Solo` client above is routed, but THIS one is not — and
|
|
10145
|
+
* `solo.trae.cn`, the primary it lands on, does not publish every model a
|
|
10146
|
+
* tenant credential can call.
|
|
10147
|
+
*
|
|
10148
|
+
* Measured 2026-10-24 on the reporter's live CN tenant credential:
|
|
10149
|
+
* `solo.trae.cn` answered 36 ids with no `DeepSeek-V4.1-Flash-1017`;
|
|
10150
|
+
* `console.enterprise.trae.cn` answered 24 that include it, and the
|
|
10151
|
+
* gateway really dispatching a distinct backend for it
|
|
10152
|
+
* (`deepseek-v4.1-flash-economic-ark`, vs `deepseek-v4.1-flash-ark` for
|
|
10153
|
+
* the plain id) is how issue #46's report was confirmed.
|
|
10154
|
+
*
|
|
10155
|
+
* UNION, never replace, and never a fallback candidate. The primary is
|
|
10156
|
+
* the richer host — 19 of its ids are absent from the tenant answer,
|
|
10157
|
+
* including the `deepseek-v4.1-flash` the reporter has saved — so
|
|
10158
|
+
* replacing it would trade one missing model for nineteen. And a
|
|
10159
|
+
* candidate base cannot work at all: the base loop returns on the first
|
|
10160
|
+
* host that answers, and the primary always answers.
|
|
10161
|
+
*
|
|
10162
|
+
* Gated on a tenant pool being bindable here (an enterprise panel, or a
|
|
10163
|
+
* consumer panel under `DSH_TRAE_TENANT_IN_CONSUMER`). A personal
|
|
10164
|
+
* consumer account has nothing to gain from the tenant host, so it is not
|
|
10165
|
+
* made to pay the extra request.
|
|
10166
|
+
*
|
|
10167
|
+
* The fix is not about 1017 specifically: the tenant host is a second
|
|
10168
|
+
* pool, and every model it alone serves was invisible for the same
|
|
10169
|
+
* reason. Verified against the live hosts: 0 rows lost, 0 duplicated.
|
|
10170
|
+
*
|
|
10171
|
+
* The client decides WHEN to ask it, not this construction site: the
|
|
10172
|
+
* deciding fact is whether the credential being resolved is a tenant one,
|
|
10173
|
+
* which does not exist until `credential()` runs. Gating on the panel here
|
|
10174
|
+
* missed the reporters most likely to hit #46 in the first place — a
|
|
10175
|
+
* consumer store does not hard-exclude tenant accounts (`auth.ts`'s
|
|
10176
|
+
* `offered()` backs off when the filter would hide everything), so a
|
|
10177
|
+
* machine whose only login is a tenant account resolves one under the
|
|
10178
|
+
* Trae panel too, and an enterprise-panel-only gate never opened the
|
|
10179
|
+
* union for it. `unionRegion` additionally keeps an `ai` credential from
|
|
10180
|
+
* asking a `cn` host, which the old `DSH_TRAE_TENANT_IN_CONSUMER` gate on
|
|
10181
|
+
* its own did not.
|
|
10182
|
+
*
|
|
10183
|
+
* Providing the host unconditionally is therefore safe: a personal
|
|
10184
|
+
* credential and an `ai` credential both skip it before any request is
|
|
10185
|
+
* made, so neither pays for a pool it cannot use.
|
|
10186
|
+
*/
|
|
10187
|
+
unionBaseUrls: [`${TRAE_ENTERPRISE_HOST}/api/remote/v1`],
|
|
10188
|
+
unionRegion: "cn"
|
|
9978
10189
|
});
|
|
9979
10190
|
/**
|
|
9980
10191
|
* The IDE's own roster: which models the vendor's picker shows, per
|
|
@@ -10003,7 +10214,10 @@ function apply(ctx, config) {
|
|
|
10003
10214
|
* gateway and stamps the `session_id` it requires. The discovery client
|
|
10004
10215
|
* below stays UNROUTED on purpose — it owns the roster/probe path and, for
|
|
10005
10216
|
* a tenant credential, reads both pools so the shared catalogue keeps every
|
|
10006
|
-
* model either one serves.
|
|
10217
|
+
* model either one serves. That second pool is supplied by `unionBaseUrls`
|
|
10218
|
+
* at the catalog client above, which is what makes this paragraph true;
|
|
10219
|
+
* routing THIS client instead would have read one pool and dropped the
|
|
10220
|
+
* other (see the note there, and issue #46).
|
|
10007
10221
|
*/
|
|
10008
10222
|
/**
|
|
10009
10223
|
* Latest successful discovery for this panel, mutated by discoverModels
|
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": "4.
|
|
5
|
+
"version": "4.5.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"verify:release": "node scripts/verify-release.mjs",
|
|
53
53
|
"card:shot": "vitest run --config vitest.card.config.ts",
|
|
54
54
|
"promo:shot": "vitest run --config vitest.card.config.ts -t \"promo shot\"",
|
|
55
|
-
"panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\""
|
|
55
|
+
"panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\"",
|
|
56
|
+
"prebuild": "node scripts/warn-live-host.mjs"
|
|
56
57
|
},
|
|
57
58
|
"dsh": {
|
|
58
59
|
"bundle": {
|