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 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 ${status.payStatus.hasPackage ? "dsm-trae-usage-stat-badge-on" : "dsm-trae-usage-stat-badge-off"}`,
922
- children: t(status.payStatus.hasPackage ? "row.subscribed" : "row.noPackage")
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(status.payStatus.hasPackage ? "row.subscribed" : "row.noPackage")
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
- * Directory functions to union per region, in priority order.
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 payload = decoded.data;
2360
- const code = typeof payload?.["code"] === "number" ? payload["code"] : void 0;
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 response = await this.fetchImpl(`${base}/models?functions=solo_agent_remote,solo_work_remote`, {
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 preferred?.models ?? []) {
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.4.0",
5
+ "version": "2.5.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",