dsh-xdpool 2.3.0 → 2.4.0

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,6 +1,31 @@
1
1
  # Changelog
2
2
  本项目的所有重要变更都会记录在此文件。
3
3
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+ ## 2.4.0 — 会话身份头、清单不再冻结、中文路由可存
5
+ > 这一版收的是「用着用着就报错」的那一类问题:调用被网关判成非法渠道(11128)、国际版模型清单永远停在启动那一刻、中文名字的路由点了保存却没存下、上下文选择一选就弹回去。
6
+ ### 会话身份头:修掉 400 / 11128「Illegal API invocation from an unapproved channel」
7
+ 网关对「这一轮对话属于谁」有一组身份头,缺一个就整轮拒绝,报 `11128 Illegal API invocation from an unapproved channel`,看起来像密钥失效,其实是新会话没自报身份。补齐官方客户端的契约:
8
+ - **会话稳定三头**(同一会话内不变):`X-Conversation-ID`、`X-Conversation-Request-ID`、`acp-connection-id`。
9
+ - **每请求两头**(逐次随机):`X-Conversation-Message-ID`、`X-Request-ID`。
10
+ - 另附 `X-Agent-Intent`、`X-Agent-Purpose`、`x-codebuddy-request`、`X-Private-Data`、`X-Product` 与 W3C/B3 追踪族。
11
+
12
+ 实测口径:三个稳定头缺任一个即 400 11128,齐全即 200;值本身不校验(非 UUID 字符串也过)。池车道(WorkBuddy 国内/国际)与扫码渠道的 `send` 两条路径都已接上。
13
+ 顺带修掉同源的一处:`role: "developer"` 的消息在国内网关同样触发 11128,现统一上送为 `system`。
14
+ ### 国际版模型清单:不再冻结在启动那一刻
15
+ `createProvider` 的 `models` 是在创建 provider 时快照下来的数组,而原来的 `invalidate()` 只重排了 profile、复用了同一个 provider 对象,宿主按对象身份记忆化 `current()`,于是**新拿到的模型清单永远送不到选择器**——表现是「重新获取模型」点了没反应、国际版清单停在开机那一刻。现在每次 `invalidate()` 重建 provider 与 profile 两个对象,并通过 adapter handle 的 `replace()` 重新登记路由,由宿主自己发出 `llm/adapters-updated`。
16
+ (附带核查:国际版本来就有独立的 `modelSelectionGlobal` 键,与国内白名单分开读写,这一处无需改动。)
17
+ ### 中文路由名现在存得下
18
+ 链名 slug 原来是 `[^a-z0-9]+ → '-'`,中文名整串被压成空 id,规范化时被当作无效条目静默丢弃——于是「保存成功」但什么都没存。改为 Unicode 感知的 slug(`\p{L}`/`\p{N}`),拉丁名形态完全不变,已存的路由 id 继续可解析。
19
+ ### 上下文大小:选中态不再一选就弹回
20
+ 渠道卡把「从未保存过预算」当成了「已收窄到 200K」显示,用户点「完整」清空预算后该行又算回 200K,选中态立刻跳回第一项。现在未保存过预算就显示完整窗口;清空预算时同时保留 `nativeContextWindow`,radio 组不会在指针底下消失。
21
+ ### 思考档位:不再只认一档
22
+ pack 兜底表把十几个模型的 `reasoningEfforts` 写成了单档(`["medium"]` / `["high"]`),于是国际版 `Deepseek-V4.1-Flash` 只能选 High。逐模型实测(两网关、五档全跑)后按真实能力补齐:凡是能调通的模型五档都接受、且各档 `reasoning_content` 长度不同。`gpt-5.3-codex` 五档皆 200 但 reasoning 恒空——档位合法而不可观测,已经在表里注明;`gemini-3.5-flash` 与 `primary-model` 的 400 是账号套餐权限(`model [X] is not allowed for user type [personal]`),与档位无关,未动。
23
+ ### 重复账号:同一个人不再显示成两个
24
+ 桌面端凭据同时带数字 `uin` 与 UUID `uid`,扫码渠道只带 `uid`,而账号 id 是按 `uin` 优先哈希的——同一个人走两条登录路径就整出两个池条目,各自带一部分模型。现在按 `uid` 建次级索引合并到同一个池条目,既不改哈希(否则全部冷却、失效标记、忽略名单、持久化快照一次性作废),也与凭据到达顺序无关。
25
+ ### 「重新检测账号」不再静默失败
26
+ `listAllAccounts` 漏了 `await`,拿到的是一个 Promise,随后的 `.filter` 抛 `is not a function` 并被外层 try/catch 吞掉,表现是扫码账号全部并进池、重新检测永远没结果。
27
+ ### 其他
28
+ - 测试:新增会话身份头与中文路由名的回归用例,全套 68 文件 910 用例。
4
29
  ## 2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
