dsh-connect-trae 2.0.3 → 2.0.5
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 +23 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +65 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5 (2026-09-22)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **修复「缓存统计一直是 0」**(issue #10:用户反馈「我测试了一下一直0缓存」,并猜测是接口原因或 Trae 本身不缓存):
|
|
8
|
+
- **根因在插件自己这一层**:Trae 上游**确实有前缀缓存、也确实在报**(实测逐字节相同的长 prompt 连发,第二次起 `cache_read_input_tokens` 从 0 升到 5248、9216),但插件解码 `token_usage` 事件时**只搬了 4 个计数器**(`prompt_tokens` / `completion_tokens` / `total_tokens` / `reasoning_tokens`),`cache_read_input_tokens` 与 `cache_creation_input_tokens` 被直接丢弃;`TraeStreamEvent.usage` 在**类型层就无法表达**这两个值,转发层自然也无从搬运。
|
|
9
|
+
- **不是「放行」就够,必须做字段名映射**:`dsh-llm-pi-ai` 只在 `usage.cacheRead > 0` 时写入 `cacheReadTokens`,而 pi-ai 的 `parseChunkUsage` 只识别 `prompt_tokens_details.cached_tokens` / `prompt_cache_hit_tokens` / `cached_tokens` 三种拼写——Trae 的原生字段名不在其中,原样透传依然读不到。现在映射为规范的 `prompt_tokens_details.cached_tokens` / `.cache_write_tokens`。
|
|
10
|
+
- **先确认了语义再动手**:pi-ai 会做 `input = prompt_tokens - cacheRead - cacheWrite`,该减法仅在「`prompt_tokens` 包含缓存量」(OpenAI 口径)时成立。实测同一份 prompt 的 `prompt_tokens` 恒为 9224 而 `cache_read` 从 5248 涨到 9216,若为 Anthropic 的排除口径则同一输入的实际规模会在 14472 → 18440 之间跳变,不可能——据此确认 Trae 用包含口径。端到端复核账目守恒:`8 + 9216 + 0 = 9224`,与 `prompt_tokens` 完全相等,无重复扣减。
|
|
11
|
+
- **`src/raw-chat.ts` 存在同一类缺口**(Raw Chat 通道将来启用时会踩同一个坑):`RawChatDelta.usage` 补上缓存字段,并同时接受 OpenAI 嵌套拼写与 DeepSeek/Kimi 的顶层拼写。两个通道均保证「上游没报缓存时 wire 形状不变」,不产生空 `prompt_tokens_details`。
|
|
12
|
+
- 新增 4 个回归测试,其中 bridge 用例直接以真机捕获的 `token_usage` 事件原文作为输入。
|
|
13
|
+
- 取证与复现脚本:`docs/ISSUE10_DIAGNOSIS.md`、`scripts/probe-cache-convention.mjs`。
|
|
14
|
+
- **说明**:修复只保证「上游报了缓存就如实上报」。新会话/新前缀的第一轮必然 `cache_read=0`(冷启动),第二轮起才有值;实测 Trae 当前只报读不报写(`cache_creation_input_tokens` 恒为 0)。
|
|
15
|
+
|
|
16
|
+
## 2.0.4 (2026-09-16)
|
|
17
|
+
|
|
18
|
+
### Fixes
|
|
19
|
+
|
|
20
|
+
- **修复「某个区域没有实时模型目录时,该区域的 provider 整个加载失败」**(issue #8:未安装国际版的用户看到 `Trae Global 加载失败:adapter returned invalid context metadata for provider "trae-global" model "gemini-3.1-pro"`):
|
|
21
|
+
- **根因**:两个区域的内置兜底目录里,每个模型都缺少 `contextWindow`。DSH 要求它必须是正整数(`INVALID_MODEL_CONTEXT`),而 `dsh-llm-pi-ai` 的回退链是 `entry.contextWindow → 已安装 catalog → request.defaultContextWindow`——本插件既没传 `defaultContextWindow`,兜底模型也不在已安装 catalog 里,三条全空,于是校验失败。
|
|
22
|
+
- **失败粒度是 provider 级**:不是丢单个模型,而是**整个区域不可用**。触发场景包括未安装该区客户端、未登录、目录拉取失败(该端点有已知的间歇性 401)、以及首次安装尚未刷新过目录。因此这不是「没装国际版拖累国内版」,而是**两个区域各自独立地有这个缺陷**。
|
|
23
|
+
- **修复**:(1) 兜底目录补上实测 `contextWindow`——国际版依据 2026-09-15 实测目录(`gemini-3.1-pro`/`gemini-3-flash-solo`/`minimax-m3`/`minimax-m2.7`/`kimi-k2.5` = 200000,`gpt-5.4`/`gpt-5.2` = 272000),国内版依据 SOLO 通道实测(200000);(2) adapter 传 `defaultContextWindow` 作为最终防线,将来任何来源的条目不慎缺字段时降级为「一个保守尺寸的模型」而非「整区不可用」。
|
|
24
|
+
- 新增不变量测试:**两个区域的兜底行都必须带正整数 `contextWindow`**。原先有一条测试断言「兜底条目没有 `contextWindow`」——它固化的正是这个 bug,已改正。
|
|
25
|
+
|
|
3
26
|
## 2.0.3 (2026-09-16)
|
|
4
27
|
|
|
5
28
|
### Fixes
|
package/lib/index.d.ts
CHANGED
|
@@ -126,6 +126,14 @@ interface TraeModelInfo {
|
|
|
126
126
|
* `DeepSeek-V4-Pro-Official` carry the `-Official` suffix, and there is no
|
|
127
127
|
* `auto` config_name — an `auto` row was previously served here and would have
|
|
128
128
|
* failed on selection.
|
|
129
|
+
*
|
|
130
|
+
* Every row MUST also carry a positive-integer `contextWindow`. DSH rejects an
|
|
131
|
+
* adapter whose model has no usable context (`INVALID_MODEL_CONTEXT`), and that
|
|
132
|
+
* failure is per-provider — one bad row takes the whole region offline. These
|
|
133
|
+
* rows are exactly the ones served when no live directory is available (no
|
|
134
|
+
* install for that region, signed out, or a failed refresh), which is precisely
|
|
135
|
+
* when the fallback is doing its job, so a missing field here is fatal rather
|
|
136
|
+
* than cosmetic. See docs/ISSUE8_DIAGNOSIS.md.
|
|
129
137
|
*/
|
|
130
138
|
declare const FALLBACK_TRAE_MODELS: readonly TraeModelInfo[];
|
|
131
139
|
/**
|
|
@@ -135,6 +143,10 @@ declare const FALLBACK_TRAE_MODELS: readonly TraeModelInfo[];
|
|
|
135
143
|
* has no Gemini/GPT/MiniMax entries), so an international account must never
|
|
136
144
|
* be seeded with the CN list. Like the CN fallback it is replaced by the live
|
|
137
145
|
* refresh; image input stays the user's explicit opt-in (`imageModelIds`).
|
|
146
|
+
*
|
|
147
|
+
* The `contextWindow` values are the measured ones from that same capture
|
|
148
|
+
* (`docs/INTL_SG_EVIDENCE.md` §3) and are required for the same reason as the
|
|
149
|
+
* CN fallback: without them the whole `trae-global` provider fails to load.
|
|
138
150
|
*/
|
|
139
151
|
declare const FALLBACK_TRAE_MODELS_AI: readonly TraeModelInfo[];
|
|
140
152
|
/**
|
|
@@ -748,6 +760,10 @@ type RawChatDelta = {
|
|
|
748
760
|
inputTokens?: number;
|
|
749
761
|
outputTokens?: number;
|
|
750
762
|
totalTokens?: number;
|
|
763
|
+
/** Cache-read tokens (`prompt_tokens_details.cached_tokens` / `cached_tokens`). */
|
|
764
|
+
cacheReadTokens?: number;
|
|
765
|
+
/** Cache-write tokens (`prompt_tokens_details.cache_write_tokens`). */
|
|
766
|
+
cacheWriteTokens?: number;
|
|
751
767
|
} | {
|
|
752
768
|
type: 'done';
|
|
753
769
|
finishReason: string;
|
|
@@ -1006,6 +1022,17 @@ type TraeStreamEvent = {
|
|
|
1006
1022
|
outputTokens?: number;
|
|
1007
1023
|
totalTokens?: number;
|
|
1008
1024
|
reasoningTokens?: number;
|
|
1025
|
+
/**
|
|
1026
|
+
* Trae's prompt-cache accounting (`cache_read_input_tokens` /
|
|
1027
|
+
* `cache_creation_input_tokens` on the `token_usage` event). Verified
|
|
1028
|
+
* 2026-09-22 (docs/ISSUE10_DIAGNOSIS.md): a byte-identical 9224-token
|
|
1029
|
+
* prompt reports `prompt_tokens: 9224` while `cache_read_input_tokens`
|
|
1030
|
+
* rises to 9216, so these are a *subset* of `prompt_tokens` — the OpenAI
|
|
1031
|
+
* convention, not Anthropic's exclusive one. Forwarding them as
|
|
1032
|
+
* `cached_tokens` is therefore arithmetically correct.
|
|
1033
|
+
*/
|
|
1034
|
+
cacheReadTokens?: number;
|
|
1035
|
+
cacheWriteTokens?: number;
|
|
1009
1036
|
} | {
|
|
1010
1037
|
type: 'done';
|
|
1011
1038
|
finishReason: string;
|
package/lib/index.js
CHANGED
|
@@ -29,23 +29,35 @@ import { promisify } from "node:util";
|
|
|
29
29
|
* `DeepSeek-V4-Pro-Official` carry the `-Official` suffix, and there is no
|
|
30
30
|
* `auto` config_name — an `auto` row was previously served here and would have
|
|
31
31
|
* failed on selection.
|
|
32
|
+
*
|
|
33
|
+
* Every row MUST also carry a positive-integer `contextWindow`. DSH rejects an
|
|
34
|
+
* adapter whose model has no usable context (`INVALID_MODEL_CONTEXT`), and that
|
|
35
|
+
* failure is per-provider — one bad row takes the whole region offline. These
|
|
36
|
+
* rows are exactly the ones served when no live directory is available (no
|
|
37
|
+
* install for that region, signed out, or a failed refresh), which is precisely
|
|
38
|
+
* when the fallback is doing its job, so a missing field here is fatal rather
|
|
39
|
+
* than cosmetic. See docs/ISSUE8_DIAGNOSIS.md.
|
|
32
40
|
*/
|
|
33
41
|
const FALLBACK_TRAE_MODELS = [
|
|
34
42
|
{
|
|
35
43
|
id: "DeepSeek-V4-Flash-Official",
|
|
36
|
-
name: "DeepSeek-V4-Flash"
|
|
44
|
+
name: "DeepSeek-V4-Flash",
|
|
45
|
+
contextWindow: 2e5
|
|
37
46
|
},
|
|
38
47
|
{
|
|
39
48
|
id: "DeepSeek-V4-Pro-Official",
|
|
40
|
-
name: "DeepSeek-V4-Pro"
|
|
49
|
+
name: "DeepSeek-V4-Pro",
|
|
50
|
+
contextWindow: 2e5
|
|
41
51
|
},
|
|
42
52
|
{
|
|
43
53
|
id: "glm-5.2",
|
|
44
|
-
name: "GLM-5.2"
|
|
54
|
+
name: "GLM-5.2",
|
|
55
|
+
contextWindow: 2e5
|
|
45
56
|
},
|
|
46
57
|
{
|
|
47
58
|
id: "kimi-k2.6",
|
|
48
|
-
name: "Kimi-K2.6"
|
|
59
|
+
name: "Kimi-K2.6",
|
|
60
|
+
contextWindow: 2e5
|
|
49
61
|
}
|
|
50
62
|
];
|
|
51
63
|
/**
|
|
@@ -55,35 +67,46 @@ const FALLBACK_TRAE_MODELS = [
|
|
|
55
67
|
* has no Gemini/GPT/MiniMax entries), so an international account must never
|
|
56
68
|
* be seeded with the CN list. Like the CN fallback it is replaced by the live
|
|
57
69
|
* refresh; image input stays the user's explicit opt-in (`imageModelIds`).
|
|
70
|
+
*
|
|
71
|
+
* The `contextWindow` values are the measured ones from that same capture
|
|
72
|
+
* (`docs/INTL_SG_EVIDENCE.md` §3) and are required for the same reason as the
|
|
73
|
+
* CN fallback: without them the whole `trae-global` provider fails to load.
|
|
58
74
|
*/
|
|
59
75
|
const FALLBACK_TRAE_MODELS_AI = [
|
|
60
76
|
{
|
|
61
77
|
id: "gemini-3.1-pro",
|
|
62
|
-
name: "Gemini-3.1-Pro-Preview"
|
|
78
|
+
name: "Gemini-3.1-Pro-Preview",
|
|
79
|
+
contextWindow: 2e5
|
|
63
80
|
},
|
|
64
81
|
{
|
|
65
82
|
id: "gemini-3-flash-solo",
|
|
66
|
-
name: "Gemini-3-Flash-Preview"
|
|
83
|
+
name: "Gemini-3-Flash-Preview",
|
|
84
|
+
contextWindow: 2e5
|
|
67
85
|
},
|
|
68
86
|
{
|
|
69
87
|
id: "minimax-m3",
|
|
70
|
-
name: "MiniMax-M3"
|
|
88
|
+
name: "MiniMax-M3",
|
|
89
|
+
contextWindow: 2e5
|
|
71
90
|
},
|
|
72
91
|
{
|
|
73
92
|
id: "minimax-m2.7",
|
|
74
|
-
name: "MiniMax-M2.7"
|
|
93
|
+
name: "MiniMax-M2.7",
|
|
94
|
+
contextWindow: 2e5
|
|
75
95
|
},
|
|
76
96
|
{
|
|
77
97
|
id: "kimi-k2.5",
|
|
78
|
-
name: "Kimi-K2.5"
|
|
98
|
+
name: "Kimi-K2.5",
|
|
99
|
+
contextWindow: 2e5
|
|
79
100
|
},
|
|
80
101
|
{
|
|
81
102
|
id: "gpt-5.4",
|
|
82
|
-
name: "GPT-5.4"
|
|
103
|
+
name: "GPT-5.4",
|
|
104
|
+
contextWindow: 272e3
|
|
83
105
|
},
|
|
84
106
|
{
|
|
85
107
|
id: "gpt-5.2",
|
|
86
|
-
name: "GPT-5.2"
|
|
108
|
+
name: "GPT-5.2",
|
|
109
|
+
contextWindow: 272e3
|
|
87
110
|
}
|
|
88
111
|
];
|
|
89
112
|
/**
|
|
@@ -292,6 +315,13 @@ const REQUEST_IMAGE_BUDGETS = {
|
|
|
292
315
|
requestImagePixelBudget: 4194304,
|
|
293
316
|
requestImageMaxBytes: 1048576
|
|
294
317
|
};
|
|
318
|
+
/**
|
|
319
|
+
* Conservative context capacity used only when a served model row carries no
|
|
320
|
+
* window of its own. Every fallback row and every live Trae row is expected to
|
|
321
|
+
* state its real window; this is the backstop that keeps one unsized row from
|
|
322
|
+
* failing the entire provider route (`INVALID_MODEL_CONTEXT`).
|
|
323
|
+
*/
|
|
324
|
+
const FALLBACK_CONTEXT_WINDOW = 2e5;
|
|
295
325
|
function toPiModel(info, baseUrl, providerId) {
|
|
296
326
|
return {
|
|
297
327
|
id: info.id,
|
|
@@ -351,6 +381,7 @@ function createTraeAdapter(options) {
|
|
|
351
381
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
|
|
352
382
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
353
383
|
modelErrors: /* @__PURE__ */ new Map(),
|
|
384
|
+
defaultContextWindow: FALLBACK_CONTEXT_WINDOW,
|
|
354
385
|
...REQUEST_IMAGE_BUDGETS,
|
|
355
386
|
piProvider: provider
|
|
356
387
|
};
|
|
@@ -2135,7 +2166,9 @@ function decodeTraeEvent(event) {
|
|
|
2135
2166
|
...typeof record["prompt_tokens"] === "number" ? { inputTokens: record["prompt_tokens"] } : {},
|
|
2136
2167
|
...typeof record["completion_tokens"] === "number" ? { outputTokens: record["completion_tokens"] } : {},
|
|
2137
2168
|
...typeof record["total_tokens"] === "number" ? { totalTokens: record["total_tokens"] } : {},
|
|
2138
|
-
...typeof record["reasoning_tokens"] === "number" ? { reasoningTokens: record["reasoning_tokens"] } : {}
|
|
2169
|
+
...typeof record["reasoning_tokens"] === "number" ? { reasoningTokens: record["reasoning_tokens"] } : {},
|
|
2170
|
+
...typeof record["cache_read_input_tokens"] === "number" ? { cacheReadTokens: record["cache_read_input_tokens"] } : {},
|
|
2171
|
+
...typeof record["cache_creation_input_tokens"] === "number" ? { cacheWriteTokens: record["cache_creation_input_tokens"] } : {}
|
|
2139
2172
|
};
|
|
2140
2173
|
if (event.event === "done" || typeof record["finish_reason"] === "string" && record["response"] === void 0) return {
|
|
2141
2174
|
type: "done",
|
|
@@ -2222,12 +2255,20 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
2222
2255
|
delta["tool_calls"] = toolCalls;
|
|
2223
2256
|
}
|
|
2224
2257
|
if (Object.keys(delta).length > 0) controller.enqueue(chunk(delta));
|
|
2225
|
-
} else if (decoded.type === "usage")
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2258
|
+
} else if (decoded.type === "usage") {
|
|
2259
|
+
const cacheRead = decoded.cacheReadTokens;
|
|
2260
|
+
const cacheWrite = decoded.cacheWriteTokens;
|
|
2261
|
+
const details = {
|
|
2262
|
+
...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
|
|
2263
|
+
...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
|
|
2264
|
+
};
|
|
2265
|
+
usage = {
|
|
2266
|
+
...decoded.inputTokens === void 0 ? {} : { prompt_tokens: decoded.inputTokens },
|
|
2267
|
+
...decoded.outputTokens === void 0 ? {} : { completion_tokens: decoded.outputTokens },
|
|
2268
|
+
...decoded.totalTokens === void 0 ? {} : { total_tokens: decoded.totalTokens },
|
|
2269
|
+
...Object.keys(details).length === 0 ? {} : { prompt_tokens_details: details }
|
|
2270
|
+
};
|
|
2271
|
+
} else if (decoded.type === "done") {
|
|
2231
2272
|
upstreamEnded = true;
|
|
2232
2273
|
if (!emittedFinishReason) {
|
|
2233
2274
|
emittedFinishReason = true;
|
|
@@ -2518,11 +2559,16 @@ function decodeRawChatChunk(value) {
|
|
|
2518
2559
|
}
|
|
2519
2560
|
if (typeof record["usage"] === "object" && record["usage"] !== null) {
|
|
2520
2561
|
const usage = record["usage"];
|
|
2562
|
+
const details = typeof usage["prompt_tokens_details"] === "object" && usage["prompt_tokens_details"] !== null ? usage["prompt_tokens_details"] : {};
|
|
2563
|
+
const cacheRead = details["cached_tokens"] ?? usage["prompt_cache_hit_tokens"] ?? usage["cached_tokens"];
|
|
2564
|
+
const cacheWrite = details["cache_write_tokens"];
|
|
2521
2565
|
result.push({
|
|
2522
2566
|
type: "usage",
|
|
2523
2567
|
...typeof usage["prompt_tokens"] === "number" ? { inputTokens: usage["prompt_tokens"] } : {},
|
|
2524
2568
|
...typeof usage["completion_tokens"] === "number" ? { outputTokens: usage["completion_tokens"] } : {},
|
|
2525
|
-
...typeof usage["total_tokens"] === "number" ? { totalTokens: usage["total_tokens"] } : {}
|
|
2569
|
+
...typeof usage["total_tokens"] === "number" ? { totalTokens: usage["total_tokens"] } : {},
|
|
2570
|
+
...typeof cacheRead === "number" ? { cacheReadTokens: cacheRead } : {},
|
|
2571
|
+
...typeof cacheWrite === "number" ? { cacheWriteTokens: cacheWrite } : {}
|
|
2526
2572
|
});
|
|
2527
2573
|
}
|
|
2528
2574
|
return result.length === 0 ? [{
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "Connect locally signed-in Trae models to DeepSeek Harness, plus a read-only usage/credits summary.",
|
|
5
|
-
"version": "2.0.
|
|
5
|
+
"version": "2.0.5",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|