dsh-connect-trae 4.6.3 → 4.6.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 CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.6.5 (2026-10-30)
4
+
5
+ > **一条没带上下文窗口的目录行,会让整个 `trae` 区域不可用。** 失效的不是
6
+ > 那条模型,而是**整条路由**——DSH 逐模型校验,且校验失败是 provider 级的。
7
+ > 插件早就写好了 `FALLBACK_CONTEXT_WINDOW` 作为「最后防线」,但它被赋给了一个
8
+ > 本插件**从不会走**的字段,于是从未生效过。这一版把回退落到模型行本身。
9
+
10
+ ### 修复
11
+
12
+ - **一条没带上下文窗口的目录行不再拖垮整个区域**(#70,贡献者
13
+ [@beibing173](https://github.com/beibing173))。
14
+ - **现象**:模型选择器里 `trae` 整个区域加载失败,报
15
+ `adapter returned invalid context metadata for provider "trae" model "doubao_1.8"`。
16
+ 失败粒度是 **provider 级**,不是单模型级。
17
+ - **根因**:`FALLBACK_CONTEXT_WINDOW` 只被赋给 profile 的
18
+ `defaultContextWindow`,而那个字段由**内核自己的目录解析步骤**读取——本插件
19
+ 从不走那条路径。它用 `createProvider({ models })` 手工装配 pi-ai provider,
20
+ `Models.getModel()` 直接返回 `toPiModel` 产出的原始行,**不做任何补默认值**;
21
+ 而 `toPiModel` 在 `contextWindow === undefined` 时**省略**该字段。于是
22
+ `PiAiAdapter.modelInfo()` 解析出 `context: { contextWindow: undefined }`,
23
+ DSH 拒绝,且拒绝的是整条路由。常量写了,没有生效点。
24
+ - **修法**:把回退**落到行本身**。新增 `servedContextWindow()`,`toPiModel`
25
+ 现在**总是**输出 `contextWindow`:行自己的正整数窗口原样保留,其余一切
26
+ (缺失、`NaN`、`0`、负数、小数)回退到 `FALLBACK_CONTEXT_WINDOW`——因为
27
+ DSH 只接受正整数。profile 上的 `defaultContextWindow` 保留(内核路径仍会读),
28
+ 但注释改成与事实一致:真正的保证在 `toPiModel`,那只是保险带。
29
+ - **为什么小数也要回退**:目录解析器的 `finitePositive` 只校验 `isFinite` 与
30
+ `> 0`,所以上游若发布 `context_window_tokens.dev = 200000.5`,它会原样进到
31
+ 适配器;DSH 对非整数的拒绝与缺失完全相同。这条此前没有任何用例锁住。
32
+ - **测试**:`tests/context-window-fallback.spec.ts` 走宿主的真实路径
33
+ (`adapter.current()` → `profiles` → `models.getModel()`,以及
34
+ `adapter.resolveModel()`),覆盖未定尺寸行、混合目录逐行可解析、行自带窗口
35
+ 不被覆盖,以及小数/零/负数/NaN 四种畸形窗口一律回退。
36
+ **变异验证**:把 `toPiModel` 的改动回退,8 例中 7 例转红。
37
+ - **不变的部分**:不改变任何已有窗口的行;不改变名称、reasoning、图像能力、
38
+ 计费倍率等元数据;兜底目录本身仍然每行手写真实窗口(`tests/catalog.spec.ts`
39
+ 继续锁这个不变量)。本修复只是让「万一漏了一条」退化成一条保守尺寸的模型,
40
+ 而不是死掉一个区域。
41
+
42
+ ## 4.6.4 (2026-10-28)
43
+
44
+ > **缓存命中恒为 0% 不是插件丢了数字,是上游没有分配缓存池。** Trae 的
45
+ > `metadata` 事件里 `prompt_cache_pool_record` 非空才代表这次请求挂了前缀
46
+ > 缓存池;实测 CN/AI 双区、4 个账号、7 个模型、4 种 function,该字段
47
+ > **100% 为 `null`**。没有池,`cache_read_input_tokens` 只能是 0 —— 0% 是
48
+ > 正确读数。本版把这个事实变成可观测的,用户不必再怀疑插件吞了数据。
49
+
50
+ ### 修复
51
+
52
+ - **上游的缓存池事实与管线名不再被丢弃**(#67)。
53
+ - **现象**:无论简单还是复杂提问,缓存命中恒为 0%,十个会话一致;
54
+ 同时 tps 在 60–80 之间浮动。
55
+ - **根因**:不是插件。issue #10 的转发修复仍在(`lib/index.js` 含
56
+ `cached_tokens`,同版本下 `workbuddy`/`dsh-opencode-go` 命中显示正常),
57
+ 是上游 `llm_utils_chat` 改由 `llm_raw_chat_v2` 管线、火山方舟 `ep-…`
58
+ 端点承载后,不再为这些请求分配前缀缓存池。当初产出 5248/9216 的
59
+ `scripts/probe-cache-convention.mjs` 原样重跑,现在三轮全是 0。
60
+ - **修法**:`sse.ts` 新增 `diagnostic` 事件类型,解码 `metadata`
61
+ (`prompt_cache_pool_record`)与 `timing_cost`(`name` 管线名、
62
+ `provider_model_name`);`solo-bridge.ts` 通过可选 `log` 上报
63
+ `promptCachePoolRecord` / `pipeline` / `providerModelName`;
64
+ `index.ts` 四个 `TraeSoloBridge` 构造点接入 `ctx.logger.warn`。
65
+ 诊断事件不进答案文本、不改 usage 映射,**对外行为不变**。
66
+ - **为什么区分 `null` 与「字段不存在」**:`null` 是上游明确表示「没挂池」,
67
+ 字段缺失只是没这条信息;两者混为一谈就又回到「看不出是谁的问题」。
68
+
69
+ ### 修复(顺带守住一个回归)
70
+
71
+ - **`metadata` 不能被 peek 当成「上游已应答」**。它现在是被认识的事件类型,
72
+ 若据此停止 peek,紧跟其后的拒绝(如 `1005` 未订阅)会退化成截断的 200 流
73
+ —— 正是 issue #19 的故障形态。`openTraeSource` 显式把 `diagnostic` 排除在
74
+ 「已应答」之外。
75
+
76
+ ### 测试
77
+
78
+ - `tests/sse.spec.ts`:`metadata` / `timing_cost` 解码为 `diagnostic`,
79
+ 保留 `prompt_cache_pool_record: null`,且区分字段缺失。
80
+ - `tests/solo-bridge.spec.ts`:诊断事实上报(含 `null` 原样保留、事件不泄漏进
81
+ 答案);`metadata` 之后紧跟的 `1005` 仍转成 402 `hard_credit`。
82
+
83
+ ### 说明
84
+
85
+ - 诊断细节与完整证据链见 `docs/ISSUE67_DIAGNOSIS.md`(不随 npm 包发布)。
86
+ - 上游若开始分配缓存池,`cached_tokens` 会自动恢复显示,插件侧无需再改。
87
+
3
88
  ## 4.6.3 (2026-10-27)
4
89
 
5
90
  > **国际版美国区账号不会再被当成新加坡区。** 它的凭据明明写着
package/lib/index.d.ts CHANGED
@@ -3426,6 +3426,29 @@ type TraeStreamEvent = {
3426
3426
  */
3427
3427
  cacheReadTokens?: number;
3428
3428
  cacheWriteTokens?: number;
3429
+ } | {
3430
+ /**
3431
+ * The upstream's own account of how the request was served: `metadata`
3432
+ * (acceptance, plus `prompt_cache_pool_record`) and `timing_cost` (the
3433
+ * pipeline and the backing provider model).
3434
+ *
3435
+ * These events were previously folded into `unknown` and dropped. That
3436
+ * mattered for issue #67: `prompt_cache_pool_record` is the ONLY upstream
3437
+ * signal that says whether a prompt-cache pool was even attached to the
3438
+ * request, so with it dropped a "0% cache" report could not be told apart
3439
+ * from a plugin that lost the numbers. See docs/ISSUE67_DIAGNOSIS.md.
3440
+ */
3441
+ type: 'diagnostic';
3442
+ /**
3443
+ * `metadata.prompt_cache_pool_record`. `null` is a real, observed value
3444
+ * (100% of measured requests on 2026-10-28) and means the upstream attached
3445
+ * no cache pool; it is kept distinct from "the field was absent".
3446
+ */
3447
+ promptCachePoolRecord?: unknown;
3448
+ /** `timing_cost.name`, e.g. `llm_raw_chat_v2`. */
3449
+ pipeline?: string;
3450
+ /** `timing_cost.provider_model_name` — the model that actually answered. */
3451
+ providerModelName?: string;
3429
3452
  } | {
3430
3453
  type: 'done';
3431
3454
  finishReason: string;
@@ -3569,7 +3592,21 @@ declare class TraeSoloBridge implements TraeUpstreamClient {
3569
3592
  private readonly upstream;
3570
3593
  private readonly catalog?;
3571
3594
  private readonly wireResolver?;
3572
- constructor(upstream: TraeUpstreamClient, catalog?: Pick<TraeCatalog, "current"> | undefined, wireResolver?: TraeWireResolver | undefined);
3595
+ /**
3596
+ * Optional sink for the upstream's own diagnostics (`metadata` /
3597
+ * `timing_cost`). Issue #67: without it, `prompt_cache_pool_record: null`
3598
+ * was invisible, so "the upstream attached no cache pool" and "the plugin
3599
+ * lost the numbers" looked identical to a user reading a 0% cache rate.
3600
+ */
3601
+ private readonly log?;
3602
+ constructor(upstream: TraeUpstreamClient, catalog?: Pick<TraeCatalog, "current"> | undefined, wireResolver?: TraeWireResolver | undefined,
3603
+ /**
3604
+ * Optional sink for the upstream's own diagnostics (`metadata` /
3605
+ * `timing_cost`). Issue #67: without it, `prompt_cache_pool_record: null`
3606
+ * was invisible, so "the upstream attached no cache pool" and "the plugin
3607
+ * lost the numbers" looked identical to a user reading a 0% cache rate.
3608
+ */
3609
+ log?: ((message: string, detail?: unknown) => void) | undefined);
3573
3610
  chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
3574
3611
  }
3575
3612
  //#endregion
package/lib/index.js CHANGED
@@ -866,8 +866,30 @@ const REQUEST_IMAGE_BUDGETS = {
866
866
  * window of its own. Every fallback row and every live Trae row is expected to
867
867
  * state its real window; this is the backstop that keeps one unsized row from
868
868
  * failing the entire provider route (`INVALID_MODEL_CONTEXT`).
869
+ *
870
+ * APPLIED IN {@link toPiModel}, NOT ONLY ON THE PROFILE. The profile's
871
+ * `defaultContextWindow` is read by the kernel's catalog-resolution step, which
872
+ * this plugin never runs: it builds its pi-ai provider BY HAND through
873
+ * `createProvider({ models })`, and `Models.getModel()` then returns the raw
874
+ * `toPiModel` row with no defaulting applied anywhere. Leaving the fallback on
875
+ * the profile alone is what produced
876
+ * `adapter returned invalid context metadata for provider "trae" model
877
+ * "doubao_1.8"` — a row the live directory published without a window reached
878
+ * DSH unsized and took the whole `trae` route down.
869
879
  */
870
880
  const FALLBACK_CONTEXT_WINDOW = 2e5;
881
+ /**
882
+ * The window a served row is actually advertised with.
883
+ *
884
+ * Positive integers pass through untouched — a row's own window is never
885
+ * second-guessed. Anything else (missing, `NaN`, `0`, a negative or a
886
+ * fractional count from a malformed directory entry) falls back, because DSH
887
+ * accepts ONLY a positive integer and rejects the entire provider otherwise.
888
+ */
889
+ function servedContextWindow(info) {
890
+ const window = info.contextWindow;
891
+ return Number.isInteger(window) && window > 0 ? window : FALLBACK_CONTEXT_WINDOW;
892
+ }
871
893
  function toPiModel(info, baseUrl, providerId, withRate = true) {
872
894
  return {
873
895
  id: info.id,
@@ -906,7 +928,7 @@ thinkingLevelMap: {
906
928
  xhigh: info.reasoningEfforts.xhigh ?? null,
907
929
  max: info.reasoningEfforts.max ?? null
908
930
  } },
909
- ...info.contextWindow === void 0 ? {} : { contextWindow: info.contextWindow },
931
+ contextWindow: servedContextWindow(info),
910
932
  ...info.maxTokens === void 0 ? {} : { maxTokens: info.maxTokens },
911
933
  compat: { supportsReasoningEffort: info.reasoningEfforts !== void 0 }
912
934
  };
@@ -5581,6 +5603,12 @@ function decodeTraeEvent(event) {
5581
5603
  ...typeof record["cache_read_input_tokens"] === "number" ? { cacheReadTokens: record["cache_read_input_tokens"] } : {},
5582
5604
  ...typeof record["cache_creation_input_tokens"] === "number" ? { cacheWriteTokens: record["cache_creation_input_tokens"] } : {}
5583
5605
  };
5606
+ if (event.event === "metadata" || event.event === "timing_cost") return {
5607
+ type: "diagnostic",
5608
+ ...record["prompt_cache_pool_record"] === void 0 ? {} : { promptCachePoolRecord: record["prompt_cache_pool_record"] },
5609
+ ...typeof record["name"] === "string" ? { pipeline: record["name"] } : {},
5610
+ ...typeof record["provider_model_name"] === "string" ? { providerModelName: record["provider_model_name"] } : {}
5611
+ };
5584
5612
  if (event.event === "done" || typeof record["finish_reason"] === "string" && record["response"] === void 0) return {
5585
5613
  type: "done",
5586
5614
  finishReason: typeof record["finish_reason"] === "string" ? record["finish_reason"] : "stop"
@@ -5747,7 +5775,7 @@ function bridgeTraeSoloStream(response, model) {
5747
5775
  return bridgeTraeSource(source, model);
5748
5776
  }
5749
5777
  /** {@link bridgeTraeSoloStream} over an already-opened byte stream. */
5750
- function bridgeTraeSource(source, model) {
5778
+ function bridgeTraeSource(source, model, log) {
5751
5779
  const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
5752
5780
  const created = Math.floor(Date.now() / 1e3);
5753
5781
  const decoder = new TextDecoder();
@@ -5780,6 +5808,15 @@ function bridgeTraeSource(source, model) {
5780
5808
  if (failure !== void 0) upstreamError = new Error(describeTraeFailure(failure));
5781
5809
  return;
5782
5810
  }
5811
+ if (decoded.type === "diagnostic") {
5812
+ if (decoded.promptCachePoolRecord !== void 0 || decoded.pipeline !== void 0) log?.("dsh-connect-trae: upstream request diagnostics", {
5813
+ model,
5814
+ promptCachePoolRecord: decoded.promptCachePoolRecord,
5815
+ pipeline: decoded.pipeline,
5816
+ providerModelName: decoded.providerModelName
5817
+ });
5818
+ return;
5819
+ }
5783
5820
  if (decoded.type === "delta") {
5784
5821
  const delta = {};
5785
5822
  if (decoded.text !== "") delta["content"] = decoded.text;
@@ -5932,7 +5969,7 @@ async function openTraeSource(response) {
5932
5969
  message: describeTraeFailure(failure)
5933
5970
  };
5934
5971
  }
5935
- if (decoded.type !== "unknown") answered = true;
5972
+ if (decoded.type !== "unknown" && decoded.type !== "diagnostic") answered = true;
5936
5973
  }
5937
5974
  if (answered) break;
5938
5975
  }
@@ -5956,12 +5993,12 @@ async function openTraeSource(response) {
5956
5993
  * The sibling {@link bridgeTraeSoloStream} stays synchronous and always answers
5957
5994
  * 200; it is the primitive, and this is the policy the bridge class uses.
5958
5995
  */
5959
- async function bridgeTraeSoloStreamOrFail(response, model) {
5996
+ async function bridgeTraeSoloStreamOrFail(response, model, log) {
5960
5997
  const opened = await openTraeSource(response);
5961
5998
  if (!opened.ok) return opened;
5962
5999
  return {
5963
6000
  ok: true,
5964
- response: bridgeTraeSource(opened.source, model)
6001
+ response: bridgeTraeSource(opened.source, model, log)
5965
6002
  };
5966
6003
  }
5967
6004
  /**
@@ -6002,10 +6039,12 @@ var TraeSoloBridge = class {
6002
6039
  upstream;
6003
6040
  catalog;
6004
6041
  wireResolver;
6005
- constructor(upstream, catalog, wireResolver) {
6042
+ log;
6043
+ constructor(upstream, catalog, wireResolver, log) {
6006
6044
  this.upstream = upstream;
6007
6045
  this.catalog = catalog;
6008
6046
  this.wireResolver = wireResolver;
6047
+ this.log = log;
6009
6048
  }
6010
6049
  async chatStream(bodyJson, signal) {
6011
6050
  let model = "glm-5.2";
@@ -6093,7 +6132,7 @@ var TraeSoloBridge = class {
6093
6132
  }
6094
6133
  const result = await this.upstream.chatStream(prepared, signal);
6095
6134
  if (!result.ok) return result;
6096
- return await bridgeTraeSoloStreamOrFail(result.response, model);
6135
+ return await bridgeTraeSoloStreamOrFail(result.response, model, this.log);
6097
6136
  }
6098
6137
  };
6099
6138
  //#endregion
@@ -11189,7 +11228,7 @@ function apply(ctx, config) {
11189
11228
  route,
11190
11229
  log: (message, detail) => ctx.logger.warn(message, detail)
11191
11230
  });
11192
- const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver));
11231
+ const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver, (message, detail) => ctx.logger.warn(message, detail)));
11193
11232
  routeClients[provider] = {
11194
11233
  provider,
11195
11234
  route,
@@ -11230,7 +11269,7 @@ function apply(ctx, config) {
11230
11269
  runtime: resolved.runtime
11231
11270
  }
11232
11271
  }),
11233
- solo: new TraeSoloBridge(primary.client, catalog, wireResolver),
11272
+ solo: new TraeSoloBridge(primary.client, catalog, wireResolver, (message, detail) => ctx.logger.warn(message, detail)),
11234
11273
  endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
11235
11274
  edition: resolved.identity.edition,
11236
11275
  identity: {
@@ -11627,7 +11666,7 @@ function apply(ctx, config) {
11627
11666
  identity: () => stack.identity(),
11628
11667
  route: stack.routeOf(providerOfPanel(panel)),
11629
11668
  log: (message, detail) => ctx.logger.warn(message, detail)
11630
- }), stack.catalog, stack.wireResolver)
11669
+ }), stack.catalog, stack.wireResolver, (message, detail) => ctx.logger.warn(message, detail))
11631
11670
  };
11632
11671
  }
11633
11672
  /**
@@ -11709,7 +11748,7 @@ function apply(ctx, config) {
11709
11748
  identity: () => stack.identity(),
11710
11749
  route: stack.routeOf(providerOfPanel(panel)),
11711
11750
  log: (message, detail) => ctx.logger.warn(message, detail)
11712
- }), stack.catalog, stack.wireResolver);
11751
+ }), stack.catalog, stack.wireResolver, (message, detail) => ctx.logger.warn(message, detail));
11713
11752
  const chat = (body, signal) => bridge.chatStream(body, signal);
11714
11753
  bridges.set(accountId, chat);
11715
11754
  return chat;
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.6.3",
5
+ "version": "4.6.5",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",