dsh-xdpool 2.3.0 → 2.4.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 CHANGED
@@ -1,6 +1,65 @@
1
1
  # Changelog
2
2
  本项目的所有重要变更都会记录在此文件。
3
3
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+ ## 2.4.1 — 11128 的真正成因:system 内容指纹
5
+ > 2.4.0 之后仍有用户撞上 `400 / 11128 Illegal API invocation from an unapproved channel`。定位下来,2.4.0 的归因是错的,这一版修的是真原因。
6
+ ### 11128 不是身份头的问题,是 system 内容里的自报句
7
+ 网关按**内容指纹**判定「这是不是官方客户端发出来的」,命中串是 system 消息里的 `You are an AI agent powered by DeepSeek Harness`。它只匹配 `role === "system"`(放进 user/tool 不管),字面子串、大小写不敏感,且带起始位置倾向——放在提示词第一行会被拦,前面垫一行普通文字就放行。
8
+
9
+ 这句话由宿主的第一方插件 `@deepseek-ai/dsh-system-prompt` 固定注入,**任何未改装的安装都会 100% 命中**,与账号、密钥、渠道、UA 全部无关——这也解释了为什么它在「1 个账号失败后不再重试」:每个账号都会失败,重试没有意义。
10
+
11
+ 修法是在发送前把 system 里的这句改写成网关放行的同义句(`powered by` → `running on`),只动命中的文本块,图片/缓存块的引用原样透传;没命中时返回同一个引用,不为「什么都没改」付深拷贝代价。改写清单做成了数组,将来网关加句只是多一行。
12
+
13
+ **实测(真实国内网关,同一账号三组对照)**:
14
+ | 请求体 | 结果 |
15
+ |---|---|
16
+ | 经本插件改写 | `429 / 6004`(通过渠道校验,仅受频率限制) |
17
+ | 原样含该句(对照) | `400 / 11128` |
18
+ | system 干净(基线) | `429 / 6004` |
19
+
20
+ ### 国际网关要求首条消息是 system
21
+ `www.workbuddy.ai` 对一个以 `user` 开头的请求体回 `400 / 11128 "first message is not system prompt"`,而**同一个请求体在国内网关是被接受的**。同一个错误码,两种完全不同成因——这正是不能只按业务码分支的原因。现在国际版请求若首条不是 system,会在最前面补一条占位 system,用户自己的消息位置和措辞一个字符都不动。
22
+
23
+ ### 思考档位「关闭」会被两区同时拒绝
24
+ 适配层把 `thinkingLevelMap.off` 钉成字面量 `'off'`,而 pi-ai 对「没有显式指定档位」的请求发的就是它——所以选「默认」和选「关闭」产生了同一个请求体。两区都不接受这个值:国际版 GPT 族回 `400 / 11133`(`extError.param === 'reasoning.effort'`),国内版在声明了 `onlyReasoning` 的模型上报错。现在发送前把它删掉,让上游走自己的默认档。**省略**是唯一在各个区、各个模型上都实测通过的写法;换成字面量 `'none'` 不是安全替代。
25
+
26
+ **实测(真实双网关,同一账号前后对照)**:
27
+
28
+ | 请求 | 修复前 | 修复后 |
29
+ |---|---|---|
30
+ | 国际版,首条为 user | `400 / 11128 first message is not system prompt` | `200` |
31
+ | 国际版,`reasoning_effort: "off"` | `400 / 11133 extError.param=reasoning.effort` | `200` |
32
+
33
+ ### 免费模型限流提示:说清配额是按会话算的
34
+ 免费车道的拒绝原文是「the provider asked to wait 74558s」——二十小时,读起来像是整个免费车道被封了。实际配额**按上游会话计**,换一个会话(或换一个模型)就另有一份额度。现在限流错误的文案补上了这一句;只有限流类错误带这句,凭据失效、模型不存在之类加上去只会引导用户去做无用重试。
35
+ ### 其他
36
+ - 测试:新增 system 指纹改写 8 例 + 区域专属请求体规则 12 例 + 免费车道会话配额提示 1 例,全套 71 文件 932 用例。
37
+ ## 2.4.0 — 会话身份头、清单不再冻结、中文路由可存
38
+ > 这一版收的是「用着用着就报错」的那一类问题:调用被网关判成非法渠道(11128)、国际版模型清单永远停在启动那一刻、中文名字的路由点了保存却没存下、上下文选择一选就弹回去。
39
+ ### 会话身份头:补齐官方客户端的契约
40
+ 网关对「这一轮对话属于谁」有一组身份头。补齐官方客户端的契约:
41
+ - **会话稳定三头**(同一会话内不变):`X-Conversation-ID`、`X-Conversation-Request-ID`、`acp-connection-id`。
42
+ - **每请求两头**(逐次随机):`X-Conversation-Message-ID`、`X-Request-ID`。
43
+ - 另附 `X-Agent-Intent`、`X-Agent-Purpose`、`x-codebuddy-request`、`X-Private-Data`、`X-Product` 与 W3C/B3 追踪族。
44
+
45
+ > **口径更正(见 2.4.1)**:本版曾把 `400 / 11128` 归因于「缺会话身份头」。这个归因是错的——实测国内网关根本不检查这组头(不带任何身份头、system 干净时正常放行)。身份头照官方契约补齐没有坏处,但 11128 的真正成因是 system 内容里的自报句,已在 2.4.1 修掉。
46
+
47
+ 顺带修掉同源的一处:`role: "developer"` 的消息在国内网关同样触发 11128,现统一上送为 `system`。
48
+ ### 国际版模型清单:不再冻结在启动那一刻
49
+ `createProvider` 的 `models` 是在创建 provider 时快照下来的数组,而原来的 `invalidate()` 只重排了 profile、复用了同一个 provider 对象,宿主按对象身份记忆化 `current()`,于是**新拿到的模型清单永远送不到选择器**——表现是「重新获取模型」点了没反应、国际版清单停在开机那一刻。现在每次 `invalidate()` 重建 provider 与 profile 两个对象,并通过 adapter handle 的 `replace()` 重新登记路由,由宿主自己发出 `llm/adapters-updated`。
50
+ (附带核查:国际版本来就有独立的 `modelSelectionGlobal` 键,与国内白名单分开读写,这一处无需改动。)
51
+ ### 中文路由名现在存得下
52
+ 链名 slug 原来是 `[^a-z0-9]+ → '-'`,中文名整串被压成空 id,规范化时被当作无效条目静默丢弃——于是「保存成功」但什么都没存。改为 Unicode 感知的 slug(`\p{L}`/`\p{N}`),拉丁名形态完全不变,已存的路由 id 继续可解析。
53
+ ### 上下文大小:选中态不再一选就弹回
54
+ 渠道卡把「从未保存过预算」当成了「已收窄到 200K」显示,用户点「完整」清空预算后该行又算回 200K,选中态立刻跳回第一项。现在未保存过预算就显示完整窗口;清空预算时同时保留 `nativeContextWindow`,radio 组不会在指针底下消失。
55
+ ### 思考档位:不再只认一档
56
+ 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]`),与档位无关,未动。
57
+ ### 重复账号:同一个人不再显示成两个
58
+ 桌面端凭据同时带数字 `uin` 与 UUID `uid`,扫码渠道只带 `uid`,而账号 id 是按 `uin` 优先哈希的——同一个人走两条登录路径就整出两个池条目,各自带一部分模型。现在按 `uid` 建次级索引合并到同一个池条目,既不改哈希(否则全部冷却、失效标记、忽略名单、持久化快照一次性作废),也与凭据到达顺序无关。
59
+ ### 「重新检测账号」不再静默失败
60
+ `listAllAccounts` 漏了 `await`,拿到的是一个 Promise,随后的 `.filter` 抛 `is not a function` 并被外层 try/catch 吞掉,表现是扫码账号全部并进池、重新检测永远没结果。
61
+ ### 其他
62
+ - 测试:新增会话身份头与中文路由名的回归用例,全套 68 文件 910 用例。
4
63
  ## 2.3.0 — 扫码渠道接入账号池,免密车道修复,数据看板有了真数
5
64
  > 这一版收掉三件大事:扫码登录的账号真正归入账号池并按渠道展示;免费模型车道跟上上游的新校验、不再动辄 403;数据看板不再是一排零。
6
65
  ### 账号池:扫码渠道按渠道入池、逐模型可控
package/lib/bin.js CHANGED
@@ -277,6 +277,164 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
277
277
  "X-IDE-Type": "WorkBuddy",
278
278
  "X-IDE-Version": "5.5.6"
279
279
  });
280
+ /**
281
+ * System-prompt strings the gateway treats as evidence of a non-official
282
+ * client, and the rewriting that clears the check.
283
+ *
284
+ * WHY THIS EXISTS
285
+ *
286
+ * `code 11128 Illegal API invocation from an unapproved channel` has a SECOND
287
+ * trigger, separate from the conversation-identity headers below: the gateway
288
+ * matches the CONTENT of system messages against a list of strings that identify
289
+ * other agents' clients. The first-party DSH system-prompt plugin injects
290
+ * `You are an AI agent powered by DeepSeek Harness.` as the opening line of
291
+ * every conversation, so every request from an unmodified install is rejected —
292
+ * regardless of which account in the pool is used, which is why the reported
293
+ * symptom was a 400 on the first account with no retry.
294
+ *
295
+ * Boundary, measured against the live domestic gateway (429 = accepted,
296
+ * 400/11128 = blocked):
297
+ * - the match is a literal substring, BUT only near the START of a system
298
+ * message: the sentence blocked a longer prompt when it was the first line,
299
+ * and passed when one plain line was prepended to it ("trailing text after
300
+ * it" above is the same finding from the other side).
301
+ * - it is case-insensitive (`you are an ai agent powered by deepseek harness`
302
+ * is blocked too).
303
+ * - only `role: "system"` is inspected: the same sentence in a `user` or
304
+ * `tool` message passes, and it is blocked in a second system message too.
305
+ * - `deepseek harness` alone, or `powered by deepseek` alone, passes — the
306
+ * whole phrase is what matches.
307
+ * - replacing `powered by` with `running on` passes, which is the rewrite
308
+ * below: it keeps the meaning for the model and shares no substring with
309
+ * the fingerprint.
310
+ *
311
+ * A prepended blank line would equally defeat a start-anchored check, but it
312
+ * would leave the banned string in the body: the list is server-side and may
313
+ * lose its anchor tomorrow. Rewriting the phrase is what the report's repro
314
+ * verified, so that is what this does.
315
+ *
316
+ * The list is an ARRAY because these fingerprints are appended to over time; a
317
+ * future rejection should be one more row here, not a new code path.
318
+ */
319
+ const BANNED_SYSTEM_PHRASES = Object.freeze([[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"]]);
320
+ /**
321
+ * Rewrite fingerprinted system content, leaving everything else byte-identical.
322
+ *
323
+ * System content arrives as either a plain string or an array of content blocks
324
+ * (an OpenAI-compatible body allows both), so both shapes are handled — fixing
325
+ * only the string form would miss every request that carries an image or a
326
+ * cache-control block.
327
+ *
328
+ * Returns the SAME reference when nothing matched: a long conversation's system
329
+ * prompt can be large, and paying for a deep copy to change nothing is waste.
330
+ * Only matched `text` blocks are rebuilt; image and cache blocks pass through
331
+ * untouched so the request shape survives.
332
+ */
333
+ function sanitizeSystemContent(content) {
334
+ if (typeof content === "string") return sanitizeSystemText(content);
335
+ if (!Array.isArray(content)) return content;
336
+ let changed = false;
337
+ const next = content.map((block) => {
338
+ if (typeof block !== "object" || block === null || Array.isArray(block)) return block;
339
+ const record = block;
340
+ const text = record["text"];
341
+ if (typeof text !== "string") return block;
342
+ const cleaned = sanitizeSystemText(text);
343
+ if (cleaned === text) return block;
344
+ changed = true;
345
+ return {
346
+ ...record,
347
+ text: cleaned
348
+ };
349
+ });
350
+ return changed ? next : content;
351
+ }
352
+ /**
353
+ * Apply every banned-phrase rewrite to one string.
354
+ *
355
+ * The RegExp is rebuilt per call on purpose: a shared `g`-flagged instance
356
+ * carries `lastIndex` between calls, so the second request in a conversation
357
+ * could skip a match the first one caught.
358
+ */
359
+ function sanitizeSystemText(text) {
360
+ let out = text;
361
+ for (const [pattern, replacement] of BANNED_SYSTEM_PHRASES) out = out.replace(new RegExp(pattern.source, pattern.flags), replacement);
362
+ return out;
363
+ }
364
+ /**
365
+ * The conversation-identity headers the gateway's channel check demands.
366
+ *
367
+ * WHY THIS EXISTS
368
+ *
369
+ * `www.workbuddy.ai` turned "channel verification" from a soft check into a
370
+ * hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
371
+ * `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
372
+ * business code 11128 (`msg: "request illegal"` on the international gateway,
373
+ * `msg: "conversation id missing"` in the earlier wording). Verified by
374
+ * elimination against real credentials: delete any one of the three and the
375
+ * call 400s; send all three and it 200s with normal SSE. Every other header the
376
+ * desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
377
+ * `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
378
+ * so only these three are load-bearing.
379
+ *
380
+ * The values are NOT validated — plain non-UUID strings pass — so the gateway
381
+ * is checking for presence and consistency, not for a signature. That lets us
382
+ * derive them from the conversation instead of registering state with upstream.
383
+ *
384
+ * 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
385
+ * `msg: "Illegal API invocation from an unapproved channel"` for a different
386
+ * cause entirely: a `developer`-role message. `prepareChatBody()` already
387
+ * rewrites that role; see its comment. Never branch on the business code alone.
388
+ */
389
+ const CONVERSATION_STABLE_SALT = "conversation";
390
+ /**
391
+ * The three stable conversation ids for one logical conversation.
392
+ *
393
+ * Stable across every call and every account in a conversation, because the
394
+ * upstream treats the triple as one session: regenerating them per request
395
+ * would read as a new conversation on each turn. Derived rather than stored so
396
+ * the identity survives a restart and needs no registry — the same reasoning
397
+ * `deriveDeviceId` uses for the device id.
398
+ *
399
+ * Falls back to a random seed when the caller has no key (a one-shot compaction
400
+ * outside any conversation), which is safe because presence is all that is
401
+ * checked.
402
+ */
403
+ function conversationIdentityOf(conversationKey, seed = randomUUID) {
404
+ const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
405
+ const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
406
+ const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
407
+ return {
408
+ conversationId: asUuid(hash("id")),
409
+ conversationRequestId: asUuid(hash("request")),
410
+ acpConnectionId: asUuid(hash("acp"))
411
+ };
412
+ }
413
+ /**
414
+ * Full header set for one chat completion.
415
+ *
416
+ * `chatHeaders()` stays untouched on purpose: it is shared with the
417
+ * points-automation endpoints, which have never been shown to carry
418
+ * conversation identity, and adding it there would change how that traffic is
419
+ * attributed. The per-request ids are fresh UUIDs every call; only the triple
420
+ * from `conversationIdentityOf` is stable.
421
+ */
422
+ function chatCompletionHeaders(credential, conversationKey) {
423
+ const identity = conversationIdentityOf(conversationKey);
424
+ return {
425
+ ...chatHeaders(credential),
426
+ ...CHAT_IDENTITY_HEADERS,
427
+ "X-Conversation-ID": identity.conversationId,
428
+ "X-Conversation-Request-ID": identity.conversationRequestId,
429
+ "acp-connection-id": identity.acpConnectionId,
430
+ "X-Conversation-Message-ID": randomUUID(),
431
+ "X-Request-ID": randomUUID(),
432
+ "X-Agent-Intent": "craft",
433
+ "X-Agent-Purpose": "conversation",
434
+ "X-Private-Data": "true",
435
+ "x-codebuddy-request": "1"
436
+ };
437
+ }
280
438
  /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
281
439
  function refreshHeaders(credential) {
282
440
  const headers = {
@@ -804,7 +962,15 @@ var WorkBuddyUpstreamClient = class {
804
962
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
805
963
  * force `stream: true` (the upstream rejects non-streaming), convert the
806
964
  * DSH `developer` role into `system` (upstream rejects `developer` with
807
- * business code 11128), and flatten `tool_choice` into its string form.
965
+ * business code 11128), rewrite system content the gateway fingerprints as a
966
+ * non-official client (also 11128), and flatten `tool_choice` into its string
967
+ * form.
968
+ *
969
+ * This is the REGION-BLIND half. Everything here is measured good on both
970
+ * gateways, so it runs before the region is known — the pool lane picks its
971
+ * account after this call. The two region-only rules live in
972
+ * {@link prepareChatBodyFor} instead; see its comment for why they cannot be
973
+ * folded in here.
808
974
  */
809
975
  prepareChatBody(raw) {
810
976
  let body;
@@ -823,6 +989,7 @@ var WorkBuddyUpstreamClient = class {
823
989
  if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
824
990
  const message = value;
825
991
  if (message["role"] === "developer") message["role"] = "system";
992
+ if (message["role"] === "system") message["content"] = sanitizeSystemContent(message["content"]);
826
993
  }
827
994
  const choice = obj["tool_choice"];
828
995
  if (typeof choice === "string") {
@@ -849,6 +1016,61 @@ var WorkBuddyUpstreamClient = class {
849
1016
  return JSON.stringify(obj);
850
1017
  }
851
1018
  /**
1019
+ * The region-only half of body normalisation, applied once the account is
1020
+ * known.
1021
+ *
1022
+ * WHY IT IS SEPARATE
1023
+ *
1024
+ * The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
1025
+ * the body has to be ready for whichever account wins. So anything that
1026
+ * depends on the region cannot live there. Two rules do:
1027
+ *
1028
+ * 1. **International requires a leading `system`.** Measured: a body whose
1029
+ * first message is `user` answers `400 / 11128 "first message is not
1030
+ * system prompt"` on `www.workbuddy.ai`, while the identical body is
1031
+ * accepted on the domestic gateway. Same code, different cause — the
1032
+ * domestic gateway uses 11128 for the `developer` role and for the
1033
+ * content fingerprint, which is why nothing may branch on the code alone.
1034
+ * A placeholder assistant prompt is prepended, never merged: every
1035
+ * caller-supplied message keeps its position and wording.
1036
+ *
1037
+ * 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
1038
+ * `thinkingLevelMap.off` to the literal `'off'` so the level stays
1039
+ * selectable, and pi-ai emits that value for any request carrying no
1040
+ * explicit level — so "Default" and "Off" produce the same body.
1041
+ * Measured: the international GPT family answers `400 / 11133`
1042
+ * (`extError.param === 'reasoning.effort'`), and the domestic gateway
1043
+ * rejects it on models that declare `onlyReasoning`. Omission is the only
1044
+ * spelling measured good everywhere; a literal `'none'` is NOT a safe
1045
+ * substitute (accepted by the GPT-5.6 family, rejected by others).
1046
+ *
1047
+ * Both rules are no-ops on a body that already complies, so this is safe to
1048
+ * run over an already-prepared body.
1049
+ */
1050
+ prepareChatBodyFor(raw, region) {
1051
+ const prepared = this.prepareChatBody(raw);
1052
+ let body;
1053
+ try {
1054
+ body = JSON.parse(prepared);
1055
+ } catch {
1056
+ return prepared;
1057
+ }
1058
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return prepared;
1059
+ const obj = body;
1060
+ if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
1061
+ if (region === "global") {
1062
+ const messages = obj["messages"];
1063
+ if (Array.isArray(messages)) {
1064
+ const first = messages[0];
1065
+ if (!(typeof first === "object" && first !== null && !Array.isArray(first) && first["role"] === "system")) messages.unshift({
1066
+ role: "system",
1067
+ content: "You are a helpful assistant."
1068
+ });
1069
+ }
1070
+ }
1071
+ return JSON.stringify(obj);
1072
+ }
1073
+ /**
852
1074
  * Parse a raw OpenAI chat body without normalising it.
853
1075
  *
854
1076
  * The compactor needs the message array as objects, while `chatStream` only
@@ -864,12 +1086,20 @@ var WorkBuddyUpstreamClient = class {
864
1086
  if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
865
1087
  return body;
866
1088
  }
867
- /** Re-serialise `base` with a rewritten `messages` array, still normalised. */
868
- buildChatBody(base, messages) {
869
- return this.prepareChatBody(JSON.stringify({
1089
+ /**
1090
+ * Re-serialise `base` with a rewritten `messages` array, still normalised.
1091
+ *
1092
+ * `region` is optional so the composite lanes (aggregate / free) can keep
1093
+ * calling it without one; when supplied, the region-only rules of
1094
+ * {@link prepareChatBodyFor} ride along. Both in-repo callers are the
1095
+ * compaction paths, which always know their shim's region.
1096
+ */
1097
+ buildChatBody(base, messages, region) {
1098
+ const raw = JSON.stringify({
870
1099
  ...base,
871
1100
  messages
872
- }));
1101
+ });
1102
+ return region === void 0 ? this.prepareChatBody(raw) : this.prepareChatBodyFor(raw, region);
873
1103
  }
874
1104
  /**
875
1105
  * Run one NON-streaming completion and return the assistant text.
@@ -879,13 +1109,10 @@ var WorkBuddyUpstreamClient = class {
879
1109
  * single string. Throws on any failure: the compactor then falls back to
880
1110
  * plain truncation rather than failing the user's turn.
881
1111
  */
882
- async completeChat(credential, prepared, signal) {
1112
+ async completeChat(credential, prepared, signal, conversationKey) {
883
1113
  const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
884
1114
  method: "POST",
885
- headers: {
886
- ...chatHeaders(credential),
887
- ...CHAT_IDENTITY_HEADERS
888
- },
1115
+ headers: chatCompletionHeaders(credential, conversationKey),
889
1116
  body: prepared,
890
1117
  ...signal === void 0 ? {} : { signal }
891
1118
  });
@@ -897,15 +1124,12 @@ var WorkBuddyUpstreamClient = class {
897
1124
  return await readCompletionText(response.body);
898
1125
  }
899
1126
  /** Forward one chat completion. Never throws for upstream failures. */
900
- async chatStream(credential, prepared, signal) {
1127
+ async chatStream(credential, prepared, signal, conversationKey) {
901
1128
  let response;
902
1129
  try {
903
1130
  response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
904
1131
  method: "POST",
905
- headers: {
906
- ...chatHeaders(credential),
907
- ...CHAT_IDENTITY_HEADERS
908
- },
1132
+ headers: chatCompletionHeaders(credential, conversationKey),
909
1133
  body: prepared,
910
1134
  ...signal === void 0 ? {} : { signal }
911
1135
  });
@@ -3325,8 +3549,17 @@ var WorkBuddyAccountPool = class {
3325
3549
  });
3326
3550
  }