5
30
  > 这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
6
31
  ### 账号池:扫码渠道按渠道入池、逐模型可控
package/lib/bin.js CHANGED
@@ -277,6 +277,80 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
277
277
  "X-IDE-Type": "WorkBuddy",
278
278
  "X-IDE-Version": "5.5.6"
279
279
  });
280
+ /**
281
+ * The conversation-identity headers the gateway's channel check demands.
282
+ *
283
+ * WHY THIS EXISTS
284
+ *
285
+ * `www.workbuddy.ai` turned "channel verification" from a soft check into a
286
+ * hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
287
+ * `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
288
+ * business code 11128 (`msg: "request illegal"` on the international gateway,
289
+ * `msg: "conversation id missing"` in the earlier wording). Verified by
290
+ * elimination against real credentials: delete any one of the three and the
291
+ * call 400s; send all three and it 200s with normal SSE. Every other header the
292
+ * desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
293
+ * `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
294
+ * so only these three are load-bearing.
295
+ *
296
+ * The values are NOT validated — plain non-UUID strings pass — so the gateway
297
+ * is checking for presence and consistency, not for a signature. That lets us
298
+ * derive them from the conversation instead of registering state with upstream.
299
+ *
300
+ * 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
301
+ * `msg: "Illegal API invocation from an unapproved channel"` for a different
302
+ * cause entirely: a `developer`-role message. `prepareChatBody()` already
303
+ * rewrites that role; see its comment. Never branch on the business code alone.
304
+ */
305
+ const CONVERSATION_STABLE_SALT = "conversation";
306
+ /**
307
+ * The three stable conversation ids for one logical conversation.
308
+ *
309
+ * Stable across every call and every account in a conversation, because the
310
+ * upstream treats the triple as one session: regenerating them per request
311
+ * would read as a new conversation on each turn. Derived rather than stored so
312
+ * the identity survives a restart and needs no registry — the same reasoning
313
+ * `deriveDeviceId` uses for the device id.
314
+ *
315
+ * Falls back to a random seed when the caller has no key (a one-shot compaction
316
+ * outside any conversation), which is safe because presence is all that is
317
+ * checked.
318
+ */
319
+ function conversationIdentityOf(conversationKey, seed = randomUUID) {
320
+ const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
321
+ const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
322
+ const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
323
+ return {
324
+ conversationId: asUuid(hash("id")),
325
+ conversationRequestId: asUuid(hash("request")),
326
+ acpConnectionId: asUuid(hash("acp"))
327
+ };
328
+ }
329
+ /**
330
+ * Full header set for one chat completion.
331
+ *
332
+ * `chatHeaders()` stays untouched on purpose: it is shared with the
333
+ * points-automation endpoints, which have never been shown to carry
334
+ * conversation identity, and adding it there would change how that traffic is
335
+ * attributed. The per-request ids are fresh UUIDs every call; only the triple
336
+ * from `conversationIdentityOf` is stable.
337
+ */
338
+ function chatCompletionHeaders(credential, conversationKey) {
339
+ const identity = conversationIdentityOf(conversationKey);
340
+ return {
341
+ ...chatHeaders(credential),
342
+ ...CHAT_IDENTITY_HEADERS,
343
+ "X-Conversation-ID": identity.conversationId,
344
+ "X-Conversation-Request-ID": identity.conversationRequestId,
345
+ "acp-connection-id": identity.acpConnectionId,
346
+ "X-Conversation-Message-ID": randomUUID(),
347
+ "X-Request-ID": randomUUID(),
348
+ "X-Agent-Intent": "craft",
349
+ "X-Agent-Purpose": "conversation",
350
+ "X-Private-Data": "true",
351
+ "x-codebuddy-request": "1"
352
+ };
353
+ }
280
354
  /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
