dsh-connect-trae 4.0.0 → 4.1.0
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 +88 -0
- package/README.en.md +6 -0
- package/README.md +6 -0
- package/lib/client.js +63 -0
- package/lib/index.d.ts +138 -2
- package/lib/index.js +303 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.0 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
> **受限网络(企业网只放行少数域名)下「刷新模型 500 / 聊天 4001」的完整修复。**
|
|
6
|
+
>
|
|
7
|
+
> 链路是三段式的:`solo.trae.cn` 的 CONNECT 被代理拒绝(403)→ 目录 `fetch failed`(刷新 500)→
|
|
8
|
+
> 模型丢失 `wireFunction` → 聊天按默认 `solo_work_lite` 发请求 → 模型不在该 function 的 config 名单里就
|
|
9
|
+
> 4001 `config_name rejected`。本版对三段各有一处改动:目录 host 回退到聊天网关的同路径(带冷却重试)、
|
|
10
|
+
> 目录分组的 function 兜底 `wireFunction`(只认可聊分组)、bridge 对「只带 function」的目录行保留
|
|
11
|
+
> resolver 的 `config_name` 大小写(issue #38 的坑)。
|
|
12
|
+
>
|
|
13
|
+
> 合并 PR #43 前做了对抗性复现,修掉了四处会伤到真实用户的回归——**最关键的一处**:原实现把功能名
|
|
14
|
+
> 属于 `solo_agent`/`builder_v3` 的目录行整行丢弃,而受限网络下那正是 9 个只在 `solo_agent` 分组里
|
|
15
|
+
> 出现的模型(`gpt-5.5`、`gpt-5.6-*`、`gpt-6-astra`、`glm-5.2`、`kimi-k2.7-code`、`Dola-Seed-2.0-Code`、
|
|
16
|
+
> `deepseek-v4-flash-0731`),它们正是 issue #19 修复要暴露的那批、且实测在默认 chat function 下
|
|
17
|
+
> 都能正常应答。现在只把非可聊 function **降级清空**,行保留。
|
|
18
|
+
|
|
19
|
+
### 修复
|
|
20
|
+
|
|
21
|
+
- **受限网络下模型目录走聊天网关备用 host,目录分组 function 兜底 `wireFunction`**(`fix(cn)`)。修复分三层:
|
|
22
|
+
- **目录 host 回退**:官方 remote host 网络错误 / 超时 / 5xx 时,按序尝试聊天网关的同一路径
|
|
23
|
+
(CN 为 `trae-api-cn.mchost.guru/api/remote/v1`,实测同路径同响应结构);401/403/404 等语义性拒绝**不回退**。
|
|
24
|
+
进程内记住最近一次**给出了非空目录**的 host。官方 host 失败后进入 5 分钟冷却:冷却期内先走记住的备用
|
|
25
|
+
host、官方 host 排在链尾(备用也挂了时仍有一次机会),冷却到期后官方 host 重新排第一 —— 只把官方 host
|
|
26
|
+
「排在第二」并不能让它被重试,因为循环在第一个成功处返回,所以必须显式冷却重试,恢复的网络才会在
|
|
27
|
+
一个冷却窗口内重新接管。空目录(200-but-empty)既不算成功(不记忆)也不终止链路(继续试下一个 base),
|
|
28
|
+
否则一个还没同步的镜像会永久堵死官方 host。回退只作用于 remote/v1 目录这一个 base;
|
|
29
|
+
`get_detail_param`、`llm_utils_chat` 照旧走各自原有 host。
|
|
30
|
+
- **目录分组 function 归因**:`fetchModels` 不再丢弃 `data.list[].function`——解析时把**可聊**分组的 function
|
|
31
|
+
附到每个模型(同一模型出现于多个分组时按 `TRAE_DIRECTORY_FUNCTIONS` 的实测优先级取,而非请求参数
|
|
32
|
+
`functions=` 的顺序;后者是历史列表,与 wire roster 自己的收集顺序不同,混用会让同一模型在「roster 有没有
|
|
33
|
+
返回」两种网络下拿到不同 function,`solo_agent_remote` 在 Remote 付费门后,优先它会多出 1005)。
|
|
34
|
+
`mergeTraeModelSources` 的 `wireFunction` 优先取 `get_detail_param` 的结论,目录分组兜底(roster 知道
|
|
35
|
+
账号的付费/可用状态,目录不知道)。同一 function 名重复出现时按名分桶保留全部,不再只留最后一组。
|
|
36
|
+
- **非可聊 function 只降级、不丢行**:`solo_agent`、`builder_v3` 是 IDE/Remote-agent roster,聊天通道实测
|
|
37
|
+
4011/4001,所以**不把这两个 function 写进 `wireFunction`**;模型本身**保留**,改由默认 chat function
|
|
38
|
+
调用。把它们整行丢掉等于在本次修复针对的受限网络里重新藏起 issue #19 的成果(详见上方导语)。
|
|
39
|
+
仅带 `wireFunction`(无 `wireConfigName`)的目录行,聊天请求会带上该 function,但 `config_name` 仍由
|
|
40
|
+
resolver 提供(目录 id 只是展示名,网关 `config_name` 大小写敏感,见 issue #38)。默认 function 与
|
|
41
|
+
fallback 模型常量未动。
|
|
42
|
+
- 已知取舍:模型被目录放在 `chat_v3` 分组下、但实际不可聊时,插件仅凭目录数据无法区分,仍会展示,
|
|
43
|
+
聊天失败走现有 4001 提示翻译(诊断入口见 `docs/ISSUE19_DIAGNOSIS.md`)。
|
|
44
|
+
|
|
45
|
+
### 测试
|
|
46
|
+
|
|
47
|
+
- 回归用例:host 回退(官方网络错误 → 备用 host 同路径同查询;401 不回退;5xx 回退;**空目录继续尝试下一个
|
|
48
|
+
base 且不被记住**;记住最近成功 host;**冷却到期后官方 host 重新排第一**;显式 baseUrl 钉住单 host)、
|
|
49
|
+
分组 function 归因(可聊分组优先;**同一 function 名重复出现时不丢模型**;容量来自后分组时保留先到先得的
|
|
50
|
+
function)、合并兜底与 `withChatCallableFunctions` 的**降级而非丢弃**、bridge 对 wireFunction-only 目录行
|
|
51
|
+
保留 resolver 的 `config_name`(大小写)、以及受限网络端到端(wire roster 为空时 9 个 `solo_agent`-only
|
|
52
|
+
模型仍然在目录里)。
|
|
53
|
+
- 全仓 **921** 例通过,0 skipped;`tsc` 两个工程干净;`build` 通过;CI(ubuntu + windows)全绿。
|
|
54
|
+
|
|
55
|
+
## 4.0.1 (2026-10-19)
|
|
56
|
+
|
|
57
|
+
> CN 区模型卡「最大输出」整列空白(issue #42)修复:上游**是**发布输出上限的——`get_detail_param` 的
|
|
58
|
+
> CN 80 个 config **80/80** 都带 `model_detail_list[].max_tokens`——但目录合并 `mergeTraeModelSources()`
|
|
59
|
+
> 把 wire 侧的 `maxTokens` 丢掉了。这个字段从合并函数诞生那天(1.1.0)起就没被复制过,卡片、
|
|
60
|
+
> `/models/refresh` 与 DSH 模型注册因此永远收到「未知」。附带落地同一 issue 的批量请求:整区上下文
|
|
61
|
+
> 一键切换两个按钮。
|
|
62
|
+
|
|
63
|
+
### 修复
|
|
64
|
+
|
|
65
|
+
- **目录合并带上 wire 的 `max_tokens`**(`fix(catalog)`)。合并时其余字段(contextWindow / maxContextWindow /
|
|
66
|
+
倍率 / reasoning / multimodal / config_name)一路都复制了,唯独漏了 `maxTokens` 一行。修复后 CN 实测
|
|
67
|
+
32 行目录 30 行带值;**仍空白的行是诚实的**——`kimi-k2.8-preview`、`qwen3.8-flash` 这类 TraeCode 通道行
|
|
68
|
+
在 SOLO 名单里没有对应 config,拿不到候选值,宁空白不编造。取值链其余部分不动:`collectModels` 读
|
|
69
|
+
`model_detail_list[0]` 实测无损(71/80 config 的双项 `__dev`/`__max` 上限恒等)。该值是**元数据**,
|
|
70
|
+
请求体照旧不下发 `max_tokens`(上限由 Trae 服务端决定),行为无变化。
|
|
71
|
+
- 报告人「想确认的一点」(输出上限是否挂在后面的 detail 项或别的键)已逐项 dump 排除,全文见
|
|
72
|
+
`docs/ISSUE42_DIAGNOSIS.md`,取证脚本 `scripts/probe-cn-maxtokens.mjs`(只读,随仓库提交)。
|
|
73
|
+
|
|
74
|
+
### 新增
|
|
75
|
+
|
|
76
|
+
- **卡片模型管理区两个批量上下文按钮**(`feat(card)`,issue #42 附带需求):「**整区切到 Max**」给本区
|
|
77
|
+
每个带 Max 档的模型一次性写入其 `maxContextWindow`(正是 `applyContextBudgets` 认的精确值),
|
|
78
|
+
没有 Max 档的行不动、已有手动预算保留;「**全部恢复默认上下文**」清空本区预算。二者作用于草稿、
|
|
79
|
+
按「保存」生效,落盘形状与逐行单选完全一致——因此**没有**新增 `contextBudgetMode` 配置键,
|
|
80
|
+
运行时零新分支、旧配置零迁移。报告人数的 18 次点击现在是一次。
|
|
81
|
+
- README 新增「最大输出与上下文预算」一节(issue #42 建议 1 换内容采纳:事实已变,文档写明上游发布该值、
|
|
82
|
+
空白行另有其因)。
|
|
83
|
+
|
|
84
|
+
### 测试
|
|
85
|
+
|
|
86
|
+
- 新增合并回归用例(修复前必红:wire 行带 `maxTokens` → 合并行必须保留;join-miss 行必须缺席)与
|
|
87
|
+
5 例卡片用例(批量按钮的落盘内容、既有手动预算的保留、两按钮各自禁用条件)。
|
|
88
|
+
- 全仓 **901** 例通过,0 skipped;`tsc` 两个工程干净;`build` 通过。修复前后各跑一次实时探针对照
|
|
89
|
+
(0/32 → 30/32)。
|
|
90
|
+
|
|
3
91
|
## 4.0.0 (2026-10-19)
|
|
4
92
|
|
|
5
93
|
> 移除 issue #29 的「跟随 Trae 桌面端当前登录」整套机制,连同它派生的两处 UI。
|
package/README.en.md
CHANGED
|
@@ -127,6 +127,12 @@ That is the upstream refusal stated plainly, rather than a silent failure or som
|
|
|
127
127
|
|
|
128
128
|
To use these three models today, use the **Trae IDE itself**.
|
|
129
129
|
|
|
130
|
+
## Max output and context budgets
|
|
131
|
+
|
|
132
|
+
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).
|
|
133
|
+
|
|
134
|
+
Context windows are a per-model choice between Trae's default (dev) window and its Max window, stored per panel as `contextBudgets`. For a whole-region switch, the model section now has two buttons: "**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). Both 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).
|
|
135
|
+
|
|
130
136
|
## Install
|
|
131
137
|
|
|
132
138
|
```sh
|
package/README.md
CHANGED
|
@@ -140,6 +140,12 @@ Trae does not serve this model under the SOLO function the request used
|
|
|
140
140
|
|
|
141
141
|
如果你现在就要用这 3 个模型,请直接在 **Trae IDE 本体**中使用。
|
|
142
142
|
|
|
143
|
+
## 最大输出与上下文预算
|
|
144
|
+
|
|
145
|
+
模型卡上的「最大输出」来自 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 服务端决定)。
|
|
146
|
+
|
|
147
|
+
上下文窗口在 Trae 公布的默认(dev)档与 Max 档之间二选一,按面板逐模型存成 `contextBudgets`。想整区一次切换:模型管理区有「**整区切到 Max**」(给每个带 Max 档的行写入其 `maxContextWindow`,没有 Max 档的行不动)和「**全部恢复默认上下文**」(清空本区预算)两个按钮,作用在草稿上、按「保存」生效,与逐行单选落盘的结果完全一致(issue #42 附带需求——CN 有 18 个 Max 模型,以前要点 18 次)。
|
|
148
|
+
|
|
143
149
|
## 安装
|
|
144
150
|
|
|
145
151
|
推荐使用 DSH 插件命令安装 npm 已发布版本:
|
package/lib/client.js
CHANGED
|
@@ -679,6 +679,10 @@ window.__ModuleLoader__.load({
|
|
|
679
679
|
color:var(--dsw-alias-brand-primary,#2563eb);font-size:11px;line-height:16px;
|
|
680
680
|
}
|
|
681
681
|
.dsm-trae-models-hint{margin:4px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
|
|
682
|
+
/* Bulk context-budget row (issue #42): under the model list, above the
|
|
683
|
+
capability note. Wraps on narrow cards; keeps the two buttons visually a
|
|
684
|
+
pair and distinct from the save footer below. */
|
|
685
|
+
.dsm-trae-context-bulk{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
|
682
686
|
/* Verdict badge. Only a real refusal is coloured as a warning; an unknown
|
|
683
687
|
verdict stays neutral because an inconclusive probe is not evidence against
|
|
684
688
|
the model. */
|
|
@@ -3879,6 +3883,39 @@ window.__ModuleLoader__.load({
|
|
|
3879
3883
|
};
|
|
3880
3884
|
});
|
|
3881
3885
|
};
|
|
3886
|
+
/**
|
|
3887
|
+
* Bulk context-budget switch (issue #42 side request). The per-model radios
|
|
3888
|
+
* stay exactly as they are; these two act on the whole region draft at once.
|
|
3889
|
+
* The reporter counted 18 CN models whose Max window differs from the
|
|
3890
|
+
* default and described clicking 18 times per region — 「整区切到 Max」 is
|
|
3891
|
+
* those 18 clicks as one button. It writes `maxContextWindow` per row, which
|
|
3892
|
+
* is precisely the spelling `applyContextBudgets` recognizes (a budget equal
|
|
3893
|
+
* to the native Max window widens the effective window), so bulk and per-row
|
|
3894
|
+
* paths land on identical saved state. Models without a Max window are left
|
|
3895
|
+
* untouched — inventing a budget for them would be filtered out anyway.
|
|
3896
|
+
*/
|
|
3897
|
+
const setAllContextMax = () => {
|
|
3898
|
+
editDraft((current) => {
|
|
3899
|
+
const next = { ...current.contextBudgets };
|
|
3900
|
+
for (const model of current.models) if (model.maxContextWindow !== void 0) next[model.id] = model.maxContextWindow;
|
|
3901
|
+
return {
|
|
3902
|
+
...current,
|
|
3903
|
+
contextBudgets: next
|
|
3904
|
+
};
|
|
3905
|
+
});
|
|
3906
|
+
};
|
|
3907
|
+
/**
|
|
3908
|
+
* 「恢复默认上下文」 clears every override in this region's draft. Empty
|
|
3909
|
+
* dict = every row falls back to its default window (the same state as a
|
|
3910
|
+
* region that was never configured), and clearing is what makes the bulk
|
|
3911
|
+
* action reversible without 18 more clicks.
|
|
3912
|
+
*/
|
|
3913
|
+
const resetAllContextBudgets = () => {
|
|
3914
|
+
editDraft((current) => ({
|
|
3915
|
+
...current,
|
|
3916
|
+
contextBudgets: {}
|
|
3917
|
+
}));
|
|
3918
|
+
};
|
|
3882
3919
|
const discardModels = () => {
|
|
3883
3920
|
setDrafts((prev) => {
|
|
3884
3921
|
const next = { ...prev };
|
|
@@ -4589,6 +4626,24 @@ window.__ModuleLoader__.load({
|
|
|
4589
4626
|
})]
|
|
4590
4627
|
}, model.id))
|
|
4591
4628
|
}),
|
|
4629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4630
|
+
className: "dsm-trae-context-bulk",
|
|
4631
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4632
|
+
type: "button",
|
|
4633
|
+
className: "dsm-btn dsm-btn-outline",
|
|
4634
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || !visibleModels.some((model) => model.maxContextWindow !== void 0),
|
|
4635
|
+
title: t("row.contextAllMaxHint"),
|
|
4636
|
+
onClick: setAllContextMax,
|
|
4637
|
+
children: t("row.contextAllMax")
|
|
4638
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4639
|
+
type: "button",
|
|
4640
|
+
className: "dsm-btn dsm-btn-outline",
|
|
4641
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || Object.keys(activeContextBudgets).length === 0,
|
|
4642
|
+
title: t("row.contextAllDefaultHint"),
|
|
4643
|
+
onClick: resetAllContextBudgets,
|
|
4644
|
+
children: t("row.contextAllDefault")
|
|
4645
|
+
})]
|
|
4646
|
+
}),
|
|
4592
4647
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4593
4648
|
className: "dsm-trae-model-capability-note",
|
|
4594
4649
|
children: t("row.modelCapabilityPending")
|
|
@@ -5507,6 +5562,10 @@ window.__ModuleLoader__.load({
|
|
|
5507
5562
|
"row.expiryEarliest": "Earliest: {time}",
|
|
5508
5563
|
"row.settingsDirty": "Unsaved changes",
|
|
5509
5564
|
"row.modelsTestEmpty": "Enable at least one model first.",
|
|
5565
|
+
"row.contextAllMax": "Switch all to Max",
|
|
5566
|
+
"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.",
|
|
5567
|
+
"row.contextAllDefault": "Restore default context",
|
|
5568
|
+
"row.contextAllDefaultHint": "Clears every context-budget override in this region, so all models go back to Trae's default window. Still a draft — press Save to apply.",
|
|
5510
5569
|
"row.modelsTestError": "Test failed: {message}",
|
|
5511
5570
|
"row.modelsTestDone": "{available} of {total} answered.",
|
|
5512
5571
|
"row.probeAvailable": "works",
|
|
@@ -5823,6 +5882,10 @@ window.__ModuleLoader__.load({
|
|
|
5823
5882
|
"row.expiryEarliest": "最早到期:{time}",
|
|
5824
5883
|
"row.settingsDirty": "有未保存的修改",
|
|
5825
5884
|
"row.modelsTestEmpty": "请先启用至少一个模型。",
|
|
5885
|
+
"row.contextAllMax": "整区切到 Max",
|
|
5886
|
+
"row.contextAllMaxHint": "把本区所有带 Max 档的模型的 DSH 上下文预算一次性设为各自的 Max 档。这只是草稿——要按「保存」才生效。没有 Max 档的模型不会被改动。",
|
|
5887
|
+
"row.contextAllDefault": "全部恢复默认上下文",
|
|
5888
|
+
"row.contextAllDefaultHint": "清掉本区所有上下文预算覆盖,让每个模型都回到 Trae 的默认窗口。这只是草稿——要按「保存」才生效。",
|
|
5826
5889
|
"row.modelsTestError": "测试失败:{message}",
|
|
5827
5890
|
"row.modelsTestDone": "{total} 个里有 {available} 个正常应答。",
|
|
5828
5891
|
"row.probeAvailable": "可用",
|
package/lib/index.d.ts
CHANGED
|
@@ -30,9 +30,29 @@ interface TraeDiscoveredModel {
|
|
|
30
30
|
creditMultiplier?: number;
|
|
31
31
|
reasoningSupported: boolean;
|
|
32
32
|
reasoning?: TraeDiscoveredReasoning;
|
|
33
|
+
/**
|
|
34
|
+
* The directory group the model was seen under (`data.list[].function`).
|
|
35
|
+
*
|
|
36
|
+
* The Remote `/models` answer is grouped by SOLO function and the group is
|
|
37
|
+
* the authoritative statement of which function the model belongs to — the
|
|
38
|
+
* `config_name` the gateway accepts is isolated per function, so a model in
|
|
39
|
+
* `solo_work_remote` answers 4001 when asked through `solo_work_lite`. The
|
|
40
|
+
* wire join (see `mergeTraeModelSources`) prefers its own
|
|
41
|
+
* `get_detail_param` function and uses this as the fallback when the roster
|
|
42
|
+
* has nothing to say (restricted networks often return an empty
|
|
43
|
+
* `config_info_list` for the chat functions).
|
|
44
|
+
*/
|
|
45
|
+
function?: string;
|
|
33
46
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Parse only capabilities explicitly advertised by Trae's remote model API.
|
|
49
|
+
*
|
|
50
|
+
* @param groupFunction The `data.list[].function` of the group this raw row
|
|
51
|
+
* was read from; attached verbatim so downstream can replay the model's own
|
|
52
|
+
* function instead of the `llm_utils_chat` default (see
|
|
53
|
+
* {@link TraeDiscoveredModel.function}).
|
|
54
|
+
*/
|
|
55
|
+
declare function parseTraeRemoteModel(value: unknown, groupFunction?: string): TraeDiscoveredModel | undefined;
|
|
36
56
|
//#endregion
|
|
37
57
|
//#region src/paths.d.ts
|
|
38
58
|
type TraeEdition = 'cn' | 'sg' | 'solo' | 'solo-sg';
|
|
@@ -2810,6 +2830,13 @@ interface TraeSoloRemoteCatalogOptions {
|
|
|
2810
2830
|
credential(): Promise<TraeCredential>;
|
|
2811
2831
|
fetchImpl?: typeof fetch;
|
|
2812
2832
|
baseUrl?: string;
|
|
2833
|
+
/**
|
|
2834
|
+
* Diagnostics sink (wired to `ctx.logger.warn` by the plugin). Called only
|
|
2835
|
+
* on the interesting path — when a NON-official host served the directory
|
|
2836
|
+
* or is reused as the remembered first candidate — never on the happy path
|
|
2837
|
+
* of official-host-first.
|
|
2838
|
+
*/
|
|
2839
|
+
log?: (message: string, detail?: unknown) => void;
|
|
2813
2840
|
}
|
|
2814
2841
|
/**
|
|
2815
2842
|
* Read-only model catalog client for the SOLO Web API.
|
|
@@ -2821,9 +2848,118 @@ declare class TraeSoloRemoteCatalogClient {
|
|
|
2821
2848
|
private readonly options;
|
|
2822
2849
|
private readonly fetchImpl;
|
|
2823
2850
|
private readonly baseUrl;
|
|
2851
|
+
/**
|
|
2852
|
+
* The last base that answered the directory with a NON-EMPTY catalog in this
|
|
2853
|
+
* process.
|
|
2854
|
+
*
|
|
2855
|
+
* A proxy-blocked network pays the failing official attempt on EVERY refresh
|
|
2856
|
+
* otherwise, so a backup that has served the catalog is tried first on later
|
|
2857
|
+
* refreshes. Putting the official base "second" is NOT enough to re-probe it:
|
|
2858
|
+
* the loop returns on the first success, so a backup that keeps answering
|
|
2859
|
+
* means the official base is never asked again. The official host is
|
|
2860
|
+
* therefore re-probed on a COOLDOWN instead (see {@link OFFICIAL_RETRY_MS}):
|
|
2861
|
+
* while the cooldown runs the backup is used and the official host is
|
|
2862
|
+
* skipped; when it expires the official host is tried FIRST again, so a
|
|
2863
|
+
* healed network takes over within one cooldown window rather than never.
|
|
2864
|
+
*/
|
|
2865
|
+
private lastSuccessfulBase;
|
|
2866
|
+
/**
|
|
2867
|
+
* When the official base may be probed again after a transport/5xx failure,
|
|
2868
|
+
* as epoch milliseconds (0 = probe it on the next refresh).
|
|
2869
|
+
*/
|
|
2870
|
+
private officialRetryAtMs;
|
|
2871
|
+
/**
|
|
2872
|
+
* How long a failing official host is skipped before it is re-probed.
|
|
2873
|
+
*
|
|
2874
|
+
* Long enough that a blocked network stops paying the failing attempt on
|
|
2875
|
+
* every refresh (the point of the fallback), short enough that a healed
|
|
2876
|
+
* network takes over without a restart. Each probe costs one connect timeout
|
|
2877
|
+
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
2878
|
+
*/
|
|
2879
|
+
private static readonly OFFICIAL_RETRY_MS;
|
|
2824
2880
|
constructor(options: TraeSoloRemoteCatalogOptions);
|
|
2825
2881
|
private headers;
|
|
2826
2882
|
fetchModels(signal?: AbortSignal): Promise<TraeDiscoveredModel[]>;
|
|
2883
|
+
/**
|
|
2884
|
+
* Bases to try for one refresh, in order.
|
|
2885
|
+
*
|
|
2886
|
+
* While the official host is in its post-failure cooldown the backup hosts go
|
|
2887
|
+
* first and the official base is kept LAST as a final chance (so a still-down
|
|
2888
|
+
* backup does not fail the whole refresh while the official host happens to
|
|
2889
|
+
* be reachable). Once the cooldown expires the official base is first again:
|
|
2890
|
+
* that is what lets a healed network take over — the loop returns on the
|
|
2891
|
+
* first success, so simply placing the official base second never re-probes
|
|
2892
|
+
* it while a backup keeps answering.
|
|
2893
|
+
*/
|
|
2894
|
+
private candidateBases;
|
|
2895
|
+
/**
|
|
2896
|
+
* Union every returned group into one deduplicated list, attributing each
|
|
2897
|
+
* model to the CHAT function the directory listed it under.
|
|
2898
|
+
*
|
|
2899
|
+
* Each group is a roster the gateway is willing to serve; a model listed by
|
|
2900
|
+
* any of them is a model the IDE can offer, and the wire join downstream (see
|
|
2901
|
+
* mergeTraeModelSources) is what decides whether it is actually callable.
|
|
2902
|
+
* Restricting this to `solo_agent_remote` dropped the nine models issue #19
|
|
2903
|
+
* was opened about.
|
|
2904
|
+
*
|
|
2905
|
+
* FUNCTION ATTRIBUTION (2026-10-20): a group is the authoritative statement
|
|
2906
|
+
* of which SOLO function a model belongs to — the accepted `config_name` is
|
|
2907
|
+
* isolated per function (glm-5.3 answers `solo_work_remote` and 4001s under
|
|
2908
|
+
* `solo_work_lite`). The wire join prefers its own `get_detail_param`
|
|
2909
|
+
* function and falls back to this one when the roster says nothing, which
|
|
2910
|
+
* restricted networks often do (measured: an empty `config_info_list` for
|
|
2911
|
+
* every chat function but `chat_v3`).
|
|
2912
|
+
*
|
|
2913
|
+
* ONLY CHAT-CAPABLE GROUPS ARE ATTRIBUTED. `solo_agent` and `builder_v3` are
|
|
2914
|
+
* IDE/Remote-agent rosters: asking `llm_utils_chat` with either answers
|
|
2915
|
+
* 4011/4001 (measured 2026-10-01 on a live credential, issue #19). A model
|
|
2916
|
+
* that appears ONLY there is still not uncallable, though — the SAME models
|
|
2917
|
+
* answered normally under `solo_work_remote` / `solo_work_lite` /
|
|
2918
|
+
* `solo_agent_remote` (measured the same day, see `TRAE_DIRECTORY_FUNCTIONS`
|
|
2919
|
+
* in solo.ts), and the default function is what has always reached them:
|
|
2920
|
+
* they are exactly the nine models issue #19's fix exposes (`gpt-5.5`,
|
|
2921
|
+
* `gpt-5.6-*`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code`,
|
|
2922
|
+
* `Dola-Seed-2.0-Code`, `deepseek-v4-flash-0731` — docs/ISSUE19_DIAGNOSIS.md).
|
|
2923
|
+
* Stamping `solo_agent` onto such a row makes every call 4011; DROPPING it
|
|
2924
|
+
* undoes issue #19 for exactly the restricted-network users this fallback
|
|
2925
|
+
* exists for. It therefore keeps NO function, the bridge falls back to the
|
|
2926
|
+
* default chat function exactly as before this change, and a refusal (if the
|
|
2927
|
+
* model really is unreachable) comes back as a readable upstream message
|
|
2928
|
+
* instead of a silent absence.
|
|
2929
|
+
*
|
|
2930
|
+
* CHAT GROUPS ARE WALKED IN `TRAE_DIRECTORY_FUNCTIONS` ORDER, not in the
|
|
2931
|
+
* request's `functions=` order. That list is the measured precedence of which
|
|
2932
|
+
* function serves a shared config, and it is the order the wire roster itself
|
|
2933
|
+
* collects in (`solo.ts`) — walking a different order would attribute the
|
|
2934
|
+
* same model differently depending on whether the roster answered, and the
|
|
2935
|
+
* difference is not cosmetic: `solo_agent_remote` sits after the two
|
|
2936
|
+
* `solo_work_*` functions because it is behind the Remote plan gate, so
|
|
2937
|
+
* preferring it can turn a free-tier model into a `1005 requires a paid
|
|
2938
|
+
* plan` refusal.
|
|
2939
|
+
*
|
|
2940
|
+
* A model appears in SEVERAL groups and the groups disagree about its
|
|
2941
|
+
* capacity, so dedupe keeps the MOST CAPABLE row rather than the first one
|
|
2942
|
+
* (issue #23). "First wins" assumed the group order was stable; measured
|
|
2943
|
+
* 2026-10-05 on a live CN credential it is not — five calls returned five
|
|
2944
|
+
* different orders — so the published window became a coin flip:
|
|
2945
|
+
* `Doubao-Seed-2.1-Pro` came back as `116000`/no-Max (`builder_v3`), or
|
|
2946
|
+
* `256000`/no-Max (`solo_work_*`), or `256000`/`1000000` (`chat_v3`,
|
|
2947
|
+
* `solo_agent*`) depending on which group answered first. The Max tier is
|
|
2948
|
+
* the one users enable in the IDE, and `applyContextBudgets` can only raise
|
|
2949
|
+
* a window to `maxContextWindow` — so losing that field silently capped
|
|
2950
|
+
* every configured 1M model at 256K or less, and DSH compressed far earlier
|
|
2951
|
+
* than the chosen budget implied.
|
|
2952
|
+
*
|
|
2953
|
+
* Ordering rule: a row advertising a Max tier beats one that does not, a
|
|
2954
|
+
* larger Max wins over a smaller one, and with no Max on either side the
|
|
2955
|
+
* wider dev window wins. Every row for one id carries the same
|
|
2956
|
+
* `multimodal` / `reasoning` / credit facts (verified the same day), so
|
|
2957
|
+
* choosing on capacity loses no capability. The result no longer depends on
|
|
2958
|
+
* the gateway's group order — and the FUNCTION does not either: the winning
|
|
2959
|
+
* row's capacity may come from a later group, but the separately-tracked
|
|
2960
|
+
* attribution is re-asserted on it.
|
|
2961
|
+
*/
|
|
2962
|
+
private collectModels;
|
|
2827
2963
|
}
|
|
2828
2964
|
//#endregion
|
|
2829
2965
|
//#region src/delegating-upstream.d.ts
|
package/lib/index.js
CHANGED
|
@@ -235,6 +235,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
235
235
|
for (const model of remote) {
|
|
236
236
|
const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
|
|
237
237
|
const creditMultiplier = wireModel?.creditMultiplier ?? model.creditMultiplier;
|
|
238
|
+
const wireFunction = wireModel?.function ?? model.function;
|
|
238
239
|
result.push({
|
|
239
240
|
id: model.id,
|
|
240
241
|
name: model.name,
|
|
@@ -242,6 +243,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
242
243
|
...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
|
|
243
244
|
...creditMultiplier === void 0 ? {} : { creditMultiplier },
|
|
244
245
|
input: ["text"],
|
|
246
|
+
...wireModel?.maxTokens === void 0 ? {} : { maxTokens: wireModel.maxTokens },
|
|
245
247
|
...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
|
|
246
248
|
reasoningSupported: model.reasoningSupported,
|
|
247
249
|
...model.reasoning === void 0 ? {} : {
|
|
@@ -249,12 +251,45 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
249
251
|
reasoningEfforts: Object.fromEntries(model.reasoning.supported.map((effort) => [effort, effort === "low" ? "light" : effort === "xhigh" ? "extra_high" : "high"]))
|
|
250
252
|
},
|
|
251
253
|
...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
|
|
252
|
-
...
|
|
254
|
+
...wireFunction === void 0 ? {} : { wireFunction }
|
|
253
255
|
});
|
|
254
256
|
}
|
|
255
257
|
return result;
|
|
256
258
|
}
|
|
257
259
|
/**
|
|
260
|
+
* Demote a NON-chat `wireFunction` to "no function", keeping the row.
|
|
261
|
+
*
|
|
262
|
+
* A row pinned to an IDE/Remote-agent roster — `solo_agent`, `builder_v3` — is
|
|
263
|
+
* not callable through `llm_utils_chat` by THAT function: asking answers
|
|
264
|
+
* 4011/4001 (measured 2026-10-01, issue #19). Dropping the row is not the fix,
|
|
265
|
+
* though, and was a regression: a model can carry a non-chat function for a
|
|
266
|
+
* reason that says nothing about its chat availability. The directory lists
|
|
267
|
+
* `gpt-5.5`, `gpt-5.6-*`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code`,
|
|
268
|
+
* `Dola-Seed-2.0-Code` and `deepseek-v4-flash-0731` under `solo_agent` ALONE
|
|
269
|
+
* (docs/ISSUE19_DIAGNOSIS.md), yet the same models answer normally under
|
|
270
|
+
* `solo_work_remote` / `solo_work_lite` / `solo_agent_remote` — they are
|
|
271
|
+
* exactly the models issue #19's fix was opened to expose. Dropping them
|
|
272
|
+
* re-hid the whole set on the restricted networks this fallback targets, where
|
|
273
|
+
* the wire roster is empty and the directory group is the only attribution.
|
|
274
|
+
*
|
|
275
|
+
* Keeping the row with the function CLEARED preserves both facts: the model
|
|
276
|
+
* stays visible (issue #19), and the bridge sends the region's DEFAULT chat
|
|
277
|
+
* function instead of one that is guaranteed to refuse. That is the pre-change
|
|
278
|
+
* behaviour for these rows — a genuinely unreachable model still fails with a
|
|
279
|
+
* readable upstream message rather than a silent absence.
|
|
280
|
+
*
|
|
281
|
+
* `chatFunctions` must be the region's own `TRAE_DIRECTORY_FUNCTIONS` list —
|
|
282
|
+
* the functions `llm_utils_chat` accepts. Callers should log the demoted rows:
|
|
283
|
+
* "which function did it lose, and why" deserves an answer.
|
|
284
|
+
*/
|
|
285
|
+
function withChatCallableFunctions(rows, chatFunctions) {
|
|
286
|
+
return rows.map((model) => {
|
|
287
|
+
if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) return model;
|
|
288
|
+
const { wireFunction: _demoted, ...withoutFunction } = model;
|
|
289
|
+
return withoutFunction;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
258
293
|
* Apply the saved local budget. Trae advertises two windows per model (dev and
|
|
259
294
|
* Max), so the budget may only switch a model to its own advertised Max value —
|
|
260
295
|
* never to a fabricated number. Everything else keeps the dev window.
|
|
@@ -276,6 +311,7 @@ function discoveredCatalog(models) {
|
|
|
276
311
|
input: ["text"],
|
|
277
312
|
...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
|
|
278
313
|
...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
|
|
314
|
+
...model.function === void 0 ? {} : { wireFunction: model.function },
|
|
279
315
|
reasoningSupported: model.reasoningSupported,
|
|
280
316
|
...model.reasoning === void 0 ? {} : {
|
|
281
317
|
reasoning: model.reasoning,
|
|
@@ -4701,8 +4737,9 @@ var TraeSoloBridge = class {
|
|
|
4701
4737
|
if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
|
|
4702
4738
|
const entryKey = model.trim().toLowerCase();
|
|
4703
4739
|
const entry = this.catalog?.current().find((item) => item.id === model || item.id.trim().toLowerCase() === entryKey);
|
|
4704
|
-
const
|
|
4705
|
-
|
|
4740
|
+
const catalogConfigName = entry?.wireConfigName;
|
|
4741
|
+
const fromCatalog = entry === void 0 || catalogConfigName === void 0 && entry.wireFunction === void 0 ? void 0 : {
|
|
4742
|
+
configName: catalogConfigName ?? entry.id,
|
|
4706
4743
|
...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
|
|
4707
4744
|
};
|
|
4708
4745
|
const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
|
|
@@ -4717,11 +4754,28 @@ var TraeSoloBridge = class {
|
|
|
4717
4754
|
* directory, so borrowing its function is safe exactly when both sources
|
|
4718
4755
|
* agree on the wire config_name; a disagreement means the resolver found
|
|
4719
4756
|
* a different row and its function would be wrong.
|
|
4757
|
+
*
|
|
4758
|
+
* The MIRROR case is equally load-bearing and is the reason the row's own
|
|
4759
|
+
* id is not accepted as its wire spelling: a row that carries only the
|
|
4760
|
+
* FUNCTION (`wireConfigName` absent) has no config_name of its own, and
|
|
4761
|
+
* its display id is the wrong-cased spelling of the wire name in exactly
|
|
4762
|
+
* the cases issue #38 is about. The resolver's config_name is then the
|
|
4763
|
+
* only correct one, so it must win for the NAME while the row keeps
|
|
4764
|
+
* winning for the FUNCTION.
|
|
4720
4765
|
*/
|
|
4721
4766
|
const sameWireConfig = fromCatalog !== void 0 && fromResolver !== void 0 && fromCatalog.configName.toLowerCase() === fromResolver.configName.toLowerCase();
|
|
4767
|
+
/**
|
|
4768
|
+
* Whether the catalog row knows its own wire `config_name` (as opposed to
|
|
4769
|
+
* only carrying the display id plus a function). Only such a row may
|
|
4770
|
+
* decide the name on its own.
|
|
4771
|
+
*/
|
|
4772
|
+
const catalogKnowsConfigName = catalogConfigName !== void 0;
|
|
4722
4773
|
const target = fromCatalog !== void 0 && fromCatalog.function === void 0 && sameWireConfig && fromResolver !== void 0 ? {
|
|
4723
4774
|
configName: fromCatalog.configName,
|
|
4724
4775
|
...fromResolver.function === void 0 ? {} : { function: fromResolver.function }
|
|
4776
|
+
} : fromCatalog !== void 0 && !catalogKnowsConfigName && fromResolver !== void 0 ? {
|
|
4777
|
+
configName: fromResolver.configName,
|
|
4778
|
+
...fromCatalog.function === void 0 ? {} : { function: fromCatalog.function }
|
|
4725
4779
|
} : fromCatalog ?? fromResolver;
|
|
4726
4780
|
/**
|
|
4727
4781
|
* Last line of defence: the discovered row's own id.
|
|
@@ -4785,8 +4839,15 @@ const EFFORT_MAP = {
|
|
|
4785
4839
|
high: "high",
|
|
4786
4840
|
extra_high: "xhigh"
|
|
4787
4841
|
};
|
|
4788
|
-
/**
|
|
4789
|
-
|
|
4842
|
+
/**
|
|
4843
|
+
* Parse only capabilities explicitly advertised by Trae's remote model API.
|
|
4844
|
+
*
|
|
4845
|
+
* @param groupFunction The `data.list[].function` of the group this raw row
|
|
4846
|
+
* was read from; attached verbatim so downstream can replay the model's own
|
|
4847
|
+
* function instead of the `llm_utils_chat` default (see
|
|
4848
|
+
* {@link TraeDiscoveredModel.function}).
|
|
4849
|
+
*/
|
|
4850
|
+
function parseTraeRemoteModel(value, groupFunction) {
|
|
4790
4851
|
const raw = record(value);
|
|
4791
4852
|
if (raw === void 0 || typeof raw.name !== "string" || raw.name === "") return void 0;
|
|
4792
4853
|
const context = record(raw.context_window_tokens);
|
|
@@ -4817,7 +4878,8 @@ function parseTraeRemoteModel(value) {
|
|
|
4817
4878
|
...supported.length === 0 ? {} : { reasoning: {
|
|
4818
4879
|
supported,
|
|
4819
4880
|
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
4820
|
-
} }
|
|
4881
|
+
} },
|
|
4882
|
+
...groupFunction === void 0 || groupFunction === "" ? {} : { function: groupFunction }
|
|
4821
4883
|
};
|
|
4822
4884
|
}
|
|
4823
4885
|
//#endregion
|
|
@@ -4884,6 +4946,23 @@ const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
|
|
|
4884
4946
|
]
|
|
4885
4947
|
};
|
|
4886
4948
|
/**
|
|
4949
|
+
* Backup bases for the model directory, in try order after the official base.
|
|
4950
|
+
*
|
|
4951
|
+
* In restricted (proxy-sandboxed) networks the official catalog host
|
|
4952
|
+
* `solo.trae.cn` is behind a CONNECT rejection while the region's chat gateway
|
|
4953
|
+
* stays reachable — and that gateway serves the SAME `/api/remote/v1/models`
|
|
4954
|
+
* endpoint with an identical response shape (verified against
|
|
4955
|
+
* `https://trae-api-cn.mchost.guru/api/remote/v1` 2026-10-20; `chat` already
|
|
4956
|
+
* carries `/api/remote/v1`'s parent in {@link REGION_GATEWAYS}). The backup is
|
|
4957
|
+
* therefore derived from the region's chat base, mirrored on the remote path;
|
|
4958
|
+
* `ai` shares a single host for chat and remote, so it yields no candidate and
|
|
4959
|
+
* keeps its single-host behavior. An explicit `baseUrl` disables the chain.
|
|
4960
|
+
*/
|
|
4961
|
+
function traeSoloRemoteFallbackBases(region, officialBase) {
|
|
4962
|
+
const backupBase = `${REGION_GATEWAYS[region].chat}/api/remote/v1`;
|
|
4963
|
+
return backupBase === officialBase ? [] : [backupBase];
|
|
4964
|
+
}
|
|
4965
|
+
/**
|
|
4887
4966
|
* Of two rows for the SAME model id, the one that advertises the stronger
|
|
4888
4967
|
* capacity (issue #23).
|
|
4889
4968
|
*
|
|
@@ -4941,10 +5020,39 @@ function remoteDressing(region) {
|
|
|
4941
5020
|
* This deliberately has no chat/session method: the Remote session protocol
|
|
4942
5021
|
* only exposes a final answer and cannot preserve DSH's structured tool loop.
|
|
4943
5022
|
*/
|
|
4944
|
-
var TraeSoloRemoteCatalogClient = class {
|
|
5023
|
+
var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
4945
5024
|
options;
|
|
4946
5025
|
fetchImpl;
|
|
4947
5026
|
baseUrl;
|
|
5027
|
+
/**
|
|
5028
|
+
* The last base that answered the directory with a NON-EMPTY catalog in this
|
|
5029
|
+
* process.
|
|
5030
|
+
*
|
|
5031
|
+
* A proxy-blocked network pays the failing official attempt on EVERY refresh
|
|
5032
|
+
* otherwise, so a backup that has served the catalog is tried first on later
|
|
5033
|
+
* refreshes. Putting the official base "second" is NOT enough to re-probe it:
|
|
5034
|
+
* the loop returns on the first success, so a backup that keeps answering
|
|
5035
|
+
* means the official base is never asked again. The official host is
|
|
5036
|
+
* therefore re-probed on a COOLDOWN instead (see {@link OFFICIAL_RETRY_MS}):
|
|
5037
|
+
* while the cooldown runs the backup is used and the official host is
|
|
5038
|
+
* skipped; when it expires the official host is tried FIRST again, so a
|
|
5039
|
+
* healed network takes over within one cooldown window rather than never.
|
|
5040
|
+
*/
|
|
5041
|
+
lastSuccessfulBase;
|
|
5042
|
+
/**
|
|
5043
|
+
* When the official base may be probed again after a transport/5xx failure,
|
|
5044
|
+
* as epoch milliseconds (0 = probe it on the next refresh).
|
|
5045
|
+
*/
|
|
5046
|
+
officialRetryAtMs = 0;
|
|
5047
|
+
/**
|
|
5048
|
+
* How long a failing official host is skipped before it is re-probed.
|
|
5049
|
+
*
|
|
5050
|
+
* Long enough that a blocked network stops paying the failing attempt on
|
|
5051
|
+
* every refresh (the point of the fallback), short enough that a healed
|
|
5052
|
+
* network takes over without a restart. Each probe costs one connect timeout
|
|
5053
|
+
* (30 s) in the worst case, so 5 minutes bounds that to ~1 attempt per 5 min.
|
|
5054
|
+
*/
|
|
5055
|
+
static OFFICIAL_RETRY_MS = 3e5;
|
|
4948
5056
|
constructor(options) {
|
|
4949
5057
|
this.options = options;
|
|
4950
5058
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -4965,24 +5073,183 @@ var TraeSoloRemoteCatalogClient = class {
|
|
|
4965
5073
|
}
|
|
4966
5074
|
async fetchModels(signal) {
|
|
4967
5075
|
const region = regionOfCredential(await this.options.credential());
|
|
4968
|
-
const
|
|
5076
|
+
const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
4969
5077
|
const headers = await this.headers(region);
|
|
4970
5078
|
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5079
|
+
let lastError;
|
|
5080
|
+
let emptyError;
|
|
5081
|
+
for (const base of this.candidateBases(region, officialBase)) {
|
|
5082
|
+
let response;
|
|
5083
|
+
try {
|
|
5084
|
+
response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
|
|
5085
|
+
headers,
|
|
5086
|
+
signal: signal ?? AbortSignal.timeout(3e4)
|
|
5087
|
+
});
|
|
5088
|
+
} catch (error) {
|
|
5089
|
+
if (signal?.aborted) throw error;
|
|
5090
|
+
if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
|
|
5091
|
+
lastError = error;
|
|
5092
|
+
continue;
|
|
5093
|
+
}
|
|
5094
|
+
if (!response.ok) {
|
|
5095
|
+
if (response.status < 500) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
5096
|
+
if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
|
|
5097
|
+
lastError = /* @__PURE__ */ new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
5098
|
+
continue;
|
|
5099
|
+
}
|
|
5100
|
+
const json = await response.json();
|
|
5101
|
+
const models = this.collectModels(region, json.data?.list ?? []);
|
|
5102
|
+
if (models.length === 0) {
|
|
5103
|
+
emptyError = /* @__PURE__ */ new Error("SOLO remote models response contained no models");
|
|
5104
|
+
lastError = emptyError;
|
|
5105
|
+
continue;
|
|
5106
|
+
}
|
|
5107
|
+
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 });
|
|
5108
|
+
else this.officialRetryAtMs = 0;
|
|
5109
|
+
this.lastSuccessfulBase = base;
|
|
5110
|
+
return models;
|
|
5111
|
+
}
|
|
5112
|
+
if (emptyError !== void 0) throw emptyError;
|
|
5113
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
5114
|
+
}
|
|
5115
|
+
/**
|
|
5116
|
+
* Bases to try for one refresh, in order.
|
|
5117
|
+
*
|
|
5118
|
+
* While the official host is in its post-failure cooldown the backup hosts go
|
|
5119
|
+
* first and the official base is kept LAST as a final chance (so a still-down
|
|
5120
|
+
* backup does not fail the whole refresh while the official host happens to
|
|
5121
|
+
* be reachable). Once the cooldown expires the official base is first again:
|
|
5122
|
+
* that is what lets a healed network take over — the loop returns on the
|
|
5123
|
+
* first success, so simply placing the official base second never re-probes
|
|
5124
|
+
* it while a backup keeps answering.
|
|
5125
|
+
*/
|
|
5126
|
+
candidateBases(region, officialBase) {
|
|
5127
|
+
if (this.baseUrl !== void 0) return [officialBase];
|
|
5128
|
+
const fallbacks = traeSoloRemoteFallbackBases(region, officialBase);
|
|
5129
|
+
if (this.officialRetryAtMs <= Date.now()) return [officialBase, ...fallbacks];
|
|
5130
|
+
const remembered = this.lastSuccessfulBase;
|
|
5131
|
+
return [
|
|
5132
|
+
...remembered === void 0 ? [] : [remembered],
|
|
5133
|
+
...fallbacks.filter((base) => base !== remembered),
|
|
5134
|
+
officialBase
|
|
5135
|
+
];
|
|
5136
|
+
}
|
|
5137
|
+
/**
|
|
5138
|
+
* Union every returned group into one deduplicated list, attributing each
|
|
5139
|
+
* model to the CHAT function the directory listed it under.
|
|
5140
|
+
*
|
|
5141
|
+
* Each group is a roster the gateway is willing to serve; a model listed by
|
|
5142
|
+
* any of them is a model the IDE can offer, and the wire join downstream (see
|
|
5143
|
+
* mergeTraeModelSources) is what decides whether it is actually callable.
|
|
5144
|
+
* Restricting this to `solo_agent_remote` dropped the nine models issue #19
|
|
5145
|
+
* was opened about.
|
|
5146
|
+
*
|
|
5147
|
+
* FUNCTION ATTRIBUTION (2026-10-20): a group is the authoritative statement
|
|
5148
|
+
* of which SOLO function a model belongs to — the accepted `config_name` is
|
|
5149
|
+
* isolated per function (glm-5.3 answers `solo_work_remote` and 4001s under
|
|
5150
|
+
* `solo_work_lite`). The wire join prefers its own `get_detail_param`
|
|
5151
|
+
* function and falls back to this one when the roster says nothing, which
|
|
5152
|
+
* restricted networks often do (measured: an empty `config_info_list` for
|
|
5153
|
+
* every chat function but `chat_v3`).
|
|
5154
|
+
*
|
|
5155
|
+
* ONLY CHAT-CAPABLE GROUPS ARE ATTRIBUTED. `solo_agent` and `builder_v3` are
|
|
5156
|
+
* IDE/Remote-agent rosters: asking `llm_utils_chat` with either answers
|
|
5157
|
+
* 4011/4001 (measured 2026-10-01 on a live credential, issue #19). A model
|
|
5158
|
+
* that appears ONLY there is still not uncallable, though — the SAME models
|
|
5159
|
+
* answered normally under `solo_work_remote` / `solo_work_lite` /
|
|
5160
|
+
* `solo_agent_remote` (measured the same day, see `TRAE_DIRECTORY_FUNCTIONS`
|
|
5161
|
+
* in solo.ts), and the default function is what has always reached them:
|
|
5162
|
+
* they are exactly the nine models issue #19's fix exposes (`gpt-5.5`,
|
|
5163
|
+
* `gpt-5.6-*`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code`,
|
|
5164
|
+
* `Dola-Seed-2.0-Code`, `deepseek-v4-flash-0731` — docs/ISSUE19_DIAGNOSIS.md).
|
|
5165
|
+
* Stamping `solo_agent` onto such a row makes every call 4011; DROPPING it
|
|
5166
|
+
* undoes issue #19 for exactly the restricted-network users this fallback
|
|
5167
|
+
* exists for. It therefore keeps NO function, the bridge falls back to the
|
|
5168
|
+
* default chat function exactly as before this change, and a refusal (if the
|
|
5169
|
+
* model really is unreachable) comes back as a readable upstream message
|
|
5170
|
+
* instead of a silent absence.
|
|
5171
|
+
*
|
|
5172
|
+
* CHAT GROUPS ARE WALKED IN `TRAE_DIRECTORY_FUNCTIONS` ORDER, not in the
|
|
5173
|
+
* request's `functions=` order. That list is the measured precedence of which
|
|
5174
|
+
* function serves a shared config, and it is the order the wire roster itself
|
|
5175
|
+
* collects in (`solo.ts`) — walking a different order would attribute the
|
|
5176
|
+
* same model differently depending on whether the roster answered, and the
|
|
5177
|
+
* difference is not cosmetic: `solo_agent_remote` sits after the two
|
|
5178
|
+
* `solo_work_*` functions because it is behind the Remote plan gate, so
|
|
5179
|
+
* preferring it can turn a free-tier model into a `1005 requires a paid
|
|
5180
|
+
* plan` refusal.
|
|
5181
|
+
*
|
|
5182
|
+
* A model appears in SEVERAL groups and the groups disagree about its
|
|
5183
|
+
* capacity, so dedupe keeps the MOST CAPABLE row rather than the first one
|
|
5184
|
+
* (issue #23). "First wins" assumed the group order was stable; measured
|
|
5185
|
+
* 2026-10-05 on a live CN credential it is not — five calls returned five
|
|
5186
|
+
* different orders — so the published window became a coin flip:
|
|
5187
|
+
* `Doubao-Seed-2.1-Pro` came back as `116000`/no-Max (`builder_v3`), or
|
|
5188
|
+
* `256000`/no-Max (`solo_work_*`), or `256000`/`1000000` (`chat_v3`,
|
|
5189
|
+
* `solo_agent*`) depending on which group answered first. The Max tier is
|
|
5190
|
+
* the one users enable in the IDE, and `applyContextBudgets` can only raise
|
|
5191
|
+
* a window to `maxContextWindow` — so losing that field silently capped
|
|
5192
|
+
* every configured 1M model at 256K or less, and DSH compressed far earlier
|
|
5193
|
+
* than the chosen budget implied.
|
|
5194
|
+
*
|
|
5195
|
+
* Ordering rule: a row advertising a Max tier beats one that does not, a
|
|
5196
|
+
* larger Max wins over a smaller one, and with no Max on either side the
|
|
5197
|
+
* wider dev window wins. Every row for one id carries the same
|
|
5198
|
+
* `multimodal` / `reasoning` / credit facts (verified the same day), so
|
|
5199
|
+
* choosing on capacity loses no capability. The result no longer depends on
|
|
5200
|
+
* the gateway's group order — and the FUNCTION does not either: the winning
|
|
5201
|
+
* row's capacity may come from a later group, but the separately-tracked
|
|
5202
|
+
* attribution is re-asserted on it.
|
|
5203
|
+
*/
|
|
5204
|
+
collectModels(region, groups) {
|
|
4977
5205
|
const byId = /* @__PURE__ */ new Map();
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5206
|
+
const attributedFunction = /* @__PURE__ */ new Map();
|
|
5207
|
+
/**
|
|
5208
|
+
* The function a row may be stamped with: only a CHAT function. A non-chat
|
|
5209
|
+
* roster is not "the function this model belongs to" in any sense the chat
|
|
5210
|
+
* route can use (see the method note above), so it is not recorded at all.
|
|
5211
|
+
*/
|
|
5212
|
+
const chatFunctions = new Set(TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
5213
|
+
const attribution = (groupFunction) => groupFunction !== void 0 && chatFunctions.has(groupFunction) ? groupFunction : void 0;
|
|
5214
|
+
const addGroup = (groupFunction, raws) => {
|
|
5215
|
+
const fn = attribution(groupFunction);
|
|
5216
|
+
for (const raw of raws) {
|
|
5217
|
+
const model = parseTraeRemoteModel(raw, fn);
|
|
5218
|
+
if (model === void 0) continue;
|
|
5219
|
+
if (fn !== void 0 && !attributedFunction.has(model.id)) attributedFunction.set(model.id, fn);
|
|
5220
|
+
byId.set(model.id, preferStrongerRow(model, byId.get(model.id)));
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
/**
|
|
5224
|
+
* Every group the gateway sent, bucketed BY FUNCTION NAME and kept as a
|
|
5225
|
+
* LIST.
|
|
5226
|
+
*
|
|
5227
|
+
* A name→group `Map` would keep only the LAST group of a name and silently
|
|
5228
|
+
* drop the models of every earlier one: nothing documents that the gateway
|
|
5229
|
+
* sends a function at most once, and the pre-change code unioned every
|
|
5230
|
+
* element of `data.list`, so a repeated name lost real models. Bucketing
|
|
5231
|
+
* keeps that union while the walk below still imposes a deterministic
|
|
5232
|
+
* order.
|
|
5233
|
+
*/
|
|
5234
|
+
const byFunction = /* @__PURE__ */ new Map();
|
|
5235
|
+
for (const group of groups) {
|
|
5236
|
+
const key = group.function ?? "";
|
|
5237
|
+
const bucket = byFunction.get(key);
|
|
5238
|
+
if (bucket === void 0) byFunction.set(key, [group]);
|
|
5239
|
+
else bucket.push(group);
|
|
4982
5240
|
}
|
|
4983
|
-
const
|
|
4984
|
-
|
|
4985
|
-
|
|
5241
|
+
for (const name of TRAE_DIRECTORY_FUNCTIONS[region]) for (const group of byFunction.get(name) ?? []) addGroup(name, group.models ?? []);
|
|
5242
|
+
for (const [name, bucket] of byFunction) {
|
|
5243
|
+
if (chatFunctions.has(name)) continue;
|
|
5244
|
+
for (const group of bucket) addGroup(name, group.models ?? []);
|
|
5245
|
+
}
|
|
5246
|
+
return [...byId.values()].map((model) => {
|
|
5247
|
+
const fn = attributedFunction.get(model.id);
|
|
5248
|
+
return fn === void 0 || fn === model.function ? model : {
|
|
5249
|
+
...model,
|
|
5250
|
+
function: fn
|
|
5251
|
+
};
|
|
5252
|
+
});
|
|
4986
5253
|
}
|
|
4987
5254
|
};
|
|
4988
5255
|
//#endregion
|
|
@@ -8755,7 +9022,10 @@ function apply(ctx, config) {
|
|
|
8755
9022
|
...panel === "enterprise" ? { route: "enterprise" } : {},
|
|
8756
9023
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
8757
9024
|
});
|
|
8758
|
-
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
9025
|
+
const remoteCatalog = new TraeSoloRemoteCatalogClient({
|
|
9026
|
+
credential: () => store.resolve(),
|
|
9027
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
9028
|
+
});
|
|
8759
9029
|
const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
|
|
8760
9030
|
/**
|
|
8761
9031
|
* One client + bridge per provider id.
|
|
@@ -8908,18 +9178,24 @@ function apply(ctx, config) {
|
|
|
8908
9178
|
},
|
|
8909
9179
|
discoverModels: async (signal) => {
|
|
8910
9180
|
const merged = mergeTraeModelSources(await withDirectoryRetry(() => remoteCatalog.fetchModels(signal)), await solo.fetchModels(signal));
|
|
8911
|
-
|
|
9181
|
+
const callable = withChatCallableFunctions(merged, TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
9182
|
+
const chatFunctions = TRAE_DIRECTORY_FUNCTIONS[region];
|
|
8912
9183
|
for (const model of merged) {
|
|
9184
|
+
if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) continue;
|
|
9185
|
+
ctx.logger.warn(`model directory row is pinned to a non-chat function and will be called through the default chat function instead: ${model.id} (pinned to function ${model.wireFunction})`);
|
|
9186
|
+
}
|
|
9187
|
+
wire.callableKeys.clear();
|
|
9188
|
+
for (const model of callable) {
|
|
8913
9189
|
wire.callableKeys.add(model.id.trim().toLowerCase());
|
|
8914
9190
|
wire.callableKeys.add(model.name.trim().toLowerCase());
|
|
8915
9191
|
}
|
|
8916
|
-
wire.resolved =
|
|
8917
|
-
const wireTargets = wireTargetsOf(
|
|
9192
|
+
wire.resolved = callable.length > 0;
|
|
9193
|
+
const wireTargets = wireTargetsOf(callable);
|
|
8918
9194
|
wire.byId = wireTargets.byId;
|
|
8919
9195
|
wire.byName = wireTargets.byName;
|
|
8920
9196
|
latestDiscovery.length = 0;
|
|
8921
|
-
latestDiscovery.push(...
|
|
8922
|
-
return
|
|
9197
|
+
latestDiscovery.push(...callable);
|
|
9198
|
+
return callable;
|
|
8923
9199
|
},
|
|
8924
9200
|
/**
|
|
8925
9201
|
* One-click availability test. The ids come from the card, so each one is
|
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.1.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|