3327
3551
  }
3552
+ const byUid = /* @__PURE__ */ new Map();
3553
+ for (const account of byId.values()) {
3554
+ const uid = account.credential.uid;
3555
+ if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
3556
+ }
3328
3557
  for (const credential of found) {
3329
- const id = workbuddyAccountId(credential);
3558
+ const byIdentity = workbuddyAccountId(credential);
3559
+ const uid = credential.uid;
3560
+ const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
3561
+ const id = knownId ?? byIdentity;
3562
+ if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
3330
3563
  const existing = byId.get(id);
3331
3564
  if (existing === void 0) {
3332
3565
  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.1";
12192
12192
  //#endregion
12193
12193
  //#region src/client/PoolCard.tsx
12194
12194
  /**
package/lib/index.d.ts CHANGED
@@ -469,9 +469,50 @@ export declare class WorkBuddyUpstreamClient {
469
469
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
470
470
  * force `stream: true` (the upstream rejects non-streaming), convert the
471
471
  * DSH `developer` role into `system` (upstream rejects `developer` with
472
- * business code 11128), and flatten `tool_choice` into its string form.
472
+ * business code 11128), rewrite system content the gateway fingerprints as a
473
+ * non-official client (also 11128), and flatten `tool_choice` into its string
474
+ * form.
475
+ *
476
+ * This is the REGION-BLIND half. Everything here is measured good on both
477
+ * gateways, so it runs before the region is known — the pool lane picks its
478
+ * account after this call. The two region-only rules live in
479
+ * {@link prepareChatBodyFor} instead; see its comment for why they cannot be
480
+ * folded in here.
473
481
  */
474
482
  prepareChatBody(raw: string): string;
483
+ /**
484
+ * The region-only half of body normalisation, applied once the account is
485
+ * known.
486
+ *
487
+ * WHY IT IS SEPARATE
488
+ *
489
+ * The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
490
+ * the body has to be ready for whichever account wins. So anything that
491
+ * depends on the region cannot live there. Two rules do:
492
+ *
493
+ * 1. **International requires a leading `system`.** Measured: a body whose
494
+ * first message is `user` answers `400 / 11128 "first message is not
495
+ * system prompt"` on `www.workbuddy.ai`, while the identical body is
496
+ * accepted on the domestic gateway. Same code, different cause — the
497
+ * domestic gateway uses 11128 for the `developer` role and for the
498
+ * content fingerprint, which is why nothing may branch on the code alone.
499
+ * A placeholder assistant prompt is prepended, never merged: every
500
+ * caller-supplied message keeps its position and wording.
501
+ *
502
+ * 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
503
+ * `thinkingLevelMap.off` to the literal `'off'` so the level stays
504
+ * selectable, and pi-ai emits that value for any request carrying no
505
+ * explicit level — so "Default" and "Off" produce the same body.
506
+ * Measured: the international GPT family answers `400 / 11133`
507
+ * (`extError.param === 'reasoning.effort'`), and the domestic gateway
508
+ * rejects it on models that declare `onlyReasoning`. Omission is the only
509
+ * spelling measured good everywhere; a literal `'none'` is NOT a safe
510
+ * substitute (accepted by the GPT-5.6 family, rejected by others).
511
+ *
512
+ * Both rules are no-ops on a body that already complies, so this is safe to
513
+ * run over an already-prepared body.
514
+ */
515
+ prepareChatBodyFor(raw: string, region: WorkBuddyRegion): string;
475
516
  /**
476
517
  * Parse a raw OpenAI chat body without normalising it.
477
518
  *
@@ -479,8 +520,15 @@ export declare class WorkBuddyUpstreamClient {
479
520
  * accepts the serialised string form.
480
521
  */
481
522
  parseChatBody(raw: string): Record<string, unknown> | undefined;
482
- /** Re-serialise `base` with a rewritten `messages` array, still normalised. */
483
- buildChatBody(base: Record<string, unknown>, messages: readonly ChatMessage[]): string;
523
+ /**
524
+ * Re-serialise `base` with a rewritten `messages` array, still normalised.
525
+ *
526
+ * `region` is optional so the composite lanes (aggregate / free) can keep
527
+ * calling it without one; when supplied, the region-only rules of
528
+ * {@link prepareChatBodyFor} ride along. Both in-repo callers are the
529
+ * compaction paths, which always know their shim's region.
530
+ */
531
+ buildChatBody(base: Record<string, unknown>, messages: readonly ChatMessage[], region?: WorkBuddyRegion): string;
484
532
  /**
485
533
  * Run one NON-streaming completion and return the assistant text.
486
534
  *
@@ -489,9 +537,9 @@ export declare class WorkBuddyUpstreamClient {
489
537
  * single string. Throws on any failure: the compactor then falls back to
490
538
  * plain truncation rather than failing the user's turn.
491
539
  */
492
- completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<string>;
540
+ completeChat(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<string>;
493
541
  /** Forward one chat completion. Never throws for upstream failures. */
494
- chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal): Promise<ChatStreamResult>;
542
+ chatStream(credential: WorkBuddyCredential, prepared: string, signal?: AbortSignal, conversationKey?: string): Promise<ChatStreamResult>;
495
543
  /** POST the token-refresh endpoint; the caller merges the outcome. */
496
544
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
497
545
  /**
package/lib/index.js CHANGED
@@ -328,6 +328,164 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
328
328
  "X-IDE-Type": "WorkBuddy",
329
329
  "X-IDE-Version": "5.5.6"
330
330
  });
331
+ /**
332
+ * System-prompt strings the gateway treats as evidence of a non-official
333
+ * client, and the rewriting that clears the check.
334
+ *
335
+ * WHY THIS EXISTS
336
+ *
337
+ * `code 11128 Illegal API invocation from an unapproved channel` has a SECOND
338
+ * trigger, separate from the conversation-identity headers below: the gateway
339
+ * matches the CONTENT of system messages against a list of strings that identify
340
+ * other agents' clients. The first-party DSH system-prompt plugin injects
341
+ * `You are an AI agent powered by DeepSeek Harness.` as the opening line of
342
+ * every conversation, so every request from an unmodified install is rejected —
343
+ * regardless of which account in the pool is used, which is why the reported
344
+ * symptom was a 400 on the first account with no retry.
345
+ *
346
+ * Boundary, measured against the live domestic gateway (429 = accepted,
347
+ * 400/11128 = blocked):
348
+ * - the match is a literal substring, BUT only near the START of a system
349
+ * message: the sentence blocked a longer prompt when it was the first line,
350
+ * and passed when one plain line was prepended to it ("trailing text after
351
+ * it" above is the same finding from the other side).
352
+ * - it is case-insensitive (`you are an ai agent powered by deepseek harness`
353
+ * is blocked too).
354
+ * - only `role: "system"` is inspected: the same sentence in a `user` or
355
+ * `tool` message passes, and it is blocked in a second system message too.
356
+ * - `deepseek harness` alone, or `powered by deepseek` alone, passes — the
357
+ * whole phrase is what matches.
358
+ * - replacing `powered by` with `running on` passes, which is the rewrite
359
+ * below: it keeps the meaning for the model and shares no substring with
360
+ * the fingerprint.
361
+ *
362
+ * A prepended blank line would equally defeat a start-anchored check, but it
363
+ * would leave the banned string in the body: the list is server-side and may
364
+ * lose its anchor tomorrow. Rewriting the phrase is what the report's repro
365
+ * verified, so that is what this does.
366
+ *
367
+ * The list is an ARRAY because these fingerprints are appended to over time; a
368
+ * future rejection should be one more row here, not a new code path.
369
+ */
370
+ const BANNED_SYSTEM_PHRASES = Object.freeze([[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"]]);
371
+ /**
372
+ * Rewrite fingerprinted system content, leaving everything else byte-identical.
373
+ *
374
+ * System content arrives as either a plain string or an array of content blocks
375
+ * (an OpenAI-compatible body allows both), so both shapes are handled — fixing
376
+ * only the string form would miss every request that carries an image or a
377
+ * cache-control block.
378
+ *
379
+ * Returns the SAME reference when nothing matched: a long conversation's system
380
+ * prompt can be large, and paying for a deep copy to change nothing is waste.
381
+ * Only matched `text` blocks are rebuilt; image and cache blocks pass through
382
+ * untouched so the request shape survives.
383
+ */
384
+ function sanitizeSystemContent(content) {
385
+ if (typeof content === "string") return sanitizeSystemText(content);
386
+ if (!Array.isArray(content)) return content;
387
+ let changed = false;
388
+ const next = content.map((block) => {
389
+ if (typeof block !== "object" || block === null || Array.isArray(block)) return block;
390
+ const record = block;
391
+ const text = record["text"];
392
+ if (typeof text !== "string") return block;
393
+ const cleaned = sanitizeSystemText(text);
394
+ if (cleaned === text) return block;
395
+ changed = true;
396
+ return {
397
+ ...record,
398
+ text: cleaned
399
+ };
400
+ });
401
+ return changed ? next : content;
402
+ }
403
+ /**
404
+ * Apply every banned-phrase rewrite to one string.
405
+ *
406
+ * The RegExp is rebuilt per call on purpose: a shared `g`-flagged instance
407
+ * carries `lastIndex` between calls, so the second request in a conversation
408
+ * could skip a match the first one caught.
409
+ */
410
+ function sanitizeSystemText(text) {
411
+ let out = text;
412
+ for (const [pattern, replacement] of BANNED_SYSTEM_PHRASES) out = out.replace(new RegExp(pattern.source, pattern.flags), replacement);
413
+ return out;
414
+ }
415
+ /**
416
+ * The conversation-identity headers the gateway's channel check demands.
417
+ *
418
+ * WHY THIS EXISTS
419
+ *
420
+ * `www.workbuddy.ai` turned "channel verification" from a soft check into a
421
+ * hard one. A `POST /v2/chat/completions` missing ANY of `X-Conversation-ID`,
422
+ * `X-Conversation-Request-ID` or `X-Conversation-Message-ID` is rejected with
423
+ * business code 11128 (`msg: "request illegal"` on the international gateway,
424
+ * `msg: "conversation id missing"` in the earlier wording). Verified by
425
+ * elimination against real credentials: delete any one of the three and the
426
+ * call 400s; send all three and it 200s with normal SSE. Every other header the
427
+ * desktop sends (`acp-connection-id`, `X-Request-ID`, `X-Agent-Intent`,
428
+ * `X-Private-Data`, the W3C/B3 tracing family) can be dropped without effect,
429
+ * so only these three are load-bearing.
430
+ *
431
+ * The values are NOT validated — plain non-UUID strings pass — so the gateway
432
+ * is checking for presence and consistency, not for a signature. That lets us
433
+ * derive them from the conversation instead of registering state with upstream.
434
+ *
435
+ * 11128 IS MULTIPLEXED. The domestic gateway returns the same code with
436
+ * `msg: "Illegal API invocation from an unapproved channel"` for a different
437
+ * cause entirely: a `developer`-role message. `prepareChatBody()` already
438
+ * rewrites that role; see its comment. Never branch on the business code alone.
439
+ */
440
+ const CONVERSATION_STABLE_SALT = "conversation";
441
+ /**
442
+ * The three stable conversation ids for one logical conversation.
443
+ *
444
+ * Stable across every call and every account in a conversation, because the
445
+ * upstream treats the triple as one session: regenerating them per request
446
+ * would read as a new conversation on each turn. Derived rather than stored so
447
+ * the identity survives a restart and needs no registry — the same reasoning
448
+ * `deriveDeviceId` uses for the device id.
449
+ *
450
+ * Falls back to a random seed when the caller has no key (a one-shot compaction
451
+ * outside any conversation), which is safe because presence is all that is
452
+ * checked.
453
+ */
454
+ function conversationIdentityOf(conversationKey, seed = randomUUID) {
455
+ const base = conversationKey === void 0 || conversationKey === "" ? seed() : conversationKey;
456
+ const hash = (suffix) => createHash("sha256").update(`${CONVERSATION_STABLE_SALT}:${suffix}:${base}`).digest("hex");
457
+ const asUuid = (hex) => `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
458
+ return {
459
+ conversationId: asUuid(hash("id")),
460
+ conversationRequestId: asUuid(hash("request")),
461
+ acpConnectionId: asUuid(hash("acp"))
462
+ };
463
+ }
464
+ /**
465
+ * Full header set for one chat completion.
466
+ *
467
+ * `chatHeaders()` stays untouched on purpose: it is shared with the
468
+ * points-automation endpoints, which have never been shown to carry
469
+ * conversation identity, and adding it there would change how that traffic is
470
+ * attributed. The per-request ids are fresh UUIDs every call; only the triple
471
+ * from `conversationIdentityOf` is stable.
472
+ */
473
+ function chatCompletionHeaders(credential, conversationKey) {
474
+ const identity = conversationIdentityOf(conversationKey);
475
+ return {
476
+ ...chatHeaders(credential),
477
+ ...CHAT_IDENTITY_HEADERS,
478
+ "X-Conversation-ID": identity.conversationId,
479
+ "X-Conversation-Request-ID": identity.conversationRequestId,
480
+ "acp-connection-id": identity.acpConnectionId,
481
+ "X-Conversation-Message-ID": randomUUID(),
482
+ "X-Request-ID": randomUUID(),
483
+ "X-Agent-Intent": "craft",
484
+ "X-Agent-Purpose": "conversation",
485
+ "X-Private-Data": "true",
486
+ "x-codebuddy-request": "1"
487
+ };
488
+ }
331
489
  /** Refresh-endpoint headers; X-Refresh-Token appears here and nowhere else. */
332
490
  function refreshHeaders(credential) {
333
491
  const headers = {
@@ -964,7 +1122,15 @@ var WorkBuddyUpstreamClient = class {
964
1122
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
965
1123
  * force `stream: true` (the upstream rejects non-streaming), convert the
966
1124
  * DSH `developer` role into `system` (upstream rejects `developer` with
967
- * business code 11128), and flatten `tool_choice` into its string form.
1125
+ * business code 11128), rewrite system content the gateway fingerprints as a
1126
+ * non-official client (also 11128), and flatten `tool_choice` into its string
1127
+ * form.
1128
+ *
1129
+ * This is the REGION-BLIND half. Everything here is measured good on both
1130
+ * gateways, so it runs before the region is known — the pool lane picks its
1131
+ * account after this call. The two region-only rules live in
1132
+ * {@link prepareChatBodyFor} instead; see its comment for why they cannot be
1133
+ * folded in here.
968
1134
  */
969
1135
  prepareChatBody(raw) {
970
1136
  let body;
@@ -983,6 +1149,7 @@ var WorkBuddyUpstreamClient = class {
983
1149
  if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
984
1150
  const message = value;
985
1151
  if (message["role"] === "developer") message["role"] = "system";
1152
+ if (message["role"] === "system") message["content"] = sanitizeSystemContent(message["content"]);
986
1153
  }
987
1154
  const choice = obj["tool_choice"];
988
1155
  if (typeof choice === "string") {
@@ -1009,6 +1176,61 @@ var WorkBuddyUpstreamClient = class {
1009
1176
  return JSON.stringify(obj);
1010
1177
  }
1011
1178
  /**
1179
+ * The region-only half of body normalisation, applied once the account is
1180
+ * known.
1181
+ *
1182
+ * WHY IT IS SEPARATE
1183
+ *
1184
+ * The pool lane calls {@link prepareChatBody} BEFORE it picks an account —
1185
+ * the body has to be ready for whichever account wins. So anything that
1186
+ * depends on the region cannot live there. Two rules do:
1187
+ *
1188
+ * 1. **International requires a leading `system`.** Measured: a body whose
1189
+ * first message is `user` answers `400 / 11128 "first message is not
1190
+ * system prompt"` on `www.workbuddy.ai`, while the identical body is
1191
+ * accepted on the domestic gateway. Same code, different cause — the
1192
+ * domestic gateway uses 11128 for the `developer` role and for the
1193
+ * content fingerprint, which is why nothing may branch on the code alone.
1194
+ * A placeholder assistant prompt is prepended, never merged: every
1195
+ * caller-supplied message keeps its position and wording.
1196
+ *
1197
+ * 2. **Neither gateway accepts `reasoning_effort: "off"`.** The adapter pins
1198
+ * `thinkingLevelMap.off` to the literal `'off'` so the level stays
1199
+ * selectable, and pi-ai emits that value for any request carrying no
1200
+ * explicit level — so "Default" and "Off" produce the same body.
1201
+ * Measured: the international GPT family answers `400 / 11133`
1202
+ * (`extError.param === 'reasoning.effort'`), and the domestic gateway
1203
+ * rejects it on models that declare `onlyReasoning`. Omission is the only
1204
+ * spelling measured good everywhere; a literal `'none'` is NOT a safe
1205
+ * substitute (accepted by the GPT-5.6 family, rejected by others).
1206
+ *
1207
+ * Both rules are no-ops on a body that already complies, so this is safe to
1208
+ * run over an already-prepared body.
1209
+ */
1210
+ prepareChatBodyFor(raw, region) {
1211
+ const prepared = this.prepareChatBody(raw);
1212
+ let body;
1213
+ try {
1214
+ body = JSON.parse(prepared);
1215
+ } catch {
1216
+ return prepared;
1217
+ }
1218
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return prepared;
1219
+ const obj = body;
1220
+ if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
1221
+ if (region === "global") {
1222
+ const messages = obj["messages"];
1223
+ if (Array.isArray(messages)) {
1224
+ const first = messages[0];
1225
+ if (!(typeof first === "object" && first !== null && !Array.isArray(first) && first["role"] === "system")) messages.unshift({
1226
+ role: "system",
1227
+ content: "You are a helpful assistant."
1228
+ });
1229
+ }
1230
+ }
1231
+ return JSON.stringify(obj);
1232
+ }
1233
+ /**
1012
1234
  * Parse a raw OpenAI chat body without normalising it.
1013
1235
  *
1014
1236
  * The compactor needs the message array as objects, while `chatStream` only
@@ -1024,12 +1246,20 @@ var WorkBuddyUpstreamClient = class {
1024
1246
  if (typeof body !== "object" || body === null || Array.isArray(body)) return void 0;
1025
1247
  return body;
1026
1248
  }
1027
- /** Re-serialise `base` with a rewritten `messages` array, still normalised. */
1028
- buildChatBody(base, messages) {
1029
- return this.prepareChatBody(JSON.stringify({
1249
+ /**
1250
+ * Re-serialise `base` with a rewritten `messages` array, still normalised.
1251
+ *
1252
+ * `region` is optional so the composite lanes (aggregate / free) can keep
1253
+ * calling it without one; when supplied, the region-only rules of
1254
+ * {@link prepareChatBodyFor} ride along. Both in-repo callers are the
1255
+ * compaction paths, which always know their shim's region.
1256
+ */
1257
+ buildChatBody(base, messages, region) {
1258
+ const raw = JSON.stringify({
1030
1259
  ...base,
1031
1260
  messages
1032
- }));
1261
+ });
1262
+ return region === void 0 ? this.prepareChatBody(raw) : this.prepareChatBodyFor(raw, region);
1033
1263
  }
1034
1264
  /**
1035
1265
  * Run one NON-streaming completion and return the assistant text.
@@ -1039,13 +1269,10 @@ var WorkBuddyUpstreamClient = class {
1039
1269
  * single string. Throws on any failure: the compactor then falls back to
1040
1270
  * plain truncation rather than failing the user's turn.
1041
1271
  */
1042
- async completeChat(credential, prepared, signal) {
1272
+ async completeChat(credential, prepared, signal, conversationKey) {
1043
1273
  const response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
1044
1274
  method: "POST",
1045
- headers: {
1046
- ...chatHeaders(credential),
1047
- ...CHAT_IDENTITY_HEADERS
1048
- },
1275
+ headers: chatCompletionHeaders(credential, conversationKey),
1049
1276
  body: prepared,
1050
1277
  ...signal === void 0 ? {} : { signal }
1051
1278
  });
@@ -1057,15 +1284,12 @@ var WorkBuddyUpstreamClient = class {
1057
1284
  return await readCompletionText(response.body);
1058
1285
  }
1059
1286
  /** Forward one chat completion. Never throws for upstream failures. */
1060
- async chatStream(credential, prepared, signal) {
1287
+ async chatStream(credential, prepared, signal, conversationKey) {
1061
1288
  let response;
1062
1289
  try {
1063
1290
  response = await this.fetchImpl(`${chatBase(credential)}/v2/chat/completions`, {
1064
1291
  method: "POST",
1065
- headers: {
1066
- ...chatHeaders(credential),
1067
- ...CHAT_IDENTITY_HEADERS
1068
- },
1292
+ headers: chatCompletionHeaders(credential, conversationKey),
1069
1293
  body: prepared,
1070
1294
  ...signal === void 0 ? {} : { signal }
1071
1295
  });
@@ -3501,8 +3725,17 @@ var WorkBuddyAccountPool = class {
3501
3725
  });
3502
3726
  }
3503
3727
  }
3728
+ const byUid = /* @__PURE__ */ new Map();
3729
+ for (const account of byId.values()) {
3730
+ const uid = account.credential.uid;
3731
+ if (typeof uid === "string" && uid !== "" && !byUid.has(uid)) byUid.set(uid, account.id);
3732
+ }
3504
3733
  for (const credential of found) {
3505
- const id = workbuddyAccountId(credential);
3734
+ const byIdentity = workbuddyAccountId(credential);
3735
+ const uid = credential.uid;
3736
+ const knownId = typeof uid === "string" && uid !== "" ? byUid.get(uid) : void 0;
3737
+ const id = knownId ?? byIdentity;
3738
+ if (knownId === void 0 && typeof uid === "string" && uid !== "") byUid.set(uid, id);
3506
3739
  const existing = byId.get(id);
3507
3740
  if (existing === void 0) {
3508
3741
  byId.set(id, {
@@ -10043,9 +10276,22 @@ function stringList$2(value) {
10043
10276
  }
10044
10277
  return out;
10045
10278
  }
10046
- /** The combo id a name slugifies to, or '' when the name cannot form one. */
10279
+ /**
10280
+ * The combo id a name slugifies to, or '' when the name cannot form one.
10281
+ *
10282
+ * Unicode-aware on purpose. This used to be `/[^a-z0-9]+/g`, which meant any
10283
+ * name that was not ASCII — every Chinese chain name — slugged to the empty
10284
+ * string, and `normalizeCombos` then dropped the entry with a bare `continue`.
10285
+ * The card reported "saved", the store held nothing, and the chain vanished on
10286
+ * the next read: the "new route cannot be saved" report. A name is user data and
10287
+ * may be written in any script, so the id has to survive one.
10288
+ *
10289
+ * Latin names keep the old shape (`writing-fallback`), because ids already
10290
+ * stored that way must keep resolving; only characters that used to be stripped
10291
+ * to nothing are now kept.
10292
+ */
10047
10293
  function comboId(name) {
10048
- const slug = text$6(name).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
10294
+ const slug = text$6(name).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48).replace(/-+$/u, "");
10049
10295
  return slug === "" ? "" : `${COMBO_PREFIX}${slug}`;
10050
10296
  }
10051
10297
  /** True when the id names a combo row. */
@@ -10068,7 +10314,9 @@ function isComboRow(row) {
10068
10314
  *
10069
10315
  * A chain with no members is DROPPED rather than kept as an empty row: a model
10070
10316
  * 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.
10317
+ * only produces a request that fails with nothing to fix. The card warns about
10318
+ * that case before the save (`route.noMembers`), which is why dropping here is
10319
+ * not a silent loss.
10072
10320
  */
10073
10321
  function normalizeCombos(raw) {
10074
10322
  if (!Array.isArray(raw)) return [];
@@ -11596,7 +11844,6 @@ const NO_COST = {
11596
11844
  cacheRead: 0,
11597
11845
  cacheWrite: 0
11598
11846
  };
11599
- /** pi-ai input modalities: images only when the catalog advertises them. */
11600
11847
  function modelInput(info) {
11601
11848
  return info.supportsImages ? ["text", "image"] : ["text"];
11602
11849
  }
@@ -11773,26 +12020,44 @@ function createWorkBuddyAdapter(options) {
11773
12020
  if (aggregateCatalog) return [...catalog.providerRows().map((row) => toAggregateModel(row, baseUrl, providerId)), ...catalog.comboRows().map((row) => toComboModel(row, baseUrl, providerId))];
11774
12021
  return catalog.visible().map((info) => toPiModel(info, baseUrl, providerId));
11775
12022
  };
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()
12023
+ /**
12024
+ * Build a fresh provider object whose `models` array is the catalog as it
12025
+ * stands right now.
12026
+ *
12027
+ * `createProvider` snapshots the array it is handed, and the host keeps its
12028
+ * own snapshot keyed by the IDENTITY of the `profiles` map
12029
+ * (`dsh-llm-pi-ai/lib/index.js` `PiAiAdapter.current()`: it rebuilds the
12030
+ * model collection only when `this.snapshot.profiles !== profiles`, then
12031
+ * calls `models.setProvider(profile.piProvider)`). Reusing one provider object
12032
+ * across `invalidate()` therefore could not work: the host would recognise the
12033
+ * unchanged profiles map, hand back its cached snapshot, and re-collect the
12034
+ * very same provider — so a model list that changed upstream (a new model
12035
+ * released, an account's entitlements changing) stayed frozen until restart.
12036
+ * That is what users saw on the international gateway. Returning a brand-new
12037
+ * provider from a brand-new profile is the only lever the adapter is given.
12038
+ */
12039
+ const makeProvider = () => {
12040
+ return {
12041
+ ...createProvider({
12042
+ id: providerId,
12043
+ name: displayName,
12044
+ auth: { apiKey: {
12045
+ name: "XD Pool loopback secret",
12046
+ async resolve({ credential }) {
12047
+ const apiKey = credential?.key;
12048
+ return apiKey === void 0 || apiKey.length === 0 ? void 0 : {
12049
+ auth: { apiKey },
12050
+ source: "XD Pool"
12051
+ };
12052
+ }
12053
+ } },
12054
+ models: buildModels(),
12055
+ api: openAICompletionsApi()
12056
+ }),
12057
+ getModels: () => buildModels()
12058
+ };
11794
12059
  };
11795
- const profile = {
12060
+ const makeProfile = (piProvider) => ({
11796
12061
  provider: providerId,
11797
12062
  displayName,
11798
12063
  streamIdleTimeoutMs: WORKBUDDY_STREAM_IDLE_TIMEOUT_MS,
@@ -11800,8 +12065,9 @@ function createWorkBuddyAdapter(options) {
11800
12065
  configuredMaxTokens: /* @__PURE__ */ new Map(),
11801
12066
  modelErrors: /* @__PURE__ */ new Map(),
11802
12067
  ...REQUEST_IMAGE_BUDGETS,
11803
- piProvider: provider
11804
- };
12068
+ piProvider
12069
+ });
12070
+ const profile = makeProfile(makeProvider());
11805
12071
  let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
11806
12072
  return {
11807
12073
  providerId,
@@ -11815,7 +12081,7 @@ function createWorkBuddyAdapter(options) {
11815
12081
  }),
11816
12082
  buildModels,
11817
12083
  invalidate: () => {
11818
- profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
12084
+ profiles = /* @__PURE__ */ new Map([[providerId, makeProfile(makeProvider())]]);
11819
12085
  }
11820
12086
  };
11821
12087
  }
@@ -11968,7 +12234,7 @@ function readPackAccountPool(ctx) {
11968
12234
  */
11969
12235
  async function readPackCredentials(pool) {
11970
12236
  if (pool === void 0) return [];
11971
- const entries = pool.listAllAccounts().filter((entry) => {
12237
+ const entries = (await pool.listAllAccounts()).filter((entry) => {
11972
12238
  const provider = text$3(entry.provider);
11973
12239
  return provider !== void 0 && PACK_REGION_CHANNELS.includes(provider);
11974
12240
  });
@@ -13330,6 +13596,24 @@ function freeFetch(url, init = {}) {
13330
13596
  });
13331
13597
  }
13332
13598
  /**
13599
+ * Add the one fact a 429 on this lane can never convey on its own.
13600
+ *
13601
+ * The free tier meters quota PER UPSTREAM SESSION, not per key and not per
13602
+ * account — the gateway's own sentence is "the provider asked to wait 74558s",
13603
+ * i.e. twenty hours, which reads like a ban on the whole provider. It is not:
13604
+ * a different conversation maps to a different session and is served
13605
+ * immediately. Saying so costs one clause and turns a message that looks like
13606
+ * "this lane is over" into one that tells the user what to do next.
13607
+ *
13608
+ * Only a rate class gets the hint: a dead credential or a missing model has
13609
+ * nothing to do with the session budget, and adding the clause there would
13610
+ * invite the user to retry something that cannot succeed.
13611
+ */
13612
+ function withSessionQuotaHint(error, retryAfterMs) {
13613
+ if (!(error instanceof AggregateError) || error.kind !== "soft_rate" || retryAfterMs === void 0 || error.message.includes("per conversation")) return error;
13614
+ return new AggregateError(error.kind, `${error.message} — this quota is counted per conversation on the free tier, so a new conversation (or another model) starts with its own budget`, error.status);
13615
+ }
13616
+ /**
13333
13617
  * POST one streaming turn and hand back chat-shaped deltas.
13334
13618
  *
13335
13619
  * The shape of the ANSWER decides how it is read, never its Content-Type: a front
@@ -13373,7 +13657,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
13373
13657
  const retryAfter = retryAfterMs(response);
13374
13658
  if (!response.ok) {
13375
13659
  const refusal = await response.text().catch(() => "");
13376
- throw classifyUpstream(response.status, refusal, retryAfter, "");
13660
+ throw withSessionQuotaHint(classifyUpstream(response.status, refusal, retryAfter, ""), retryAfter);
13377
13661
  }
13378
13662
  if (response.body === null) throw new AggregateError("server", "the free lane returned no body", response.status);
13379
13663
  let events;
@@ -13406,7 +13690,7 @@ async function postFreeStreamed({ modelId, body, session, requestId, hooks, plan
13406
13690
  } catch {
13407
13691
  continue;
13408
13692
  }
13409
- if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw classifyUpstream(response.status, event, retryAfter, "");
13693
+ if (isRecord$1(decoded) && (decoded.error !== void 0 || decoded.type === "error")) throw withSessionQuotaHint(classifyUpstream(response.status, event, retryAfter, ""), retryAfter);
13410
13694
  const translated = chatChunksFor(wire, decoded, tools);
13411
13695
  for (const delta of translated.frames) hooks.onDelta(delta);
13412
13696
  if (translated.usage !== void 0) usage = translated.usage;
@@ -18204,6 +18488,7 @@ function createWorkBuddyShim(options) {
18204
18488
  }
18205
18489
  const attempts = [];
18206
18490
  let last;
18491
+ const conversationKey = conversationKeyOf(raw);
18207
18492
  for (const member of members) {
18208
18493
  if (signal.aborted) return;
18209
18494
  attempts.push(member.modelId);
@@ -18218,8 +18503,8 @@ function createWorkBuddyShim(options) {
18218
18503
  };
18219
18504
  continue;
18220
18505
  }
18221
- const prepared = client.prepareChatBody(raw);
18222
- const result = await client.chatStream(account.credential, prepared, signal);
18506
+ const prepared = client.prepareChatBodyFor(raw, region ?? "cn");
18507
+ const result = await client.chatStream(account.credential, prepared, signal, conversationKey);
18223
18508
  if (result.ok) {
18224
18509
  const turnId = randomBytes(8).toString("hex");
18225
18510
  await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageRecorder === void 0 ? void 0 : {
@@ -18342,7 +18627,7 @@ function createWorkBuddyShim(options) {
18342
18627
  writeOpenAIError(res, 503, `${lane}_unavailable`, lane === "free" ? `${modelId} is not available right now; the free lane's model listing is refreshed on the XD Pool card` : `${modelId} is not available right now; sign in to the co-paid lane on the XD Pool card`);
18343
18628
  return;
18344
18629
  }
18345
- const prepared = client.prepareChatBody(raw);
18630
+ const prepared = client.prepareChatBodyFor(raw, region ?? "cn");
18346
18631
  const conversationKey = conversationKeyOf(raw);
18347
18632
  const tried = [];
18348
18633
  let last;
@@ -18381,7 +18666,7 @@ function createWorkBuddyShim(options) {
18381
18666
  return;
18382
18667
  }
18383
18668
  tried.push(account.label);
18384
- const result = await client.chatStream(account.credential, prepared, controller.signal);
18669
+ const result = await client.chatStream(account.credential, prepared, controller.signal, conversationKey);
18385
18670
  if (result.ok) {
18386
18671
  await serveSuccessfulStream(res, account, result, logger, refreshBalance, pool, usageContext);
18387
18672
  return;
@@ -18639,8 +18924,8 @@ async function recoverFromContextOverrun(options) {
18639
18924
  ...parsed,
18640
18925
  stream: true,
18641
18926
  max_tokens: Math.max(256, Math.floor(budget / 2))
18642
- }, request);
18643
- return await client.completeChat(summariser.credential, body, signal ?? controller.signal);
18927
+ }, request, region ?? "cn");
18928
+ return await client.completeChat(summariser.credential, body, signal ?? controller.signal, conversationKeyOf(raw));
18644
18929
  } }, controller.signal);
18645
18930
  compacted = outcome.messages;
18646
18931
  summary = outcome.summary;
@@ -18654,7 +18939,7 @@ async function recoverFromContextOverrun(options) {
18654
18939
  ok: false,
18655
18940
  detail: compactionDetail === "" ? "compaction could not reduce the prompt" : compactionDetail
18656
18941
  };
18657
- const retryBody = client.buildChatBody(parsed, compacted);
18942
+ const retryBody = client.buildChatBody(parsed, compacted, region ?? "cn");
18658
18943
  const tried = [];
18659
18944
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
18660
18945
  if (controller.signal.aborted) return {
@@ -18667,7 +18952,7 @@ async function recoverFromContextOverrun(options) {
18667
18952
  detail: "no account available after compaction"
18668
18953
  };
18669
18954
  tried.push(account.label);
18670
- const result = await client.chatStream(account.credential, retryBody, controller.signal);
18955
+ const result = await client.chatStream(account.credential, retryBody, controller.signal, conversationKeyOf(raw));
18671
18956
  if (!result.ok) usage?.recorder.record({
18672
18957
  region: usage.region,
18673
18958
  model: usage.model,
@@ -20415,6 +20700,19 @@ function apply(ctx, config = {}) {
20415
20700
  adaptersByRegion.global.invalidate();
20416
20701
  adaptersByRegion.aggregate.invalidate();
20417
20702
  adaptersByRegion.free.invalidate();
20703
+ for (const [handle, routes] of [
20704
+ [releaseAdapterCn, [POOL_PROVIDER_BY_REGION.cn]],
20705
+ [releaseAdapterGlobal, [POOL_PROVIDER_BY_REGION.global]],
20706
+ [releaseAdapterAggregate, [XDPOOL_AGGREGATE_PROVIDER]],
20707
+ [releaseAdapterFree, [XDPOOL_FREE_PROVIDER]]
20708
+ ]) {
20709
+ if (handle === void 0) continue;
20710
+ try {
20711
+ handle.replace([...routes]);
20712
+ } catch (error) {
20713
+ ctx.logger.warn("dsh-xdpool: could not re-announce adapter routes", error);
20714
+ }
20715
+ }
20418
20716
  };
20419
20717
  aggregateCatalog.onChange(() => {
20420
20718
  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.1",
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",