dsh-connect-trae 4.18.0 → 4.18.1
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 +42 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/index.js +82 -34
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.18.1 (2026-11-10)
|
|
4
|
+
|
|
5
|
+
### SOLO 的账号级错误交还号池:4008 与 4017 不再被当成请求写错
|
|
6
|
+
|
|
7
|
+
号池的 `FAILOVER_KINDS` 只认 `authentication` / `hard_credit` / `soft_rate` /
|
|
8
|
+
`server`。而 `4008`(账号 SOLO 额度用完)与 `4017`(账号被风控临时限制)原先都被
|
|
9
|
+
`classifyTraeFailure` 归成 `client`——于是「这个账号不行了」被报成「你这个请求有
|
|
10
|
+
问题」:不换号,失败也只记在当前账号上,然后就没有下文了。
|
|
11
|
+
|
|
12
|
+
现在 `4008` → `402` / `hard_credit`、`4017` → `429` / `soft_rate`,走的是号池既有的
|
|
13
|
+
冷却与换号路径。这两条都是**账号事实**,不是报文事实:换一条路由重放同一个账号既不会
|
|
14
|
+
把额度变回来,也不会解除风控,所以它们能落到 `FAILOVER_KINDS` 里正是要点。
|
|
15
|
+
|
|
16
|
+
补了 `4017` 的可读文案。此前它没有 entry,用户只能看到「Trae refused the request」
|
|
17
|
+
加一个裸码,看不出是风控。
|
|
18
|
+
|
|
19
|
+
### 缓存用量改成一份共享归一化,并容忍非对象载荷
|
|
20
|
+
|
|
21
|
+
除 `cache_read_input_tokens` / `cache_creation_input_tokens` 之外,还接受
|
|
22
|
+
`prompt_cache_hit_tokens`、`input_tokens_details`、裸 `cached_tokens` 等写法,
|
|
23
|
+
统一投影到 pi-ai 读的 `prompt_tokens_details.cached_tokens` / `.cache_write_tokens`,
|
|
24
|
+
同时**保留上游原始字段**。两个入口(SSE 解码与 SOLO 流转发)共用同一份实现,避免两处
|
|
25
|
+
各写一份而漂移。
|
|
26
|
+
|
|
27
|
+
优先级上,规范字段里的**显式 0 优先于回退候选**:0 是「这次确实没命中」,与「上游没说」
|
|
28
|
+
是两件事,后者不用于推断计费折扣。负数、NaN、Infinity 与字符串一律丢弃,缺失保持缺失。
|
|
29
|
+
|
|
30
|
+
**合并时修掉一个该 PR 引入的回归。** `normalizeUsageCache` 收的是刚 `JSON.parse` 出来的
|
|
31
|
+
载荷,却按 `Record` 用;整包归一化之后,`token_usage` 若是 `null`,会在流的 `start()` 里抛
|
|
32
|
+
`Cannot read properties of null`,把**已经开始输出**的回答打死——数组形态还会被展开成
|
|
33
|
+
`{"0":1,"1":2}` 当成 usage 发出去。已发布的 4.18.0 只逐个读类型收窄过的字段,同一输入
|
|
34
|
+
能正常跑完,所以这是新引入的。参数已收成 `unknown`、入口归一化。
|
|
35
|
+
|
|
36
|
+
### Tests
|
|
37
|
+
|
|
38
|
+
- `tests/cache-compat.spec.ts`(5 例):规范字段优先级(含显式 0)、别家字段族、
|
|
39
|
+
非法计数过滤、缺失保持缺失,以及**非对象载荷**(`null` / `[1,2]` / `"foo"` / `42` /
|
|
40
|
+
`true`)下流仍能跑完且 usage 为 `{}`。后者换回原实现立刻红。
|
|
41
|
+
- `tests/account-pool-failover.spec.ts` 新增 2 例:构造真实 SSE error 帧,经桥接层与
|
|
42
|
+
本地 shim,验证 `4008` / `4017` 会切到第二个账号、且失败记录归属第一个账号。
|
|
43
|
+
- 全仓 **115 个文件 / 1476 条测试**通过。
|
|
44
|
+
|
|
3
45
|
## 4.18.0 (2026-11-09)
|
|
4
46
|
|
|
5
47
|
### 企业面板的发现层收录租户自配模型(`config_source: 2`)
|
package/README.en.md
CHANGED
|
@@ -59,7 +59,7 @@ In the account pool, open **More settings** and choose **Claude style** or **Def
|
|
|
59
59
|
- **Per-model thinking level, and the default is no longer just `Default`** — Trae's directory has always published each model's own default level, and the plugin has always parsed it into the catalog, but **it was never reported to DSH**. The model picker could therefore only say `Default`: you could not see which level a model would run at, and you could not pin a different one. The adapter now reports that level, the host materialises it into every request that names none, and each model that declares levels gets a dropdown whose first entry names **the level it will actually run at** (e.g. "Default (high)"). Precedence is **your choice > upstream default > that model's own middle tier**; choosing "Default" DELETES the key rather than storing a level that happens to equal today's default — the latter looks identical while quietly pinning the choice, so the model would stop following upstream. A saved level the directory has since withdrawn is ignored rather than sent, because sending it is rejected with `UNSUPPORTED_REASONING_EFFORT`. Choosing nothing behaves exactly as before.
|
|
60
60
|
- **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).
|
|
61
61
|
- **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.
|
|
62
|
-
> `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.
|
|
62
|
+
> `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. `4017` (Trae's risk control temporarily restricting the account) also reports **throttled** and cools down the same way, while `4008` means the **quota is exhausted** — with the account pool enabled the request moves to the next account, and with the pool off the refusal is reported as it is.
|
|
63
63
|
- **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.
|
|
64
64
|
- **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.
|
|
65
65
|
- **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes by default (the interval is configurable — see the next bullet). **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). The panel's top right carries **two controls, Refresh and Settings** (as of 4.9.0 the Settings button targets the settings panel; an interim revision removed it on the belief that this was impossible, which was wrong — the host publishes no id-targeted *service*, but the settings shell pins its store seat to the registration so `slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` is a real route, and the host's own tests drive the dialog exactly that way). While Refresh runs its icon turns: the batch is serial, so the slowest account decides when it stops. **Refresh** asks every account checked into this panel's account pool to answer once (through the pool's `action=test` route — the same batch the pool section's own button runs) and reports the outcome in the panel's foot: `Pool tested · 2 of 2 answered`. **Two things about that press the label cannot say, so both live in the tooltip and in the foot line**: it tests the **pool's checked-in members**, not every account in the table you are looking at, and it is the **only press in this plugin that spends credits** (one minimal ping per member) — a bare "Refresh" reads as free, so the tooltip is worth reading. To change anything, open **Settings → Trae** — the plugin's only configuration surface. The balance needs no refresh button of its own — it re-reads when the panel opens, on every account switch, and on every refresh interval, which is why that button was replaced rather than joined by another. 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).
|
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
- **每个模型的思考强度可选,默认档不再显示成 `Default`** —— Trae 的目录一直在发布每个模型自己的默认档,插件也一直解析进了目录,但**从没报给 DSH**,所以模型选择器只能说 `Default`:既看不出这个模型会跑哪一档,也没法固定成别的档。现在适配器把默认档报上去,宿主会把它物化进每一个没有指名档位的请求,模型栏里每个声明了档位的模型都多一个下拉,第一项写的正是**它实际会跑的档**(例如「默认(high)」)。取值优先级是**你选的 > 上游默认 > 该模型自己的中档**;选「默认」是把这个键**删掉**,而不是存一个恰好等于今天默认值的档(后者看着一样,却把选择钉死了,上游改默认时就不再跟随)。保存的档位若已被目录撤回,会被忽略而不是发出去——发出去会被宿主以 `UNSUPPORTED_REASONING_EFFORT` 拒掉。不选任何东西时行为与之前完全一致。
|
|
65
65
|
- **积分临近到期会提醒** —— 剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」;提醒条按到期紧迫程度逐级加重(**7 天 → 24 小时 → 已过期**),写明金额、套餐数和最早到期时间,避免只盯总额时悄悄丢掉最先到期的额度。国内版在用量页与输入框积分面板两处都显示(国际版是订阅制,没有逐包到期数据)。
|
|
66
66
|
- **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`,SOLO 通道没有这个模型)/ **被限流**(`4011`,Trae 限了这条通道)/ **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
|
|
67
|
-
> `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011
|
|
67
|
+
> `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。`4017`(Trae 风控临时限制账号)与 `4011` 同归**被限流**,同样会冷却等待;而 `4008` 是**额度耗尽**——开着账号池时会直接换下一个账号,关掉池则如实报错。
|
|
68
68
|
- **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
|
|
69
69
|
- **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求。Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),所以逐账号签到始终是主路径。账号池里另有一个「全部签到」,它**不是**把同一件事盲目重复:一旦上游回 `9095`(本机今日已领),整批立刻停止并把剩余账号如实标成「本机已签」,产出的是真相而不是一排假失败。国际版没有签到活动,因此不显示该按钮。
|
|
70
70
|
- **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,默认每 5 分钟自动刷新(间隔可调,见下一条)。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」与「设置」两个控件**(4.9.0 起「设置」改指设置面板;中途曾以为做不到而删掉,是错的——宿主确实没有 id 定向的服务,但设置面板把自己的 store 座位钉在注册声明上,`slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` 是真实可用的路径,宿主自己的测试就是这么驱动的)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。要改配置就去「设置 → Trae」:那是本插件唯一一处配置界面。积分本身不需要刷新按钮:**打开浮层、切换账号、每个刷新间隔**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
## 合规说明
|
|
15
15
|
|
|
16
|
+
缓存字段兼容处理参考了 `rockswang/wild-work` 的字段归一化思路,由本项目独立实现;该处理不推断缺失计数,也不将缓存统计直接认定为实际扣费。
|
|
17
|
+
|
|
16
18
|
- `dsh-trae-api` 使用 **MIT** 宽松许可,与项目自身的 MIT 许可证兼容;`laojichao/trae-local-api` 未声明许可证(保留所有权利),本项目仅将其作为协议调研线索,未复制、修改或分发其代码。
|
|
17
19
|
- MIT 许可要求保留上游版权声明。本项目**不重新打包或再分发**上述参考项目的源码,仅参考其架构思路与协议信息;`docs/` 中的分析文档亦为独立撰写。
|
|
18
20
|
- 若后续引入新的与 Trae 接入相关的依赖或复用其他项目的代码,必须在引入时同步更新本文件,并遵守对应许可证的署名与声明要求。
|
package/lib/index.js
CHANGED
|
@@ -746,7 +746,7 @@ var TraeCatalog = class {
|
|
|
746
746
|
//#region src/promo.ts
|
|
747
747
|
/** Zone used when upstream omits `off_peak.time_zone` (CN accounts). */
|
|
748
748
|
const TRAE_PROMO_DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
749
|
-
function record$
|
|
749
|
+
function record$2(value) {
|
|
750
750
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
751
751
|
}
|
|
752
752
|
function finite(value) {
|
|
@@ -757,7 +757,7 @@ function percent(value) {
|
|
|
757
757
|
return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
|
|
758
758
|
}
|
|
759
759
|
function ratePair(raw) {
|
|
760
|
-
const entry = record$
|
|
760
|
+
const entry = record$2(raw);
|
|
761
761
|
if (entry === void 0) return {};
|
|
762
762
|
const beforeRate = finite(entry["before_consumption_rate"]);
|
|
763
763
|
const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
|
|
@@ -772,7 +772,7 @@ function windows(raw) {
|
|
|
772
772
|
const list = Array.isArray(raw) ? raw : [];
|
|
773
773
|
const parsed = [];
|
|
774
774
|
for (const item of list) {
|
|
775
|
-
const entry = record$
|
|
775
|
+
const entry = record$2(item);
|
|
776
776
|
if (entry === void 0) continue;
|
|
777
777
|
const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
|
|
778
778
|
const startMinute = finite(entry["start_minute"]);
|
|
@@ -805,22 +805,22 @@ function parseTraeActivityDiscount(value) {
|
|
|
805
805
|
return;
|
|
806
806
|
}
|
|
807
807
|
}
|
|
808
|
-
const raw = record$
|
|
808
|
+
const raw = record$2(source);
|
|
809
809
|
if (raw === void 0 || raw["enable"] !== true) return void 0;
|
|
810
|
-
const data = record$
|
|
810
|
+
const data = record$2(raw["data"]);
|
|
811
811
|
if (data === void 0) return void 0;
|
|
812
812
|
const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
|
|
813
|
-
const currentRaw = record$
|
|
813
|
+
const currentRaw = record$2(data["current"]);
|
|
814
814
|
const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
|
|
815
815
|
const current = currentRaw === void 0 ? void 0 : {
|
|
816
816
|
...ratePair(currentRaw),
|
|
817
817
|
...currentType === void 0 ? {} : { discountType: currentType }
|
|
818
818
|
};
|
|
819
819
|
const memberRaw = data["member"];
|
|
820
|
-
const member = record$
|
|
820
|
+
const member = record$2(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
|
|
821
821
|
const subsidyRaw = data["subsidy"];
|
|
822
|
-
const subsidy = record$
|
|
823
|
-
const offPeakRaw = record$
|
|
822
|
+
const subsidy = record$2(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
|
|
823
|
+
const offPeakRaw = record$2(data["off_peak"]);
|
|
824
824
|
const offPeak = offPeakRaw === void 0 ? void 0 : {
|
|
825
825
|
...ratePair(offPeakRaw),
|
|
826
826
|
...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
|
|
@@ -6225,6 +6225,58 @@ async function classifyEnterpriseResponse(response) {
|
|
|
6225
6225
|
return { refusal: data === "" ? "enterprise gateway refused the request" : data };
|
|
6226
6226
|
}
|
|
6227
6227
|
//#endregion
|
|
6228
|
+
//#region src/usage-cache.ts
|
|
6229
|
+
/** Independently implemented from the field-compatibility approach in wild-work.
|
|
6230
|
+
* Keep explicit zero, reject invalid counters, and preserve the original usage.
|
|
6231
|
+
* A missing counter is not evidence that the request was billed without caching.
|
|
6232
|
+
*/
|
|
6233
|
+
/**
|
|
6234
|
+
* A plain object, or `{}`.
|
|
6235
|
+
*
|
|
6236
|
+
* The guard matters because the input is an UPSTREAM PAYLOAD, not a typed
|
|
6237
|
+
* record: `released v4.18.0` only ever consumed individually type-checked
|
|
6238
|
+
* fields from a decoded event, so a non-object `token_usage` payload was
|
|
6239
|
+
* harmless. Normalization reads the payload as a whole, which made
|
|
6240
|
+
* `data: null` throw inside the stream's `start()` and kill a live answer —
|
|
6241
|
+
* see the regression test in `tests/cache-compat.spec.ts`. Arrays are also
|
|
6242
|
+
* excluded: spreading one turns `[1,2]` into `{"0":1,"1":2}`, which would be
|
|
6243
|
+
* forwarded as a usage object.
|
|
6244
|
+
*/
|
|
6245
|
+
function record$1(value) {
|
|
6246
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
6247
|
+
}
|
|
6248
|
+
/** First finite, non-negative candidate; anything else is "upstream did not say". */
|
|
6249
|
+
function counter(...values) {
|
|
6250
|
+
return values.find((value) => typeof value === "number" && Number.isFinite(value) && value >= 0);
|
|
6251
|
+
}
|
|
6252
|
+
/**
|
|
6253
|
+
* Normalize cache accounting into the OpenAI spelling pi-ai reads.
|
|
6254
|
+
*
|
|
6255
|
+
* @param usage - the `token_usage` payload exactly as upstream sent it. Typed
|
|
6256
|
+
* `unknown` rather than `Record<string, unknown>` on purpose: the caller has
|
|
6257
|
+
* only just parsed JSON, and a payload of `null` / an array / a scalar is a
|
|
6258
|
+
* shape this function must survive rather than a type the caller guarantees.
|
|
6259
|
+
*/
|
|
6260
|
+
function normalizeUsageCache(usage) {
|
|
6261
|
+
const source = record$1(usage);
|
|
6262
|
+
const details = record$1(source["prompt_tokens_details"]);
|
|
6263
|
+
const input = record$1(source["input_tokens_details"]);
|
|
6264
|
+
const read = counter(details["cached_tokens"], source["prompt_cache_hit_tokens"], source["cache_read_input_tokens"], input["cached_tokens"], source["cached_tokens"]);
|
|
6265
|
+
const write = counter(details["cache_write_tokens"], source["cache_creation_input_tokens"], input["cache_write_tokens"]);
|
|
6266
|
+
const normalizedDetails = { ...details };
|
|
6267
|
+
delete normalizedDetails["cached_tokens"];
|
|
6268
|
+
delete normalizedDetails["cache_write_tokens"];
|
|
6269
|
+
if (read === void 0 && write === void 0 && source["prompt_tokens_details"] === void 0) return { ...source };
|
|
6270
|
+
return {
|
|
6271
|
+
...source,
|
|
6272
|
+
prompt_tokens_details: {
|
|
6273
|
+
...normalizedDetails,
|
|
6274
|
+
...read === void 0 ? {} : { cached_tokens: read },
|
|
6275
|
+
...write === void 0 ? {} : { cache_write_tokens: write }
|
|
6276
|
+
}
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
//#endregion
|
|
6228
6280
|
//#region src/sse.ts
|
|
6229
6281
|
/** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
|
|
6230
6282
|
var SseDecoder = class {
|
|
@@ -6311,15 +6363,18 @@ function decodeTraeEvent(event) {
|
|
|
6311
6363
|
type: "progress",
|
|
6312
6364
|
notice: payload
|
|
6313
6365
|
};
|
|
6314
|
-
if (event.event === "token_usage")
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6366
|
+
if (event.event === "token_usage") {
|
|
6367
|
+
const details = normalizeUsageCache(record)["prompt_tokens_details"];
|
|
6368
|
+
return {
|
|
6369
|
+
type: "usage",
|
|
6370
|
+
...typeof record["prompt_tokens"] === "number" ? { inputTokens: record["prompt_tokens"] } : {},
|
|
6371
|
+
...typeof record["completion_tokens"] === "number" ? { outputTokens: record["completion_tokens"] } : {},
|
|
6372
|
+
...typeof record["total_tokens"] === "number" ? { totalTokens: record["total_tokens"] } : {},
|
|
6373
|
+
...typeof record["reasoning_tokens"] === "number" ? { reasoningTokens: record["reasoning_tokens"] } : {},
|
|
6374
|
+
...typeof details?.["cached_tokens"] === "number" ? { cacheReadTokens: details["cached_tokens"] } : {},
|
|
6375
|
+
...typeof details?.["cache_write_tokens"] === "number" ? { cacheWriteTokens: details["cache_write_tokens"] } : {}
|
|
6376
|
+
};
|
|
6377
|
+
}
|
|
6323
6378
|
if (event.event === "metadata" || event.event === "timing_cost") return {
|
|
6324
6379
|
type: "diagnostic",
|
|
6325
6380
|
...record["prompt_cache_pool_record"] === void 0 ? {} : { promptCachePoolRecord: record["prompt_cache_pool_record"] },
|
|
@@ -6389,6 +6444,7 @@ function decodeTraeEvent(event) {
|
|
|
6389
6444
|
* credential, plugin or account fault, and not something to re-login for.
|
|
6390
6445
|
*/
|
|
6391
6446
|
const TRAE_ERROR_HINTS = {
|
|
6447
|
+
4017: "Trae temporarily restricted this account through risk control",
|
|
6392
6448
|
1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
|
|
6393
6449
|
2001: "Trae has not published its function config for this route (an upstream condition, not a local credential or config problem)",
|
|
6394
6450
|
4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
|
|
@@ -6437,14 +6493,18 @@ function describeTraeFailure(failure) {
|
|
|
6437
6493
|
* replay — correct, since a throttled route is not a route-compatibility
|
|
6438
6494
|
* problem.
|
|
6439
6495
|
*
|
|
6496
|
+
* `4008` means exhausted SOLO quota and shares the credit failure category.
|
|
6497
|
+
* `4017` restricts the account temporarily and shares the rate-limit category.
|
|
6498
|
+
* These account failures must reach pooled-account failover; replaying another
|
|
6499
|
+
* route for the same account cannot restore quota or remove its restriction.
|
|
6440
6500
|
* Everything else stays a client-side rejection.
|
|
6441
6501
|
*/
|
|
6442
6502
|
function classifyTraeFailure(failure) {
|
|
6443
|
-
if (failure.code === 1005) return {
|
|
6503
|
+
if (failure.code === 1005 || failure.code === 4008) return {
|
|
6444
6504
|
status: 402,
|
|
6445
6505
|
kind: "hard_credit"
|
|
6446
6506
|
};
|
|
6447
|
-
if (failure.code === 4011) return {
|
|
6507
|
+
if (failure.code === 4011 || failure.code === 4017) return {
|
|
6448
6508
|
status: 429,
|
|
6449
6509
|
kind: "soft_rate"
|
|
6450
6510
|
};
|
|
@@ -6544,20 +6604,8 @@ function bridgeTraeSource(source, model, log) {
|
|
|
6544
6604
|
delta["tool_calls"] = toolCalls;
|
|
6545
6605
|
}
|
|
6546
6606
|
if (Object.keys(delta).length > 0) controller.enqueue(chunk(delta));
|
|
6547
|
-
} else if (decoded.type === "usage")
|
|
6548
|
-
|
|
6549
|
-
const cacheWrite = decoded.cacheWriteTokens;
|
|
6550
|
-
const details = {
|
|
6551
|
-
...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
|
|
6552
|
-
...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
|
|
6553
|
-
};
|
|
6554
|
-
usage = {
|
|
6555
|
-
...decoded.inputTokens === void 0 ? {} : { prompt_tokens: decoded.inputTokens },
|
|
6556
|
-
...decoded.outputTokens === void 0 ? {} : { completion_tokens: decoded.outputTokens },
|
|
6557
|
-
...decoded.totalTokens === void 0 ? {} : { total_tokens: decoded.totalTokens },
|
|
6558
|
-
...Object.keys(details).length === 0 ? {} : { prompt_tokens_details: details }
|
|
6559
|
-
};
|
|
6560
|
-
} else if (decoded.type === "done") {
|
|
6607
|
+
} else if (decoded.type === "usage") usage = normalizeUsageCache(JSON.parse(event.data));
|
|
6608
|
+
else if (decoded.type === "done") {
|
|
6561
6609
|
upstreamEnded = true;
|
|
6562
6610
|
if (!emittedFinishReason) {
|
|
6563
6611
|
emittedFinishReason = true;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
|
|
5
|
-
"version": "4.18.
|
|
5
|
+
"version": "4.18.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|