dsh-connect-trae 3.4.0 → 3.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,153 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.2 (2026-10-13)
4
+
5
+ > **插件现在与它自己解析到的那份 `@earendil-works/pi-ai` 代数对齐了**——这是 [issue #27](https://github.com/dingminhua/dsh-connect-trae/issues/27)
6
+ > 的修复。起因是报告人用任意模型都拿到 `PI_AI_ERROR / Cannot read properties of undefined (reading 'length')`,
7
+ > 且 usage 恒为 0:崩溃发生在**请求发出之前**的本地 token 估算里,所以既与模型无关,也看不到任何 HTTP 痕迹。
8
+ > 取证见 `docs/ISSUE27_DIAGNOSIS.md`。
9
+
10
+ ### 修复
11
+
12
+ - **插件现在会在两种上下文形状之间双向适配,而不是把宿主的形状原样交给自己的 api**(issue #27):
13
+ - **原行为**:`createTraeAdapter` 把 `openAICompletionsApi()` 直接交给 `createProvider`,没有任何适配层。
14
+ 插件声明 pi-ai 为 peer(`>=0.85.1`),所以它解析到哪一份**由安装环境决定**,未必是宿主那一份。
15
+ - **为什么这是必崩**:pi-ai **0.86 起**在每个公开入口调一次 `normalizeContext()`,于是 provider 收到的是
16
+ *归一化 transcript*:`{ messages: [ { role:'system', content:<字符串>, toolsAdded }, … ] }`——系统提示与工具声明
17
+ **只**挂在这条 system 消息上,顶层 `systemPrompt` / `tools` 已被删除。而 ≤0.85 的 `estimateMessageTokens`
18
+ **没有 `role === 'system'` 分支**:它 `for (const block of message.content)` 迭代一个**字符串**,逐字符拿到
19
+ `block`,再读 `block.name.length` → `TypeError`。因为这一步位于 `buildBaseOptions` 内、早于任何 HTTP 构造,
20
+ 所以现象就是「105ms 内本地崩溃 + usage 恒 0 + 换模型无效」。
21
+ - **两个方向都要修**,因为 **DSH 自己也横跨两代**(`dsh-llm-pi-ai` ≤0.2.0-rc.1 声明 `^0.85.1`,0.2.0-rc.2 起声明
22
+ `^0.87.1`,0.2.1-alpha.2 已到 `^1.0.2`),而本插件声明支持 `>=0.1.7-rc.1 <0.3.0-0`:
23
+ - **新宿主 + 旧库**(issue #27 本尊):必崩。
24
+ - **旧宿主 + 新库**:不崩,但现代 api 只从 system 消息读提示与工具,顶层字段被**静默忽略** → 系统提示与
25
+ **全部工具声明**丢失(模型永远不会产生 toolCall)。只修前者会把一个响亮的故障换成一个安静的故障。
26
+ - **改后**:新增 `withContextAdaptation(api, generation)`,按**本插件自己解析到的那一代**决定折返方向;
27
+ 两个适配函数都以**形状**做幂等短路(普通路径返回同一个对象,零分配)。
28
+ - `adaptLegacyPiAiContext`(transcript → 旧形状):把 system 文本提升为 `systemPrompt`,并把
29
+ `toolsRemoved`/`toolsAdded` **按序回放**成顶层 `tools`——工具那一步不能省,`normalizeContext` 之后它是
30
+ 工具声明的**唯一**载体。
31
+ - `adaptModernPiAiContext`(旧形状 → transcript):把 `systemPrompt`/`tools` 提到一条首部 system 消息上,
32
+ 并移除顶层字段,避免后续的形状判据误判。
33
+ - **提示文本的三条载体都要搬,`sections` 不能漏**:新代 `SystemMessage` 的提示文本有 `content` 与
34
+ `sections` 两条载体(`Record<string, string | null>`),库自己的 `getSystemMessageText` 是
35
+ 「`content` + 全部非 null section」按空行连接。只读 `content` 会让以 section 形态到达的提示整体折成
36
+ `undefined`——与该适配器要防的「静默丢失」完全同类。现已逐字对齐库的语义(`content` 在前、
37
+ 按声明顺序接 section、跳过 `null`)。
38
+ - **折上的语义与 `normalizeContext` 逐字对齐,包括「混合形状」**:当上下文**同时**带顶层字段和
39
+ system 消息时(部分归一化的宿主),pi-ai 自己的做法是**前插**(于是有两条 system 消息,provider 再按
40
+ 空行折叠),本插件因此同样前插。实测该输出经真实 0.87.1 的 `resolveTranscript` 折叠后,与
41
+ `normalizeContext` 的结果**逐字相同**。早先版本在混合形状下直接原样返回,会静默丢掉顶层提示与其
42
+ 工具——这条是评审的变异测试暴露的(见「测试」)。
43
+ - **代际判据落在 0.86,不是 0.87**(issue #27):
44
+ - 报告与兄弟插件的既有实现都按 `>= 0.87 → modern` 划分。实测**真实边界是 0.86.0**——
45
+ `dist/utils/transcript.js`、`normalizeContext`、估算器的 system 分支、以及「不再读 `systemPrompt`/`tools`」
46
+ 四件事**同时**出现在 0.86.0。
47
+ - **为什么这个偏差有害**:若插件解析到 **0.86.x**,`>= 0.87` 的判据会把它当旧代而执行折返,而 0.86+ 的 api
48
+ 根本不读顶层字段 → 系统提示与全部工具被静默丢弃。即「照抄现成实现」反而会引入一个新的静默故障。
49
+ - **新增 `src/pi-ai-runtime.ts`:世代探测不依赖版本号字符串**(issue #27):
50
+ - 主判据是解析到的那份包 manifest 的版本,用**数值**比较(`major > 0` 或 `minor >= 86` 即现代),
51
+ 因此 `0.9.0` 不会被误判为高于 `0.86.0`,已发布的 `1.x` 也无需改动即可正确归类。
52
+ - 版本读不到时退化为**包布局**判据(`dist/utils/transcript.js` 是否存在)。
53
+ - **刻意不使用 `import.meta.resolve()` 做能力探测**:bundler 风格的解析器(vite,以及在它之上的 vitest)
54
+ 会把 exports map 里的 `"./utils/*"` 通配符展开成路径**并直接返回,不检查文件是否存在**,于是对 0.85.1
55
+ 也回答「有」——实测该假阳性会把 0.85.1 判成 modern,从而**在声称按能力判断的同时放过 issue #27**。
56
+ 读包目录则不会被这样骗过。
57
+ - 解析不到 pi-ai(宿主外探测的市场安装)时取 `modern` 透传:每个做归一化的宿主都同时提供现代代,
58
+ 且透传不可能破坏一个本来就正确的上下文。
59
+
60
+ ### 诊断
61
+
62
+ - **启动时打印解析到的 pi-ai 版本与代数**(issue #27):混代安装是「安静坏掉」的那一类,日志里一行
63
+ `dsh-connect-trae: pi-ai 0.85.1 (legacy generation)` 让它在下次出现时立刻可见,而不是只表现为一次崩溃。
64
+
65
+ ### 测试
66
+
67
+ - **全仓 549 → 594(+45)**:
68
+ - `tests/pi-ai-context.spec.ts`(38 例):代际判定(含 0.86/0.87 边界、数值排序、`1.x`、不可读版本)、
69
+ `decidePiAiGeneration` 的三条分支(含本机**永不可达**的「什么都没探测到」——市场安装的默认答案,
70
+ 必须显式钉住)、折返(提示提升、**工具提升**、`toolsRemoved`/`toolsAdded` 顺序回放、既有顶层 `tools`
71
+ 权威、空文本 system 消息只丢文本不丢工具、文本块、**`sections` 三条载体**、混合形状、畸形输入)、
72
+ 折上(提升、移除顶层字段、幂等、空提示、**混合形状前插**)、
73
+ `withContextAdaptation`(两个入口都适配、同形状透传为**同一对象**、保留 `fetchDeferred` 等其余字段),
74
+ 以及**对真实解析到的那份 pi-ai 的集成对拍**:`estimateContextTokens` 不抛错、`convertMessages` 后
75
+ `messages[0].role === 'system'` 且 `tools` 仍在。
76
+ - `tests/pi-ai-adapter-wiring.spec.ts`(7 例):**接线层**用例。上面那批函数级用例在把适配层从工厂里删掉后
77
+ **依然全绿**(已用变异验证),正是「函数没问题、只是没人调用」这一原始缺陷的形状;本文件 mock 掉
78
+ `openai-completions.lazy`,经 `adapter.current() → piProvider → streamSimple` 走真实装配路径断言交付给
79
+ api 的形状。
80
+ - **变异验证 8 次,8 次全部被抓**(①–⑤ 为自查,⑥–⑧ 由独立评审提出):
81
+ ① 工厂里移除适配层 → 5 例失败;② 折返只搬 prompt 不搬 tools → 6 例失败;③ 边界写成 `>= 87` →
82
+ 1 例失败;④ 折上时不移除顶层字段 → 2 例失败;⑤ 折返不做幂等短路 → 5 例失败;
83
+ **⑥ 折返忽略 `sections` → 2 例失败**;**⑦ 折上跳过多 system 消息的混合形状 → 1 例失败**;
84
+ **⑧ 不可探测时默认 `legacy` → 1 例失败**。恢复后 57 文件 594 例全绿。
85
+ - 确认新增用例真的在跑:改动前基线 **549**、改动后 **594**,净增 45,与新增用例数一致。
86
+
87
+ ### 独立评审
88
+
89
+ 发布前由独立评审对 diff 做了对抗性复核(真实发布包 + 真实 api 模块),确认:双向四组合均不丢提示、
90
+ 不丢工具、不重复 system 消息;**0.86 边界**在 0.85.1/0.86.0/0.86.1/0.87.1/1.0.2 五个真实 tarball 上
91
+ 逐项复核成立;**`import.meta.resolve` 的假阳性被独立复现**(该模块在 0.85.1 磁盘上不存在,vite 仍返回
92
+ 其 URL);接线层用例确实覆盖工厂路径。
93
+
94
+ 评审同时提出 **3 条有效缺陷**,均已修复并补上用例(即上表 ⑥⑦⑧):`sections` 载体被漏读、
95
+ 混合形状下折上会静默丢弃顶层提示与工具、以及不可探测分支的默认值无任何用例约束。
96
+
97
+ ### 文档
98
+
99
+ - `docs/ISSUE27_DIAGNOSIS.md`:对外部报告的逐条核验——链路 6 跳的取证、双向错配四组合矩阵的实测结果、
100
+ DSH 各版本与 pi-ai 代数对照,并明确指出该报告的 **3 处硬错误**(代际边界实为 0.86 而非 0.87;
101
+ CASE B 实测为 `{"tokens":25}` 而非 `15`;`undefined maxTokens` 实为 `NaN` 而非「夹回正整数」)
102
+ 与 **2 处需修正的修复建议**(「收紧 peer 即可在安装期被拒」不成立——DSH 内核预检
103
+ `evaluatePluginCompatibility` 只覆盖 `@deepseek-ai/dsh*`,pi-ai 不在其中,报告引用的 `install-compat`
104
+ 告警来自 dshmarket 社区插件且属只告警不阻止的 `soft-incompatible`;以及「升级 DSH」解决不了问题,
105
+ 报告人本就在最新的 0.2.0-rc.2 上,而升级只会把他更牢地留在「新宿主」一侧)。
106
+
107
+ ## 3.4.1 (2026-10-12)
108
+
109
+ > **`4011` 的含义改正了:它是 Trae 对 SOLO 通道的限流,不是「该 function 不提供此模型」。**
110
+ > 起因是 [issue #33](https://github.com/dingminhua/dsh-connect-trae/issues/33):报告人用 `GLM-5.3` 拿到
111
+ > `Trae code 4011`,报文原文写着 `your requests have exceeded the rate limit`,而他额度充足、
112
+ > Trae 客户端的 SOLO 里同一模型正常。**两边都没错**——Trae 把「额度」和「限流」分成两个码,
113
+ > 插件此前把限流读成了「function 归属错误」,于是用一个与上游原文矛盾的结论来解释,用户只能来问。
114
+ > 取证过程见 `docs/ISSUE33_DIAGNOSIS.md`。
115
+
116
+ ### 修复
117
+
118
+ - **`4011` 的文案不再断言一个与上游原文矛盾的原因**(issue #33):
119
+ - **原行为**:`TRAE_ERROR_HINTS[4011]` 写死为「Trae refused this model under the SOLO function the request used」,并把 Trae 自己的原文降级成 `· upstream:` 后缀。用户看到的主句是「模型/function 错了」,于是按那个方向去查。
120
+ - **为什么那个断言站不住**:本机 CN 实时凭据上跑 **22 个 function × 6 个模型**的最小请求矩阵,
121
+ 「模型不在该 function 名单里」**无一例外回 `4001`**(`the param is invalid`)——22 个 function 全部如此,**没有一次回 4011**;而报告人用的 `glm-5.3` 归属的正是 `solo_work_remote`(插件本来就发这个 function),同一份请求体**正常出字**。也就是说「function 选错」这个条件已经被 `4001` 占用了,4011 不可能是它。
122
+ - **改后**:`4011` 如实描述为「Trae 限流了这条 SOLO 通道(是限流,不是余额)」,Trae 原文照旧附在后面。
123
+ - **`4011` 的 HTTP 状态改为 `429`(`soft_rate`),让状态码、文案与内核判定三者一致**:
124
+ - **原行为**:4011 被归类为 `400 / client`(按本插件 `fallback-upstream.ts` 的语义,`client` 属于「可换通道重放」的一类,绝不是「等一下再来」的一类)。但内核 `@deepseek-ai/dsh-llm-pi-ai` 是**按消息文本**分类的(`/\b429\b|rate.?limit/i` → `RATE_LIMIT`),而 `RATE_LIMIT` 在 `DEFAULT_RETRYABLE_CODES` 里、`maxRetries = 5`。
125
+ - **后果(issue #33 截图逐项吻合)**:失败的 4011 被内核当限流重试——`已重试模型请求 (5/5)`、重试延迟 7328 毫秒、右侧 `RATE_LIMIT` 徽标;而同一行正文却说「该 function 不提供此模型」。**被限流的通道 8 秒内又挨了 5 发请求。**
126
+ - **改后**:4011 → `429 / soft_rate`,三处判定不再打架;`soft_rate` 也正是 `fallback-upstream` 拒绝换通道重放的类别(限流不是通道兼容性问题,是对的)。
127
+ - **一键测试不再把「被限流」判成「上游不提供」**(issue #33):
128
+ - `UNSUPPORTED_CODES` 此前是 `{4001, 4011}`,于是 4011 会得到 `unsupported`——卡片文案「上游不提供」,是一个**永久性的负面结论**。这与该模块自己写在文件头的原则直接冲突(「the verdict must never turn an inconclusive answer into "unavailable"」)。报告人用一键测试时会被告知一个能用的模型已经死了。
129
+ - **改后**:4011 归入 `rate_limited`(卡片「被限流」),`unsupported` 只由 `4001` 产生。
130
+ - **顺带把「一个码只能有一个 verdict」变成结构性约束**:原来每个 verdict 一个 `Set`,一个码同时出现在两个集合里时,结果取决于分支书写顺序——实测把 4011 加回「unsupported」集合**所有测试都过**。现在改成单一 `CODE_VERDICTS` 映射表 + 加载期唯一性断言,并补了交叉断言用例钉住它。
131
+ - **登记两个此前不认识的错误码**:
132
+ - **`4008` = 额度/配额耗尽**(原文 `Your requests have exceeded the quota.`)。它与 `4011` **是两个不同的码**:Trae 把「额度用完」和「被限流」分开报,所以 issue #33 里「我额度还有很多」与「我被 4011 拦住」可以同时成立。只登记文案与分类,**不改它的既有语义**(仍 `400 / client`)。本机只在国际版账号上观察到(该账号当前对多数模型都回 4008),CN 未触发——属**未验证项**。
133
+ - **`4023` = 模型名不存在**(原文 `the model is unknown`)。
134
+ - 一键测试里 `4008` 归入 `unknown`:它是**账号级**状态,不能变成对某个**模型**的判决。
135
+
136
+ ### 测试
137
+
138
+ - **全仓 541 → 549(+8)**:
139
+ - `tests/solo-bridge.spec.ts`:4011 文案改为「限流」并保留 Trae 原文、`4008` 与 `4011` 文案分开、`4023` 命名、未登记码回退;`bridgeTraeSoloStreamOrFail` 的 4011 用例改为断言 `429 / soft_rate`;`4008` 的既有用例改为断言新文案。
140
+ - `tests/model-probe.spec.ts`:4011 → `rate_limited`(含「消息里只有码、没有 429 状态」的路径与「流中拒绝」路径)、`4008` → `unknown`、`4001` → `unsupported`,以及一条**表级交叉断言**(每个已知码都只映射到一个 verdict)——它正是为上面那个「顺序依赖」缺陷加的。
141
+ - **变异验证 6 次,6 次全部被抓**:① 去掉 4011 的 429 映射 → 1 例失败;② 恢复旧 4011 文案 → 2 例失败;③ 把 4011 在映射表里改回 `unsupported` → 4 例失败(**这条在改造前是抓不到的**);④ 去掉 4008 的 `unknown` 映射 → 2 例失败;⑤ 删除「表内唯一性」的交叉断言依赖的行为 → 2 例失败;⑥ 其余组合。恢复后 55 文件 549 例全绿。
142
+ - 确认新增用例真的在跑:改动前基线 **541**、改动后 **549**,净增 8,与新增用例数一致。
143
+
144
+ ### 文档
145
+
146
+ - `docs/ISSUE33_DIAGNOSIS.md`:完整取证——`4008`/`4011`/`4001`/`4023`/`1001`/`1005` 的码→语义对照、
147
+ 22 function × 6 模型的调用矩阵、`glm-5.3` 的归属验证、本机 30 并发未触发限流的对照,以及内核
148
+ `dsh-llm-pi-ai` 按文本分类 + `DEFAULT_RETRYABLE_CODES` 的证据链(为什么截图里是 5/5 与 7328 毫秒)。
149
+ 同时明确列出**未证实项**(4011 的粒度是「通道」还是「账号」、`4008` 的边界与恢复时间、CN 是否也用 4008)。
150
+
3
151
  ## 3.4.0 (2026-10-12)
4
152
 
5
153
  ### 新增
package/README.en.md CHANGED
@@ -43,7 +43,8 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
43
43
  - **Account switching** — refreshes the token list and lets users select an account without storing tokens in DSH settings. **Your choice is remembered and stays in effect**: the plugin will never switch accounts on its own just because the bound one ran out of credit or another usable one was found (switching bills a different person). So when the bound account is exhausted the card **says so explicitly** and lists the other accounts on this machine that still have credit, with their balances, for you to switch to in one click — instead of showing an empty panel or quietly rebinding you. "Refresh accounts & tokens" now always reports what happened (how many accounts were found, who is still bound) instead of appearing to do nothing. The dropdown names the product (`Trae` / `TRAE SOLO` / international) so a credential left behind by an uninstalled app is recognisable.
44
44
  - **Read-only usage and model management** — Work/general credits on CN accounts, subscription/trial status on international ones; enable Trae models freely. Read-only queries consume nothing.
45
45
  - **Credit-near-expiry notices** — the remaining balance only says "how much is left", not "**which pack lapses first**". The notice escalates by urgency (7 days → 24 hours → already lapsed) and names the amount, the pack count and the soonest expiry, so a user watching only the total never silently loses the pack that expires first. Shown in both the usage card and the composer credits panel on the CN side (the international side is subscription-based and has no per-pack expiry).
46
- - **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`/`4011`; no SOLO function serves it) / **throttled** / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
46
+ - **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`; no SOLO function serves it) / **throttled** (`4011`; Trae rate-limited that route) / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
47
+ > `4011` is a **rate limit**, not "this model is unavailable" (measured for issue #33): an exhausted balance reports a different code, `4008`, so "I still have plenty of credits" and "I got a 4011" are both true at once. A throttled model answers again after a short wait — do not delete it.
47
48
  - **One-click test of all models** — the same probe applied to **every visible model** of the current region, not just the enabled ones. Use it when you want to know whether the models you have switched off could still answer — after a plan change, or after Trae reshuffles its lineup.
48
49
  - **Daily check-in claiming (per account)** — on the CN card **every account row carries its own check-in button**, with the day's reward written on it (`Check in +150`). A row reads **Claimed** and disables itself once *that account* has been paid, and reads **Device used** and disables itself when *this machine* has already spent today's slot through some other account — the two are reported honestly, and the second is never dressed up as "claimed". This is the plugin's **only** operation that changes account state, and it runs only when you click; one click targets that one row's account and sends exactly one request. There is deliberately **no "claim for every account"**: Trae allows **one check-in per device per day** (switching accounts does not reset the slot), so a sweep could only manufacture a row of false failures. The international side has no check-in activity, so no button is shown there.
49
50
  - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives), with the manual refresh button in the panel. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
@@ -137,6 +138,25 @@ Restart the DSH process after install/update/uninstall.
137
138
  **Requires DSH 0.1.7-rc.1 and up** (since 2.3.0 the plugin no longer targets hosts older than 0.1.7; the peer dependency ranges are narrowed accordingly).
138
139
  **The peer upper bound is `<0.3.0-0`, so both the 0.1.7 and the 0.2 line are supported.**
139
140
 
141
+ ### Both pi-ai context generations are supported (since 3.4.2)
142
+
143
+ DSH itself spans two generations of `@earendil-works/pi-ai`, and **which one this plugin resolves to is decided by your install**, not by the host — declaring it a peer is correct, but the two generations disagree on what a context looks like:
144
+
145
+ | | legacy (≤0.85) | modern (0.86+) |
146
+ | --- | --- | --- |
147
+ | shape | `{ systemPrompt, tools, messages }` | `{ messages: [ {role:'system', content, toolsAdded}, … ] }` |
148
+ | system prompt | top-level `systemPrompt` | **only** on that system message |
149
+ | tool declarations | top-level `tools` | **only** on that system message's `toolsAdded` |
150
+
151
+ A mismatch breaks **in both directions**, so since 3.4.2 the plugin adapts bidirectionally according to the generation it resolved (issue #27):
152
+
153
+ - **Modern host + legacy library**: a guaranteed crash — the legacy token estimator has no `role === 'system'` branch, so it iterates a string and throws `Cannot read properties of undefined (reading 'length')`. This happens before the request is built, which is why it looks like "every model crashes, within milliseconds, with all-zero usage".
154
+ - **Legacy host + modern library**: no crash, but the modern api reads only that system message and silently ignores the top-level fields — the system prompt and **every tool declaration** are lost, so the model can never emit a tool call. Quieter, and also fixed.
155
+
156
+ The generation gate is the copy **this plugin resolved** (its manifest version; package layout when that is unreadable), **not the host version** — a nested older copy shadows a newer host copy and vice versa. Startup logs one line, `dsh-connect-trae: pi-ai <version> (<generation>)`; `legacy` means your install resolved to the ≤0.85 line.
157
+
158
+ The boundary is **0.86**: `dist/utils/transcript.js`, `normalizeContext`, the estimator's system branch, and the switch away from the top-level fields all land in 0.86.0 together. Gating on `0.87` misclassifies 0.86.x as legacy and folds a context the modern api consumes natively, destroying the prompt and the tools.
159
+
140
160
  The 0.2.0 line breaks **no** contract this plugin consumes — a symbol-by-symbol check confirmed every API it imports is unchanged ([full 0.2.0-rc.1 impact check](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.0_RC1_IMPACT_CHECK.md)). But since 0.1.7 DSH enforces a **bundle-level peer gate**: a bundle whose peers are unsatisfied is skipped **whole**, with one line on stderr and nothing in the UI — the plugin simply disappears.
141
161
 
142
162
  ### Why the upper bound is not `<0.2.0`
@@ -231,7 +251,7 @@ pnpm run check # typecheck + test + build
231
251
  Local dev via a `link:` install to the desktop profile (restart DSH Desktop after editing):
232
252
 
233
253
  ```sh
234
- dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-trae
254
+ dsh plugin --profile desktop add /path/to/dsh-connect-trae
235
255
  ```
236
256
 
237
257
  ## Acknowledgements
package/README.md CHANGED
@@ -44,7 +44,8 @@
44
44
  - **跟随 Trae 当前登录的账号** —— 你在 Trae 桌面端**退出并换登录**后,卡片会检测到「插件绑定的账号」与「Trae 现在登录的账号」不一致,并给出一行提示与**一键切换**(复用账号下拉的同一次写入,不引入第二条切换路径)。想让插件**自动跟随**,可打开设置卡片里的「跟随 Trae 当前登录的账号」开关:此后每次配置生效都会把 `accounts.<region>` 重绑到 App 当前登录的账号。**该开关默认关闭**——已有配置保持原来的严格绑定,自动换绑意味着换人计费,必须由你主动勾选;未勾选时上面那行提示就是你的手动通道。(issue #29)
45
45
  - **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
46
46
  - **积分临近到期会提醒** —— 剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」;提醒条按到期紧迫程度逐级加重(**7 天 → 24 小时 → 已过期**),写明金额、套餐数和最早到期时间,避免只盯总额时悄悄丢掉最先到期的额度。国内版在用量页与输入框积分面板两处都显示(国际版是订阅制,没有逐包到期数据)。
47
- - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`/`4011`,SOLO 通道没有这个模型)/ **被限流** / **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
47
+ - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`,SOLO 通道没有这个模型)/ **被限流**(`4011`,Trae 限了这条通道)/ **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
48
+ > `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。
48
49
  - **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
49
50
  - **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求,**刻意没有「一键全签」**——Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),批量只会酿成一排假失败。国际版没有签到活动,因此不显示该按钮。
50
51
  - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择),并把手动刷新按钮放在浮层里。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
@@ -142,6 +143,33 @@ npm install dsh-connect-trae
142
143
  **要求 DSH 0.1.7-rc.1 及以上**(2.3.0 起不再支持 0.1.7 之前的宿主;peer 依赖范围已按此收窄)。
143
144
  **peer 上界为 `<0.3.0-0`:0.1.7 与 0.2 两条线均受支持。**
144
145
 
146
+ ### 同时支持 pi-ai 的两代上下文(3.4.2 起)
147
+
148
+ DSH 自己横跨 `@earendil-works/pi-ai` 的两代,而**本插件解析到哪一代由你的安装环境决定**,
149
+ 未必是宿主那一代——插件声明它是 peer,这是对的,但两代对「上下文长什么样」的定义不同:
150
+
151
+ | | 旧代(≤0.85) | 新代(0.86+) |
152
+ | --- | --- | --- |
153
+ | 形状 | `{ systemPrompt, tools, messages }` | `{ messages: [ {role:'system', content, toolsAdded}, … ] }` |
154
+ | 系统提示 | 顶层 `systemPrompt` | **只**挂在那条 system 消息上 |
155
+ | 工具声明 | 顶层 `tools` | **只**挂在那条 system 消息的 `toolsAdded` 上 |
156
+
157
+ 错配**两个方向都会坏**,所以 3.4.2 起插件会按自己解析到的那一代双向适配(issue #27):
158
+
159
+ - **新宿主 + 旧库**:必崩——旧代的 token 估算没有 `role === 'system'` 分支,会去迭代一个字符串并抛
160
+ `Cannot read properties of undefined (reading 'length')`。这一步在请求构造之前,所以表现为
161
+ 「任意模型都崩、耗时极短、usage 恒为 0」。
162
+ - **旧宿主 + 新库**:不崩,但现代 api 只读那条 system 消息,顶层字段被**静默忽略**——系统提示与
163
+ **全部工具声明**丢失(模型永远不会产生 toolCall)。这类故障更隐蔽,同样已修。
164
+
165
+ 代际判据是**本插件自己解析到的那份包**(读它的 manifest 版本;读不到时看包布局),
166
+ **不是宿主版本**——因为嵌套的旧副本会盖过宿主的新副本,反之亦然。启动时日志里会有一行
167
+ `dsh-connect-trae: pi-ai <版本> (<代数>)`,`legacy` 表示你这份安装解析到了 ≤0.85 的旧代。
168
+
169
+ 分界线是 **0.86**:`dist/utils/transcript.js`、`normalizeContext`、估算器的 system 分支、
170
+ 以及「不再读顶层字段」这四件事同时出现在 0.86.0。按 `0.87` 划分会把 0.86.x 误判成旧代而错误折返,
171
+ 反而丢掉提示与工具。
172
+
145
173
  0.2.0 线对本插件**没有契约破坏**——逐符号核对确认插件消费的 API 全部未变([完整的 0.2.0-rc.1 影响面核对](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.0_RC1_IMPACT_CHECK.md))。但 0.1.7 起 DSH 有一道**bundle 级 peer 门禁**:
146
174
  peer 范围不满足的 bundle 会被**整体跳过**,只往 stderr 写一行,界面上表现为「插件凭空消失」。
147
175
 
@@ -241,7 +269,7 @@ pnpm run check # typecheck + test + build
241
269
  本地开发用 `link:` 安装到 desktop profile(改码后重启 DSH Desktop 生效):
242
270
 
243
271
  ```sh
244
- dsh plugin --profile desktop add /Users/dmh2002/DshProject/dsh-connect-trae
272
+ dsh plugin --profile desktop add /path/to/dsh-connect-trae
245
273
  ```
246
274
 
247
275
  ## 致谢
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import { Context, ProviderStreams } from "@earendil-works/pi-ai";
2
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
3
- import { Context } from "@deepseek-ai/cordis";
4
+ import { Context as Context$1 } from "@deepseek-ai/cordis";
4
5
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
6
  //#region src/reasoning.d.ts
6
7
  declare const TRAE_REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high", "xhigh"];
@@ -237,9 +238,10 @@ interface TraeWireModel {
237
238
  * their own Trae IDE offers. The row therefore ships, and its call fails
238
239
  * honestly: the bridge now translates the in-stream refusal into a readable
239
240
  * error (`4001` → "Trae does not serve this model under the SOLO function the
240
- * request used", `4011` → "Trae refused this model under the SOLO function the
241
- * request used"), so a user who picks an unusable model is told why instead of
242
- * seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
241
+ * request used", `4011` → "Trae throttled the SOLO channel for this account",
242
+ * the second one corrected by issue #33 — 4011 is a rate limit, see
243
+ * src/solo-bridge.ts), so a user who picks an unusable model is told why instead
244
+ * of seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
243
245
  * which later turned out to be callable under `chat_v3`.
244
246
  *
245
247
  * Credit multiplier precedence: the wire's `display_contact_config` rate wins
@@ -294,6 +296,113 @@ declare class TraeCatalog {
294
296
  set(models: readonly TraeModelInfo[]): void;
295
297
  }
296
298
  //#endregion
299
+ //#region src/pi-ai-runtime.d.ts
300
+ /**
301
+ * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
302
+ * context shape that copy consumes (issue #27).
303
+ *
304
+ * WHY THIS EXISTS. The plugin declares pi-ai as a peer, so the module it
305
+ * imports is whatever the install provides — and the two generations consume
306
+ * DIFFERENT context shapes:
307
+ *
308
+ * - **modern** (0.86+): every public entry point calls `normalizeContext()`
309
+ * once, so what reaches a provider is a normalized *transcript*:
310
+ * `{ messages: [ { role: 'system', content, toolsAdded }, … ] }`. The system
311
+ * prompt and the tool declarations live ONLY on that leading system message;
312
+ * the top-level `systemPrompt` / `tools` fields are gone, and the api never
313
+ * reads them back.
314
+ * - **legacy** (≤0.85): expects `{ systemPrompt, tools, messages }` and has no
315
+ * branch for a `system` message inside `messages` at all. Its token estimator
316
+ * crashes on one — `estimateMessageTokens` iterates the string `content`
317
+ * character by character and reads `.name` off a character — and its wire
318
+ * converter silently drops it.
319
+ *
320
+ * The mismatch is reachable in BOTH directions, because DSH itself spans both
321
+ * generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
322
+ * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`), while this plugin
323
+ * accepts the whole range (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer,
324
+ * the other one is wrong:
325
+ *
326
+ * | host | resolved api | outcome |
327
+ * |---|---|---|
328
+ * | legacy | legacy | fine |
329
+ * | legacy | modern | no crash, but prompt + tools silently LOST |
330
+ * | modern | legacy | CRASH: `Cannot read properties of undefined (reading 'length')` |
331
+ * | modern | modern | fine |
332
+ *
333
+ * THE BOUNDARY IS 0.86, NOT 0.87. Checked against the published tarballs:
334
+ * `dist/utils/transcript.js` — and with it `normalizeContext`, the
335
+ * `role === 'system'` estimator branch, and the switch away from reading
336
+ * `systemPrompt`/`tools` — first appears in **0.86.0**. A `>= 0.87` gate
337
+ * misclassifies 0.86.x as legacy and folds a context the modern api consumes
338
+ * natively, which deletes the only carrier of the prompt and the tools.
339
+ *
340
+ * HOW the generation is decided, in order:
341
+ *
342
+ * 1. **The manifest version** of the copy this plugin resolved. Numeric semver
343
+ * comparison (`minor >= 86`), so `0.86.0` and the already-shipped `1.x` line
344
+ * both land on modern, and `0.9.0` correctly sorts below `0.86.0`.
345
+ * 2. **The package's own layout**, when no version could be read: the transcript
346
+ * module sits at `dist/utils/transcript.js` (the exports map's
347
+ * `"./utils/*" → "./dist/utils/*.js"` convention is identical in every
348
+ * published generation, 0.82 through 1.x).
349
+ *
350
+ * Both checks are deliberately resolver-independent. `import.meta.resolve()` is
351
+ * NOT used to answer this question: bundler-style resolvers (vite, and equally
352
+ * `vitest` under it) expand the `"./utils/*"` wildcard to a path and return it
353
+ * WITHOUT checking that the file exists, so a probe built on it answers "modern"
354
+ * for 0.85.1 — the exact false positive that would leave issue #27 unfixed while
355
+ * claiming to be capability-based. Reading the package directory cannot be
356
+ * fooled that way.
357
+ *
358
+ * The version is therefore the primary signal, and it also keeps the gate
359
+ * forward-compatible for free: a future 2.x needs no change here, whereas a
360
+ * file-name check alone would have to know about it.
361
+ *
362
+ * @module dsh-connect-trae/pi-ai-runtime
363
+ */
364
+ /** The context generation a resolved pi-ai api module speaks. */
365
+ type PiAiGeneration = 'legacy' | 'modern';
366
+ /** What {@link piAiRuntimeInfo} reports about the resolved pi-ai copy. */
367
+ interface PiAiRuntimeInfo {
368
+ /** `modern` (0.86+: consumes normalized transcripts) or `legacy` (≤0.85 shape). */
369
+ generation: PiAiGeneration;
370
+ /** Version of the resolved copy, when the manifest could be read. */
371
+ version: string | undefined;
372
+ /** Package directory of the resolved copy, when it could be located. */
373
+ resolvedFrom: string | undefined;
374
+ /**
375
+ * Whether the copy could be inspected at all. `false` means pi-ai does not
376
+ * resolve from this plugin — the normal state of a market install probed
377
+ * OUTSIDE the DSH host, where the host supplies the module at plugin runtime.
378
+ * Inside DSH the copy is always inspectable: a nested one shadows host
379
+ * injection, and without a nested one the host supplies its own.
380
+ */
381
+ inspected: boolean;
382
+ }
383
+ /** Resolve (once) which pi-ai copy this plugin runs against. */
384
+ declare function piAiRuntimeInfo(): PiAiRuntimeInfo;
385
+ /**
386
+ * The generation decision table, exported so every branch is pinned by tests
387
+ * rather than by a comment here — including the two this development tree can
388
+ * never reach (no local copy, and a copy with no readable version), which are
389
+ * exactly the states a market install founds its gate on.
390
+ *
391
+ * @param version - manifest version of the resolved copy, if readable.
392
+ * @param transcriptPresent - whether that copy actually ships
393
+ * `dist/utils/transcript.js`; `undefined` when nothing was inspected.
394
+ */
395
+ declare function decidePiAiGeneration(version: string | undefined, transcriptPresent: boolean | undefined): PiAiGeneration;
396
+ /**
397
+ * The generation a version string belongs to, or `undefined` when the string
398
+ * cannot be read as a version at all.
399
+ *
400
+ * Only the minor of the `0.x` line matters for the boundary, and `major >= 1`
401
+ * is modern by definition (1.0.x is already in use by `dsh-llm-pi-ai@0.2.1-alpha.2`
402
+ * and consumes transcripts natively).
403
+ */
404
+ declare function generationOf(version: string | undefined): PiAiGeneration | undefined;
405
+ //#endregion
297
406
  //#region src/upstream.d.ts
298
407
  type TraeUpstreamErrorKind = 'authentication' | 'hard_credit' | 'soft_rate' | 'not_found' | 'server' | 'client' | 'unconfigured';
299
408
  type TraeChatResult = {
@@ -350,6 +459,67 @@ declare const TRAE_PROVIDER_DISPLAY_NAMES: Readonly<Record<TraeRegion, string>>;
350
459
  /** Region a provider route id belongs to. */
351
460
  declare function regionOfTraeProvider(provider: string): TraeRegion | undefined;
352
461
  declare const TRAE_STREAM_IDLE_TIMEOUT_MS = 300000;
462
+ /**
463
+ * Fold a NORMALIZED transcript back into the `{ systemPrompt, tools, messages }`
464
+ * shape a LEGACY (≤0.85) api consumes — issue #27's crash and its two silent
465
+ * siblings.
466
+ *
467
+ * WHY (issue #27). 0.86+ normalizes at every public entry point, and DSH's host
468
+ * calls `Models.streamSimple()`, so on a modern host the context arriving here
469
+ * ALWAYS carries its prompt and its tool declarations on a leading system
470
+ * message — the top-level fields are gone. A legacy api has no branch for that
471
+ * message: `estimateMessageTokens` iterates the string `content` character by
472
+ * character and reads `.name` off a character, throwing
473
+ * `Cannot read properties of undefined (reading 'length')` before the request
474
+ * is even built. That is what surfaced as `PI_AI_ERROR` with all-zero usage.
475
+ *
476
+ * The fold must carry MORE than the prompt. `toolsAdded` is the ONLY remaining
477
+ * carrier of the tool declarations once `normalizeContext` has run, and a
478
+ * legacy api reads tools exclusively from `context.tools` — so lifting the text
479
+ * alone would trade a loud crash for a silent one: a model that can never emit
480
+ * a `toolCall` because the request declared no tools.
481
+ *
482
+ * Shape-guarded, not version-guarded: it asks "is there a system message inside
483
+ * `messages`?" rather than "which pi-ai is loaded?". A version test would be
484
+ * wrong the moment either side moves, and this predicate is an identity for the
485
+ * legacy shape, so the ordinary path stays byte-for-byte untouched. WHICH
486
+ * branch runs at all IS generation-gated — see {@link withContextAdaptation}.
487
+ *
488
+ * Returns the SAME object when there is nothing to fold, so a legacy host
489
+ * (which never normalizes) pays one `Array.some` and nothing else.
490
+ */
491
+ declare function adaptLegacyPiAiContext(context: Context): Context;
492
+ /**
493
+ * Lift the legacy `{ systemPrompt, tools, messages }` shape into the normalized
494
+ * transcript a MODERN (0.86+) api consumes — the reverse mismatch, and just as
495
+ * silent.
496
+ *
497
+ * WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1`)
498
+ * while this plugin may resolve a newer copy than its host. A LEGACY host never
499
+ * normalizes, so it hands the api the legacy shape; a modern api reads the
500
+ * prompt from a leading system message and the tools from its `toolsAdded`, and
501
+ * never looks at the top-level fields — so both are dropped without a sound.
502
+ * Verified against the published tarballs: the request that reaches the wire
503
+ * carries no system turn and `tools: []`.
504
+ *
505
+ * Identity for anything already in transcript shape, and for a context with
506
+ * neither a prompt nor tools (pi-ai's own `createInitialSystemMessage` returns
507
+ * `undefined` there, keeping an empty transcript empty).
508
+ */
509
+ declare function adaptModernPiAiContext(context: Context): Context;
510
+ /**
511
+ * Wrap one pi-ai API module so BOTH stream entry points receive a context its
512
+ * own generation can consume.
513
+ *
514
+ * The gate is the copy THIS plugin resolved (see {@link piAiRuntimeInfo}) —
515
+ * what the host runs says nothing about it, because a nested copy on disk
516
+ * shadows host injection and vice versa. The wrapping is cheap and lazy: only
517
+ * `stream`/`streamSimple` are intercepted, the deferred-fetch entry points take
518
+ * no context and are passed through untouched, and each adapter function is an
519
+ * identity unless the context is actually the other generation's shape. So a
520
+ * healthy install does one `Array.some` per request.
521
+ */
522
+ declare function withContextAdaptation(api: ProviderStreams, generation: PiAiGeneration): ProviderStreams;
353
523
  interface TraeAdapterOptions {
354
524
  shim: TraeShim;
355
525
  catalog: TraeCatalog;
@@ -358,6 +528,12 @@ interface TraeAdapterOptions {
358
528
  /** pi-ai provider name and profile display name; defaults to the CN name. */
359
529
  displayName?: string;
360
530
  resolveAttachments?: () => AttachmentStore | undefined;
531
+ /**
532
+ * Override the resolved pi-ai generation (see {@link withContextAdaptation});
533
+ * tests use it to exercise the combination this machine does not have
534
+ * installed.
535
+ */
536
+ piAiGeneration?: PiAiGeneration;
361
537
  }
362
538
  interface TraeAdapter {
363
539
  adapter: PiAiAdapter;
@@ -2115,7 +2291,7 @@ interface TraeUsageRouteOptions {
2115
2291
  */
2116
2292
  declare function traeWebUsage(deps: TraeUsageRouteOptions, region: TraeRegion): Promise<TraeWebUsage>;
2117
2293
  /** Mount the GET usage route on an optional webServer context. */
2118
- declare function registerTraeUsageRoute(ctx: Context, deps: TraeUsageRouteOptions): void;
2294
+ declare function registerTraeUsageRoute(ctx: Context$1, deps: TraeUsageRouteOptions): void;
2119
2295
  //#endregion
2120
2296
  //#region src/model-probe.d.ts
2121
2297
  /** How one model answered. */
@@ -2332,6 +2508,6 @@ declare function regionEnabled(config: Config, region: TraeRegion): boolean;
2332
2508
  */
2333
2509
  declare function asVolatile<S>(schema: S): S;
2334
2510
  declare const Config: z<Config>;
2335
- declare function apply(ctx: Context, config: Config): void;
2511
+ declare function apply(ctx: Context$1, config: Config): void;
2336
2512
  //#endregion
2337
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
2513
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
package/lib/index.js CHANGED
@@ -3,9 +3,11 @@ import { createProvider } from "@earendil-works/pi-ai";
3
3
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
4
4
  import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
5
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { fileURLToPath } from "node:url";
6
9
  import { readFile, rm, stat } from "node:fs/promises";
7
10
  import { createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
8
- import { dirname, join } from "node:path";
9
11
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
10
12
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
11
13
  import { cpus, homedir, hostname, release, userInfo } from "node:os";
@@ -13,7 +15,6 @@ import { createServer } from "node:http";
13
15
  import { Readable } from "node:stream";
14
16
  import { execFile } from "node:child_process";
15
17
  import { promisify } from "node:util";
16
- import { existsSync } from "node:fs";
17
18
  //#region src/catalog.ts
18
19
  /**
19
20
  * Bootstrap catalog: identity only where current Trae metadata has not been
@@ -197,9 +198,10 @@ function displayKey(name) {
197
198
  * their own Trae IDE offers. The row therefore ships, and its call fails
198
199
  * honestly: the bridge now translates the in-stream refusal into a readable
199
200
  * error (`4001` → "Trae does not serve this model under the SOLO function the
200
- * request used", `4011` → "Trae refused this model under the SOLO function the
201
- * request used"), so a user who picks an unusable model is told why instead of
202
- * seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
201
+ * request used", `4011` → "Trae throttled the SOLO channel for this account",
202
+ * the second one corrected by issue #33 — 4011 is a rate limit, see
203
+ * src/solo-bridge.ts), so a user who picks an unusable model is told why instead
204
+ * of seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
203
205
  * which later turned out to be callable under `chat_v3`.
204
206
  *
205
207
  * Credit multiplier precedence: the wire's `display_contact_config` rate wins
@@ -321,6 +323,179 @@ var TraeCatalog = class {
321
323
  }
322
324
  };
323
325
  //#endregion
326
+ //#region src/pi-ai-runtime.ts
327
+ /**
328
+ * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
329
+ * context shape that copy consumes (issue #27).
330
+ *
331
+ * WHY THIS EXISTS. The plugin declares pi-ai as a peer, so the module it
332
+ * imports is whatever the install provides — and the two generations consume
333
+ * DIFFERENT context shapes:
334
+ *
335
+ * - **modern** (0.86+): every public entry point calls `normalizeContext()`
336
+ * once, so what reaches a provider is a normalized *transcript*:
337
+ * `{ messages: [ { role: 'system', content, toolsAdded }, … ] }`. The system
338
+ * prompt and the tool declarations live ONLY on that leading system message;
339
+ * the top-level `systemPrompt` / `tools` fields are gone, and the api never
340
+ * reads them back.
341
+ * - **legacy** (≤0.85): expects `{ systemPrompt, tools, messages }` and has no
342
+ * branch for a `system` message inside `messages` at all. Its token estimator
343
+ * crashes on one — `estimateMessageTokens` iterates the string `content`
344
+ * character by character and reads `.name` off a character — and its wire
345
+ * converter silently drops it.
346
+ *
347
+ * The mismatch is reachable in BOTH directions, because DSH itself spans both
348
+ * generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 declares `^0.85.1`; 0.2.0-rc.2 and
349
+ * later declare `^0.87.1`, 0.2.1-alpha.2 declares `^1.0.2`), while this plugin
350
+ * accepts the whole range (`>=0.1.7-rc.1 <0.3.0-0`). Whichever side is newer,
351
+ * the other one is wrong:
352
+ *
353
+ * | host | resolved api | outcome |
354
+ * |---|---|---|
355
+ * | legacy | legacy | fine |
356
+ * | legacy | modern | no crash, but prompt + tools silently LOST |
357
+ * | modern | legacy | CRASH: `Cannot read properties of undefined (reading 'length')` |
358
+ * | modern | modern | fine |
359
+ *
360
+ * THE BOUNDARY IS 0.86, NOT 0.87. Checked against the published tarballs:
361
+ * `dist/utils/transcript.js` — and with it `normalizeContext`, the
362
+ * `role === 'system'` estimator branch, and the switch away from reading
363
+ * `systemPrompt`/`tools` — first appears in **0.86.0**. A `>= 0.87` gate
364
+ * misclassifies 0.86.x as legacy and folds a context the modern api consumes
365
+ * natively, which deletes the only carrier of the prompt and the tools.
366
+ *
367
+ * HOW the generation is decided, in order:
368
+ *
369
+ * 1. **The manifest version** of the copy this plugin resolved. Numeric semver
370
+ * comparison (`minor >= 86`), so `0.86.0` and the already-shipped `1.x` line
371
+ * both land on modern, and `0.9.0` correctly sorts below `0.86.0`.
372
+ * 2. **The package's own layout**, when no version could be read: the transcript
373
+ * module sits at `dist/utils/transcript.js` (the exports map's
374
+ * `"./utils/*" → "./dist/utils/*.js"` convention is identical in every
375
+ * published generation, 0.82 through 1.x).
376
+ *
377
+ * Both checks are deliberately resolver-independent. `import.meta.resolve()` is
378
+ * NOT used to answer this question: bundler-style resolvers (vite, and equally
379
+ * `vitest` under it) expand the `"./utils/*"` wildcard to a path and return it
380
+ * WITHOUT checking that the file exists, so a probe built on it answers "modern"
381
+ * for 0.85.1 — the exact false positive that would leave issue #27 unfixed while
382
+ * claiming to be capability-based. Reading the package directory cannot be
383
+ * fooled that way.
384
+ *
385
+ * The version is therefore the primary signal, and it also keeps the gate
386
+ * forward-compatible for free: a future 2.x needs no change here, whereas a
387
+ * file-name check alone would have to know about it.
388
+ *
389
+ * @module dsh-connect-trae/pi-ai-runtime
390
+ */
391
+ let cached;
392
+ /** Resolve (once) which pi-ai copy this plugin runs against. */
393
+ function piAiRuntimeInfo() {
394
+ cached ??= inspectResolvedPiAi();
395
+ return cached;
396
+ }
397
+ function inspectResolvedPiAi() {
398
+ const entry = resolveModuleFile();
399
+ const root = entry === void 0 ? void 0 : findPackageRoot(entry);
400
+ const version = root === void 0 ? void 0 : readVersion(root);
401
+ return {
402
+ generation: decidePiAiGeneration(version, root === void 0 ? void 0 : hasTranscriptModule(root)),
403
+ version,
404
+ resolvedFrom: root,
405
+ inspected: root !== void 0
406
+ };
407
+ }
408
+ /**
409
+ * The generation decision table, exported so every branch is pinned by tests
410
+ * rather than by a comment here — including the two this development tree can
411
+ * never reach (no local copy, and a copy with no readable version), which are
412
+ * exactly the states a market install founds its gate on.
413
+ *
414
+ * @param version - manifest version of the resolved copy, if readable.
415
+ * @param transcriptPresent - whether that copy actually ships
416
+ * `dist/utils/transcript.js`; `undefined` when nothing was inspected.
417
+ */
418
+ function decidePiAiGeneration(version, transcriptPresent) {
419
+ const byVersion = generationOf(version);
420
+ if (byVersion !== void 0) return byVersion;
421
+ if (transcriptPresent === true) return "modern";
422
+ if (transcriptPresent === false) return "legacy";
423
+ return "modern";
424
+ }
425
+ /**
426
+ * The generation a version string belongs to, or `undefined` when the string
427
+ * cannot be read as a version at all.
428
+ *
429
+ * Only the minor of the `0.x` line matters for the boundary, and `major >= 1`
430
+ * is modern by definition (1.0.x is already in use by `dsh-llm-pi-ai@0.2.1-alpha.2`
431
+ * and consumes transcripts natively).
432
+ */
433
+ function generationOf(version) {
434
+ if (version === void 0) return void 0;
435
+ const match = /^(\d+)\.(\d+)\./u.exec(version);
436
+ if (match === null) return void 0;
437
+ const major = Number(match[1]);
438
+ const minor = Number(match[2]);
439
+ if (major > 0) return "modern";
440
+ return minor >= 86 ? "modern" : "legacy";
441
+ }
442
+ /**
443
+ * Whether the resolved package actually contains the transcript module.
444
+ *
445
+ * `dist/utils/transcript.js` is the file every modern generation ships and no
446
+ * legacy one has (checked: absent in 0.82.1 and 0.85.1; present in 0.86.0,
447
+ * 0.86.1, 0.87.1 and 1.0.2). Landing on the file directly, rather than asking a
448
+ * resolver to find it, is what keeps this honest under bundler-style resolution
449
+ * — see the module docstring.
450
+ */
451
+ function hasTranscriptModule(packageRoot) {
452
+ return existsSync(join(packageRoot, "dist", "utils", "transcript.js"));
453
+ }
454
+ /**
455
+ * The file the main entry resolved to; undefined when resolution itself fails.
456
+ *
457
+ * `import.meta.resolve` (not `require.resolve`): pi-ai's exports map exposes
458
+ * "." under the `import` condition only, so the CJS resolver refuses the bare
459
+ * specifier outright — the ESM resolver is the one this plugin actually imports
460
+ * with, so it is also the one whose answer is meaningful here. Only the package
461
+ * LOCATION is taken from it; the generation question is answered from disk.
462
+ */
463
+ function resolveModuleFile() {
464
+ try {
465
+ return fileURLToPath(import.meta.resolve("@earendil-works/pi-ai"));
466
+ } catch {
467
+ return;
468
+ }
469
+ }
470
+ /**
471
+ * The package directory owning `moduleFile`: `dist/index.js` sits directly
472
+ * inside the package, so walking up from the entry finds its manifest. The
473
+ * manifest is claimed only when its `name` matches, so a same-named ancestor (a
474
+ * pnpm virtual-store parent, for instance) cannot be misread.
475
+ */
476
+ function findPackageRoot(moduleFile) {
477
+ let directory = dirname(moduleFile);
478
+ for (let depth = 0; depth < 4; depth++) {
479
+ if (readManifest(directory)?.name === "@earendil-works/pi-ai") return directory;
480
+ const parent = dirname(directory);
481
+ if (parent === directory) return void 0;
482
+ directory = parent;
483
+ }
484
+ }
485
+ /** Version from the manifest already located by {@link findPackageRoot}. */
486
+ function readVersion(packageRoot) {
487
+ const version = readManifest(packageRoot)?.version;
488
+ return typeof version === "string" ? version : void 0;
489
+ }
490
+ function readManifest(directory) {
491
+ try {
492
+ const parsed = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
493
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
494
+ } catch {
495
+ return;
496
+ }
497
+ }
498
+ //#endregion
324
499
  //#region src/adapter.ts
325
500
  /** Provider route this bundle owns for the domestic (CN) gateway. */
326
501
  const TRAE_PROVIDER = "trae";
@@ -404,9 +579,172 @@ function toPiModel(info, baseUrl, providerId) {
404
579
  compat: { supportsReasoningEffort: info.reasoningEfforts !== void 0 }
405
580
  };
406
581
  }
582
+ function isSystemMessage(message) {
583
+ return typeof message === "object" && message !== null && message.role === "system";
584
+ }
585
+ /**
586
+ * The text of one system message: its `content` (a string, as
587
+ * `normalizeContext` writes, or text blocks, which a provider may send back)
588
+ * followed by its named `sections` — exactly the join pi-ai's own
589
+ * `getSystemMessageText` performs.
590
+ *
591
+ * `sections` is not decoration: it is the modern transcript's second carrier of
592
+ * prompt text ("named, ordered prompt sections rendered verbatim after
593
+ * content"). Folding `content` alone would silently drop a prompt that arrives
594
+ * section-shaped, which is the same asymmetric-loss failure this adapter exists
595
+ * to prevent.
596
+ */
597
+ function systemTextOf(message) {
598
+ const parts = [contentTextOf(message.content)];
599
+ const sections = message.sections;
600
+ if (typeof sections === "object" && sections !== null && !Array.isArray(sections)) {
601
+ for (const text of Object.values(sections)) if (typeof text === "string") parts.push(text);
602
+ }
603
+ return parts.filter((part) => part.length > 0).join("\n\n");
604
+ }
605
+ function contentTextOf(content) {
606
+ if (typeof content === "string") return content;
607
+ if (!Array.isArray(content)) return "";
608
+ const texts = [];
609
+ for (const block of content) {
610
+ if (typeof block !== "object" || block === null) continue;
611
+ const candidate = block;
612
+ if (candidate.type === "text" && typeof candidate.text === "string") texts.push(candidate.text);
613
+ }
614
+ return texts.join("\n\n");
615
+ }
616
+ /**
617
+ * Replay one transcript system message's tool deltas into `state`, the same
618
+ * merge pi-ai's own `getCurrentTools` performs: removals first, then additions,
619
+ * keyed by name so a mid-conversation redeclaration replaces the original.
620
+ */
621
+ function replayToolState(message, state) {
622
+ const removed = message.toolsRemoved;
623
+ if (Array.isArray(removed)) for (const tool of removed) {
624
+ const name = tool?.name;
625
+ if (typeof name === "string") state.delete(name);
626
+ }
627
+ const added = message.toolsAdded;
628
+ if (Array.isArray(added)) for (const tool of added) {
629
+ const name = tool?.name;
630
+ if (typeof name === "string") state.set(name, tool);
631
+ }
632
+ }
633
+ /**
634
+ * Fold a NORMALIZED transcript back into the `{ systemPrompt, tools, messages }`
635
+ * shape a LEGACY (≤0.85) api consumes — issue #27's crash and its two silent
636
+ * siblings.
637
+ *
638
+ * WHY (issue #27). 0.86+ normalizes at every public entry point, and DSH's host
639
+ * calls `Models.streamSimple()`, so on a modern host the context arriving here
640
+ * ALWAYS carries its prompt and its tool declarations on a leading system
641
+ * message — the top-level fields are gone. A legacy api has no branch for that
642
+ * message: `estimateMessageTokens` iterates the string `content` character by
643
+ * character and reads `.name` off a character, throwing
644
+ * `Cannot read properties of undefined (reading 'length')` before the request
645
+ * is even built. That is what surfaced as `PI_AI_ERROR` with all-zero usage.
646
+ *
647
+ * The fold must carry MORE than the prompt. `toolsAdded` is the ONLY remaining
648
+ * carrier of the tool declarations once `normalizeContext` has run, and a
649
+ * legacy api reads tools exclusively from `context.tools` — so lifting the text
650
+ * alone would trade a loud crash for a silent one: a model that can never emit
651
+ * a `toolCall` because the request declared no tools.
652
+ *
653
+ * Shape-guarded, not version-guarded: it asks "is there a system message inside
654
+ * `messages`?" rather than "which pi-ai is loaded?". A version test would be
655
+ * wrong the moment either side moves, and this predicate is an identity for the
656
+ * legacy shape, so the ordinary path stays byte-for-byte untouched. WHICH
657
+ * branch runs at all IS generation-gated — see {@link withContextAdaptation}.
658
+ *
659
+ * Returns the SAME object when there is nothing to fold, so a legacy host
660
+ * (which never normalizes) pays one `Array.some` and nothing else.
661
+ */
662
+ function adaptLegacyPiAiContext(context) {
663
+ if (context === null || typeof context !== "object") return context;
664
+ const messages = Array.isArray(context.messages) ? context.messages : void 0;
665
+ if (messages === void 0) return context;
666
+ if (!messages.some(isSystemMessage)) return context;
667
+ const systemTexts = [];
668
+ const rest = [];
669
+ const toolState = /* @__PURE__ */ new Map();
670
+ for (const message of messages) {
671
+ if (!isSystemMessage(message)) {
672
+ rest.push(message);
673
+ continue;
674
+ }
675
+ const text = systemTextOf(message);
676
+ if (text !== "") systemTexts.push(text);
677
+ replayToolState(message, toolState);
678
+ }
679
+ const texts = [...context.systemPrompt === void 0 || context.systemPrompt === "" ? [] : [context.systemPrompt], ...systemTexts];
680
+ const systemPrompt = texts.length > 0 ? texts.join("\n\n") : void 0;
681
+ const promoted = Array.isArray(context.tools) ? void 0 : [...toolState.values()];
682
+ return {
683
+ ...context,
684
+ ...systemPrompt === void 0 ? {} : { systemPrompt },
685
+ ...promoted === void 0 || promoted.length === 0 ? {} : { tools: promoted },
686
+ messages: rest
687
+ };
688
+ }
689
+ /**
690
+ * Lift the legacy `{ systemPrompt, tools, messages }` shape into the normalized
691
+ * transcript a MODERN (0.86+) api consumes — the reverse mismatch, and just as
692
+ * silent.
693
+ *
694
+ * WHY. DSH spans both generations (`dsh-llm-pi-ai` ≤0.2.0-rc.1 pins `^0.85.1`)
695
+ * while this plugin may resolve a newer copy than its host. A LEGACY host never
696
+ * normalizes, so it hands the api the legacy shape; a modern api reads the
697
+ * prompt from a leading system message and the tools from its `toolsAdded`, and
698
+ * never looks at the top-level fields — so both are dropped without a sound.
699
+ * Verified against the published tarballs: the request that reaches the wire
700
+ * carries no system turn and `tools: []`.
701
+ *
702
+ * Identity for anything already in transcript shape, and for a context with
703
+ * neither a prompt nor tools (pi-ai's own `createInitialSystemMessage` returns
704
+ * `undefined` there, keeping an empty transcript empty).
705
+ */
706
+ function adaptModernPiAiContext(context) {
707
+ if (context === null || typeof context !== "object") return context;
708
+ const messages = Array.isArray(context.messages) ? context.messages : void 0;
709
+ if (messages === void 0) return context;
710
+ const prompt = context.systemPrompt;
711
+ const tools = Array.isArray(context.tools) && context.tools.length > 0 ? context.tools : void 0;
712
+ if (!(typeof prompt === "string" && prompt.length > 0) && tools === void 0) return context;
713
+ const head = {
714
+ role: "system",
715
+ content: prompt ?? "",
716
+ ...tools === void 0 ? {} : { toolsAdded: tools }
717
+ };
718
+ const { systemPrompt: _lifted, tools: _liftedTools, ...rest } = context;
719
+ return {
720
+ ...rest,
721
+ messages: [head, ...messages]
722
+ };
723
+ }
724
+ /**
725
+ * Wrap one pi-ai API module so BOTH stream entry points receive a context its
726
+ * own generation can consume.
727
+ *
728
+ * The gate is the copy THIS plugin resolved (see {@link piAiRuntimeInfo}) —
729
+ * what the host runs says nothing about it, because a nested copy on disk
730
+ * shadows host injection and vice versa. The wrapping is cheap and lazy: only
731
+ * `stream`/`streamSimple` are intercepted, the deferred-fetch entry points take
732
+ * no context and are passed through untouched, and each adapter function is an
733
+ * identity unless the context is actually the other generation's shape. So a
734
+ * healthy install does one `Array.some` per request.
735
+ */
736
+ function withContextAdaptation(api, generation) {
737
+ const adapt = generation === "modern" ? adaptModernPiAiContext : adaptLegacyPiAiContext;
738
+ return {
739
+ ...api,
740
+ stream: (model, context, options) => api.stream(model, adapt(context), options),
741
+ streamSimple: (model, context, options) => api.streamSimple(model, adapt(context), options)
742
+ };
743
+ }
407
744
  function createTraeAdapter(options) {
408
745
  const providerId = options.provider ?? "trae";
409
746
  const providerName = options.displayName ?? "Trae";
747
+ const generation = options.piAiGeneration ?? piAiRuntimeInfo().generation;
410
748
  const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId));
411
749
  const provider = {
412
750
  ...createProvider({
@@ -423,7 +761,7 @@ function createTraeAdapter(options) {
423
761
  }
424
762
  } },
425
763
  models: buildModels(),
426
- api: openAICompletionsApi()
764
+ api: withContextAdaptation(openAICompletionsApi(), generation)
427
765
  }),
428
766
  getModels: () => buildModels()
429
767
  };
@@ -2407,9 +2745,14 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
2407
2745
  * live SG credential, issue #19): it is a Remote-agent roster function, not a
2408
2746
  * chat function. Every model sent with `function: "solo_agent"` — `gpt-5.4`,
2409
2747
  * `gpt-5.6-sol`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code` — answered
2410
- * `event:error {"code":4011}` (message says "rate limit", it is really "not
2411
- * served here"), while the same model with the same body answered normally
2412
- * under `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`.
2748
+ * `event:error {"code":4011}`, while the same model with the same body answered
2749
+ * normally under `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`.
2750
+ * Issue #33 corrected the reading of that code: `4011` is Trae's rate limit on
2751
+ * the route that was asked (its own text says so, and issue #33's reporter hit
2752
+ * it on the CORRECT function), and the "this route does not serve that model"
2753
+ * condition is `4001` — measured on all 22 functions of a live CN credential.
2754
+ * The conclusion for this list is unchanged either way: a route that refuses
2755
+ * chat is not a chat route, so `solo_agent` stays out.
2413
2756
  *
2414
2757
  * Note `get_detail_param` DOES answer `solo_agent` with a 43-entry list, so
2415
2758
  * asking it looks helpful while making 8 of the 10 exposed ai-region models
@@ -2809,22 +3152,38 @@ function decodeTraeEvent(event) {
2809
3152
  //#endregion
2810
3153
  //#region src/solo-bridge.ts
2811
3154
  /**
2812
- * What Trae's in-stream error codes mean. Measured 2026-10-01 on a live SG
2813
- * credential (issue #19), by sending the same body to several functions:
3155
+ * What Trae's in-stream error codes mean. Measured 2026-10-01 on live
3156
+ * credentials (issue #19), and re-measured 2026-10-12 on a live CN credential
3157
+ * (issue #33) with a 22-function × 6-model chat matrix:
2814
3158
  *
2815
3159
  * - `1005` — the model needs a paid plan. Trae answers HTTP 200 with
2816
3160
  * `event:error {"code":1005,"message":"","extra":"{\"plan\":1}"}`, so the
2817
3161
  * gate is only visible in `extra.plan`; `gpt-6-sol` returned it under both
2818
3162
  * `solo_work_remote` and `solo_agent_remote` on a free account.
2819
- * - `4011` — the function the request named does not serve that model. Its
2820
- * message claims "exceeded the rate limit", which is misleading: the same
2821
- * model answers normally when sent under `solo_work_remote`.
3163
+ * - `4011` — Trae throttled the SOLO route for this account. Trae's own text
3164
+ * is `your requests have exceeded the rate limit. Please wait and try again
3165
+ * later.`, and issue #33 is why that text is now taken at face value: the
3166
+ * earlier reading of 4011 as "this function does not serve that model" is
3167
+ * contradicted by measurement. On CN that condition is `4001`, and all 22
3168
+ * functions answered `4001` — never `4011` — for a model absent from their
3169
+ * roster, while `glm-5.3` streamed normally under its own function. Rate
3170
+ * limiting is per account, route and moment, which is exactly the shape of
3171
+ * that report: the same model through the same function was fine for days
3172
+ * and then failed, with Trae's own text saying "rate limit".
2822
3173
  * - `4001` — `config_name` is not one this function serves.
3174
+ * - `4008` — the account's SOLO quota is exhausted. Deliberately a different
3175
+ * code from `4011`: Trae words this one `your requests have exceeded the
3176
+ * quota`, so "I still have plenty of credits" and "I am being throttled" are
3177
+ * two different statements, and a user hits the second without touching the
3178
+ * first (issue #33).
3179
+ * - `4023` — the model name is unknown on the route that was asked.
2823
3180
  */
2824
3181
  const TRAE_ERROR_HINTS = {
2825
3182
  1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
2826
3183
  4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
2827
- 4011: "Trae refused this model under the SOLO function the request used"
3184
+ 4008: "Trae reports the account's SOLO quota is exhausted for this request",
3185
+ 4011: "Trae throttled the SOLO channel for this account (a rate limit, not the credit balance)",
3186
+ 4023: "Trae does not recognise this model name on the SOLO route that was asked"
2828
3187
  };
2829
3188
  /** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
2830
3189
  function traeErrorPlan(extra) {
@@ -2854,13 +3213,30 @@ function describeTraeFailure(failure) {
2854
3213
  * answer with a real error instead of a truncated 200 stream.
2855
3214
  *
2856
3215
  * `1005` is a subscription gate: 402 is what makes clients stop rather than
2857
- * retry, and it is truthful. Everything else is a client-side rejection.
3216
+ * retry, and it is truthful.
3217
+ *
3218
+ * `4011` is a rate limit and is reported as one — `429` / `soft_rate` (issue
3219
+ * #33). It previously answered `400` / `client`, which contradicted both its own
3220
+ * message and the client's own behaviour: pi-ai classifies an error by its text
3221
+ * (`/\b429\b|rate.?limit/i` → `RATE_LIMIT`), and `RATE_LIMIT` is retryable, so
3222
+ * the kernel retried five times with backoff while this function called the same
3223
+ * failure a non-retryable client error. Reporting the real status makes all
3224
+ * three agree, and `soft_rate` is already what `fallback-upstream` refuses to
3225
+ * replay — correct, since a throttled route is not a route-compatibility
3226
+ * problem.
3227
+ *
3228
+ * Everything else stays a client-side rejection.
2858
3229
  */
2859
3230
  function classifyTraeFailure(failure) {
2860
- return failure.code === 1005 ? {
3231
+ if (failure.code === 1005) return {
2861
3232
  status: 402,
2862
3233
  kind: "hard_credit"
2863
- } : {
3234
+ };
3235
+ if (failure.code === 4011) return {
3236
+ status: 429,
3237
+ kind: "soft_rate"
3238
+ };
3239
+ return {
2864
3240
  status: 400,
2865
3241
  kind: "client"
2866
3242
  };
@@ -3934,9 +4310,41 @@ var TraeDelegatingUpstreamClient = class {
3934
4310
  * beyond this the answer is not worth waiting for and is reported as unknown.
3935
4311
  */
3936
4312
  const TRAE_PROBE_TIMEOUT_MS = 3e4;
3937
- /** Upstream error codes the probe recognises, and what they mean. */
3938
- const GATE_CODES = /* @__PURE__ */ new Set([1005]);
3939
- const UNSUPPORTED_CODES = /* @__PURE__ */ new Set([4001, 4011]);
4313
+ /**
4314
+ * Every upstream code the probe recognises, mapped to the verdict it implies.
4315
+ *
4316
+ * ONE table, not a set per verdict: with separate sets the outcome depended on
4317
+ * the order the branches happened to be written in, so a code left in two sets
4318
+ * was silently decided by whichever line ran first (measured: re-adding `4011`
4319
+ * to the "unsupported" set passed every test). A single map makes a code mean
4320
+ * exactly one thing, and {@link assertCodesAreDistinct} pins that down at
4321
+ * module load.
4322
+ *
4323
+ * - `1005` — the plan does not cover this model (`extra.plan` carries the
4324
+ * tier). A paying account may pass, so it is a gate, not a dead model.
4325
+ * - `4001` — this function's roster does not contain that `config_name`. The
4326
+ * ONLY code that may produce `unsupported`, measured on all 22 functions of
4327
+ * a live CN credential (issue #33).
4328
+ * - `4011` — Trae throttled the route; its own text says the rate limit was
4329
+ * exceeded. Issue #33 moved this here from `unsupported`: reporting a
4330
+ * throttle as "the channel does not serve it" told users a working model was
4331
+ * dead.
4332
+ * - `4008` — the ACCOUNT's SOLO quota is exhausted. It says nothing about any
4333
+ * one model, so it must not become a model verdict; it stays inconclusive
4334
+ * and carries the upstream text, so the user retests once the quota is back.
4335
+ */
4336
+ const CODE_VERDICTS = {
4337
+ 1005: "gated",
4338
+ 4001: "unsupported",
4339
+ 4008: "unknown",
4340
+ 4011: "rate_limited"
4341
+ };
4342
+ /** A code that maps to two verdicts would make the table order-dependent. */
4343
+ function assertCodesAreDistinct(table) {
4344
+ const codes = Object.keys(table);
4345
+ if (new Set(codes).size !== codes.length) throw new Error("dsh-connect-trae: a Trae probe code maps to more than one verdict");
4346
+ }
4347
+ assertCodesAreDistinct(CODE_VERDICTS);
3940
4348
  /** Pull `Trae code N` out of an already-translated failure message. */
3941
4349
  function codeOf(message) {
3942
4350
  const match = /Trae code (\d+)/u.exec(message);
@@ -3992,12 +4400,9 @@ function classifyProbeResult(result, outputSeen) {
3992
4400
  detail: message
3993
4401
  };
3994
4402
  const code = codeOf(message);
3995
- if (code !== void 0 && GATE_CODES.has(code)) return {
3996
- verdict: "gated",
3997
- detail: message
3998
- };
3999
- if (code !== void 0 && UNSUPPORTED_CODES.has(code)) return {
4000
- verdict: "unsupported",
4403
+ const mapped = code === void 0 ? void 0 : CODE_VERDICTS[code];
4404
+ if (mapped !== void 0) return {
4405
+ verdict: mapped,
4001
4406
  detail: message
4002
4407
  };
4003
4408
  if (result.kind === "client" && result.status >= 400 && result.status < 500) return {
@@ -5736,6 +6141,8 @@ function apply(ctx, config) {
5736
6141
  Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
5737
6142
  if (stopped) return;
5738
6143
  try {
6144
+ const piAi = piAiRuntimeInfo();
6145
+ ctx.logger.info(`dsh-connect-trae: pi-ai ${piAi.version ?? "unknown"} (${piAi.generation} generation)${piAi.inspected ? "" : " — not resolved locally, the host supplies it"}`);
5739
6146
  for (const region of REGION_KEYS) {
5740
6147
  const stack = stacks[region];
5741
6148
  const trae = createTraeAdapter({
@@ -5809,4 +6216,4 @@ function apply(ctx, config) {
5809
6216
  });
5810
6217
  }
5811
6218
  //#endregion
5812
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
6219
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
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": "3.4.0",
5
+ "version": "3.4.2",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",