dsh-connect-trae 2.4.0 → 2.5.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 +41 -0
- package/lib/client.js +39 -4
- package/lib/index.d.ts +17 -0
- package/lib/index.js +254 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0 (2026-10-01)
|
|
4
|
+
|
|
5
|
+
> **国际版(AI 区域)三条用户可见故障的修复**,全部来自 [issue #19](https://github.com/dingminhua/dsh-connect-trae/issues/19) 的报告与
|
|
6
|
+
> 在真实 SG 凭据上的逐条实测(取证过程见 `docs/ISSUE19_DIAGNOSIS.md`,档位接口探索见 `docs/INTL_PAID_TIER_PROBE.md`)。
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **国际版模型目录改为读取全部组并集——此前只读 `solo_agent_remote` 一个组,另外 9 个模型从未出现**(issue #19 症状一):
|
|
11
|
+
- Trae 的 remote `/models` 答复**按 function 分组**,且组间不是互替关系:实测 `solo_agent` 组 19 个模型、`solo_agent_remote` 组 10 个。旧代码请求 `solo_agent_remote,solo_work_remote` 后**只遍历 preferred 一个组**,报告人 Trae IDE 里能看到、插件里却没有的 `GPT-6-Astra`、`GPT-5.6-Sol/Terra/Luna`、`GPT-5.5`、`GLM-5.2` 等**恰好全部在没读的 `solo_agent` 组里**。
|
|
12
|
+
- 现在 `TRAE_REMOTE_DIRECTORY_FUNCTIONS` 按区域声明要请求的组(AI 区域三个全取),**每个返回的组都并入目录**,同一模型先出现者胜、只发一次。请求一个网关不认识的组是无害的(未知组名被忽略而非拒绝)。
|
|
13
|
+
- **发现面变宽,调用面反而收窄**:目录 union 让模型「可见」,但模型**只能通过列出它的那个 function 调用**。实测 `get_detail_param` 对 `solo_agent` 有求必应地返回 43 条,而 `llm_utils_chat` 对**所有**以 `function: "solo_agent"` 发出的模型一律回 `code 4011`——所以 wire 函数表 `TRAE_DIRECTORY_FUNCTIONS.ai` 从 `['solo_agent','solo_work_remote','solo_work_lite']` 改为 `['solo_work_remote','solo_work_lite','solo_agent_remote']`:让目录可见的新模型仍通过真正可调用的 function 去调,`solo_agent_remote` 放最后,使两个 function 都列出的模型保持无 Remote 门禁的 IDE function。CN 区域未重新测量,维持原列表不变(union 会保留网关返回的任何组)。
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **国际版订阅状态显示错误——Pro 会员被显示为「暂无有效套餐」**(issue #19 症状二):
|
|
18
|
+
- 卡片的判据只有 `has_package`,但那个字段回答的是「是否存在套餐权益对象」,不是「账号处于什么档位」——付费会员可以持有档位而没有套餐行,报告人的账号正是如此。
|
|
19
|
+
- Trae 同时返回的**权威档位字段** `user_pay_identity` / `user_pay_identity_str`(实测免费账号为 `0` / `"Free"`)此前在插件源码里**零引用**,被直接丢弃。
|
|
20
|
+
- 现在 `payStatus()` 完整保留两个档位字段(缺失时省略,不伪造),卡片判定改为 `has_package ∪ 档位非 Free`,并把档位名渲染进套餐行:「已有有效套餐 · Pro」/「暂无有效套餐 · Free」——免费用户看到**为什么**没有套餐,而不是只看到一个布尔。
|
|
21
|
+
- **只有精确的 `Free`(大小写不敏感)算免费**:未知或未来的档位名按付费处理。把付费会员错标成免费,比把免费账号显示成未知档位,是伤害更大的那个错误。
|
|
22
|
+
- **订阅门禁错误被吞成「Stream ended without finish_reason」**(issue #19 症状三):
|
|
23
|
+
- **现象**:免费/低档位账号选中需订阅的模型(如 `GPT-6-Sol`),得到的是一句像网络故障的报错,且会被重试多次。Trae 实际返回的是 **HTTP 200** + `event:error {"code":1005,"extra":"{\"plan\":N}"}`(`extra` 是 wire 上的 JSON **字符串**,订阅门禁只体现在 `extra.plan`)。
|
|
24
|
+
- **根因**:旧实现把上游答复原样以 200 转发,中途 `controller.error()` 截断流——HTTP 头已按 200 发出,`code 1005` 与 `plan` 在 HTTP 边界处丢失,pi-ai 只看到「流没给 finish_reason 就断了」,按传输故障处理。
|
|
25
|
+
- **修法**:`bridgeTraeSoloStreamOrFail` 在转发前**窥探流头**——任何输出开始之前的拒绝被翻译成真实的错误结果:`1005` → `402 hard_credit`(携带 plan 与「需要付费档」的文案,`hard_credit` 是让客户端停止而非重试的语义,402 是如实的状态码),`4011` / `4001` / 未知码 → `400 client`。code、plan、以及 Trae 那句与 code 相矛盾的原文(如 4011 自称"rate limit")**全部保留在 message 里**。
|
|
26
|
+
- 输出已开始后到来的拒绝(拒绝打断正在进行的回答):流以翻译后的可读错误终止,而不是裸截断。
|
|
27
|
+
- **窥探不伤健康流**:被窥探消费的头部字节逐字节重放;队列/进度/输出等任何 Trae 自有事件即触发直通,缓冲不超过首个事件。排队事件、分片工具调用等既有行为不变。
|
|
28
|
+
- **顺带登记两个此前不认识的错误码**(`grep` 证实源码与测试均零命中):`1005` = 订阅门禁(档位不够,`extra.plan` 指明所需档位);`4011` = 该 function 不提供此模型(其自报文案声称 rate limit,具有误导性)。已知的 `4001` = `config_name` 不被该 function 接受,语义不变。
|
|
29
|
+
|
|
30
|
+
### Tests
|
|
31
|
+
|
|
32
|
+
- **全仓 373 → 392(+19),含 5 次变异验证全部被抓**:
|
|
33
|
+
- `tests/solo-bridge.spec.ts` **+11**:`describeTraeFailure` 文案 5 例(1005 带 plan、4011 用 hint 而把误导原文降级为 detail、未知码回退、无 code 事件不虚构后缀、已解包的 extra 对象也能读 plan);`bridgeTraeSoloStreamOrFail` 策略 5 例(1005→402 `hard_credit`、4011→400 `client`、无名事件经错误码带识别、健康流含排队事件直通且头部重放逐字节不丢、输出先行后拒绝的流中场景以可读 message 终止);`TraeSoloBridge` 端到端 1 例(完整链路上订阅门禁成为错误结果而非被截断的 200 流)。其中「流中拒绝」用分块流构造:单块 body 会被窥探一起解码,无法测到该路径。
|
|
34
|
+
- `tests/card-subscription.spec.tsx` **新文件 5 例**:渲染真实卡片断言档位行——Pro 从档位单独判定、无档位时套餐行仍工作、Free 命名而非只报布尔、试用窗口优先于档位文案,及 `traePlanIsPaid` 判据表(只有精确 Free 算免费,未知档位按付费)。一例说明:无档位字符串时 badge 与 value 渲染同一裸标签,`getByText` 会命中多个元素,须用 `getAllByText`——这例测试在上一会话中断前正以多元素匹配失败,本次接续修复。
|
|
35
|
+
- `tests/usage.spec.ts` **+2**:档位字段与 `has_package` 分开保留;上游不带档位时省略而非伪造。
|
|
36
|
+
- `tests/solo-remote.spec.ts` **+1**、`tests/solo.spec.ts` 改写既有断言:目录 union 行为、AI 区域 function 顺序(`solo_agent` 必须绝迹)。
|
|
37
|
+
- **变异验证**(确认测试有约束力,5 次全部被抓):① 分类忽略 1005 → 2 例失败;② plan 提取禁用 → 4 例失败;③ 窥探失效(打回旧行为,即本次要修的 bug)→ 4 例失败;④ 档位恒判免费 → 2 例失败;⑤ union 退回单组 → 3 例失败。恢复后 45 文件 392 例全绿。
|
|
38
|
+
|
|
39
|
+
### Docs
|
|
40
|
+
|
|
41
|
+
- `docs/ISSUE19_DIAGNOSIS.md`(随 2.4.0 后的 `009d9a7` 提交):三个症状的完整取证——remote 目录两组的差集与报告人 IDE 的模型逐一吻合、`user_pay_identity_str` 零引用的 grep 证据、以真实上游字节喂给 bridge 复现「截断的 200」。
|
|
42
|
+
- `docs/INTL_PAID_TIER_PROBE.md`(`d76800d`):付费账号是否有专属接口的探索结论——**没有**,免费/付费同端点同目录,差别在调用时授权;含 `1005` / `4011` 的对照实验记录。
|
|
43
|
+
|
|
3
44
|
## 2.4.0 (2026-09-29)
|
|
4
45
|
|
|
5
46
|
> **包含仍未发布的 2.3.2。** 2.3.2 的 peer 范围修复(`>=0.1.7-rc.1 <0.2.0-0` → `<0.3.0-0`,见下一节)
|
package/lib/client.js
CHANGED
|
@@ -140,6 +140,21 @@ window.__ModuleLoader__.load({
|
|
|
140
140
|
enabled
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether Trae's plan identity says this account is on a paying plan.
|
|
145
|
+
*
|
|
146
|
+
* `has_package` alone is NOT that question — it reports whether a package
|
|
147
|
+
* entitlement row exists, and a member can be on a plan without one (issue
|
|
148
|
+
* #19: a Pro account rendered as "No active package"). Trae spells the plan in
|
|
149
|
+
* `user_pay_identity_str`, so this reads that field, and only the exact word
|
|
150
|
+
* `Free` counts as free: an unknown or future tier name is treated as paid,
|
|
151
|
+
* because claiming "Free" for a tier we simply do not recognise is the more
|
|
152
|
+
* damaging of the two errors.
|
|
153
|
+
*/
|
|
154
|
+
function traePlanIsPaid(payIdentityStr) {
|
|
155
|
+
const tier = payIdentityStr?.trim();
|
|
156
|
+
return tier !== void 0 && tier !== "" && !/^free$/i.test(tier);
|
|
157
|
+
}
|
|
143
158
|
//#endregion
|
|
144
159
|
//#region src/client/icon.ts
|
|
145
160
|
/**
|
|
@@ -355,6 +370,24 @@ window.__ModuleLoader__.load({
|
|
|
355
370
|
if (value >= 1e3 && value % 1e3 === 0) return `${value / 1e3}K`;
|
|
356
371
|
return formatNumber(value);
|
|
357
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* The subscription row's verdict.
|
|
375
|
+
*
|
|
376
|
+
* Two independent things can entitle an international account: a package row
|
|
377
|
+
* (`has_package`) and a plan identity (`user_pay_identity_str`). Reading only
|
|
378
|
+
* the first is what rendered a Pro member as "No active package" in issue #19,
|
|
379
|
+
* so either one counts. The tier string is carried separately so the row can
|
|
380
|
+
* name the plan instead of just asserting a boolean.
|
|
381
|
+
*/
|
|
382
|
+
function payVerdict(payStatus) {
|
|
383
|
+
const tier = payStatus.payIdentityStr?.trim();
|
|
384
|
+
const paid = payStatus.hasPackage || traePlanIsPaid(payStatus.payIdentityStr);
|
|
385
|
+
return {
|
|
386
|
+
paid,
|
|
387
|
+
...tier === void 0 || tier === "" ? {} : { tier },
|
|
388
|
+
label: paid ? "row.subscribed" : "row.noPackage"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
358
391
|
function dotStyle(status) {
|
|
359
392
|
return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
|
|
360
393
|
}
|
|
@@ -680,6 +713,8 @@ window.__ModuleLoader__.load({
|
|
|
680
713
|
* message — it never sends one by itself.
|
|
681
714
|
*/
|
|
682
715
|
const rowImageIsAutomatic = (model) => model.multimodal === true;
|
|
716
|
+
/** The subscription row's verdict for the current account, when it has one. */
|
|
717
|
+
const payPlan = status.status === "signed-in" && status.payStatus !== void 0 ? payVerdict(status.payStatus) : void 0;
|
|
683
718
|
const activeContextBudgets = draft?.contextBudgets ?? savedContextBudgets;
|
|
684
719
|
const dirty = draft !== void 0;
|
|
685
720
|
/** Apply an edit to the ACTIVE tab's draft, seeding it from the current view. */
|
|
@@ -905,7 +940,7 @@ window.__ModuleLoader__.load({
|
|
|
905
940
|
})
|
|
906
941
|
}) : null,
|
|
907
942
|
status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
908
|
-
status.payStatus === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
943
|
+
status.payStatus === void 0 || payPlan === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
909
944
|
className: "dsm-trae-usage-list",
|
|
910
945
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
911
946
|
className: "dsm-trae-usage-stats dsm-trae-usage-stats-two",
|
|
@@ -918,13 +953,13 @@ window.__ModuleLoader__.load({
|
|
|
918
953
|
className: "dsm-trae-usage-stat-label",
|
|
919
954
|
children: t("row.subscriptionLabel")
|
|
920
955
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
921
|
-
className: `dsm-trae-usage-stat-badge ${
|
|
922
|
-
children: t(
|
|
956
|
+
className: `dsm-trae-usage-stat-badge ${payPlan.paid ? "dsm-trae-usage-stat-badge-on" : "dsm-trae-usage-stat-badge-off"}`,
|
|
957
|
+
children: t(payPlan.label)
|
|
923
958
|
})]
|
|
924
959
|
}),
|
|
925
960
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
926
961
|
className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-general",
|
|
927
|
-
children: status.payStatus.inTrial && status.payStatus.trialEndTimeMs > 0 ? t("row.trialUntil", { date: formatDateTime(status.payStatus.trialEndTimeMs) }) : t(
|
|
962
|
+
children: status.payStatus.inTrial && status.payStatus.trialEndTimeMs > 0 ? t("row.trialUntil", { date: formatDateTime(status.payStatus.trialEndTimeMs) }) : [t(payPlan.label), payPlan.tier].filter(Boolean).join(" · ")
|
|
928
963
|
}),
|
|
929
964
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
930
965
|
className: "dsm-trae-usage-stat-hint",
|
package/lib/index.d.ts
CHANGED
|
@@ -1326,6 +1326,19 @@ interface TraePayStatus {
|
|
|
1326
1326
|
enableSoloBuilder: boolean;
|
|
1327
1327
|
enableSoloCoder: boolean;
|
|
1328
1328
|
enableSoloWeb: boolean;
|
|
1329
|
+
/**
|
|
1330
|
+
* The account's plan identity (`user_pay_identity`), Trae's authoritative
|
|
1331
|
+
* tier number: `0` is Free. Measured 2026-10-01 on a live SG community
|
|
1332
|
+
* account, which reported `0` / `"Free"`.
|
|
1333
|
+
*
|
|
1334
|
+
* This is a DIFFERENT question from {@link hasPackage}, which asks whether a
|
|
1335
|
+
* package/entitlement object exists. A paying member can hold a plan
|
|
1336
|
+
* identity without any package row, which is why issue #19 saw "No active
|
|
1337
|
+
* package" on a Pro account — the card only ever read `has_package`.
|
|
1338
|
+
*/
|
|
1339
|
+
payIdentity?: number;
|
|
1340
|
+
/** The plan identity as Trae spells it (`user_pay_identity_str`), e.g. `Free`. */
|
|
1341
|
+
payIdentityStr?: string;
|
|
1329
1342
|
/** The fission (referral) program window and cap, when exposed. */
|
|
1330
1343
|
fission?: {
|
|
1331
1344
|
startTimeMs: number;
|
|
@@ -1695,6 +1708,10 @@ interface TraeWebPayStatus {
|
|
|
1695
1708
|
enableSoloBuilder: boolean;
|
|
1696
1709
|
enableSoloCoder: boolean;
|
|
1697
1710
|
enableSoloWeb: boolean;
|
|
1711
|
+
/** Trae's authoritative tier number (`user_pay_identity`); `0` means Free. */
|
|
1712
|
+
payIdentity?: number;
|
|
1713
|
+
/** Trae's authoritative tier label (`user_pay_identity_str`), e.g. `Free`. */
|
|
1714
|
+
payIdentityStr?: string;
|
|
1698
1715
|
fission?: {
|
|
1699
1716
|
startTimeMs: number;
|
|
1700
1717
|
expireTimeMs: number;
|
package/lib/index.js
CHANGED
|
@@ -1942,7 +1942,7 @@ function applyReasoningEffort(body, effort, capability) {
|
|
|
1942
1942
|
//#region src/solo.ts
|
|
1943
1943
|
const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
1944
1944
|
/**
|
|
1945
|
-
*
|
|
1945
|
+
* `llm_utils_chat` functions to union per region, in priority order.
|
|
1946
1946
|
*
|
|
1947
1947
|
* Trae spreads its callable roster across several SOLO-mode functions, and a
|
|
1948
1948
|
* model is only usable through the one that lists it: `glm-5.3` is absent from
|
|
@@ -1950,14 +1950,32 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
|
1950
1950
|
* calling it through the former answers `4001 param is invalid`, through the
|
|
1951
1951
|
* latter streams normally). Rather than betting on a single function, the
|
|
1952
1952
|
* directory unions them; the first function to provide a config wins, so the
|
|
1953
|
-
* order below decides which wire name a model is called with.
|
|
1953
|
+
* order below decides which wire name AND which function a model is called with.
|
|
1954
|
+
*
|
|
1955
|
+
* THIS LIST IS NOT "directory functions" — it is the set of functions
|
|
1956
|
+
* `llm_utils_chat` actually accepts, because ownership here is what the chat
|
|
1957
|
+
* call replays. `solo_agent` is deliberately ABSENT (measured 2026-10-01 on a
|
|
1958
|
+
* live SG credential, issue #19): it is a Remote-agent roster function, not a
|
|
1959
|
+
* chat function. Every model sent with `function: "solo_agent"` — `gpt-5.4`,
|
|
1960
|
+
* `gpt-5.6-sol`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code` — answered
|
|
1961
|
+
* `event:error {"code":4011}` (message says "rate limit", it is really "not
|
|
1962
|
+
* served here"), while the same model with the same body answered normally
|
|
1963
|
+
* under `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`.
|
|
1964
|
+
*
|
|
1965
|
+
* Note `get_detail_param` DOES answer `solo_agent` with a 43-entry list, so
|
|
1966
|
+
* asking it looks helpful while making 8 of the 10 exposed ai-region models
|
|
1967
|
+
* uncallable: it won precedence and stamped its own name onto configs it
|
|
1968
|
+
* cannot serve. `solo_agent_remote` is the chat-callable half of that roster
|
|
1969
|
+
* (it answers `minimax-m3`, `gemini-3.1-pro`, … that no `solo_work_*` lists)
|
|
1970
|
+
* and therefore takes the last slot — after the two IDE functions that serve
|
|
1971
|
+
* their shared configs without the Remote plan gate.
|
|
1954
1972
|
*/
|
|
1955
1973
|
const TRAE_DIRECTORY_FUNCTIONS = {
|
|
1956
1974
|
cn: ["solo_work_remote", TRAE_SOLO_FUNCTION],
|
|
1957
1975
|
ai: [
|
|
1958
|
-
"solo_agent",
|
|
1959
1976
|
"solo_work_remote",
|
|
1960
|
-
TRAE_SOLO_FUNCTION
|
|
1977
|
+
TRAE_SOLO_FUNCTION,
|
|
1978
|
+
"solo_agent_remote"
|
|
1961
1979
|
]
|
|
1962
1980
|
};
|
|
1963
1981
|
const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
|
|
@@ -2304,6 +2322,75 @@ function decodeTraeEvent(event) {
|
|
|
2304
2322
|
}
|
|
2305
2323
|
//#endregion
|
|
2306
2324
|
//#region src/solo-bridge.ts
|
|
2325
|
+
/**
|
|
2326
|
+
* What Trae's in-stream error codes mean. Measured 2026-10-01 on a live SG
|
|
2327
|
+
* credential (issue #19), by sending the same body to several functions:
|
|
2328
|
+
*
|
|
2329
|
+
* - `1005` — the model needs a paid plan. Trae answers HTTP 200 with
|
|
2330
|
+
* `event:error {"code":1005,"message":"","extra":"{\"plan\":1}"}`, so the
|
|
2331
|
+
* gate is only visible in `extra.plan`; `gpt-6-sol` returned it under both
|
|
2332
|
+
* `solo_work_remote` and `solo_agent_remote` on a free account.
|
|
2333
|
+
* - `4011` — the function the request named does not serve that model. Its
|
|
2334
|
+
* message claims "exceeded the rate limit", which is misleading: the same
|
|
2335
|
+
* model answers normally when sent under `solo_work_remote`.
|
|
2336
|
+
* - `4001` — `config_name` is not one this function serves.
|
|
2337
|
+
*/
|
|
2338
|
+
const TRAE_ERROR_HINTS = {
|
|
2339
|
+
1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
|
|
2340
|
+
4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
|
|
2341
|
+
4011: "Trae refused this model under the SOLO function the request used"
|
|
2342
|
+
};
|
|
2343
|
+
/** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
|
|
2344
|
+
function traeErrorPlan(extra) {
|
|
2345
|
+
let value = extra;
|
|
2346
|
+
if (typeof value === "string") try {
|
|
2347
|
+
value = JSON.parse(value);
|
|
2348
|
+
} catch {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2352
|
+
const plan = value["plan"];
|
|
2353
|
+
return typeof plan === "number" ? plan : void 0;
|
|
2354
|
+
}
|
|
2355
|
+
/** A readable message for an in-stream Trae failure, with its code preserved. */
|
|
2356
|
+
function describeTraeFailure(failure) {
|
|
2357
|
+
const plan = traeErrorPlan(failure.extra);
|
|
2358
|
+
const parts = [
|
|
2359
|
+
failure.code === void 0 ? "Trae refused the request" : TRAE_ERROR_HINTS[failure.code] ?? "Trae refused the request",
|
|
2360
|
+
...plan === void 0 ? [] : [`plan ${plan}`],
|
|
2361
|
+
...failure.code === void 0 ? [] : [`Trae code ${failure.code}`]
|
|
2362
|
+
];
|
|
2363
|
+
const detail = failure.message.trim();
|
|
2364
|
+
return detail === "" ? parts.join(" · ") : `${parts.join(" · ")} · upstream: ${detail}`;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* HTTP status + error kind for an in-stream failure, so the loopback shim can
|
|
2368
|
+
* answer with a real error instead of a truncated 200 stream.
|
|
2369
|
+
*
|
|
2370
|
+
* `1005` is a subscription gate: 402 is what makes clients stop rather than
|
|
2371
|
+
* retry, and it is truthful. Everything else is a client-side rejection.
|
|
2372
|
+
*/
|
|
2373
|
+
function classifyTraeFailure(failure) {
|
|
2374
|
+
return failure.code === 1005 ? {
|
|
2375
|
+
status: 402,
|
|
2376
|
+
kind: "hard_credit"
|
|
2377
|
+
} : {
|
|
2378
|
+
status: 400,
|
|
2379
|
+
kind: "client"
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
/** Recognise an in-stream failure event, or return undefined for anything else. */
|
|
2383
|
+
function traeFailureOf(decoded) {
|
|
2384
|
+
if (decoded.type !== "unknown") return void 0;
|
|
2385
|
+
const payload = decoded.data;
|
|
2386
|
+
const code = typeof payload?.["code"] === "number" ? payload["code"] : void 0;
|
|
2387
|
+
if (decoded.event !== "error" && !(code !== void 0 && code >= 4e3)) return void 0;
|
|
2388
|
+
return {
|
|
2389
|
+
...code === void 0 ? {} : { code },
|
|
2390
|
+
message: typeof payload?.["message"] === "string" ? payload["message"] : "",
|
|
2391
|
+
...payload?.["extra"] === void 0 ? {} : { extra: payload["extra"] }
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2307
2394
|
function normalizeToolCalls(value) {
|
|
2308
2395
|
if (!Array.isArray(value)) return [];
|
|
2309
2396
|
const calls = [];
|
|
@@ -2328,6 +2415,10 @@ function normalizeToolCalls(value) {
|
|
|
2328
2415
|
function bridgeTraeSoloStream(response, model) {
|
|
2329
2416
|
const source = response.body;
|
|
2330
2417
|
if (source === null) return new Response(null, { status: 502 });
|
|
2418
|
+
return bridgeTraeSource(source, model);
|
|
2419
|
+
}
|
|
2420
|
+
/** {@link bridgeTraeSoloStream} over an already-opened byte stream. */
|
|
2421
|
+
function bridgeTraeSource(source, model) {
|
|
2331
2422
|
const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
2332
2423
|
const created = Math.floor(Date.now() / 1e3);
|
|
2333
2424
|
const decoder = new TextDecoder();
|
|
@@ -2356,9 +2447,8 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
2356
2447
|
const consume = (event) => {
|
|
2357
2448
|
const decoded = decodeTraeEvent(event);
|
|
2358
2449
|
if (decoded.type === "unknown") {
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
if (decoded.event === "error" || code !== void 0 && code >= 4e3) upstreamError = new Error(typeof payload?.["message"] === "string" && payload["message"] !== "" ? payload["message"] : `Trae upstream error (code ${code ?? "?"})`);
|
|
2450
|
+
const failure = traeFailureOf(decoded);
|
|
2451
|
+
if (failure !== void 0) upstreamError = new Error(describeTraeFailure(failure));
|
|
2362
2452
|
return;
|
|
2363
2453
|
}
|
|
2364
2454
|
if (decoded.type === "delta") {
|
|
@@ -2428,6 +2518,123 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
2428
2518
|
headers: { "Content-Type": "text/event-stream" }
|
|
2429
2519
|
});
|
|
2430
2520
|
}
|
|
2521
|
+
/**
|
|
2522
|
+
* How far {@link openTraeSource} reads before giving up on finding a failure.
|
|
2523
|
+
* Trae opens a real answer with small `metadata` / `timing_cost` events, so a
|
|
2524
|
+
* healthy stream is recognised well inside these bounds; they exist only so a
|
|
2525
|
+
* pathological stream cannot stall the response headers.
|
|
2526
|
+
*/
|
|
2527
|
+
const PEEK_MAX_CHUNKS = 64;
|
|
2528
|
+
const PEEK_MAX_BYTES = 262144;
|
|
2529
|
+
/** Replay the bytes the peek consumed, then drain the rest of the upstream. */
|
|
2530
|
+
function replayTraeSource(buffered, reader, ended) {
|
|
2531
|
+
let index = 0;
|
|
2532
|
+
return new ReadableStream({
|
|
2533
|
+
async pull(controller) {
|
|
2534
|
+
const chunk = buffered[index];
|
|
2535
|
+
if (chunk !== void 0) {
|
|
2536
|
+
index += 1;
|
|
2537
|
+
controller.enqueue(chunk);
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
if (ended) {
|
|
2541
|
+
controller.close();
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
const next = await reader.read();
|
|
2545
|
+
if (next.done) {
|
|
2546
|
+
controller.close();
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
controller.enqueue(next.value);
|
|
2550
|
+
},
|
|
2551
|
+
async cancel(reason) {
|
|
2552
|
+
await reader.cancel(reason).catch(() => {});
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Open the upstream answer, failing fast when Trae refuses BEFORE producing any
|
|
2558
|
+
* output.
|
|
2559
|
+
*
|
|
2560
|
+
* This exists because Trae reports authorisation and subscription failures
|
|
2561
|
+
* inside a **200** SSE body. Relaying that as a 200 and aborting mid-stream
|
|
2562
|
+
* (what this bridge used to do) loses the code at the HTTP boundary: pi-ai only
|
|
2563
|
+
* sees a truncated stream and reports `Stream ended without finish_reason`,
|
|
2564
|
+
* which reads as a network fault and is retried five times — the exact symptom
|
|
2565
|
+
* in issue #19. Peeking until the first real output lets a refusal become a
|
|
2566
|
+
* normal error response with its code and plan intact.
|
|
2567
|
+
*
|
|
2568
|
+
* The peek is limited to the head of the stream and replayed verbatim, so a
|
|
2569
|
+
* healthy answer keeps streaming with no buffering beyond the first event.
|
|
2570
|
+
*/
|
|
2571
|
+
async function openTraeSource(response) {
|
|
2572
|
+
const body = response.body;
|
|
2573
|
+
if (body === null) return {
|
|
2574
|
+
ok: false,
|
|
2575
|
+
status: 502,
|
|
2576
|
+
kind: "server",
|
|
2577
|
+
message: "Trae upstream returned no response body"
|
|
2578
|
+
};
|
|
2579
|
+
const reader = body.getReader();
|
|
2580
|
+
const decoder = new TextDecoder();
|
|
2581
|
+
const sse = new SseDecoder();
|
|
2582
|
+
const buffered = [];
|
|
2583
|
+
let bytes = 0;
|
|
2584
|
+
let ended = false;
|
|
2585
|
+
try {
|
|
2586
|
+
while (buffered.length < PEEK_MAX_CHUNKS && bytes < PEEK_MAX_BYTES) {
|
|
2587
|
+
const next = await reader.read();
|
|
2588
|
+
if (next.done) {
|
|
2589
|
+
ended = true;
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
buffered.push(next.value);
|
|
2593
|
+
bytes += next.value.byteLength;
|
|
2594
|
+
let answered = false;
|
|
2595
|
+
for (const event of sse.push(decoder.decode(next.value, { stream: true }))) {
|
|
2596
|
+
const decoded = decodeTraeEvent(event);
|
|
2597
|
+
const failure = traeFailureOf(decoded);
|
|
2598
|
+
if (failure !== void 0) {
|
|
2599
|
+
await reader.cancel().catch(() => {});
|
|
2600
|
+
return {
|
|
2601
|
+
ok: false,
|
|
2602
|
+
...classifyTraeFailure(failure),
|
|
2603
|
+
message: describeTraeFailure(failure)
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
if (decoded.type !== "unknown") answered = true;
|
|
2607
|
+
}
|
|
2608
|
+
if (answered) break;
|
|
2609
|
+
}
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
reader.releaseLock();
|
|
2612
|
+
return {
|
|
2613
|
+
ok: false,
|
|
2614
|
+
status: 0,
|
|
2615
|
+
kind: "server",
|
|
2616
|
+
message: `transport error: ${String(error)}`
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
return {
|
|
2620
|
+
ok: true,
|
|
2621
|
+
source: replayTraeSource(buffered, reader, ended)
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Bridge an upstream answer, turning an early refusal into a real error result.
|
|
2626
|
+
*
|
|
2627
|
+
* The sibling {@link bridgeTraeSoloStream} stays synchronous and always answers
|
|
2628
|
+
* 200; it is the primitive, and this is the policy the bridge class uses.
|
|
2629
|
+
*/
|
|
2630
|
+
async function bridgeTraeSoloStreamOrFail(response, model) {
|
|
2631
|
+
const opened = await openTraeSource(response);
|
|
2632
|
+
if (!opened.ok) return opened;
|
|
2633
|
+
return {
|
|
2634
|
+
ok: true,
|
|
2635
|
+
response: bridgeTraeSource(opened.source, model)
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2431
2638
|
/** Native SOLO client wrapper used by the loopback OpenAI adapter. */
|
|
2432
2639
|
var TraeSoloBridge = class {
|
|
2433
2640
|
upstream;
|
|
@@ -2475,10 +2682,7 @@ var TraeSoloBridge = class {
|
|
|
2475
2682
|
}
|
|
2476
2683
|
const result = await this.upstream.chatStream(prepared, signal);
|
|
2477
2684
|
if (!result.ok) return result;
|
|
2478
|
-
return
|
|
2479
|
-
ok: true,
|
|
2480
|
-
response: bridgeTraeSoloStream(result.response, model)
|
|
2481
|
-
};
|
|
2685
|
+
return await bridgeTraeSoloStreamOrFail(result.response, model);
|
|
2482
2686
|
}
|
|
2483
2687
|
};
|
|
2484
2688
|
//#endregion
|
|
@@ -2541,6 +2745,38 @@ function parseTraeRemoteModel(value) {
|
|
|
2541
2745
|
//#region src/solo-remote.ts
|
|
2542
2746
|
const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
|
|
2543
2747
|
/**
|
|
2748
|
+
* Remote-directory functions to ask for, per region.
|
|
2749
|
+
*
|
|
2750
|
+
* The remote `/models` answer is **grouped by function** and the groups are not
|
|
2751
|
+
* interchangeable (measured 2026-10-01, issue #19):
|
|
2752
|
+
*
|
|
2753
|
+
* ```
|
|
2754
|
+
* solo_agent : 19 models ← superset; also the ONLY source of
|
|
2755
|
+
* gpt-6-astra / gpt-5.6-sol|terra|luna /
|
|
2756
|
+
* glm-5.2 / gpt-5.5 / Seed-2.1-Turbo
|
|
2757
|
+
* solo_agent_remote : 10 models ← what this client used to read, and all it
|
|
2758
|
+
* read: the preferred group only
|
|
2759
|
+
* ```
|
|
2760
|
+
*
|
|
2761
|
+
* `solo_agent` ⊇ `solo_agent_remote`, so reading just the latter hid nine
|
|
2762
|
+
* models that the Trae IDE does show — exactly what the reporter saw. Both
|
|
2763
|
+
* groups are therefore requested, and every returned group is unioned.
|
|
2764
|
+
*
|
|
2765
|
+
* Asking for a function the gateway does not serve is harmless: unknown names
|
|
2766
|
+
* are ignored rather than rejected (`solo_work_lite` returns no group, and the
|
|
2767
|
+
* request still answers HTTP 200). The CN list is left as it was — the CN
|
|
2768
|
+
* rosters were not re-measured here, and the union keeps whichever groups come
|
|
2769
|
+
* back.
|
|
2770
|
+
*/
|
|
2771
|
+
const TRAE_REMOTE_DIRECTORY_FUNCTIONS = {
|
|
2772
|
+
cn: ["solo_agent_remote", "solo_work_remote"],
|
|
2773
|
+
ai: [
|
|
2774
|
+
"solo_agent",
|
|
2775
|
+
"solo_agent_remote",
|
|
2776
|
+
"solo_work_remote"
|
|
2777
|
+
]
|
|
2778
|
+
};
|
|
2779
|
+
/**
|
|
2544
2780
|
* Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
|
|
2545
2781
|
* CN locale headers; the international directory lives on the shared
|
|
2546
2782
|
* `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
|
|
@@ -2590,16 +2826,16 @@ var TraeSoloRemoteCatalogClient = class {
|
|
|
2590
2826
|
const region = regionOfCredential(await this.options.credential());
|
|
2591
2827
|
const base = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
2592
2828
|
const headers = await this.headers(region);
|
|
2593
|
-
const
|
|
2829
|
+
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
2830
|
+
const response = await this.fetchImpl(`${base}/models?functions=${functions}`, {
|
|
2594
2831
|
headers,
|
|
2595
2832
|
signal: signal ?? AbortSignal.timeout(3e4)
|
|
2596
2833
|
});
|
|
2597
2834
|
if (!response.ok) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
2598
2835
|
const groups = (await response.json()).data?.list ?? [];
|
|
2599
|
-
const preferred = groups.find((group) => group.function === "solo_agent_remote") ?? groups[0];
|
|
2600
2836
|
const seen = /* @__PURE__ */ new Set();
|
|
2601
2837
|
const models = [];
|
|
2602
|
-
for (const raw of
|
|
2838
|
+
for (const group of groups) for (const raw of group.models ?? []) {
|
|
2603
2839
|
const model = parseTraeRemoteModel(raw);
|
|
2604
2840
|
if (model === void 0 || seen.has(model.id)) continue;
|
|
2605
2841
|
seen.add(model.id);
|
|
@@ -3285,6 +3521,8 @@ var TraeUsageClient = class {
|
|
|
3285
3521
|
const fissionStart = asNumber(payload["solo_fission_start_time"]);
|
|
3286
3522
|
const fissionExpire = asNumber(payload["solo_fission_expire_time"]);
|
|
3287
3523
|
const fissionMax = asNumber(payload["solo_fission_max_usage"]);
|
|
3524
|
+
const payIdentity = asNumber(payload["user_pay_identity"]);
|
|
3525
|
+
const payIdentityStr = typeof payload["user_pay_identity_str"] === "string" && payload["user_pay_identity_str"].trim() !== "" ? payload["user_pay_identity_str"].trim() : void 0;
|
|
3288
3526
|
return {
|
|
3289
3527
|
isDollarUsageBilling: flag("is_dollar_usage_billing"),
|
|
3290
3528
|
hasPackage: flag("has_package"),
|
|
@@ -3295,6 +3533,8 @@ var TraeUsageClient = class {
|
|
|
3295
3533
|
enableSoloBuilder: flag("enable_solo_builder"),
|
|
3296
3534
|
enableSoloCoder: flag("enable_solo_coder"),
|
|
3297
3535
|
enableSoloWeb: flag("enable_solo_web"),
|
|
3536
|
+
...payIdentity === void 0 ? {} : { payIdentity },
|
|
3537
|
+
...payIdentityStr === void 0 ? {} : { payIdentityStr },
|
|
3298
3538
|
...fissionStart === void 0 || fissionExpire === void 0 || fissionMax === void 0 ? {} : { fission: {
|
|
3299
3539
|
startTimeMs: fissionStart,
|
|
3300
3540
|
expireTimeMs: fissionExpire,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.5.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|