281
355
  function refreshHeaders(credential) {
282
356
  const headers = {
@@ -879,13 +953,10 @@ var WorkBuddyUpstreamClient = class {
879
953
  * single string. Throws on any failure: the compactor then falls back to
880
954
  * plain truncation rather than failing the user's turn.
881
955
  */
882
- async completeChat(credential, prepared, signal) {
956
+ async completeChat(credential, prepared, signal, conversationKey) {
883
957
  const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
884
958
  method: "POST",
885
- headers: {
886
- ...chatHeaders(credential),
887
- ...CHAT_IDENTITY_HEADERS
888
- },
959
+ headers: chatCompletionHeaders(credential, conversationKey),
889
960
  body: prepared,
890
961
  ...signal === void 0 ? {} : { signal }
891
962
  });
@@ -897,15 +968,12 @@ var WorkBuddyUpstreamClient = class {
897
968
  return await readCompletionText(response.body);
898
969
  }
899
970
  /** Forward one chat completion. Never throws for upstream failures. */
900
- async chatStream(credential, prepared, signal) {
971
+ async chatStream(credential, prepared, signal, conversationKey) {
901
972
  let response;
902
973
  try {
903
974
  response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
904
975
  method: "POST",
905
- headers: {
906
- ...chatHeaders(credential),
907
- ...CHAT_IDENTITY_HEADERS
908
- },
976
+ headers: chatCompletionHeaders(credential, conversationKey),
909
977
  body: prepared,
910
978
  ...signal === void 0 ? {} : { signal }
911
979
  });
@@ -3325,8 +3393,17 @@ var WorkBuddyAccountPool = class {
3325
3393
  });
3326
3394
  }
3327
3395
  }
3396
+ const byUid = /* @__PURE__ */ new Map();
3397
+ for (const account of byId.values()) {
3398
+ const uid = account.credential.uid;
3399
+ if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
3400
+ }
3328
3401
  for (const credential of found) {
3329
- const id = workbuddyAccountId(credential);
3402
+ const byIdentity = workbuddyAccountId(credential);
3403
+ const uid = credential.uid;
3404
+ const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
3405
+ const id = knownId ?? byIdentity;
3406
+ if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
3330
3407
  const existing = byId.get(id);
3331
3408
  if (existing === void 0) {
3332
3409
  byId.set(id, {
package/lib/client.js CHANGED
@@ -11674,7 +11674,7 @@ window.__ModuleLoader__.load({
11674
11674
  ...model,
11675
11675
  ...budget === void 0 ? {
11676
11676
  contextBudget: void 0,
11677
- nativeContextWindow: void 0
11677
+ contextWindow: model.nativeContextWindow ?? model.contextWindow
11678
11678
  } : {
11679
11679
  contextBudget: budget,
11680
11680
  contextWindow: budget,
@@ -12079,7 +12079,7 @@ window.__ModuleLoader__.load({
12079
12079
  const recuse = recommendUse(merged);
12080
12080
  const native = model.nativeContextWindow ?? model.contextWindow;
12081
12081
  const capped = native !== void 0 && native > CONTEXT_BUDGET_DEFAULT;
12082
- const budget = model.contextBudget ?? (capped ? CONTEXT_BUDGET_DEFAULT : native);
12082
+ const budget = model.contextBudget ?? native;
12083
12083
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
12084
12084
  className: "dsm-xdpool-cpool-modelrow",
12085
12085
  "data-off": !on,
@@ -12188,7 +12188,7 @@ window.__ModuleLoader__.load({
12188
12188
  *
12189
12189
  * @module dsh-xdpool/version
12190
12190
  */
12191
- const POOL_VERSION = "2.3.0";
12191
+ const POOL_VERSION = "2.4.0";
12192
12192
  //#endregion
12193
12193
  //#region src/client/PoolCard.tsx
12194
12194
  /**
package/lib/index.d.ts CHANGED
@@ -489,9 +489,9 @@ export declare class WorkBuddyUpstreamClient {
489
489
  * single string. Throws on any failure: the compactor then falls back to
490
490
  * plain truncation rather than failing the user's turn.
491
491
  */
492
- completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<string>;
492
+ completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<string>;
493
493
  /** Forward one chat completion. Never throws for upstream failures. */
494
- chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
494
+ chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<ChatStreamResult>;
495
495
  /** POST the token-refresh endpoint; the caller merges the outcome. */
496
496
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
497
497
  /**
package/lib/index.js CHANGED
@@ -328,6 +328,80 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
328
328
  "X-IDE-Type": "WorkBuddy",
329
329
  "X-IDE-Version": "5.5.6"
330
330
  });
331
+ /**
332
+ * The conversation-identity headers the gateway's channel check demands.
333
+ *
334
+ * WHY THIS EXISTS
335
+ *
336
+ * `www.workbuddy.ai` turned "channel verification" from a soft check into a
337
+ * hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
338
+ * `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
339
+ * business code 11128 (`msg: "request illegal"` on the international gateway,
340
+ * `msg: "conversation id missing"` in the earlier wording). Verified by
341
+ * elimination against real credentials: delete any one of the three and the
342
+ * call 400s; send all three and it 200s with normal SSE. Every other header the
343
+ * desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
344
+ * `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
345
+ * so only these three are load-bearing.
346
+ *
347
+ * The values are NOT validated — plain non-UUID strings pass — so the gateway
348
+ * is checking for presence and consistency, not for a signature. That lets us
349
+ * derive them from the conversation instead of registering state with upstream.
350
+ *
351
+ * 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
352
+ * `msg: "Illegal API invocation from an unapproved channel"` for a different
353
+ * cause entirely: a `developer`-role message. `prepareChatBody()` already
354
+ * rewrites that role; see its comment. Never branch on the business code alone.
355
+ */
356
+ const CONVERSATION_STABLE_SALT = "conversation";
357
+ /**
358
+ * The three stable conversation ids for one logical conversation.
359
+ *
360
+ * Stable across every call and every account in a conversation, because the
361
+ * upstream treats the triple as one session: regenerating them per request
362
+ * would read as a new conversation on each turn. Derived rather than stored so
363
+ * the identity survives a restart and needs no registry — the same reasoning
364
+ * `deriveDeviceId` uses for the device id.
365
+ *
366
+ * Falls back to a random seed when the caller has no key (a one-shot compaction
367
+ * outside any conversation), which is safe because presence is all that is
368
+ * checked.
369
+ */
370
+ function conversationIdentityOf(conversationKey, seed = randomUUID) {
371
+ const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
372
+ const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
373
+ const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
374
+ return {
375
+ conversationId: asUuid(hash("id")),
376
+ conversationRequestId: asUuid(hash("request")),
377
+ acpConnectionId: asUuid(hash("acp"))
378
+ };
379
+ }
380
+ /**
381
+ * Full header set for one chat completion.
382
+ *
383
+ * `chatHeaders()` stays untouched on purpose: it is shared with the
384
+ * points-automation endpoints, which have never been shown to carry
385
+ * conversation identity, and adding it there would change how that traffic is
386
+ * attributed. The per-request ids are fresh UUIDs every call; only the triple
387
+ * from `conversationIdentityOf` is stable.
388
+ */
389
+ function chatCompletionHeaders(credential, conversationKey) {
390
+ const identity = conversationIdentityOf(conversationKey);
391
+ return {
392
+ ...chatHeaders(credential),
393
+ ...CHAT_IDENTITY_HEADERS,
394
+ "X-Conversation-ID": identity.conversationId,
395
+ "X-Conversation-Request-ID": identity.conversationRequestId,
396
+ "acp-connection-id": identity.acpConnectionId,
397
+ "X-Conversation-Message-ID": randomUUID(),
398
+ "X-Request-ID": randomUUID(),
399
+ "X-Agent-Intent": "craft",
400
+ "X-Agent-Purpose": "conversation",
401
+ "X-Private-Data": "true",
402
+ "x-codebuddy-request": "1"
403
+ };
404
+ }
331
405
  /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
332
406
  function refreshHeaders(credential) {
333
407
  const headers = {
@@ -1039,13 +1113,10 @@ var WorkBuddyUpstreamClient = class {
1039
1113
  * single string. Throws on any failure: the compactor then falls back to
1040
1114
  * plain truncation rather than failing the user's turn.
1041
1115
  */
1042
- async completeChat(credential, prepared, signal) {
1116
+ async completeChat(credential, prepared, signal, conversationKey) {
1043
1117
  const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
1044
1118
  method: "POST",
1045
- headers: {
1046
- ...chatHeaders(credential),
1047
- ...CHAT_IDENTITY_HEADERS
1048
- },
1119
+ headers: chatCompletionHeaders(credential, conversationKey),
1049
1120
  body: prepared,
1050
1121
  ...signal === void 0 ? {} : { signal }
1051
1122
  });
@@ -1057,15 +1128,12 @@ var WorkBuddyUpstreamClient = class {
1057
1128
  return await readCompletionText(response.body);
1058
1129
  }
1059
1130
  /** Forward one chat completion. Never throws for upstream failures. */
1060
- async chatStream(credential, prepared, signal) {
1131
+ async chatStream(credential, prepared, signal, conversationKey) {
1061
1132
  let response;
1062
1133
  try {
1063
1134
  response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
1064
1135
  method: "POST",
1065
- headers: {
1066
- ...chatHeaders(credential),
1067
- ...CHAT_IDENTITY_HEADERS
1068
- },
1136
+ headers: chatCompletionHeaders(credential, conversationKey),
1069
1137
  body: prepared,
1070
1138
  ...signal === void 0 ? {} : { signal }
1071
1139
  });
@@ -3501,8 +3569,17 @@ var WorkBuddyAccountPool = class {
3501
3569
  });
3502
3570
  }
3503
3571
  }
3572
+ const byUid = /* @__PURE__ */ new Map();
3573
+ for (const account of byId.values()) {
3574
+ const uid = account.credential.uid;
3575
+ if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
3576
+ }
3504
3577
  for (const credential of found) {
3505
- const id = workbuddyAccountId(credential);
3578
+ const byIdentity = workbuddyAccountId(credential);
3579
+ const uid = credential.uid;
3580
+ const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
3581
+ const id = knownId ?? byIdentity;
3582
+ if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
3506
3583
  const existing = byId.get(id);
3507
3584
  if (existing === void 0) {
3508
3585
  byId.set(id, {
@@ -10043,9 +10120,22 @@ function stringList$2(value) {
10043
10120
  }
10044
10121
  return out;
10045
10122
  }
10046
- /** The combo id a name slugifies to, or '' when the name cannot form one. */
10123
+ /**
10124
+ * The combo id a name slugifies to, or '' when the name cannot form one.
10125
+ *
10126
+ * Unicode-aware on purpose. This used to be `/[^a-z0-9]+/g`, which meant any
10127
+ * name that was not ASCII — every Chinese chain name — slugged to the empty
10128
+ * string, and `normalizeCombos` then dropped the entry with a bare `continue`.
10129
+ * The card reported "saved", the store held nothing, and the chain vanished on
10130
+ * the next read: the "new route cannot be saved" report. A name is user data and
10131
+ * may be written in any script, so the id has to survive one.
10132
+ *
10133
+ * Latin names keep the old shape (`writing-fallback`), because ids already
10134
+ * stored that way must keep resolving; only characters that used to be stripped
10135
+ * to nothing are now kept.
10136
+ */
10047
10137
  function comboId(name) {
10048
- const slug = text$6(name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
10138
+ const slug = text$6(name).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48).replace(/-+$/u, "");
10049
10139
  return slug === "" ? "" : `${COMBO_PREFIX}${slug}`;
10050
10140
  }
10051
10141
  /** True when the id names a combo row. */
@@ -10068,7 +10158,9 @@ function isComboRow(row) {
10068
10158
  *
10069
10159
  * A chain with no members is DROPPED rather than kept as an empty row: a model
10070
10160
  * that cannot answer anything is not a model, and showing one in the picker
10071
- * only produces a request that fails with nothing to fix.
10161
+ * only produces a request that fails with nothing to fix. The card warns about
10162
+ * that case before the save (`route.noMembers`), which is why dropping here is
10163
+ * not a silent loss.
10072
10164
  */
10073
10165
  function normalizeCombos(raw) {
10074
10166
  if (!Array.isArray(raw)) return [];
@@ -11596,7 +11688,6 @@ const NO_COST = {
11596
11688
  cacheRead: 0,
11597
11689
  cacheWrite: 0
11598
11690
  };
11599
- /** pi-ai input modalities: images only when the catalog advertises them. */
11600
11691
  function modelInput(info) {
11601
11692
  return info.supportsImages ? ["text", "image"] : ["text"];
11602
11693
  }
@@ -11773,26 +11864,44 @@ function createWorkBuddyAdapter(options) {
11773
11864
  if (aggregateCatalog) return [...catalog.providerRows().map((row) => toAggregateModel(row, baseUrl, providerId)), ...catalog.comboRows().map((row) => toComboModel(row, baseUrl, providerId))];
11774
11865
  return catalog.visible().map((info) => toPiModel(info, baseUrl, providerId));
11775
11866
  };
11776
- const provider = {
11777
- ...createProvider({
11778
- id: providerId,
11779
- name: displayName,
11780
- auth: { apiKey: {
11781
- name: "XD Pool loopback secret",
11782
- async resolve({ credential }) {
11783
- const apiKey = credential?.key;
11784
- return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
11785
- auth: { apiKey },
11786
- source: "XD Pool"
11787
- };
11788
- }
11789
- } },
11790
- models: buildModels(),
11791
- api: openAICompletionsApi()
11792
- }),
11793
- getModels: () => buildModels()
11867
+ /**
11868
+ * Build a fresh provider object whose `models` array is the catalog as it
11869
+ * stands right now.
11870
+ *
11871
+ * `createProvider` snapshots the array it is handed, and the host keeps its
11872
+ * own snapshot keyed by the IDENTITY of the `profiles` map
11873
+ * (`dsh-llm-pi-ai/lib/index.js` `PiAiAdapter.current()`: it rebuilds the
11874
+ * model collection only when `this.snapshot.profiles !== profiles`, then
11875
+ * calls `models.setProvider(profile.piProvider)`). Reusing one provider object
11876
+ * across `invalidate()` therefore could not work: the host would recognise the
11877
+ * unchanged profiles map, hand back its cached snapshot, and re-collect the
11878
+ * very same provider — so a model list that changed upstream (a new model
11879
+ * released, an account's entitlements changing) stayed frozen until restart.
11880
+ * That is what users saw on the international gateway. Returning a brand-new
11881
+ * provider from a brand-new profile is the only lever the adapter is given.
11882
+ */
11883
+ const makeProvider = () => {
11884
+ return {
11885
+ ...createProvider({
11886
+ id: providerId,
11887
+ name: displayName,
11888
+ auth: { apiKey: {
11889
+ name: "XD Pool loopback secret",
11890
+ async resolve({ credential }) {
11891
+ const apiKey = credential?.key;
11892
+ return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
11893
+ auth: { apiKey },
11894
+ source: "XD Pool"
11895
+ };
11896
+ }
11897
+ } },
11898
+ models: buildModels(),
11899
+ api: openAICompletionsApi()
11900
+ }),
11901
+ getModels: () => buildModels()
11902
+ };
11794
11903
  };
11795
- const profile = {
11904
+ const makeProfile = (piProvider) => ({
11796
11905
  provider: providerId,
11797
11906
  displayName,
11798
11907
  streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
@@ -11800,8 +11909,9 @@ function createWorkBuddyAdapter(options) {
11800
11909
  configuredMaxTokens: /* @__PURE__ */ new Map(),
11801
11910
  modelErrors: /* @__PURE__ */ new Map(),
11802
11911
  ...REQUEST_IMAGE_BUDGETS,
11803
- piProvider: provider
11804
- };
11912
+ piProvider
11913
+ });
11914
+ const profile = makeProfile(makeProvider());
11805
11915
  let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
11806
11916
  return {
11807
11917
  providerId,
@@ -11815,7 +11925,7 @@ function createWorkBuddyAdapter(options) {
11815
11925
  }),
11816
11926
  buildModels,
11817
11927
  invalidate: () => {
11818
- profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
11928
+ profiles = /* @__PURE__ */ new Map([[providerId, makeProfile(makeProvider())]]);
11819
11929
  }
11820
11930
  };
11821
11931
  }
@@ -11968,7 +12078,7 @@ function readPackAccountPool(ctx) {
11968
12078
  */
11969
12079
  async function readPackCredentials(pool) {
11970
12080
  if (pool === void 0) return [];
11971
- const entries = pool.listAllAccounts().filter((entry) => {
12081
+ const entries = (await pool.listAllAccounts()).filter((entry) => {
11972
12082
  const provider = text$3(entry.provider);
11973
12083
  return provider !== void 0 && PACK_REGION_CHANNELS.includes(provider);
11974
12084
  });
@@ -18204,6 +18314,7 @@ function createWorkBuddyShim(options) {
18204
18314
  }
18205
18315
  const attempts = [];
18206
18316
  let last;
18317
+ const conversationKey = conversationKeyOf(raw);
18207
18318
  for (const member of members) {
18208
18319
  if (signal.aborted) return;
18209
18320
  attempts.push(member.modelId);
@@ -18219,7 +18330,7 @@ function createWorkBuddyShim(options) {
18219
18330
  continue;
18220
18331
  }
18221
18332
  const prepared = client.prepareChatBody(raw);
18222
- const result = await client.chatStream(account.credential, prepared, signal);
18333
+ const result = await client.chatStream(account.credential, prepared, signal, conversationKey);
18223
18334
  if (result.ok) {
18224
18335
  const turnId = randomBytes(8).toString("hex");
18225
18336
  await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageRecorder === void 0 ? void 0 : {
@@ -18381,7 +18492,7 @@ function createWorkBuddyShim(options) {
18381
18492
  return;
18382
18493
  }
18383
18494
  tried.push(account.label);
18384
- const result = await client.chatStream(account.credential, prepared, controller.signal);
18495
+ const result = await client.chatStream(account.credential, prepared, controller.signal, conversationKey);
18385
18496
  if (result.ok) {
18386
18497
  await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageContext);
18387
18498
  return;
@@ -18640,7 +18751,7 @@ async function recoverFromContextOverrun(options) {
18640
18751
  stream: true,
18641
18752
  max_tokens: Math.max(256, Math.floor(budget / 2))
18642
18753
  }, request);
18643
- return await client.completeChat(summariser.credential, body, signal ?? controller.signal);
18754
+ return await client.completeChat(summariser.credential, body, signal ?? controller.signal, conversationKeyOf(raw));
18644
18755
  } }, controller.signal);
18645
18756
  compacted = outcome.messages;
18646
18757
  summary = outcome.summary;
@@ -18667,7 +18778,7 @@ async function recoverFromContextOverrun(options) {
18667
18778
  detail: "no account available after compaction"
18668
18779
  };
18669
18780
  tried.push(account.label);
18670
- const result = await client.chatStream(account.credential, retryBody, controller.signal);
18781
+ const result = await client.chatStream(account.credential, retryBody, controller.signal, conversationKeyOf(raw));
18671
18782
  if (!result.ok) usage?.recorder.record({
18672
18783
  region: usage.region,
18673
18784
  model: usage.model,
@@ -20415,6 +20526,19 @@ function apply(ctx, config = {}) {
20415
20526
  adaptersByRegion.global.invalidate();
20416
20527
  adaptersByRegion.aggregate.invalidate();
20417
20528
  adaptersByRegion.free.invalidate();
20529
+ for (const [handle, routes] of [
20530
+ [releaseAdapterCn, [POOL_PROVIDER_BY_REGION.cn]],
20531
+ [releaseAdapterGlobal, [POOL_PROVIDER_BY_REGION.global]],
20532
+ [releaseAdapterAggregate, [XDPOOL_AGGREGATE_PROVIDER]],
20533
+ [releaseAdapterFree, [XDPOOL_FREE_PROVIDER]]
20534
+ ]) {
20535
+ if (handle === void 0) continue;
20536
+ try {
20537
+ handle.replace([...routes]);
20538
+ } catch (error) {
20539
+ ctx.logger.warn("dsh-xdpool: could not re-announce adapter routes", error);
20540
+ }
20541
+ }
20418
20542
  };
20419
20543
  aggregateCatalog.onChange(() => {
20420
20544
  invalidateCatalog();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-xdpool",
3
3
  "displayName": "DSH XD Pool",
4
4
  "description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
5
- "version": "2.3.0",
5
+ "version": "2.4.0",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-xdpool#readme",
@@ -34239,7 +34239,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34239
34239
  name: "GLM-5.1",
34240
34240
  contextWindow: 2e5,
34241
34241
  supportsImages: true,
34242
- reasoningEfforts: ["medium"],
34242
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34243
+ // 200 with a distinct reasoning_content length per level. The single level
34244
+ // this used to declare was a guess, and it hid four working levels from
34245
+ // the picker.
34246
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34243
34247
  maxOutputTokens: 48e3
34244
34248
  },
34245
34249
  {
@@ -34247,7 +34251,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34247
34251
  name: "GLM-5V-Turbo",
34248
34252
  contextWindow: 2e5,
34249
34253
  supportsImages: true,
34250
- reasoningEfforts: ["medium"],
34254
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34255
+ // 200 with a distinct reasoning_content length per level. The single level
34256
+ // this used to declare was a guess, and it hid four working levels from
34257
+ // the picker.
34258
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34251
34259
  maxOutputTokens: 64e3
34252
34260
  },
34253
34261
  {
@@ -34255,7 +34263,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34255
34263
  name: "Kimi-K3-1",
34256
34264
  contextWindow: 1e6,
34257
34265
  supportsImages: true,
34258
- reasoningEfforts: ["medium"],
34266
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34267
+ // 200 with a distinct reasoning_content length per level. The single level
34268
+ // this used to declare was a guess, and it hid four working levels from
34269
+ // the picker.
34270
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34259
34271
  maxOutputTokens: 32e3
34260
34272
  },
34261
34273
  {
@@ -34274,7 +34286,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34274
34286
  name: "Kimi-K2.7",
34275
34287
  contextWindow: 256e3,
34276
34288
  supportsImages: true,
34277
- reasoningEfforts: ["medium"],
34289
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34290
+ // 200 with a distinct reasoning_content length per level. The single level
34291
+ // this used to declare was a guess, and it hid four working levels from
34292
+ // the picker.
34293
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34278
34294
  maxOutputTokens: 32e3
34279
34295
  },
34280
34296
  {
@@ -34282,7 +34298,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34282
34298
  name: "Kimi-K2.6",
34283
34299
  contextWindow: 256e3,
34284
34300
  supportsImages: true,
34285
- reasoningEfforts: ["medium"],
34301
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34302
+ // 200 with a distinct reasoning_content length per level. The single level
34303
+ // this used to declare was a guess, and it hid four working levels from
34304
+ // the picker.
34305
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34286
34306
  maxOutputTokens: 32e3
34287
34307
  },
34288
34308
  {
@@ -34290,7 +34310,11 @@ var CODEBUDDY_FALLBACK_MODELS = [
34290
34310
  name: "MiniMax-M3",
34291
34311
  contextWindow: 512e3,
34292
34312
  supportsImages: true,
34293
- reasoningEfforts: ["medium"],
34313
+ // Measured 2026-10-21 against the live gateway: all five levels answered
34314
+ // 200 with a distinct reasoning_content length per level. The single level
34315
+ // this used to declare was a guess, and it hid four working levels from
34316
+ // the picker.
34317
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34294
34318
  maxOutputTokens: 64e3
34295
34319
  }
34296
34320
  ];
@@ -34316,8 +34340,8 @@ var WORKBUDDY_FALLBACK_MODELS = [
34316
34340
  // (`node scripts/dump-max-output.mjs`)。该值就是用户在 IDE 里实际拿到的
34317
34341
  // 单次输出额度,远端不可用时由本表顶替。远端未下发的模型保持 undefined。
34318
34342
  { id: "default-model", name: "Auto", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
34319
- { id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34320
- { id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34343
+ { id: "fast-model", name: "Fast", contextWindow: 2e5, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34344
+ { id: "balanced-model", name: "Balanced", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34321
34345
  { id: "primary-model", name: "Primary", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["high"], maxOutputTokens: 72e3 },
34322
34346
  { id: "deep-model", name: "Deep", contextWindow: 176e3, supportsImages: true, maxOutputTokens: 24e3 },
34323
34347
  {
@@ -34349,14 +34373,14 @@ var WORKBUDDY_FALLBACK_MODELS = [
34349
34373
  defaultReasoningEffort: "high",
34350
34374
  maxOutputTokens: 64e3
34351
34375
  },
34352
- { id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["high"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
34376
+ { id: "deepseek-v4.1-flash", name: "Deepseek-V4.1-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], defaultReasoningEffort: "high", maxOutputTokens: 128e3 },
34353
34377
  {
34354
34378
  // 2026-09 补录:新加坡区的同代模型(-sg 后缀),远端下发且实测能看图。
34355
34379
  id: "deepseek-v4.1-flash-sg",
34356
34380
  name: "Deepseek-V4.1-Flash",
34357
34381
  contextWindow: 1e6,
34358
34382
  supportsImages: true,
34359
- reasoningEfforts: ["high"],
34383
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
34360
34384
  defaultReasoningEffort: "high",
34361
34385
  maxOutputTokens: 128e3
34362
34386
  },
@@ -34416,7 +34440,11 @@ var WORKBUDDY_FALLBACK_MODELS = [
34416
34440
  },
34417
34441
  // gpt-5.3-codex:远端未下发 maxOutputTokens,故不填(保持 undefined,
34418
34442
  // 交由网关默认),不臆造数值。
34419
- { id: "gpt-5.3-codex", name: "GPT-5.3-Codex", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["medium"] },
34443
+ // gpt-5.3-codex answered 200 at every level, but returned an EMPTY
34444
+ // reasoning_content at all five — the levels are legal yet not observably
34445
+ // distinct. Declaring the ladder states what may be sent; it does not
34446
+ // claim the model thinks.
34447
+ { id: "gpt-5.3-codex", name: "GPT-5.3-Codex", contextWindow: 272e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"] },
34420
34448
  { id: "gemini-3.5-flash", name: "Gemini-3.5-Flash", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 65536 },
34421
34449
  {
34422
34450
  id: "glm-5.3",
@@ -34436,7 +34464,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
34436
34464
  defaultReasoningEffort: "high",
34437
34465
  maxOutputTokens: 48e3
34438
34466
  },
34439
- { id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 },
34467
+ { id: "kimi-k3", name: "Kimi-K3", contextWindow: 1e6, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 },
34440
34468
  {
34441
34469
  // 2026-09 补录:远端 /v3/config 的 cli agent 白名单里有它,兜底表原先漏了。
34442
34470
  id: "kimi-k2.8-preview",
@@ -34447,7 +34475,7 @@ var WORKBUDDY_FALLBACK_MODELS = [
34447
34475
  defaultReasoningEffort: "high",
34448
34476
  maxOutputTokens: 32e3
34449
34477
  },
34450
- { id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["medium"], maxOutputTokens: 32e3 }
34478
+ { id: "kimi-k2.6", name: "Kimi-K2.6", contextWindow: 256e3, supportsImages: true, reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], maxOutputTokens: 32e3 }
34451
34479
  ];
34452
34480
  var WORKBUDDY = {
34453
34481
  id: "workbuddy",