dsh-connect-trae 4.5.0 → 4.5.2
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 +84 -0
- package/lib/client.js +7 -0
- package/lib/index.d.ts +95 -1
- package/lib/index.js +270 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.2 (2026-10-25)
|
|
4
|
+
|
|
5
|
+
> **思考档位不再被吞,也不再被谎报。** 插件原先只认识三档,未知档位一律按 `high`
|
|
6
|
+
> 发出去——用户选 Max、实际收到 high,无报错也无日志。这一版把词表与 Trae 客户端
|
|
7
|
+
> 对齐(`low / medium / high / extra_high / max`),并把「不认识的档位」从谎报改成
|
|
8
|
+
> 丢弃,让上游用它自己的默认值。
|
|
9
|
+
>
|
|
10
|
+
> 词表变宽,**声称没变**:能选哪些档位仍然只取决于上游为该模型实际下发的
|
|
11
|
+
> `options`,所以今天只发三档的模型不会凭空多出 Max。
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
|
|
15
|
+
- **思考档位词表配平,未知档位不再静默降档成 high**(#60)。
|
|
16
|
+
- **三处吞档位**:解析侧 `EFFORT_MAP` / `TRAE_REASONING_EFFORTS` 缺 `medium` 与
|
|
17
|
+
`max`;`toPiModel` 的 `thinkingLevelMap` 把 `minimal / medium / max` 硬编码成
|
|
18
|
+
`null`;反向映射写成三元链
|
|
19
|
+
`effort === 'low' ? 'light' : effort === 'xhigh' ? 'extra_high' : 'high'`。
|
|
20
|
+
- **最坏的是第三处**:任何未预期档位都被映射成 `high` 发给上游。Trae 客户端自己
|
|
21
|
+
认识五档(`reasoning_effort.{low,medium,high,extra_high,max}`),并按 agent model
|
|
22
|
+
灰度下发,所以上游哪天发 `max`,用户选 Max 就会被静默降成 high。**静默降档比
|
|
23
|
+
不给选择器更坏**——它给出一个便宜了、但用户没要的答案。
|
|
24
|
+
- **改法**:反向映射抽成 `WIRE_EFFORT_MAP` 查表 + `wireEffortsOf()`,三处调用点
|
|
25
|
+
统一走它(报告人点到两处,第三处在 wire 行提升的分支里,一起改才不会有同一份
|
|
26
|
+
目录两种行为)。查不到即**丢弃**,而不是猜一个默认值。
|
|
27
|
+
- **实测**(2026-10-25,本机 CN 凭据,98 行目录):当前 `options` 只有
|
|
28
|
+
`["light","high"]`、`["high","extra_high"]`、`["light","high","extra_high"]`
|
|
29
|
+
三种,`max` 尚未下发——所以这是「今天就备好、明天才用上」,不是伪造档位。
|
|
30
|
+
- 测试:新增 10 例(词表、丢弃未知档位、既有三档逐字不变、`discoveredCatalog`
|
|
31
|
+
同步、不伪造档位、`thinkingLevelMap` 按模型而非硬编码)。三处均做变异验证:
|
|
32
|
+
恢复旧映射 → 5 例红;恢复硬编码 `null` → 1 例红;去掉解析侧新键 → 1 例红。
|
|
33
|
+
- 贡献者:[@ahian-lee](https://github.com/ahian-lee)(issue #60,附诊断与补丁)。
|
|
34
|
+
|
|
35
|
+
### 文档
|
|
36
|
+
|
|
37
|
+
- **卡片模型栏写明「上下文预算只影响 DSH 侧」**(#60 的顺带问题)。
|
|
38
|
+
- 每行显示的是 Trae 自己公布的最大窗口,而下面的预算单选只写 DSH 的本地预算;
|
|
39
|
+
请求体里既没有窗口字段、也没有 `max_mode`,所以把预算提到 1M 并不会让上游按
|
|
40
|
+
1M 服务。原先的标签读起来像「已经放开到 1M」。
|
|
41
|
+
- 上游是否接受 `v2_max_mode_enabled` 这类字段**尚未取证**,所以这次不动请求体
|
|
42
|
+
(乱加字段可能让每次请求 400),只把事实说出来:常驻说明,不用悬停提示。
|
|
43
|
+
|
|
44
|
+
## 4.5.1 (2026-10-25)
|
|
45
|
+
|
|
46
|
+
> **只在租户池上架的模型不再隐形。** 插件的模型目录只问了 `solo.trae.cn` 一处,
|
|
47
|
+
> 而它并不发布租户池的完整名单——`DeepSeek-V4.1-Flash-1017` 就在其中。这一版把
|
|
48
|
+
> `console.enterprise.trae.cn` 的目录**并入**主目录,两个池的模型都保留。
|
|
49
|
+
>
|
|
50
|
+
> 成因不是合并层丢行,而是**读错了 host**:目录基址循环在第一个答非空的 host 就
|
|
51
|
+
> `return`,而主目录总会答——所以只在租户池上架的模型,即便拿着租户凭据也永远看不到
|
|
52
|
+
> (#46)。
|
|
53
|
+
|
|
54
|
+
### 修复
|
|
55
|
+
|
|
56
|
+
- **模型目录并入租户池,只在租户侧上架的模型不再隐形**(#46)。
|
|
57
|
+
- **实测**(2026-10-24,真实 CN 租户凭据):`solo.trae.cn` 答 36 个 id、**没有**
|
|
58
|
+
`DeepSeek-V4.1-Flash-1017`;`console.enterprise.trae.cn` 答 24 个、**有**。
|
|
59
|
+
4.4.7 与 4.5.0 表现一致,所以升级修不了。
|
|
60
|
+
- **并集而非替换**:两侧没有一方包含另一方——主目录的 36 个里有 **19** 个不在租户
|
|
61
|
+
应答中,其中就包括 `deepseek-v4.1-flash`。换 host 等于用 1 个模型换掉 19 个。
|
|
62
|
+
- **也不能做成兜底候选**:基址循环在第一个答非空的 host 就返回,而主目录总会答,
|
|
63
|
+
候选恰好在唯一会出错的情形下被跳过。因此额外 host 单独读取后再并入
|
|
64
|
+
(`TraeSoloRemoteCatalogOptions.unionBaseUrls`)。
|
|
65
|
+
- **冲突保留主目录的拼写**,按不区分大小写比较。这里不能用 `preferStrongerRow`
|
|
66
|
+
打擂台:`deriveCatalog` 用 `enabled.has(model.id)` **精确匹配**,改名会让用户
|
|
67
|
+
已存进 `enabledModelIds` 的那一项静默失配、模型从选择里消失。
|
|
68
|
+
- **逐 host 尽力而为**:传输失败、非 2xx、响应体读不出、应答为空,都记日志后跳过,
|
|
69
|
+
只有调用方 abort 才向上抛。某个额外 host 坏掉,损失的是它的模型,而不是整份目录。
|
|
70
|
+
- **门控按凭据、不按面板**:只登录租户账号的机器,在 Trae(消费端)面板下
|
|
71
|
+
`store.resolve()` 依然会返回租户凭据(`offered()` 是「藏光了就回退」),所以按
|
|
72
|
+
面板判会漏掉这类用户。现在判「解析出的凭据 `tenantId` 非空」,个人凭据在发出请求
|
|
73
|
+
前就跳过。
|
|
74
|
+
- **`unionRegion` 挡住跨区域请求**:`DSH_TRAE_TENANT_IN_CONSUMER` 是环境变量、不分
|
|
75
|
+
区域,原先国际版面板也会挂上 CN 租户 host(用 SG 凭据实测确实发出、拿 401 后被
|
|
76
|
+
跳过)。现在区域不符即跳过。
|
|
77
|
+
- **额外 host 有负缓存**:`discoverModels()` 不传 signal、超时回落到 30 秒,原先企业
|
|
78
|
+
host 不可达时每次刷新都要多等。失败后与主 host 同款冷却 5 分钟。
|
|
79
|
+
- **它不是别名**:用插件自己的 bridge 驱动真实网关,两个 id 派往不同后端
|
|
80
|
+
(`DeepSeek-V4.1-Flash-1017` → `deepseek-v4.1-flash-economic-ark`,
|
|
81
|
+
`DeepSeek-V4.1-Flash` → `deepseek-v4.1-flash-ark`)。
|
|
82
|
+
- 测试:`tests/solo-remote.spec.ts` 新增 8 例,覆盖并集、仅大小写不同的冲突、
|
|
83
|
+
额外 host 不可达/被拒/应答为空、个人凭据跳过、跨区域跳过、冷却、已经是主 host 时
|
|
84
|
+
不重复请求,以及未配置 `unionBaseUrls` 时行为与改动前完全一致。
|
|
85
|
+
- 贡献者:[@yuantao313](https://github.com/yuantao313)(PR #59)。
|
|
86
|
+
|
|
3
87
|
## 4.5.0 (2026-10-24)
|
|
4
88
|
|
|
5
89
|
> **宽窗模型多一个 500K 上下文中间档。** Trae 每个模型公布两个窗口——默认(dev)档和
|
package/lib/client.js
CHANGED
|
@@ -5659,6 +5659,7 @@ window.__ModuleLoader__.load({
|
|
|
5659
5659
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
5660
5660
|
className: "dsm-trae-context-budget",
|
|
5661
5661
|
"aria-label": t("row.contextBudget"),
|
|
5662
|
+
title: t("row.contextBudgetHint"),
|
|
5662
5663
|
children: contextBudgetOptions(model).map((option) => {
|
|
5663
5664
|
const selectable = model.maxContextWindow !== void 0;
|
|
5664
5665
|
const checked = (activeContextBudgets[model.id] ?? model.contextWindow) === option;
|
|
@@ -5726,6 +5727,10 @@ window.__ModuleLoader__.load({
|
|
|
5726
5727
|
})
|
|
5727
5728
|
]
|
|
5728
5729
|
}),
|
|
5730
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5731
|
+
className: "dsm-trae-model-capability-note",
|
|
5732
|
+
children: t("row.contextBudgetHint")
|
|
5733
|
+
}),
|
|
5729
5734
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5730
5735
|
className: "dsm-trae-model-capability-note",
|
|
5731
5736
|
children: t("row.modelCapabilityPending")
|
|
@@ -6898,6 +6903,7 @@ window.__ModuleLoader__.load({
|
|
|
6898
6903
|
"row.veilSaving": "Saving changes…",
|
|
6899
6904
|
"row.modelContext": "Max context {context}",
|
|
6900
6905
|
"row.contextBudget": "DSH context budget",
|
|
6906
|
+
"row.contextBudgetHint": "Affects the DSH-side context budget only. The plugin does not send a window or Max-mode field upstream, so this does not change the window Trae serves the request with.",
|
|
6901
6907
|
"row.modelOutput": "Output {output}",
|
|
6902
6908
|
"row.modelImage": "Image",
|
|
6903
6909
|
"row.modelImageAuto": "Enabled automatically: Trae marks this model multimodal.",
|
|
@@ -7277,6 +7283,7 @@ window.__ModuleLoader__.load({
|
|
|
7277
7283
|
"row.veilSaving": "正在保存更改…",
|
|
7278
7284
|
"row.modelContext": "最大上下文 {context}",
|
|
7279
7285
|
"row.contextBudget": "DSH 上下文预算",
|
|
7286
|
+
"row.contextBudgetHint": "只影响 DSH 侧的上下文预算。插件不会向上游发送窗口或 Max 模式字段,所以这不会改变 Trae 实际服务这次请求所用的窗口。",
|
|
7280
7287
|
"row.modelOutput": "最大输出 {output}",
|
|
7281
7288
|
"row.modelImage": "图片",
|
|
7282
7289
|
"row.modelImageAuto": "已自动开启:Trae 把该模型标为多模态。",
|
package/lib/index.d.ts
CHANGED
|
@@ -44,7 +44,23 @@ interface TraeActivityDiscount {
|
|
|
44
44
|
}
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region src/reasoning.d.ts
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The DSH thinking levels this plugin can carry, in ascending order.
|
|
49
|
+
*
|
|
50
|
+
* `max` is the fifth level Trae's own client advertises — its bundle ships the
|
|
51
|
+
* i18n keys `trae-chat-core.model.reasoning_effort.{low,medium,high,extra_high,max}`
|
|
52
|
+
* and `ai.model.reasoning_effort.{low,medium,high,xhigh,max}`, and publishes the
|
|
53
|
+
* per-model list from a remote config (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`).
|
|
54
|
+
* Leaving it out of this list meant a level the vendor could start serving was
|
|
55
|
+
* filtered away at PARSE time, before anything downstream could decide (issue
|
|
56
|
+
* #60). Listing it does not fabricate the level: `supported` is still built
|
|
57
|
+
* only from what the upstream response actually contains, so a model that does
|
|
58
|
+
* not advertise `max` never offers it.
|
|
59
|
+
*
|
|
60
|
+
* `minimal` is kept for the same reason in reverse — it is a DSH level, so the
|
|
61
|
+
* type must be able to represent it even though no CN model has advertised it.
|
|
62
|
+
*/
|
|
63
|
+
declare const TRAE_REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
48
64
|
type TraeReasoningEffort = typeof TRAE_REASONING_EFFORTS[number];
|
|
49
65
|
interface TraeReasoningCapability {
|
|
50
66
|
supported: readonly TraeReasoningEffort[];
|
|
@@ -3486,6 +3502,51 @@ interface TraeSoloRemoteCatalogOptions {
|
|
|
3486
3502
|
* of official-host-first.
|
|
3487
3503
|
*/
|
|
3488
3504
|
log?: (message: string, detail?: unknown) => void;
|
|
3505
|
+
/**
|
|
3506
|
+
* Extra directory hosts whose rows are UNIONED into the primary answer.
|
|
3507
|
+
*
|
|
3508
|
+
* The base loop below stops at the FIRST host that answers non-empty. That is
|
|
3509
|
+
* the right rule for a FALLBACK chain (one host, mirrored), but it cannot
|
|
3510
|
+
* express "these hosts serve DIFFERENT pools" — and the primary host
|
|
3511
|
+
* answering successfully is exactly the case where another host's models
|
|
3512
|
+
* stay invisible. So these hosts are read SEPARATELY and merged, never added
|
|
3513
|
+
* as another candidate base: a candidate is skipped precisely when the
|
|
3514
|
+
* primary succeeds, which is always in the scenario that fails today.
|
|
3515
|
+
*
|
|
3516
|
+
* Measured 2026-10-24 on a live CN tenant credential:
|
|
3517
|
+
* `solo.trae.cn` (the primary) answered 36 ids and does NOT list
|
|
3518
|
+
* `DeepSeek-V4.1-Flash-1017`, while `console.enterprise.trae.cn` answered 24
|
|
3519
|
+
* and DOES list it (issue #46). Neither host is a superset of the other: 19
|
|
3520
|
+
* of the primary's ids are absent from the tenant answer, so REPLACING the
|
|
3521
|
+
* primary would trade one missing model for nineteen — a union is the only
|
|
3522
|
+
* shape that loses nothing.
|
|
3523
|
+
*
|
|
3524
|
+
* The primary answer wins every id collision, compared case-insensitively, and
|
|
3525
|
+
* a colliding extra row is dropped rather than capacity-competed: a
|
|
3526
|
+
* `preferStrongerRow` contest could rename a model the user has already saved
|
|
3527
|
+
* in `enabledModelIds` (`deriveCatalog` matches those ids EXACTLY), silently
|
|
3528
|
+
* dropping the model out of the selection.
|
|
3529
|
+
*
|
|
3530
|
+
* An extra host is asked ONLY when the credential being used can call that
|
|
3531
|
+
* pool at all (`region` must match the host, and a tenant host additionally
|
|
3532
|
+
* requires a tenant credential). The gate lives here rather than at the
|
|
3533
|
+
* construction site because the deciding fact — "is this credential a tenant
|
|
3534
|
+
* one?" — only exists once `credential()` has resolved, which is per refresh.
|
|
3535
|
+
* Gating on the PANEL instead missed the case that matters: a consumer store
|
|
3536
|
+
* does not hard-exclude tenant accounts (`auth.ts`'s `offered()` backs off
|
|
3537
|
+
* when its filter would hide everything), so a machine whose only login is a
|
|
3538
|
+
* tenant account resolves a tenant credential under the Trae panel too.
|
|
3539
|
+
*/
|
|
3540
|
+
unionBaseUrls?: readonly string[];
|
|
3541
|
+
/**
|
|
3542
|
+
* Region an extra host serves. An `ai` credential never asks a `cn` host.
|
|
3543
|
+
*
|
|
3544
|
+
* `unionBaseUrls` used to be ungated by region, so a `DSH_TRAE_TENANT_IN_CONSUMER`
|
|
3545
|
+
* machine on the international panel issued the CN tenant request on every
|
|
3546
|
+
* refresh — a 401 the union skipped, but one wasted request and one warn log
|
|
3547
|
+
* per refresh (measured with an SG credential 2026-10-24).
|
|
3548
|
+
*/
|
|
3549
|
+
unionRegion?: TraeRegion;
|
|
3489
3550
|
}
|
|
3490
3551
|
/**
|
|
3491
3552
|
* Read-only model catalog client for the SOLO Web API.
|
|
@@ -3526,9 +3587,42 @@ declare class TraeSoloRemoteCatalogClient {
|
|
|
3526
3587
|
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
3527
3588
|
*/
|
|
3528
3589
|
private static readonly OFFICIAL_RETRY_MS;
|
|
3590
|
+
/**
|
|
3591
|
+
* When each extra union host may be asked again after it failed, as epoch
|
|
3592
|
+
* milliseconds. Absent = ask it on the next refresh.
|
|
3593
|
+
*
|
|
3594
|
+
* The official host has {@link OFFICIAL_RETRY_MS} because a blocked network
|
|
3595
|
+
* would otherwise pay its failing connect on every refresh; an extra host has
|
|
3596
|
+
* the same problem and, since discovery (`discoverModels()`) passes no signal,
|
|
3597
|
+
* the same 30 s ceiling. A tenant host that is unreachable from this network
|
|
3598
|
+
* would therefore stall every refresh for up to 30 s. Same cooldown, same
|
|
3599
|
+
* reasoning — an unhelpful host is skipped for a while instead of retried
|
|
3600
|
+
* immediately.
|
|
3601
|
+
*/
|
|
3602
|
+
private readonly extraRetryAtMs;
|
|
3603
|
+
private readonly unionBaseUrls;
|
|
3604
|
+
private readonly unionRegion;
|
|
3529
3605
|
constructor(options: TraeSoloRemoteCatalogOptions);
|
|
3530
3606
|
private headers;
|
|
3531
3607
|
fetchModels(signal?: AbortSignal): Promise<TraeDiscoveredModel[]>;
|
|
3608
|
+
/**
|
|
3609
|
+
* Union the extra directory hosts' rows into the primary answer (issue #46).
|
|
3610
|
+
*
|
|
3611
|
+
* The base loop above returns on the FIRST host that answers non-empty, which
|
|
3612
|
+
* is correct for a FALLBACK chain but cannot express "these hosts serve
|
|
3613
|
+
* different pools" — the primary host answering is exactly the case where the
|
|
3614
|
+
* extra host's models would stay invisible forever. Hence a separate read and
|
|
3615
|
+
* a merge, not another candidate base (see `unionBaseUrls`).
|
|
3616
|
+
*
|
|
3617
|
+
* Every extra host is BEST-EFFORT: transport failure, non-2xx, an unreadable
|
|
3618
|
+
* body and an empty answer are each logged and skipped. Only an aborted
|
|
3619
|
+
* `signal` propagates — that is the caller cancelling, not a host being
|
|
3620
|
+
* unhelpful. Failing here must never fail a refresh the primary host already
|
|
3621
|
+
* satisfied, which is why a broken extra host costs models, not the catalog.
|
|
3622
|
+
* A failure also puts that host on {@link extraRetryAtMs} so a network that
|
|
3623
|
+
* cannot reach it stops paying its connect timeout on every refresh.
|
|
3624
|
+
*/
|
|
3625
|
+
private unionWithExtraHosts;
|
|
3532
3626
|
/**
|
|
3533
3627
|
* Bases to try for one refresh, in order.
|
|
3534
3628
|
*
|
package/lib/index.js
CHANGED
|
@@ -282,6 +282,53 @@ function displayKey(name) {
|
|
|
282
282
|
return name.trim().toLowerCase();
|
|
283
283
|
}
|
|
284
284
|
/**
|
|
285
|
+
* DSH thinking level -> the `reasoning_effort` name Trae's wire accepts.
|
|
286
|
+
*
|
|
287
|
+
* The INVERSE of `EFFORT_MAP` in `model-metadata.ts`, and it must stay an exact
|
|
288
|
+
* mirror of it: a level this table does not name has no wire spelling, so it is
|
|
289
|
+
* DROPPED rather than guessed (issue #60).
|
|
290
|
+
*
|
|
291
|
+
* The previous form was a ternary chain ending in a bare `'high'`:
|
|
292
|
+
*
|
|
293
|
+
* effort === 'low' ? 'light' : effort === 'xhigh' ? 'extra_high' : 'high'
|
|
294
|
+
*
|
|
295
|
+
* so EVERY unnamed level was sent as `high`. That is not a harmless default: it
|
|
296
|
+
* is a silent downgrade. Trae's own vocabulary carries five levels
|
|
297
|
+
* (`low / medium / high / extra_high / max`, verified in the client bundle's
|
|
298
|
+
* `reasoning_effort` i18n keys), and its remote config publishes the list per
|
|
299
|
+
* model (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`). The day
|
|
300
|
+
* upstream starts advertising `max`, a user picking Max would have sent `high`
|
|
301
|
+
* — no error, no log, and a cheaper answer than the one they asked for. A
|
|
302
|
+
* dropped field lets the upstream apply its own default, which is at least the
|
|
303
|
+
* model's real behaviour instead of a wrong claim about it.
|
|
304
|
+
*
|
|
305
|
+
* Measured 2026-10-25 on a live CN credential (98 directory rows): options are
|
|
306
|
+
* `["light","high"]`, `["high","extra_high"]`, `["light","high","extra_high"]`
|
|
307
|
+
* or absent — `max` is not published YET, which is exactly why this must not
|
|
308
|
+
* wait for it to be.
|
|
309
|
+
*/
|
|
310
|
+
const WIRE_EFFORT_MAP = {
|
|
311
|
+
low: "light",
|
|
312
|
+
medium: "medium",
|
|
313
|
+
high: "high",
|
|
314
|
+
xhigh: "extra_high",
|
|
315
|
+
max: "max"
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Map a model's advertised levels to their wire names, dropping unnamed ones.
|
|
319
|
+
*
|
|
320
|
+
* `flatMap` rather than `map` + a placeholder: an unmappable level must not
|
|
321
|
+
* produce an entry at all, because every consumer reads "is there a value here"
|
|
322
|
+
* as "may this level be sent" (see `thinkingLevelMap` in `adapter.ts` and the
|
|
323
|
+
* `delete input['reasoning_effort']` fallback in `solo-bridge.ts`).
|
|
324
|
+
*/
|
|
325
|
+
function wireEffortsOf(supported) {
|
|
326
|
+
return Object.fromEntries(supported.flatMap((effort) => {
|
|
327
|
+
const wire = WIRE_EFFORT_MAP[effort];
|
|
328
|
+
return wire === void 0 ? [] : [[effort, wire]];
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
285
332
|
* Merge the two Trae model sources into one authoritative catalog.
|
|
286
333
|
*
|
|
287
334
|
* `remote` (the solo.trae.cn `/models` directory) is the authoritative model
|
|
@@ -360,7 +407,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
360
407
|
reasoningSupported: model.reasoningSupported,
|
|
361
408
|
...model.reasoning === void 0 ? {} : {
|
|
362
409
|
reasoning: model.reasoning,
|
|
363
|
-
reasoningEfforts:
|
|
410
|
+
reasoningEfforts: wireEffortsOf(model.reasoning.supported)
|
|
364
411
|
},
|
|
365
412
|
...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
|
|
366
413
|
...wireFunction === void 0 ? {} : { wireFunction }
|
|
@@ -433,7 +480,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
433
480
|
supported: [...wireModel.reasoning.supported],
|
|
434
481
|
...wireModel.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: wireModel.reasoning.defaultEffort }
|
|
435
482
|
},
|
|
436
|
-
reasoningEfforts:
|
|
483
|
+
reasoningEfforts: wireEffortsOf(wireModel.reasoning.supported)
|
|
437
484
|
},
|
|
438
485
|
...wireModel.function === void 0 ? {} : { wireFunction: wireModel.function }
|
|
439
486
|
});
|
|
@@ -507,7 +554,7 @@ function discoveredCatalog(models) {
|
|
|
507
554
|
reasoningSupported: model.reasoningSupported,
|
|
508
555
|
...model.reasoning === void 0 ? {} : {
|
|
509
556
|
reasoning: model.reasoning,
|
|
510
|
-
reasoningEfforts:
|
|
557
|
+
reasoningEfforts: wireEffortsOf(model.reasoning.supported)
|
|
511
558
|
}
|
|
512
559
|
});
|
|
513
560
|
return result;
|
|
@@ -836,14 +883,28 @@ function toPiModel(info, baseUrl, providerId, withRate = true) {
|
|
|
836
883
|
cacheWrite: 0
|
|
837
884
|
},
|
|
838
885
|
reasoning: info.reasoningEfforts !== void 0,
|
|
839
|
-
...info.reasoningEfforts === void 0 ? {} : {
|
|
886
|
+
...info.reasoningEfforts === void 0 ? {} : {
|
|
887
|
+
/**
|
|
888
|
+
* Which DSH levels this model may be asked for, and under which wire name.
|
|
889
|
+
*
|
|
890
|
+
* Every level is read from the model's OWN advertised capability, never
|
|
891
|
+
* hardcoded. `minimal / medium / max` used to be pinned to `null`, so a
|
|
892
|
+
* level the upstream published could not be selected even after it was
|
|
893
|
+
* parsed — the second of the three places issue #60 reported. `null`
|
|
894
|
+
* still means "not offered", which is what a model that does not
|
|
895
|
+
* advertise the level reports: nothing here fabricates a level.
|
|
896
|
+
*
|
|
897
|
+
* `off` stays `null` deliberately: Trae's wire has no "no reasoning"
|
|
898
|
+
* spelling, so an explicit off is not something this provider can send.
|
|
899
|
+
*/
|
|
900
|
+
thinkingLevelMap: {
|
|
840
901
|
off: null,
|
|
841
|
-
minimal: null,
|
|
902
|
+
minimal: info.reasoningEfforts.minimal ?? null,
|
|
842
903
|
low: info.reasoningEfforts.low ?? null,
|
|
843
|
-
medium: null,
|
|
904
|
+
medium: info.reasoningEfforts.medium ?? null,
|
|
844
905
|
high: info.reasoningEfforts.high ?? null,
|
|
845
906
|
xhigh: info.reasoningEfforts.xhigh ?? null,
|
|
846
|
-
max: null
|
|
907
|
+
max: info.reasoningEfforts.max ?? null
|
|
847
908
|
} },
|
|
848
909
|
...info.contextWindow === void 0 ? {} : { contextWindow: info.contextWindow },
|
|
849
910
|
...info.maxTokens === void 0 ? {} : { maxTokens: info.maxTokens },
|
|
@@ -4752,12 +4813,29 @@ function parseTraeActivityDiscount(value) {
|
|
|
4752
4813
|
}
|
|
4753
4814
|
//#endregion
|
|
4754
4815
|
//#region src/reasoning.ts
|
|
4816
|
+
/**
|
|
4817
|
+
* The DSH thinking levels this plugin can carry, in ascending order.
|
|
4818
|
+
*
|
|
4819
|
+
* `max` is the fifth level Trae's own client advertises — its bundle ships the
|
|
4820
|
+
* i18n keys `trae-chat-core.model.reasoning_effort.{low,medium,high,extra_high,max}`
|
|
4821
|
+
* and `ai.model.reasoning_effort.{low,medium,high,xhigh,max}`, and publishes the
|
|
4822
|
+
* per-model list from a remote config (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`).
|
|
4823
|
+
* Leaving it out of this list meant a level the vendor could start serving was
|
|
4824
|
+
* filtered away at PARSE time, before anything downstream could decide (issue
|
|
4825
|
+
* #60). Listing it does not fabricate the level: `supported` is still built
|
|
4826
|
+
* only from what the upstream response actually contains, so a model that does
|
|
4827
|
+
* not advertise `max` never offers it.
|
|
4828
|
+
*
|
|
4829
|
+
* `minimal` is kept for the same reason in reverse — it is a DSH level, so the
|
|
4830
|
+
* type must be able to represent it even though no CN model has advertised it.
|
|
4831
|
+
*/
|
|
4755
4832
|
const TRAE_REASONING_EFFORTS = [
|
|
4756
4833
|
"minimal",
|
|
4757
4834
|
"low",
|
|
4758
4835
|
"medium",
|
|
4759
4836
|
"high",
|
|
4760
|
-
"xhigh"
|
|
4837
|
+
"xhigh",
|
|
4838
|
+
"max"
|
|
4761
4839
|
];
|
|
4762
4840
|
function parseReasoningCapability(value) {
|
|
4763
4841
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -5863,10 +5941,31 @@ function parseFeatures(value) {
|
|
|
5863
5941
|
return;
|
|
5864
5942
|
}
|
|
5865
5943
|
}
|
|
5944
|
+
/**
|
|
5945
|
+
* Trae's wire effort name -> the DSH thinking level.
|
|
5946
|
+
*
|
|
5947
|
+
* The FORWARD half of the mapping; `WIRE_EFFORT_MAP` in `catalog.ts` is its
|
|
5948
|
+
* exact inverse. Both halves must name the same levels, or a level survives one
|
|
5949
|
+
* direction and vanishes in the other.
|
|
5950
|
+
*
|
|
5951
|
+
* `medium` and `max` are here because Trae's own client advertises them
|
|
5952
|
+
* (`reasoning_effort.{low,medium,high,extra_high,max}` in its i18n bundle) and
|
|
5953
|
+
* publishes the per-model list remotely — so the day upstream starts sending
|
|
5954
|
+
* either, the parser must not filter it away before anything can use it (issue
|
|
5955
|
+
* #60). Measured 2026-10-25 on a live CN credential, the published options were
|
|
5956
|
+
* only `light / high / extra_high`; these entries are therefore inert today and
|
|
5957
|
+
* ready tomorrow, not a claim that the levels are served now.
|
|
5958
|
+
*
|
|
5959
|
+
* An unknown name is deliberately ABSENT rather than defaulted: `parse` drops
|
|
5960
|
+
* it, and the reverse map drops it too, so an unrecognised level can never be
|
|
5961
|
+
* reported to the user or sent upstream under a name the vendor never said.
|
|
5962
|
+
*/
|
|
5866
5963
|
const EFFORT_MAP = {
|
|
5867
5964
|
light: "low",
|
|
5965
|
+
medium: "medium",
|
|
5868
5966
|
high: "high",
|
|
5869
|
-
extra_high: "xhigh"
|
|
5967
|
+
extra_high: "xhigh",
|
|
5968
|
+
max: "max"
|
|
5870
5969
|
};
|
|
5871
5970
|
/**
|
|
5872
5971
|
* Parse only capabilities explicitly advertised by Trae's remote model API.
|
|
@@ -6025,6 +6124,17 @@ function traeSoloRemoteFallbackBases(region, officialBase) {
|
|
|
6025
6124
|
return backupBase === officialBase ? [] : [backupBase];
|
|
6026
6125
|
}
|
|
6027
6126
|
/**
|
|
6127
|
+
* Case-folded identity for a directory id.
|
|
6128
|
+
*
|
|
6129
|
+
* A local copy of `catalog.ts`'s private `displayKey`: the two hosts of the
|
|
6130
|
+
* union spell the same model differently (they disagree on at least 3 ids), and
|
|
6131
|
+
* ids that differ only by case MUST be treated as the same model — otherwise a
|
|
6132
|
+
* union would list one model twice under two spellings.
|
|
6133
|
+
*/
|
|
6134
|
+
function directoryKey(id) {
|
|
6135
|
+
return id.trim().toLowerCase();
|
|
6136
|
+
}
|
|
6137
|
+
/**
|
|
6028
6138
|
* Of two rows for the SAME model id, the one the catalog should keep.
|
|
6029
6139
|
*
|
|
6030
6140
|
* The gateway groups its directory by function and reports a model's capacity
|
|
@@ -6163,10 +6273,27 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6163
6273
|
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
6164
6274
|
*/
|
|
6165
6275
|
static OFFICIAL_RETRY_MS = 3e5;
|
|
6276
|
+
/**
|
|
6277
|
+
* When each extra union host may be asked again after it failed, as epoch
|
|
6278
|
+
* milliseconds. Absent = ask it on the next refresh.
|
|
6279
|
+
*
|
|
6280
|
+
* The official host has {@link OFFICIAL_RETRY_MS} because a blocked network
|
|
6281
|
+
* would otherwise pay its failing connect on every refresh; an extra host has
|
|
6282
|
+
* the same problem and, since discovery (`discoverModels()`) passes no signal,
|
|
6283
|
+
* the same 30 s ceiling. A tenant host that is unreachable from this network
|
|
6284
|
+
* would therefore stall every refresh for up to 30 s. Same cooldown, same
|
|
6285
|
+
* reasoning — an unhelpful host is skipped for a while instead of retried
|
|
6286
|
+
* immediately.
|
|
6287
|
+
*/
|
|
6288
|
+
extraRetryAtMs = /* @__PURE__ */ new Map();
|
|
6289
|
+
unionBaseUrls;
|
|
6290
|
+
unionRegion;
|
|
6166
6291
|
constructor(options) {
|
|
6167
6292
|
this.options = options;
|
|
6168
6293
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
6169
6294
|
this.baseUrl = options.baseUrl;
|
|
6295
|
+
this.unionBaseUrls = options.unionBaseUrls ?? [];
|
|
6296
|
+
this.unionRegion = options.unionRegion;
|
|
6170
6297
|
}
|
|
6171
6298
|
async headers(region) {
|
|
6172
6299
|
const credential = await this.options.credential();
|
|
@@ -6182,7 +6309,8 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6182
6309
|
};
|
|
6183
6310
|
}
|
|
6184
6311
|
async fetchModels(signal) {
|
|
6185
|
-
const
|
|
6312
|
+
const credential = await this.options.credential();
|
|
6313
|
+
const region = regionOfCredential(credential);
|
|
6186
6314
|
const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
6187
6315
|
const headers = await this.headers(region);
|
|
6188
6316
|
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
@@ -6217,12 +6345,88 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6217
6345
|
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 });
|
|
6218
6346
|
else this.officialRetryAtMs = 0;
|
|
6219
6347
|
this.lastSuccessfulBase = base;
|
|
6220
|
-
return models;
|
|
6348
|
+
return await this.unionWithExtraHosts(region, credential, headers, models, signal);
|
|
6221
6349
|
}
|
|
6222
6350
|
if (emptyError !== void 0) throw emptyError;
|
|
6223
6351
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
6224
6352
|
}
|
|
6225
6353
|
/**
|
|
6354
|
+
* Union the extra directory hosts' rows into the primary answer (issue #46).
|
|
6355
|
+
*
|
|
6356
|
+
* The base loop above returns on the FIRST host that answers non-empty, which
|
|
6357
|
+
* is correct for a FALLBACK chain but cannot express "these hosts serve
|
|
6358
|
+
* different pools" — the primary host answering is exactly the case where the
|
|
6359
|
+
* extra host's models would stay invisible forever. Hence a separate read and
|
|
6360
|
+
* a merge, not another candidate base (see `unionBaseUrls`).
|
|
6361
|
+
*
|
|
6362
|
+
* Every extra host is BEST-EFFORT: transport failure, non-2xx, an unreadable
|
|
6363
|
+
* body and an empty answer are each logged and skipped. Only an aborted
|
|
6364
|
+
* `signal` propagates — that is the caller cancelling, not a host being
|
|
6365
|
+
* unhelpful. Failing here must never fail a refresh the primary host already
|
|
6366
|
+
* satisfied, which is why a broken extra host costs models, not the catalog.
|
|
6367
|
+
* A failure also puts that host on {@link extraRetryAtMs} so a network that
|
|
6368
|
+
* cannot reach it stops paying its connect timeout on every refresh.
|
|
6369
|
+
*/
|
|
6370
|
+
async unionWithExtraHosts(region, credential, headers, primary, signal) {
|
|
6371
|
+
if (this.unionBaseUrls.length === 0) return primary;
|
|
6372
|
+
if (this.unionRegion !== void 0 && this.unionRegion !== region) return primary;
|
|
6373
|
+
if (!isEnterpriseAccount(credential)) return primary;
|
|
6374
|
+
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
6375
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
6376
|
+
for (const model of primary) byKey.set(directoryKey(model.id), model);
|
|
6377
|
+
for (const base of this.unionBaseUrls) {
|
|
6378
|
+
if (base === this.lastSuccessfulBase) continue;
|
|
6379
|
+
if ((this.extraRetryAtMs.get(base) ?? 0) > Date.now()) continue;
|
|
6380
|
+
let response;
|
|
6381
|
+
try {
|
|
6382
|
+
response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
|
|
6383
|
+
headers,
|
|
6384
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
6385
|
+
});
|
|
6386
|
+
} catch (error) {
|
|
6387
|
+
if (signal?.aborted) throw error;
|
|
6388
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6389
|
+
this.options.log?.(`model directory union host ${base} was unreachable; its models are missing from this refresh`, {
|
|
6390
|
+
base,
|
|
6391
|
+
error: String(error)
|
|
6392
|
+
});
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
if (!response.ok) {
|
|
6396
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6397
|
+
this.options.log?.(`model directory union host ${base} answered HTTP ${response.status}; its models are missing from this refresh`, { base });
|
|
6398
|
+
continue;
|
|
6399
|
+
}
|
|
6400
|
+
let extra;
|
|
6401
|
+
try {
|
|
6402
|
+
const json = await response.json();
|
|
6403
|
+
extra = this.collectModels(region, json.data?.list ?? []);
|
|
6404
|
+
} catch (error) {
|
|
6405
|
+
this.extraRetryAtMs.set(base, Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS);
|
|
6406
|
+
this.options.log?.(`model directory union host ${base} answered a body that could not be read; its models are missing from this refresh`, {
|
|
6407
|
+
base,
|
|
6408
|
+
error: String(error)
|
|
6409
|
+
});
|
|
6410
|
+
continue;
|
|
6411
|
+
}
|
|
6412
|
+
this.extraRetryAtMs.delete(base);
|
|
6413
|
+
let added = 0;
|
|
6414
|
+
for (const model of extra) {
|
|
6415
|
+
const key = directoryKey(model.id);
|
|
6416
|
+
if (byKey.has(key)) continue;
|
|
6417
|
+
byKey.set(key, model);
|
|
6418
|
+
added += 1;
|
|
6419
|
+
}
|
|
6420
|
+
const collided = extra.length - added;
|
|
6421
|
+
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)` : ""}`, {
|
|
6422
|
+
base,
|
|
6423
|
+
served: extra.length,
|
|
6424
|
+
added
|
|
6425
|
+
});
|
|
6426
|
+
}
|
|
6427
|
+
return [...byKey.values()];
|
|
6428
|
+
}
|
|
6429
|
+
/**
|
|
6226
6430
|
* Bases to try for one refresh, in order.
|
|
6227
6431
|
*
|
|
6228
6432
|
* While the official host is in its post-failure cooldown the backup hosts go
|
|
@@ -10030,7 +10234,57 @@ function apply(ctx, config) {
|
|
|
10030
10234
|
});
|
|
10031
10235
|
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
10032
10236
|
credential: () => store.resolve(),
|
|
10033
|
-
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
10237
|
+
log: (message, detail) => ctx.logger.warn(message, detail),
|
|
10238
|
+
/**
|
|
10239
|
+
* The TENANT directory, UNIONED into the consumer one (issue #46).
|
|
10240
|
+
*
|
|
10241
|
+
* This is the "reads both pools" half of the routing note below, which
|
|
10242
|
+
* was intent without an implementation: only the consumer host was ever
|
|
10243
|
+
* asked. The `Solo` client above is routed, but THIS one is not — and
|
|
10244
|
+
* `solo.trae.cn`, the primary it lands on, does not publish every model a
|
|
10245
|
+
* tenant credential can call.
|
|
10246
|
+
*
|
|
10247
|
+
* Measured 2026-10-24 on the reporter's live CN tenant credential:
|
|
10248
|
+
* `solo.trae.cn` answered 36 ids with no `DeepSeek-V4.1-Flash-1017`;
|
|
10249
|
+
* `console.enterprise.trae.cn` answered 24 that include it, and the
|
|
10250
|
+
* gateway really dispatching a distinct backend for it
|
|
10251
|
+
* (`deepseek-v4.1-flash-economic-ark`, vs `deepseek-v4.1-flash-ark` for
|
|
10252
|
+
* the plain id) is how issue #46's report was confirmed.
|
|
10253
|
+
*
|
|
10254
|
+
* UNION, never replace, and never a fallback candidate. The primary is
|
|
10255
|
+
* the richer host — 19 of its ids are absent from the tenant answer,
|
|
10256
|
+
* including the `deepseek-v4.1-flash` the reporter has saved — so
|
|
10257
|
+
* replacing it would trade one missing model for nineteen. And a
|
|
10258
|
+
* candidate base cannot work at all: the base loop returns on the first
|
|
10259
|
+
* host that answers, and the primary always answers.
|
|
10260
|
+
*
|
|
10261
|
+
* Gated on a tenant pool being bindable here (an enterprise panel, or a
|
|
10262
|
+
* consumer panel under `DSH_TRAE_TENANT_IN_CONSUMER`). A personal
|
|
10263
|
+
* consumer account has nothing to gain from the tenant host, so it is not
|
|
10264
|
+
* made to pay the extra request.
|
|
10265
|
+
*
|
|
10266
|
+
* The fix is not about 1017 specifically: the tenant host is a second
|
|
10267
|
+
* pool, and every model it alone serves was invisible for the same
|
|
10268
|
+
* reason. Verified against the live hosts: 0 rows lost, 0 duplicated.
|
|
10269
|
+
*
|
|
10270
|
+
* The client decides WHEN to ask it, not this construction site: the
|
|
10271
|
+
* deciding fact is whether the credential being resolved is a tenant one,
|
|
10272
|
+
* which does not exist until `credential()` runs. Gating on the panel here
|
|
10273
|
+
* missed the reporters most likely to hit #46 in the first place — a
|
|
10274
|
+
* consumer store does not hard-exclude tenant accounts (`auth.ts`'s
|
|
10275
|
+
* `offered()` backs off when the filter would hide everything), so a
|
|
10276
|
+
* machine whose only login is a tenant account resolves one under the
|
|
10277
|
+
* Trae panel too, and an enterprise-panel-only gate never opened the
|
|
10278
|
+
* union for it. `unionRegion` additionally keeps an `ai` credential from
|
|
10279
|
+
* asking a `cn` host, which the old `DSH_TRAE_TENANT_IN_CONSUMER` gate on
|
|
10280
|
+
* its own did not.
|
|
10281
|
+
*
|
|
10282
|
+
* Providing the host unconditionally is therefore safe: a personal
|
|
10283
|
+
* credential and an `ai` credential both skip it before any request is
|
|
10284
|
+
* made, so neither pays for a pool it cannot use.
|
|
10285
|
+
*/
|
|
10286
|
+
unionBaseUrls: [`${TRAE_ENTERPRISE_HOST}/api/remote/v1`],
|
|
10287
|
+
unionRegion: "cn"
|
|
10034
10288
|
});
|
|
10035
10289
|
/**
|
|
10036
10290
|
* The IDE's own roster: which models the vendor's picker shows, per
|
|
@@ -10059,7 +10313,10 @@ function apply(ctx, config) {
|
|
|
10059
10313
|
* gateway and stamps the `session_id` it requires. The discovery client
|
|
10060
10314
|
* below stays UNROUTED on purpose — it owns the roster/probe path and, for
|
|
10061
10315
|
* a tenant credential, reads both pools so the shared catalogue keeps every
|
|
10062
|
-
* model either one serves.
|
|
10316
|
+
* model either one serves. That second pool is supplied by `unionBaseUrls`
|
|
10317
|
+
* at the catalog client above, which is what makes this paragraph true;
|
|
10318
|
+
* routing THIS client instead would have read one pool and dropped the
|
|
10319
|
+
* other (see the note there, and issue #46).
|
|
10063
10320
|
*/
|
|
10064
10321
|
/**
|
|
10065
10322
|
* 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.
|
|
5
|
+
"version": "4.5.2",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|