dsh-connect-trae 4.17.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 +135 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/index.d.ts +24 -5
- package/lib/index.js +160 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,140 @@
|
|
|
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
|
+
|
|
45
|
+
## 4.18.0 (2026-11-09)
|
|
46
|
+
|
|
47
|
+
### 企业面板的发现层收录租户自配模型(`config_source: 2`)
|
|
48
|
+
|
|
49
|
+
企业租户可以在 Trae 企业控制台配置自己的模型。Trae 自己的选择器能看到它们,插件的企业
|
|
50
|
+
面板一直不列——这批行由企业侧 `get_detail_param` 返回(`config_source: 2` +
|
|
51
|
+
`usage: 'chat_completion'`),**不在**企业侧 Remote 目录里,所以只有「wire 提升」一条路
|
|
52
|
+
能看见它们;而提升门槛只接受 `config_source === 1`,又拒绝「`display_name` 等于
|
|
53
|
+
`config_name`」的行——那恰好是租户配置模型的常见形态。两条门槛各自都足以挡掉它们。
|
|
54
|
+
|
|
55
|
+
**改法。** 提升门槛接受 `2`,且这类行允许用 `config_name` 当显示名。提升改为 **opt-in**
|
|
56
|
+
(`mergeTraeModelSources(..., { allowTenantConfigured })`),**只有企业面板传 `true`**:
|
|
57
|
+
这些行计费落在租户池,放进消费面板等于让用户选到一个不属于自己计费面的模型。另外两条
|
|
58
|
+
区域本来也取不到它们——它们的 wire 客户端钉在消费网关,那批行只出现在企业网关的名册里。
|
|
59
|
+
|
|
60
|
+
**这批模型现在调用不通,这是刻意不做的部分。** 租户配置的模型属于
|
|
61
|
+
`custom_openai_compatible`、客户端直连(`use_remote_service: false`、自带 base_url),
|
|
62
|
+
插件没有对应传输通道,实测走 `llm_utils_chat` 返回 `4001`。本次只修发现层,通道另开
|
|
63
|
+
issue 讨论——不做「静默假设能调」这种事。真要选中时,拿到的是桥接层翻译过的可读错误
|
|
64
|
+
(`4001` → 该 SOLO function 不提供此模型),不是一次空白回复。
|
|
65
|
+
|
|
66
|
+
### 企业版图片请求预算降到 8 MiB:网关只有 10 MiB,图文长任务不再 413
|
|
67
|
+
|
|
68
|
+
**症状。** 企业版跑带图的多步任务,跑到某一步整轮失败:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
400: {"message":"<html>...<title>413 Request Entity Too Large</title>...openresty/1.27.1.2...",
|
|
72
|
+
"type":"client","code":"client"}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
换模型、重试、少给几张图都没用——任务只要「长到某个长度」就必然挂。实测一次真实会话:
|
|
76
|
+
15 张图、base64 合计 **12.32 MB**,在第 20 步被拒。
|
|
77
|
+
|
|
78
|
+
**根因。** `maxRequestImageBytes` 不是口味设置,它决定宿主要不要走「卸掉最老的图、重发」
|
|
79
|
+
那条路:DSH 拿它和请求里**所有图片的 base64 总量**比较,超了就抛 `IMAGE_OFFLOAD_REQUIRED`,
|
|
80
|
+
由 `dsh-compaction-image-offload` 写占位符重试。**这条链路只有在预算低于网关实测上限时才成立**;
|
|
81
|
+
预算高于上限,宿主认为「装得下」,请求照发,被网关拍死在传输层——没有模型回复,无从恢复。
|
|
82
|
+
|
|
83
|
+
而 `REQUEST_IMAGE_BUDGETS` 把**所有区域**都钉在宿主默认的 20 MiB,注释理由是「对齐宿主默认值」。
|
|
84
|
+
那个理由对计数字段(`maxRequestImages` / `imageOffloadCountQuantum`,确是宿主契约)成立,
|
|
85
|
+
对**字节上限**不成立:字节上限要对齐的是**网关**,而三个网关差 2.5 倍。
|
|
86
|
+
|
|
87
|
+
**实测(2026-11-09,未鉴权探针)。** openresty 在**鉴权之前**就按 body 体积拒绝,所以状态码本身就是
|
|
88
|
+
上限,且不消耗任何凭据或积分:
|
|
89
|
+
|
|
90
|
+
| 网关 | 10,485,758 字节 | 10,485,762 字节 | 结论 |
|
|
91
|
+
| --- | --- | --- | --- |
|
|
92
|
+
| `console.enterprise.trae.cn`(`create_agent_task`) | `401` | **`413`** | 上限 10 MiB |
|
|
93
|
+
| `console.enterprise.trae.cn`(`llm_utils_chat`) | `401` | **`413`** | 上限 10 MiB |
|
|
94
|
+
| `trae-api-cn.mchost.guru` | `401`(测到 48 MiB) | — | 未触顶 |
|
|
95
|
+
| `coresg-normal.trae.ai` | `401`(测到 20 MiB) | — | 未触顶 |
|
|
96
|
+
|
|
97
|
+
**修复。** 只有企业版降到 **8 MiB**,另外两条区域保持 20 MiB:
|
|
98
|
+
|
|
99
|
+
| 区域 | 改前 | 改后 |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `trae-enterprise` | 20 MiB | **8 MiB** |
|
|
102
|
+
| `trae`(CN) | 20 MiB | 20 MiB |
|
|
103
|
+
| `trae-global`(国际) | 20 MiB | 20 MiB |
|
|
104
|
+
|
|
105
|
+
**为什么是 8 而不是 10。** 预算只管图片,请求还要带系统提示、工具声明和消息文本。8 MiB 留 2 MiB
|
|
106
|
+
信封,而实测请求远没到这个量级(工具声明约 49 KB)。**为什么不干脆把三条都降下来**:CN 与国际
|
|
107
|
+
网关实测 48 MiB / 20 MiB 都收,降它们只会白白卸掉用户本来能看到的图——那是静默的质量回退,
|
|
108
|
+
不是修复。
|
|
109
|
+
|
|
110
|
+
用那次失败会话的同一批图片跑宿主真实的卸载算法(`imageOffloadCountQuantum: 10`,超额按 10 的
|
|
111
|
+
整数倍卸):
|
|
112
|
+
|
|
113
|
+
| | 改前(20 MiB) | 改后(8 MiB) |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| 卸载是否触发 | 从不 | 第 13 张图时 |
|
|
116
|
+
| 卸载数量 | 0 / 15 | 5 / 15 |
|
|
117
|
+
| 实际发出 | ≈11.92 MiB → **413** | ≈7.99 MiB → 通过 |
|
|
118
|
+
|
|
119
|
+
**没选的做法。** ① 改宿主默认值——那是所有 provider 共享的契约,不该为一个网关让步。② 直接把
|
|
120
|
+
`imageOffloadCountQuantum` 调小——卸载粒度变细会增加重试轮次,而这次的问题是预算根本没触发
|
|
121
|
+
卸载,与粒度无关。③ 只发占位符不重试——用户会永久失去图片内容,而重试路径本来就是为此设计的。
|
|
122
|
+
|
|
123
|
+
### Tests
|
|
124
|
+
|
|
125
|
+
- 新增 `tests/adapter-image-budget.spec.ts`(3 例):企业版预算**严格小于**实测 10 MiB 上限;
|
|
126
|
+
CN 与国际**仍是**宿主默认 20 MiB;三条区域的计数上限都保持 50 / 10。
|
|
127
|
+
第一例在修复前失败(`expected 20971520 to be less than 10485760`),修复后通过。
|
|
128
|
+
另外两例钉住「不要把三条一起降下来」这条回退路径。
|
|
129
|
+
- **合并时补了一条下界**(≥ 5 MiB,即实测上限的一半):「小于 10 MiB」是个**任何更小的数**
|
|
130
|
+
都满足的断言——改成 1 MiB 也照样绿,而那会把网关收得下的图白白卸掉,正是另外两条区域
|
|
131
|
+
被保护着不许犯的那个错,只是从保守那一侧犯。实测把常量改成 `1_048_576` 会红
|
|
132
|
+
(`expected 1048576 to be greater than or equal to 5242880`)。
|
|
133
|
+
- `tests/catalog.spec.ts` 新增 4 例(租户自配模型):被提升(含 `config_name` 当显示名的形态);
|
|
134
|
+
未 opt-in 时一行都不出(缺省与显式 `false` 两路);`usage` 非 `chat_completion` 仍被拒;
|
|
135
|
+
空名与 `-` 仍被拒。变异验证:删掉 `allowTenantConfigured` 门控 → 第二例立刻变红。
|
|
136
|
+
- 全仓 **114 个文件 / 1469 条测试**通过。
|
|
137
|
+
|
|
3
138
|
## 4.17.0 (2026-11-09)
|
|
4
139
|
|
|
5
140
|
### 自动刷新改为可选项:整个插件一个节奏,默认 5 分钟
|
package/README.en.md
CHANGED
|
@@ -44,6 +44,7 @@ In the account pool, open **More settings** and choose **Claude style** or **Def
|
|
|
44
44
|
- **Three parallel providers** — the CN side registers as the `trae` provider (e.g. `DeepSeek-V4-Flash`, `DeepSeek-V4-Pro`), the enterprise side as `trae-enterprise`, and the international side as `trae-global` (the Gemini/GPT/MiniMax roster); **all three rosters appear in the DSH model picker simultaneously**: different sessions can each pick a side without interfering.
|
|
45
45
|
- **Enterprise is a separate billing channel, not an alias for CN** — the same CN credential, when it belongs to a `tob_online` tenant, is served by **two** gateways: the consumer pool (billed against personal credits) and the tenant pool (billed against the enterprise allowance). Same model, two payers, so **you** choose who pays; the plugin never guesses. `trae` never touches the enterprise gateway, and `trae-enterprise` **never falls back** — a refusal is reported as a refusal instead of quietly re-running on personal credits. Enterprise is **off by default**: a personal-only account cannot use the tenant pool, so shipping it enabled would mean handing everyone a provider that fails on first use.
|
|
46
46
|
- **Any panel can be switched off on its own** — each tab carries its own checkbox (CN and international checked by default, enterprise not): unchecking **withdraws that provider from the DSH model picker entirely** (not merely hiding the tab) and stops all of its requests; checking it again restores it with the account, directory, picks, and context budgets intact. If you never use the international side, just uncheck it.
|
|
47
|
+
- **Enterprise models configured by the tenant now show up** — a tenant can configure models of its own in the Trae enterprise console. Those rows (`config_source: 2`) are absent from the enterprise-side Remote directory, so the plugin used to hide them entirely; they now appear on the enterprise panel and in the DSH picker. **They are not callable yet**: those models are client-direct (`use_remote_service: false`, carrying their own `base_url`) and the plugin has no transport for that channel, so selecting one fails honestly (`4001` → this SOLO function does not serve it) instead of failing silently or pretending to answer. They bill the tenant pool, which is why they appear **only on the enterprise panel**; CN and international are untouched — those two read consumer-gateway rosters to begin with.
|
|
47
48
|
- **Tabbed settings card** — the card's top carries a "Domestic / Enterprise / Global" tab bar; each tab holds its own account picker, usage overview, and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions. CN and enterprise share one CN credential pool but keep **separate account bindings and model picks**: the two rosters differ (`DeepSeek-V4.1-Flash` is listed only on the enterprise pool), so picks never overwrite each other.
|
|
48
49
|
- **The card then splits into "Account / Models / Diagnostics"** (learned from the sibling `dsh-connect-workbuddy`). The region row above is the **scope** — which provider this whole card is about — while these are sections of that one card: Account covers who serves, what is left, the check-in and the pool; Models covers which models to tick, image input, the context budget and Save; Diagnostics read-only lists the panel, the provider id, the serving account, both token deadlines, the catalog/enabled model counts, whether the pool and the check-in are offered, and the **verbatim** text of this panel's last reported failure. **As of 4.4.7 the diagnostics section carries one more line, "quarantined copies"**: a credential copy that could not be read — and was therefore renamed aside instead of being erased — is named there, together with how many accounts it still holds. "Where did the account I remember go?" is now answered by the card instead of by browsing `~/.dsh` for a file name; the signed-out screen shows the same notice, because that is the screen a user lands on after an account disappears. The split exists because the two halves **commit differently**: an account change lands on the click, a catalog change is a draft until Save — so a single long column put the save button three screens away from what it saves. Switching sections never refetches, never drops a half-filled model draft, and never rebuilds the pool's log. A signed-out card renders no section strip, so its one screen of instructions cannot be hidden behind a tab the user cannot see. **The Models section lays every model out at once, with no inner scrollbar** — it is not a little window scrolling inside a page that already scrolls; the card's own page is the only scroll.
|
|
49
50
|
- **Multiplier in the model name** — model names show the credit multiplier in Trae's own menu format (e.g. `GLM-5.2 · x0.79`), updated with each directory refresh.
|
|
@@ -58,7 +59,7 @@ In the account pool, open **More settings** and choose **Claude style** or **Def
|
|
|
58
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.
|
|
59
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).
|
|
60
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.
|
|
61
|
-
> `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.
|
|
62
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.
|
|
63
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.
|
|
64
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
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
- **文生图(`trae_draw_image`)** —— 把登录账号的 Trae 文生图能力接进会话:模型画一张图,插件把**图片字节一起取回来**、写进会话目录再显示给你。**默认关闭、按面板单独开**——生图真实消耗积分(实测 CN 个人版 `1920x1920` 一张约 8.8),所以它和识图那种「让不让看图」的开关不同,只能由你点头。协议、尺寸边界、取图那两步,以及为什么「取图」那条 GET 其实是又一次生成,见下方「[文生图](#文生图trae_draw_image)」。
|
|
57
57
|
- **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。**美国区(`userRegion: "US"` / `*.traeapi.us`)目前「识别但不接入」**:插件不会把 US 凭据当成新加坡区发出去,而会在账号面板明确写「美国区(US)暂不支持」并点名账号,同时说明重新登录不会改变结果(issue #66)。
|
|
58
58
|
- **目录与勾选按面板隔离** —— 三个面板各一套模型目录、勾选、图片开关与上下文预算,各自读各自的槽位。名录从**该面板所钉的那条网关**读取——原本钉在企业池的客户端去读消费池名录,会永远发现不了自己池的独占模型。
|
|
59
|
+
- **企业面板会列出租户自配的模型** —— 企业租户可以在 Trae 企业控制台配置自己的模型,这类行(`config_source: 2`)不在企业侧的 Remote 目录里,过去在插件里完全看不见,现在会出现在企业面板与 DSH 选择器里。**它们目前还调不通**:这批模型是客户端直连(`use_remote_service: false`、自带 `base_url`),插件没有对应传输通道,选中后调用会如实报错(`4001` → 该 SOLO function 不提供此模型),不会静默失败或假装成功。计费属租户池,所以**只在企业面板出现**,国内版与国际版不受影响——那两个面板本来也只读消费网关的名录。
|
|
59
60
|
- **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
|
|
60
61
|
- **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。**你的选择会被记住并长期生效**:插件不会因为某个账号额度用尽、或检测到另一个可用账号就自作主张替你切换(换账号意味着换人计费)——唯一的例外是你**亲自打开**的账号池(见下一条),那是一个显式选择,而不是插件的自作主张。所以当绑定的账号额度耗尽时,卡片会**明确告诉你**,并把本机其他还有额度的账号连同其可用积分数列出来,由你一键切换——而不是让面板一直空着、也不自动把你切走。「刷新账号与 Token」每次都会回报结果(找到几个账号、当前仍绑定谁),不会再「点了没反应」。下拉里显示的是产品名(`Trae` / `TRAE SOLO` / 国际版),便于分辨凭据来自哪个安装。
|
|
61
62
|
- **账号导入 / 导出** —— 不用再为了用第二个账号而把凭据手工写进默认路径、顶掉上一个。在卡片账号区的「导入 / 导出账号」里:**导出**勾选若干账号下载一份 JSON;**导入**选择文件后先看**脱敏预览**(只显示账号标识与「是否有 Token」,不含凭据本身)再勾选导入。文件格式与同族 `dsh-connect-workbuddy` **互通**,两边导出的文件可以互相导入。导入按账号去重(同一账号再导入即覆盖更新),**认不出所属区域或因缺 Token 的记录会被跳过并计数**——国内版与国际版是两条并行计费通道,跨区导入会导致跨区计费,所以这里只认同能**正向识别**的区域(企业版共用 CN 凭据池,不构成第三条导入通道)。导出文件里是**有效 Token,请当作密码保管**。(issue #37)
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
- **每个模型的思考强度可选,默认档不再显示成 `Default`** —— Trae 的目录一直在发布每个模型自己的默认档,插件也一直解析进了目录,但**从没报给 DSH**,所以模型选择器只能说 `Default`:既看不出这个模型会跑哪一档,也没法固定成别的档。现在适配器把默认档报上去,宿主会把它物化进每一个没有指名档位的请求,模型栏里每个声明了档位的模型都多一个下拉,第一项写的正是**它实际会跑的档**(例如「默认(high)」)。取值优先级是**你选的 > 上游默认 > 该模型自己的中档**;选「默认」是把这个键**删掉**,而不是存一个恰好等于今天默认值的档(后者看着一样,却把选择钉死了,上游改默认时就不再跟随)。保存的档位若已被目录撤回,会被忽略而不是发出去——发出去会被宿主以 `UNSUPPORTED_REASONING_EFFORT` 拒掉。不选任何东西时行为与之前完全一致。
|
|
64
65
|
- **积分临近到期会提醒** —— 剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」;提醒条按到期紧迫程度逐级加重(**7 天 → 24 小时 → 已过期**),写明金额、套餐数和最早到期时间,避免只盯总额时悄悄丢掉最先到期的额度。国内版在用量页与输入框积分面板两处都显示(国际版是订阅制,没有逐包到期数据)。
|
|
65
66
|
- **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`,SOLO 通道没有这个模型)/ **被限流**(`4011`,Trae 限了这条通道)/ **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
|
|
66
|
-
> `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011
|
|
67
|
+
> `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。`4017`(Trae 风控临时限制账号)与 `4011` 同归**被限流**,同样会冷却等待;而 `4008` 是**额度耗尽**——开着账号池时会直接换下一个账号,关掉池则如实报错。
|
|
67
68
|
- **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
|
|
68
69
|
- **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求。Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),所以逐账号签到始终是主路径。账号池里另有一个「全部签到」,它**不是**把同一件事盲目重复:一旦上游回 `9095`(本机今日已领),整批立刻停止并把剩余账号如实标成「本机已签」,产出的是真相而不是一排假失败。国际版没有签到活动,因此不显示该按钮。
|
|
69
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.d.ts
CHANGED
|
@@ -462,14 +462,22 @@ interface TraeWireModel {
|
|
|
462
462
|
* account's custom models (`usage: 'custom_model'`); none of those belong
|
|
463
463
|
* in a model picker. Measured: this one test removed 23 of 50 wire-only
|
|
464
464
|
* rows, and every row it removed was a tool or a custom model.
|
|
465
|
-
* - `config_source === 1`
|
|
466
|
-
*
|
|
467
|
-
*
|
|
465
|
+
* - `config_source === 1`, or `2` once the caller opts in — `1` is a Trae
|
|
466
|
+
* preset; `2` is a model the tenant's own enterprise console configured
|
|
467
|
+
* (see `allowTenantConfigured` on {@link mergeTraeModelSources}, which is
|
|
468
|
+
* what decides whether a `2` may pass). `3` is the account's own custom
|
|
469
|
+
* model, carrying the user's own base_url and key: the plugin must not
|
|
470
|
+
* offer to bill a model it does not host, so `3` never passes.
|
|
468
471
|
* - a REAL display name — not empty, not the upstream placeholder `-`, not a
|
|
469
472
|
* bare config identifier (`search_agent_qwen_fast`,
|
|
470
473
|
* `computer_use_subagent`), and not the config's OWN id. A model with no
|
|
471
474
|
* name has nothing to show in the picker.
|
|
472
475
|
*
|
|
476
|
+
* A tenant-configured row (`2`) is EXEMPT from the "own id" clause: those
|
|
477
|
+
* rows commonly publish the config_name as the display name, so the clause
|
|
478
|
+
* would reject precisely the rows the opt-in exists to surface. The other
|
|
479
|
+
* name tests still apply to them.
|
|
480
|
+
*
|
|
473
481
|
* The display-name-collision test lives in the merge itself (it needs the
|
|
474
482
|
* Remote rows to compare against); see {@link mergeTraeModelSources}. It is a
|
|
475
483
|
* CAPABILITY test there, not a name test: a collision only suppresses a row
|
|
@@ -535,8 +543,19 @@ declare function isPromotableWireModel(model: TraeWireModel): boolean;
|
|
|
535
543
|
* PROMOTED (issue #46) — subject to {@link isPromotableWireModel}. The
|
|
536
544
|
* skeleton systematically omits the dated snapshot configs, so without
|
|
537
545
|
* promotion every snapshot the IDE offers is unreachable from the plugin.
|
|
538
|
-
|
|
539
|
-
|
|
546
|
+
*
|
|
547
|
+
* @param options.allowTenantConfigured - may a `config_source: 2` row (a model
|
|
548
|
+
* the tenant's enterprise console configured) be promoted? Opt-in, and only
|
|
549
|
+
* the enterprise panel opts in: those rows bill the tenant pool, so surfacing
|
|
550
|
+
* them on a consumer panel would offer a model outside the caller's billing
|
|
551
|
+
* face. The rows also are not callable through `llm_utils_chat` today — they
|
|
552
|
+
* are client-direct with their own base_url (`use_remote_service: false`) and
|
|
553
|
+
* the gateway answers `4001` — which the discovery layer does not hide, but
|
|
554
|
+
* which is why this is a narrow opt-in rather than a default.
|
|
555
|
+
*/
|
|
556
|
+
declare function mergeTraeModelSources(remote: readonly TraeDiscoveredModel[], wire: readonly TraeWireModel[], options?: {
|
|
557
|
+
allowTenantConfigured?: boolean;
|
|
558
|
+
}): TraeModelInfo[];
|
|
540
559
|
/**
|
|
541
560
|
* Demote a NON-chat `wireFunction` to "no function", keeping the row.
|
|
542
561
|
*
|
package/lib/index.js
CHANGED
|
@@ -371,14 +371,22 @@ function applyImageSelection(models, selected) {
|
|
|
371
371
|
* account's custom models (`usage: 'custom_model'`); none of those belong
|
|
372
372
|
* in a model picker. Measured: this one test removed 23 of 50 wire-only
|
|
373
373
|
* rows, and every row it removed was a tool or a custom model.
|
|
374
|
-
* - `config_source === 1`
|
|
375
|
-
*
|
|
376
|
-
*
|
|
374
|
+
* - `config_source === 1`, or `2` once the caller opts in — `1` is a Trae
|
|
375
|
+
* preset; `2` is a model the tenant's own enterprise console configured
|
|
376
|
+
* (see `allowTenantConfigured` on {@link mergeTraeModelSources}, which is
|
|
377
|
+
* what decides whether a `2` may pass). `3` is the account's own custom
|
|
378
|
+
* model, carrying the user's own base_url and key: the plugin must not
|
|
379
|
+
* offer to bill a model it does not host, so `3` never passes.
|
|
377
380
|
* - a REAL display name — not empty, not the upstream placeholder `-`, not a
|
|
378
381
|
* bare config identifier (`search_agent_qwen_fast`,
|
|
379
382
|
* `computer_use_subagent`), and not the config's OWN id. A model with no
|
|
380
383
|
* name has nothing to show in the picker.
|
|
381
384
|
*
|
|
385
|
+
* A tenant-configured row (`2`) is EXEMPT from the "own id" clause: those
|
|
386
|
+
* rows commonly publish the config_name as the display name, so the clause
|
|
387
|
+
* would reject precisely the rows the opt-in exists to surface. The other
|
|
388
|
+
* name tests still apply to them.
|
|
389
|
+
*
|
|
382
390
|
* The display-name-collision test lives in the merge itself (it needs the
|
|
383
391
|
* Remote rows to compare against); see {@link mergeTraeModelSources}. It is a
|
|
384
392
|
* CAPABILITY test there, not a name test: a collision only suppresses a row
|
|
@@ -391,9 +399,10 @@ function applyImageSelection(models, selected) {
|
|
|
391
399
|
*/
|
|
392
400
|
function isPromotableWireModel(model) {
|
|
393
401
|
if (model.usage !== "chat_completion") return false;
|
|
394
|
-
if (model.configSource !== 1) return false;
|
|
402
|
+
if (model.configSource !== 1 && model.configSource !== 2) return false;
|
|
395
403
|
const name = model.name.trim();
|
|
396
404
|
if (name === "" || name === "-") return false;
|
|
405
|
+
if (model.configSource === 2) return true;
|
|
397
406
|
if (name === model.id.trim()) return false;
|
|
398
407
|
if (!name.includes(" ") && name.includes("_")) return false;
|
|
399
408
|
return true;
|
|
@@ -503,8 +512,17 @@ function wireEffortsOf(supported) {
|
|
|
503
512
|
* PROMOTED (issue #46) — subject to {@link isPromotableWireModel}. The
|
|
504
513
|
* skeleton systematically omits the dated snapshot configs, so without
|
|
505
514
|
* promotion every snapshot the IDE offers is unreachable from the plugin.
|
|
515
|
+
*
|
|
516
|
+
* @param options.allowTenantConfigured - may a `config_source: 2` row (a model
|
|
517
|
+
* the tenant's enterprise console configured) be promoted? Opt-in, and only
|
|
518
|
+
* the enterprise panel opts in: those rows bill the tenant pool, so surfacing
|
|
519
|
+
* them on a consumer panel would offer a model outside the caller's billing
|
|
520
|
+
* face. The rows also are not callable through `llm_utils_chat` today — they
|
|
521
|
+
* are client-direct with their own base_url (`use_remote_service: false`) and
|
|
522
|
+
* the gateway answers `4001` — which the discovery layer does not hide, but
|
|
523
|
+
* which is why this is a narrow opt-in rather than a default.
|
|
506
524
|
*/
|
|
507
|
-
function mergeTraeModelSources(remote, wire) {
|
|
525
|
+
function mergeTraeModelSources(remote, wire, options = {}) {
|
|
508
526
|
const wireByName = /* @__PURE__ */ new Map();
|
|
509
527
|
const wireById = /* @__PURE__ */ new Map();
|
|
510
528
|
for (const model of wire) if (!wireById.has(displayKey(model.id))) wireById.set(displayKey(model.id), model);
|
|
@@ -574,6 +592,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
574
592
|
for (const model of remote) claim(model.name, model.contextWindow);
|
|
575
593
|
for (const wireModel of wire) {
|
|
576
594
|
if (remoteIds.has(displayKey(wireModel.id))) continue;
|
|
595
|
+
if (wireModel.configSource === 2 && options.allowTenantConfigured !== true) continue;
|
|
577
596
|
if (!isPromotableWireModel(wireModel)) continue;
|
|
578
597
|
const collides = claimedNames.has(displayKey(wireModel.name));
|
|
579
598
|
if (collides && (wireModel.contextWindow ?? 0) <= (claimedNames.get(displayKey(wireModel.name)) ?? 0)) continue;
|
|
@@ -727,7 +746,7 @@ var TraeCatalog = class {
|
|
|
727
746
|
//#region src/promo.ts
|
|
728
747
|
/** Zone used when upstream omits `off_peak.time_zone` (CN accounts). */
|
|
729
748
|
const TRAE_PROMO_DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
730
|
-
function record$
|
|
749
|
+
function record$2(value) {
|
|
731
750
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
732
751
|
}
|
|
733
752
|
function finite(value) {
|
|
@@ -738,7 +757,7 @@ function percent(value) {
|
|
|
738
757
|
return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
|
|
739
758
|
}
|
|
740
759
|
function ratePair(raw) {
|
|
741
|
-
const entry = record$
|
|
760
|
+
const entry = record$2(raw);
|
|
742
761
|
if (entry === void 0) return {};
|
|
743
762
|
const beforeRate = finite(entry["before_consumption_rate"]);
|
|
744
763
|
const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
|
|
@@ -753,7 +772,7 @@ function windows(raw) {
|
|
|
753
772
|
const list = Array.isArray(raw) ? raw : [];
|
|
754
773
|
const parsed = [];
|
|
755
774
|
for (const item of list) {
|
|
756
|
-
const entry = record$
|
|
775
|
+
const entry = record$2(item);
|
|
757
776
|
if (entry === void 0) continue;
|
|
758
777
|
const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
|
|
759
778
|
const startMinute = finite(entry["start_minute"]);
|
|
@@ -786,22 +805,22 @@ function parseTraeActivityDiscount(value) {
|
|
|
786
805
|
return;
|
|
787
806
|
}
|
|
788
807
|
}
|
|
789
|
-
const raw = record$
|
|
808
|
+
const raw = record$2(source);
|
|
790
809
|
if (raw === void 0 || raw["enable"] !== true) return void 0;
|
|
791
|
-
const data = record$
|
|
810
|
+
const data = record$2(raw["data"]);
|
|
792
811
|
if (data === void 0) return void 0;
|
|
793
812
|
const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
|
|
794
|
-
const currentRaw = record$
|
|
813
|
+
const currentRaw = record$2(data["current"]);
|
|
795
814
|
const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
|
|
796
815
|
const current = currentRaw === void 0 ? void 0 : {
|
|
797
816
|
...ratePair(currentRaw),
|
|
798
817
|
...currentType === void 0 ? {} : { discountType: currentType }
|
|
799
818
|
};
|
|
800
819
|
const memberRaw = data["member"];
|
|
801
|
-
const member = record$
|
|
820
|
+
const member = record$2(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
|
|
802
821
|
const subsidyRaw = data["subsidy"];
|
|
803
|
-
const subsidy = record$
|
|
804
|
-
const offPeakRaw = record$
|
|
822
|
+
const subsidy = record$2(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
|
|
823
|
+
const offPeakRaw = record$2(data["off_peak"]);
|
|
805
824
|
const offPeak = offPeakRaw === void 0 ? void 0 : {
|
|
806
825
|
...ratePair(offPeakRaw),
|
|
807
826
|
...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
|
|
@@ -1504,6 +1523,10 @@ const INERT_AUTH = {
|
|
|
1504
1523
|
*
|
|
1505
1524
|
* `imageOffloadCountQuantum` must be a positive integer no greater than
|
|
1506
1525
|
* `maxRequestImages`; 10 ≤ 50 keeps the host's default ratio.
|
|
1526
|
+
*
|
|
1527
|
+
* The BYTE bound is the one field here that is not a host contract: it has to
|
|
1528
|
+
* match the gateway at the other end, and the gateways disagree (issue #81).
|
|
1529
|
+
* See {@link ENTERPRISE_MAX_REQUEST_IMAGE_BYTES}.
|
|
1507
1530
|
*/
|
|
1508
1531
|
const REQUEST_IMAGE_BUDGETS = {
|
|
1509
1532
|
maxRequestImageBytes: 20971520,
|
|
@@ -1512,6 +1535,54 @@ const REQUEST_IMAGE_BUDGETS = {
|
|
|
1512
1535
|
maxRequestImages: 50,
|
|
1513
1536
|
imageOffloadCountQuantum: 10
|
|
1514
1537
|
};
|
|
1538
|
+
/**
|
|
1539
|
+
* Request-image byte bound for the enterprise route, in bytes (8 MiB).
|
|
1540
|
+
*
|
|
1541
|
+
* WHY THE HOST DEFAULT IS WRONG HERE. `maxRequestImageBytes` is not a taste
|
|
1542
|
+
* setting — DSH compares it against the base64 aggregate of every image a
|
|
1543
|
+
* request re-inlines, and raising `IMAGE_OFFLOAD_REQUIRED` is how the oldest
|
|
1544
|
+
* images get replaced by placeholders and the request retried. That only works
|
|
1545
|
+
* if the bound is BELOW what the gateway accepts: a bound above it means the
|
|
1546
|
+
* host believes an unsendable request fits and sends it anyway.
|
|
1547
|
+
*
|
|
1548
|
+
* Measured 2026-11-09 with unauthenticated probes (openresty rejects on body
|
|
1549
|
+
* size before authentication, so no credentials or credits are involved):
|
|
1550
|
+
*
|
|
1551
|
+
* console.enterprise.trae.cn, /api/agent/v3/create_agent_task AND
|
|
1552
|
+
* /api/agent/v3/llm_utils_chat:
|
|
1553
|
+
* 10_485_758 bytes -> 401 10_485_762 bytes -> 413
|
|
1554
|
+
* => the cap is exactly 10 MiB (10_485_760)
|
|
1555
|
+
*
|
|
1556
|
+
* The other two regions have no comparable cap: `trae-api-cn.mchost.guru`
|
|
1557
|
+
* answered 401 at 48 MiB and `coresg-normal.trae.ai` at 20 MiB, so they keep
|
|
1558
|
+
* the host default — lowering them would offload images those gateways accept.
|
|
1559
|
+
*
|
|
1560
|
+
* WHY 8 MiB AND NOT 10. The budget bounds the IMAGES; the request also carries
|
|
1561
|
+
* the system prompt, tool declarations and message text. 8 MiB leaves 2 MiB of
|
|
1562
|
+
* envelope, which the measured requests did not come close to (tool
|
|
1563
|
+
* declarations ~49 KB). The failure this fixes: a real 15-image session reached
|
|
1564
|
+
* 12.32 MB of base64 image payload, under the old 20 MiB budget and over the
|
|
1565
|
+
* 10 MiB cap, and died at step 20 with `413 Request Entity Too Large`.
|
|
1566
|
+
*/
|
|
1567
|
+
const ENTERPRISE_MAX_REQUEST_IMAGE_BYTES = 8388608;
|
|
1568
|
+
/**
|
|
1569
|
+
* Image-request budgets for one route.
|
|
1570
|
+
*
|
|
1571
|
+
* The byte bound is region-specific; every other field is shared. Kept as a
|
|
1572
|
+
* function rather than two constants so the shared fields cannot drift apart.
|
|
1573
|
+
*
|
|
1574
|
+
* The return type is the profile's own field types rather than
|
|
1575
|
+
* `typeof REQUEST_IMAGE_BUDGETS`: that constant is `as const`, so its
|
|
1576
|
+
* `maxRequestImageBytes` is the LITERAL `20_971_520` and the enterprise arm
|
|
1577
|
+
* would not typecheck against it.
|
|
1578
|
+
* @param providerId - the route whose gateway will receive the request.
|
|
1579
|
+
*/
|
|
1580
|
+
function requestImageBudgets(providerId) {
|
|
1581
|
+
return providerId === "trae-enterprise" ? {
|
|
1582
|
+
...REQUEST_IMAGE_BUDGETS,
|
|
1583
|
+
maxRequestImageBytes: ENTERPRISE_MAX_REQUEST_IMAGE_BYTES
|
|
1584
|
+
} : REQUEST_IMAGE_BUDGETS;
|
|
1585
|
+
}
|
|
1515
1586
|
function toPiModel(info, baseUrl, providerId, withRate = true) {
|
|
1516
1587
|
return {
|
|
1517
1588
|
id: info.id,
|
|
@@ -1885,7 +1956,7 @@ function createTraeAdapter(options) {
|
|
|
1885
1956
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
1886
1957
|
modelErrors: /* @__PURE__ */ new Map(),
|
|
1887
1958
|
defaultContextWindow: FALLBACK_CONTEXT_WINDOW,
|
|
1888
|
-
...
|
|
1959
|
+
...requestImageBudgets(providerId),
|
|
1889
1960
|
piProvider: provider
|
|
1890
1961
|
};
|
|
1891
1962
|
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
@@ -6154,6 +6225,58 @@ async function classifyEnterpriseResponse(response) {
|
|
|
6154
6225
|
return { refusal: data === "" ? "enterprise gateway refused the request" : data };
|
|
6155
6226
|
}
|
|
6156
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
|
|
6157
6280
|
//#region src/sse.ts
|
|
6158
6281
|
/** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
|
|
6159
6282
|
var SseDecoder = class {
|
|
@@ -6240,15 +6363,18 @@ function decodeTraeEvent(event) {
|
|
|
6240
6363
|
type: "progress",
|
|
6241
6364
|
notice: payload
|
|
6242
6365
|
};
|
|
6243
|
-
if (event.event === "token_usage")
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
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
|
+
}
|
|
6252
6378
|
if (event.event === "metadata" || event.event === "timing_cost") return {
|
|
6253
6379
|
type: "diagnostic",
|
|
6254
6380
|
...record["prompt_cache_pool_record"] === void 0 ? {} : { promptCachePoolRecord: record["prompt_cache_pool_record"] },
|
|
@@ -6318,6 +6444,7 @@ function decodeTraeEvent(event) {
|
|
|
6318
6444
|
* credential, plugin or account fault, and not something to re-login for.
|
|
6319
6445
|
*/
|
|
6320
6446
|
const TRAE_ERROR_HINTS = {
|
|
6447
|
+
4017: "Trae temporarily restricted this account through risk control",
|
|
6321
6448
|
1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
|
|
6322
6449
|
2001: "Trae has not published its function config for this route (an upstream condition, not a local credential or config problem)",
|
|
6323
6450
|
4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
|
|
@@ -6366,14 +6493,18 @@ function describeTraeFailure(failure) {
|
|
|
6366
6493
|
* replay — correct, since a throttled route is not a route-compatibility
|
|
6367
6494
|
* problem.
|
|
6368
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.
|
|
6369
6500
|
* Everything else stays a client-side rejection.
|
|
6370
6501
|
*/
|
|
6371
6502
|
function classifyTraeFailure(failure) {
|
|
6372
|
-
if (failure.code === 1005) return {
|
|
6503
|
+
if (failure.code === 1005 || failure.code === 4008) return {
|
|
6373
6504
|
status: 402,
|
|
6374
6505
|
kind: "hard_credit"
|
|
6375
6506
|
};
|
|
6376
|
-
if (failure.code === 4011) return {
|
|
6507
|
+
if (failure.code === 4011 || failure.code === 4017) return {
|
|
6377
6508
|
status: 429,
|
|
6378
6509
|
kind: "soft_rate"
|
|
6379
6510
|
};
|
|
@@ -6473,20 +6604,8 @@ function bridgeTraeSource(source, model, log) {
|
|
|
6473
6604
|
delta["tool_calls"] = toolCalls;
|
|
6474
6605
|
}
|
|
6475
6606
|
if (Object.keys(delta).length > 0) controller.enqueue(chunk(delta));
|
|
6476
|
-
} else if (decoded.type === "usage")
|
|
6477
|
-
|
|
6478
|
-
const cacheWrite = decoded.cacheWriteTokens;
|
|
6479
|
-
const details = {
|
|
6480
|
-
...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
|
|
6481
|
-
...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
|
|
6482
|
-
};
|
|
6483
|
-
usage = {
|
|
6484
|
-
...decoded.inputTokens === void 0 ? {} : { prompt_tokens: decoded.inputTokens },
|
|
6485
|
-
...decoded.outputTokens === void 0 ? {} : { completion_tokens: decoded.outputTokens },
|
|
6486
|
-
...decoded.totalTokens === void 0 ? {} : { total_tokens: decoded.totalTokens },
|
|
6487
|
-
...Object.keys(details).length === 0 ? {} : { prompt_tokens_details: details }
|
|
6488
|
-
};
|
|
6489
|
-
} else if (decoded.type === "done") {
|
|
6607
|
+
} else if (decoded.type === "usage") usage = normalizeUsageCache(JSON.parse(event.data));
|
|
6608
|
+
else if (decoded.type === "done") {
|
|
6490
6609
|
upstreamEnded = true;
|
|
6491
6610
|
if (!emittedFinishReason) {
|
|
6492
6611
|
emittedFinishReason = true;
|
|
@@ -12127,7 +12246,7 @@ function apply(ctx, config) {
|
|
|
12127
12246
|
discoverModels: async (signal) => {
|
|
12128
12247
|
const remote = await withDirectoryRetry(() => remoteCatalog.fetchModels(signal));
|
|
12129
12248
|
const [wireModels, roster] = await Promise.all([solo.fetchModels(signal), officialRoster.fetch(signal)]);
|
|
12130
|
-
const merged = mergeTraeModelSources(remote, wireModels);
|
|
12249
|
+
const merged = mergeTraeModelSources(remote, wireModels, { allowTenantConfigured: panel === "enterprise" });
|
|
12131
12250
|
const callable = withChatCallableFunctions(merged, TRAE_DIRECTORY_FUNCTIONS[region]);
|
|
12132
12251
|
const chatFunctions = TRAE_DIRECTORY_FUNCTIONS[region];
|
|
12133
12252
|
for (const model of merged) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.18.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|