dsh-connect-workbuddy 3.4.4 → 3.5.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,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.0 (2026-10-04)
4
+
5
+ ### Features
6
+
7
+ - **每个模型行新增「可关闭思考」勾选框:`off` 档位不再由上游声明单独决定(issue #34)。**
8
+ - **背景**:本版之前,某个模型是否提供「关闭思考」(`reasoning_effort: "off"`)完全由上游的 `canDisableThinking` 声明决定,而那个声明对部分模型是**错的**——它们声称可关闭思考,实际对 `off` 返回 HTTP 400,且报文里不说是哪个参数被拒。
9
+ - **做什么**:模型行上多一个「可关闭思考」勾选框,勾上 = 提供 `off`,取消 = 不提供。**每个模型都有这个勾选框**,最终由用户决定;上游声明与内置名单只决定**默认值**,不再是一道墙。
10
+ - **默认怎么来**:内置一份按精确 id 复核的名单(`src/off-thinking.ts` 的 `REFUSES_OFF_MODEL_IDS`,9 个模型),首次读取时用它填勾选状态,所以「不勾」的模型从一开始就不是这 9 个会失败的。这份名单**只是种子**,不是墙:
11
+ - 上游新增一个拒绝 `off` 的模型 → **取消勾选**即可,不必等我们发版;
12
+ - 名单误伤了一个其实可用的模型 → **勾上**即可恢复。
13
+ - **首次保存即生效**:保存的是勾选后的 id 列表(`offModelIds: string[]`)。`undefined`(从未保存)与 `[]`(用户全不勾)是可区分的,前者用内置名单填,后者保持空——`regionStateOf` 读原始槽位,所以这个区分在数据层天然存在。
14
+ - **刷新不覆盖**:与「图片」勾选刻意不同。图片的勾选在刷新时会被上游声明重新同步;`off` 的勾选**保持不动**,因为它存在的理由就是「上游声明不可信」,刷新重写等于抹掉用户的修正。
15
+
16
+ ### Fixes
17
+
18
+ - **保存模型列表不再抹掉 `canDisableThinking`,`off` 不会在保存后从全部模型上消失(issue #34 同族缺陷,发布前发现)。**
19
+ - **现象**:在 3.4.5 及更早版本上,只要在卡片上按过一次「保存」,**所有模型**的「关闭思考」档位就会一起消失——连 `glm-5.3`、`kimi-k3` 这些确实接受 `off` 的也一起没了。
20
+ - **根因**:`toWebModel` 把 `canDisableThinking` 从卡片负载里丢掉了,而卡片保存时会把整行模型**原样写回** `lastCatalog`。于是这一次保存把上游的声明从每一行里删掉了;下次解析名册读到「没有声明」,而缺省一律视为**不提供 `off`**(这个保守缺省必须保留:`hy4-preview`、`hy3-x` 明确声明 `false`,把缺省翻成 `true` 会让它们重新开始报 400)。
21
+ - **修法**:`canDisableThinking` 现在随卡片负载往返,与旁边的 `supportsImages` 同一种处理——它是**上游的声明**,只负责持久化,不是运行时答案(运行时读的是勾选状态盖上去的值)。
22
+ - **已有配置**:曾经保存过的 profile 里那份声明已经丢了。按一次「从 WorkBuddy 刷新」再「保存」即可补回;在那之前也可以直接在勾选框上手动勾选。
23
+
24
+ - **对上游声明为可关闭、实际返回 HTTP 400 的模型,不再提供 `off` 档位(issue #34)。**
25
+ - **现象**:选中 `off` 后**每一次**请求都硬失败,且报文里 `extError.param` 是空的——上游不说是哪个参数被拒。因为 `off` 属于按模型保存的选择,失败会一直持续到用户手动换档,看起来像「这个模型坏了」。
26
+ - **依据(报告者实测,3.4.4,两个网关)**:28 次观测中 18 次接受 `off`(HTTP 200)、10 次返回 400(去重后 9 个模型 id);**除 `off` 之外**的每一档(`minimal`/`low`/`medium`/`high`/`xhigh`/`max`)以及完全不传该字段,在两个网关上都是 200——`off` 是唯一失败的档位。两个网关用**不同的错误码报同一个原因**:国际版 `11133`(`Invalid request parameters`,附 `extError.code: model_param_invalid`),国内版 `11150`(`the reasoning effort value is not supported by the current model`)。
27
+ - **不再信任声明**:`src/off-thinking.ts` 新增一份按**精确模型 id** 复核的名单(`deepseek-v4.1-flash` / `-sg` / `-pro`、`primary-model`、`gpt-6-astra`、`gpt-5.6-sol` / `-terra` / `-luna`、`gemini-3.5-flash`),与 `native-modality.ts` 的图片能力表同一种做法。没有任何**声明字段**能分开这两组:接受侧是 GLM/Kimi/MiniMax 加 `fast-model`/`balanced-model`/`auto`/`hy3`,拒绝侧是 DeepSeek-v4/GPT/Gemini 前沿模型加 `primary-model`,两组公布的是一模一样的能力。
28
+ - **按 id 而非按区域**:`deepseek-v4.1-flash` 在**两个网关上都拒绝**,分组跟着模型家族走而不是网关。代价是不对称的——提供一个必定失败的档位,代价是用户该模型的**每一次**请求;误藏一个本来可用的档位,代价只是一个选项——所以名单**宁可多藏**,并交给上面的勾选框兜底。
29
+ - **为什么是「与 200K 同样的取法」**:`null` 才是撤销档位的唯一正确手段,与档位的**值**是两个不同的位置——pi-ai 的 `getSupportedThinkingLevels` 只在 `map[level] === null` 时把该档位移出选择器,而线上取值走的是 `map[level] ?? level`,所以给个字符串仍会照发。
30
+
31
+ ### Docs
32
+
33
+ - `docs/reasoning-investigation.md` 的实测表(`hy3-x`、`hy4-preview`、`hy4-preview-x` 声明 `canDisableThinking: false`)是「缺省不能翻成 `true`」这一判断的依据,本版沿用该结论而未改动该文档。
34
+ - 两份 README 的「模型管理」一节补上「可关闭思考」勾选框,并修正 3.4.5 起已失效的「窗口最高 200K」表述(issue #33 已改为按原生窗口宣称)。
35
+
36
+ ## 3.4.5 (2026-10-04)
37
+
38
+ ### Fixes
39
+
40
+ - **保存设置不再把「本该用实时目录」的模型槽写空,正在用的模型不会凭空消失(issue #32)。**
41
+ - **现象**:在 `regions` 从未成功写入过的 profile 上(如 issue #31 所述的环境),第一次保存(哪怕只改了一处偏好)之后,模型选择器里**正在使用的默认模型突然不见了**,其他只在上游目录里、不在内置静态名单里的模型也一起消失,且**没有任何报错**。
42
+ - **根因**(两半,缺一不可):
43
+ 1. **写半边**——`__save` 的 `merged` 以宿主解析值为种子。当 `regions` 还是空对象时,`{"cn":{"imageModelIds":[]}}` 这样的载荷会落下**一个没有 `lastCatalog` 的 cn 槽**。槽本身合法(schema 里 `lastCatalog` 默认就是 `[]`),但它缺了这份目录。
44
+ 2. **读半边**——`configuredModels` / `displayModels` 把 roster 解析成两步:`state.lastCatalog?.length ? lastCatalog : 静态名单`。「槽存在但没有目录」和「从未配置过」于是被当成同一件事,都退回**内置的 12 条静态名单**;而 `applySelection` 又把这份静态名单 `catalog.set()` 进**实时目录**——实时发现注册的 17 条就此被覆盖。正在用的 `space-bunny` 只在实时目录里、不在静态名单里,当场消失。
45
+ - **修法**:
46
+ - 写半边:`__save` 在合并之后、落盘之前,对本次载荷**真正触及**的区域(只碰 `incoming` 里出现过的键,不动调用方没提到的兄弟区域),若该槽缺目录(`lastCatalog` 缺失或为空数组),就用 `displayModels` 取到的**实时名册**补上。用 `displayModels` 而非上游往返:保存不该依赖网络,也不该因为上游不可用而失败。调用方显式传来的数组(含非空数组)仍然优先,实时名册为空则维持原状、不写空数组。
47
+ - 读半边:roster 解析改成三步 `lastCatalog → 该区域当前已注册的实时目录 → 静态名单`。静态名单退回它本来该待的位置——上游离线、什么都发现不了时的最后兜底。`applySelection` 在 `set()` **之前**读一次当前目录,避免观察到自己正要写的值。
48
+ - 这样「槽存在但缺目录」会**降到实时目录**,永远不会越过它掉到静态名单。
49
+
50
+ - **未显式设置预算的模型不再被强制压到 200K,1M 模型的真实窗口得以保留(issue #33)。**
51
+ - **现象**:上游目录里 17 个模型有 15 个被改报成 200000——`space-bunny`、`deepseek-v4.1-flash`、`deepseek-v4-pro`、`glm-5.3`、`glm-5.3-flash`、`glm-5.2`、`kimi-k3-1`、`kimi-k2.8-preview` 的 1000000、`hy4-preview` 的 960000、`minimax-m3` 的 512000、`auto`/`kimi-k2.6`/`kimi-k2.7` 的 256000 全部变成 200000。
52
+ - **根因**:`applyContextBudgets` 里 `budgets[model.id] ?? 200_000` 的默认值——**未设置预算被当成「设成 200K」**。这条默认值在 CHANGELOG 里给出的理由是「让 DSH 提前压缩以避开上游节流」,但同一份 CHANGELOG 又自述实测节流线是 **2 万~3 万 token 且单次请求即触顶**:200K 比那条线高一个数量级,压到 200K 并没有把压缩提前到安全区。200K 在静态目录里也无出处(`auto` 是 168000、`hy3` 是 192000、`default-model` 是 176000、`primary-model` 是 272000);插件自己的静态目录还把 `glm-5.3` 等声明为 1000000,与钳制自相矛盾。
53
+ - **修法**:`applyContextBudgets` 不再有默认值——**只做上限**:未设置预算时保留模型的原生窗口,设置了就用 `Math.min(原生, 预算)`。`contextBudgets` 的语义恢复成「用户设的上限」,而不是一个隐式的全局改写。(`toWebModel` 里那份重复的 `?? 200_000` 也一并去掉,改为调用同一个函数,避免卡片与运行时目录各说各话。)
54
+ - 想继续压缩的用户仍然可以显式设置 `regions.<region>.contextBudgets`;卡片上原本只在原生窗口 > 200K 时才出现的「DSH 上下文预算」选择器,现在因为不再默认钳制而更有意义。
55
+
3
56
  ## 3.4.4 (2026-10-03)
4
57
 
5
58
  ### Features
package/README.en.md CHANGED
@@ -24,10 +24,10 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
24
24
  - **Dual parallel providers (CN + international)** — the CN side registers as the `workbuddy` provider (`GLM-5.3`, `DeepSeek-V4-Pro`, `Kimi-K3`, `MiniMax-M3`, `Hy3`, etc.), the international side as `workbuddy-global` (`GPT-5.6`, `Gemini-3.5-Flash`, `GLM-5.3`, `Kimi-K3`, etc.). **A region's models appear in the DSH model picker when that region has an account**: with accounts on both sides, both rosters are selectable at once, and different sessions can each pick a side without interfering. **A region with no local sign-in lists no models** — it cannot serve a single request, so listing them would offer nothing but failures. Sign in once for that region and its models come back on "detect accounts again" or after a restart; the provider itself stays registered either way, because its settings card is how you sign in (re-detection lives in the pool block). Model names carry the upstream credit multiplier (e.g. `GLM-5.3 · x0.79`), matching WorkBuddy's own model menu.
25
25
  - **Tabbed settings card** — the card's top carries a "Domestic / Global" tab bar; each tab holds its own account pool and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions.
26
26
  - **Switch either provider off on its own** — each tab carries an on/off checkbox (both checked by default). Unchecking one **withdraws that provider from DSH's model picker entirely** (it is not merely a hidden tab): its adapter route and its "Settings → Models" directory entry are both pulled, and startup no longer sends pointless requests for it. Account, credits, directory, and selection are all kept, and re-checking restores everything. The typical use is "I have no use for the international side" — switch it off and keep the picker clean. Note: a session that had already selected a model from the switched-off provider will report `NO_ADAPTER` on its next call; the card states this plainly on the switched-off tab instead of failing silently.
27
- - **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally.
27
+ - **Model management** — refresh the full catalog from upstream and enable or disable each model individually; refresh is a draft operation that only takes effect on save. Upstream also reports credit multiplier, context/output limits, and reasoning efforts; **image input is decided by a vendor-verified table**: a refresh pre-checks only models the vendor's own documentation calls natively multimodal, leaves documented text-only models unchecked, and treats anything without a vendor source as unverified and unchecked rather than guessing. Upstream's `supportsImages` is a *platform* declaration, not the model's capability, so it does not drive that decision at all. You can still tick any box by hand, and your selection is kept locally. **Every model row also carries a “Can disable” (`off`) checkbox**: it decides whether that model gets the “turn thinking off entirely” level. The default comes from a reviewed built-in table (the 9 models that claim the capability but answer HTTP 400 for it start unticked), and you can change it per model — **untick** for a newly refusing upstream model, **tick** for one the table wrongly hides or you have verified works; your ticks are saved on first save and recomputed no further.
28
28
  - **Model availability testing, including cooldown recovery times** — working out when a limited model frees up used to mean guessing through repeated failures. Now every model row has a **Test** button that gives you a direct answer for whether the model works right now and, if not, when it comes back. **It sends a real-volume request (about 25k input tokens)**, rather than "ping it and see if it answers" — because the upstream throttles on request **SIZE** (measured: about 20k tokens pass, about 30k are refused), so a small request looks fine while every request in a long conversation may be refused. That is exactly where "the test passes but I am limited" came from. **Where the recovery time comes from**: the upstream's 429 carries **no** `Retry-After` header (and no `X-RateLimit-*`), but it writes the reset time into the response **body** — `{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`. The plugin parses that time and converts it using the UTC offset the message states, so the result does not depend on your machine's timezone; the card then shows “✗ Rate limited · usable again after 2026/9/30 02:30:30”. When none of the sources states a time, the plugin **never invents a countdown** and says plainly that no time was given. A rate limit deliberately does **not** borrow the monthly refresh time, which would tell you to wait weeks for a limit that clears in seconds. Probes run in the Host, so **the credential never reaches the page**, and results are not written to settings — your saved selection is untouched.
29
29
 
30
- **Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises up to 200K to DSH, so DSH compacts very late — hitting the throttle in a long session follows directly from that.
30
+ **Two things to know.** ① **It really does spend credits** — one measured probe reported `credit` 0.02 (a refused one 429s and costs almost nothing). So testing is **manual and per-model only**: there is deliberately no “Test selected” batch button, because sweeping the whole list from one click is an uncontrolled spend — and the Host enforces the same rule, **accepting a single model only** and refusing multi-model requests. ② The upstream advertises a 1M context yet throttles far below it, and the plugin advertises each model's **native** window to DSH (no longer clamped to 200K as of 3.5.0), so DSH compacts very late — hitting the throttle in a long session follows directly from that. To make DSH compact sooner, set a “DSH context budget” cap on the model's row.
31
31
  - **Local account switching** — discovers the multiple sign-in credentials WorkBuddy's desktop app leaves behind and lets you switch per region. Tokens are never written to DSH settings.
32
32
  - **Account pool: batch actions across several accounts, plus automatic failover** — tick the accounts you want into a pool and you get **one-click check-in for every account**, **one-click testing of every account against a model you choose** (defaulting to the region's zero-multiplier free model), and with **Enable the account pool** switched on, **automatic failover**: a request that fails upstream is retried against the pool's other usable accounts, in order, until one serves it or none is left. Membership starts **empty and is an explicit opt-in**: "signed in on this machine" is not the same as "spend this account's credits and claim its rewards". The pool's **ranking decides who serves every request** (usable first, then higher credits, soonest-expiring, freshest credential), and on a failure the next usable member is tried. It **never writes back your saved selection** — switch the pool off and your own account applies again immediately. **The switch only decides whether the PLUGIN chooses** — with it off, the plugin stops picking and switching accounts for you, but check-in and testing stay available (they are manual actions). Testing reuses the model rows' real-volume probe, so it **does cost real credits**; **automatic check-in is deliberately not offered** — a check-in claims real rewards, so it only ever runs when you press the button. See [Account pool](#account-pool).
33
33
  - **Actionable advice when a credential is refused** — when the upstream rejects the selected account's token, the plugin actually **probes** the other local sign-ins: if one still answers, it tells you to switch to it (with a one-click switch) instead of vaguely asking you to sign in again — re-authenticating fixes nothing while you are pinned to a revoked backup. Only when there is genuinely no other local account does it ask you to sign in again.
package/README.md CHANGED
@@ -31,10 +31,10 @@
31
31
  - **双供应商并行接入(国内版 + 国际版)** —— 国内版注册为 `workbuddy` provider(`GLM-5.3`、`DeepSeek-V4-Pro`、`Kimi-K3`、`MiniMax-M3`、`Hy3` 等),国际版注册为 `workbuddy-global` provider(`GPT-5.6`、`Gemini-3.5-Flash`、`GLM-5.3`、`Kimi-K3` 等)。**有账号的区域其模型会出现在 DSH 模型选择器里**:两边都有账号时两组同时可选,不同会话可以各选一边,互不干扰。**本机没有账号的区域不列出任何模型**(该区域一个请求也发不出去,列出来只会点到就失败);只要在该区域登录一次,「重新检测账号」或重启后模型就会自动出现——provider 本身始终注册着,因为它的设置卡片正是登录的入口(重新检测在账号池区块里)。模型名内嵌上游积分倍率(如 `GLM-5.3 · x0.79`),与 WorkBuddy 自身模型菜单一致。
32
32
  - **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 国际版」两个 tab,各含独立的账号池与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。
33
33
  - **可单独关闭任一版本供应商** —— 每个 tab 右侧有一个勾选框(默认都勾选)。**取消勾选即把该供应商从 DSH 模型选择器里彻底撤掉**(不是只藏起 tab):它的路由与「设置 → 模型」页的目录条目一并摘除,启动时也不再为它发无用请求。账号、积分、目录与勾选**全部保留**,重新勾选即完整恢复。典型用法是「我根本用不到国际版」——关掉它,让选择器干净。注意:若某会话此前选中的正是被关闭供应商的模型,该会话再调用会报 `NO_ADAPTER`,卡片会在关闭态的 tab 上明确提示这一点,不会静默失败。
34
- - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。
34
+ - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于上游新出现的拒绝模型,**勾上**用于名单误伤、或你已确认可用的模型;首次保存即写入你的勾选,之后不再重算。
35
35
  - **模型可用性测试(含冷却恢复时间)** —— 限量模型的冷却时间过去只能靠反复试错去猜。现在每个模型行有一个「测试」按钮,直接告诉你它此刻能否使用、不能用时何时恢复。**它发的是一条真实体积的请求(约 25k 输入 token)**,而不是「发个 ping 看通不通」——因为上游的限流**按请求体积触发**(实测约 20k 能过、约 30k 被拒),小请求看着正常、长对话里却可能每次都被拒,那正是「测试通过、实际被限」的来源。**恢复时间的来源**:上游的 429 **不带** `Retry-After` 响应头(也没有 `X-RateLimit-*`),但它把重置时间写在**响应体**里——`{"code":6004,"msg":"…将在 2026-09-30 02:30:30 UTC+8 重置…"}`。插件从响应体解析该时间(并按其声明的 UTC 偏移换算,不受你本机时区影响),因此卡片能直接显示「✗ 被限流 · 2026/9/30 02:30:30 之后可再用」。三处来源都没说时,插件**不编造倒计时**,如实显示「上游未给出何时恢复」。限流**不会**去借月周期包的刷新时间(那会让人为几秒的限流去等几周)。探测在后台发起,**凭据不会传到页面**;结果不写入设置,也不改变你已保存的勾选。
36
36
 
37
- **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件向 DSH 宣称的窗口最高 200K,所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。
37
+ **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限。
38
38
  - **本机账号切换** —— 自动发现 WorkBuddy 桌面端留下的多个登录凭据,可按区域切换账号;Token 不写入 DSH 设置。
39
39
  - **账号池:多账号批量操作与自动换号** —— 把要用的账号勾进池子,即可**一键签到所有账号**、**一键测试所有账号 × 你指定的模型**(默认挑该区域倍率为 0 的免费模型);开启**启用账号池**即同时打开**自动换号**——某个请求在上游失败时,插件会依次用池内其他可用账号重试,直到有一个接手、或全部不可用为止,然后才把错误报给你。成员**默认一个都不含**、必须显式勾选(「本机有登录」不等于「要拿它去签到和花积分」);**开启后由池子的排序决定每一次请求用谁**(可用优先,其次积分高、快过期的积分包、凭据更新的);失败时依次用池内其他可用账号重试。排序**从不写回你保存的选择**——关掉开关,你选定的账号立刻完全生效。只有请求本身不合法(HTTP 400)不换号:同一个请求体换谁都是 400,重试只会白等。**这个开关只管「插件要不要自己决定」**:关掉它,插件不再替你选账号、也不再替你换号,但**签到与测试仍然可用**(它们是手动动作)。测试走的是与模型行同一套真实体积探测,因此**有真实成本**;**不提供自动签到**——签到会真实领取奖励,只在你点按钮时执行。详见[账号池](#账号池)。
40
40
  - **凭据失效时给出可执行的建议** —— 上游拒绝当前账号的 token 时,插件会**实际探测**本机其他账号:若有账号仍然可用,就直接告诉你「切换到它即可」(并附一键切换),而不是笼统地让你重新登录——被钉在一个作废的旧备份上时,重新登录并不能解决问题。只有本机确实没有别的账号可切换时,才会提示重新登录。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as legacyWorkbuddyOwnAuthPath, H as WORKBUDDY_APP_EXECUTABLE_ENV, J as findWorkbuddyAppExecutableWithSource, N as WorkBuddyUpstreamClient, S as defaultDesktopAuthDirs, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, x as defaultDesktopAuthCandidates } from "./host-heartbeat-DTuwptkM.js";
2
+ import { A as workbuddyOwnAuthPath, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, P as WorkBuddyUpstreamClient, S as defaultDesktopAuthCandidates, U as WORKBUDDY_APP_EXECUTABLE_ENV, Y as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-C6G_goTD.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -1715,6 +1715,8 @@ window.__ModuleLoader__.load({
1715
1715
  .dsm-workbuddy-model-enabled input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe);flex:none}
1716
1716
  .dsm-workbuddy-model-image{display:inline-flex;align-items:center;gap:5px;flex:none;cursor:pointer;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px}
1717
1717
  .dsm-workbuddy-model-image input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1718
+ .dsm-workbuddy-model-off{display:inline-flex;align-items:center;gap:5px;flex:none;cursor:pointer;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px}
1719
+ .dsm-workbuddy-model-off input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1718
1720
  .dsm-workbuddy-model-copy{display:flex;align-items:baseline;gap:8px;min-width:0}
1719
1721
  .dsm-workbuddy-model-name{display:inline-flex;align-items:baseline;gap:7px;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;font-weight:500;line-height:19px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1720
1722
  .dsm-workbuddy-model-name-rate{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:400;line-height:16px;flex:none}
@@ -2473,6 +2475,7 @@ window.__ModuleLoader__.load({
2473
2475
  models: fresh,
2474
2476
  enabledIds: new Set(stillEnabled),
2475
2477
  imageIds: new Set(upstreamImages),
2478
+ offIds: new Set(activeOffIds),
2476
2479
  contextBudgets: stillBudgets
2477
2480
  }
2478
2481
  }));
@@ -2495,6 +2498,8 @@ window.__ModuleLoader__.load({
2495
2498
  const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
2496
2499
  const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
2497
2500
  const activeImageIds = draft?.imageIds ?? savedImageIds;
2501
+ const savedOffIds = status.status === "signed-in" ? new Set(status.offModelIds) : /* @__PURE__ */ new Set();
2502
+ const activeOffIds = draft?.offIds ?? savedOffIds;
2498
2503
  settingsScope?.getSnapshot().value;
2499
2504
  const savedContextBudgets = status.status === "signed-in" ? status.contextBudgets ?? {} : {};
2500
2505
  const activeContextBudgets = draft?.contextBudgets ?? savedContextBudgets;
@@ -2518,6 +2523,7 @@ window.__ModuleLoader__.load({
2518
2523
  models: [...visibleModels],
2519
2524
  enabledIds: new Set(activeEnabledIds),
2520
2525
  imageIds: new Set(activeImageIds),
2526
+ offIds: new Set(activeOffIds),
2521
2527
  contextBudgets: { ...activeContextBudgets }
2522
2528
  })
2523
2529
  }));
@@ -2542,6 +2548,24 @@ window.__ModuleLoader__.load({
2542
2548
  };
2543
2549
  });
2544
2550
  };
2551
+ /**
2552
+ * Flip one row's `off` answer (issue #34).
2553
+ *
2554
+ * Writes an EXPLICIT answer in both directions, so the built-in rule is only
2555
+ * consulted while the user has no opinion. That is also why there is no
2556
+ * "reset" control: once an answer matches the rule, the two are
2557
+ * indistinguishable in effect, and the tooltip already names the override.
2558
+ */
2559
+ const toggleOff = (modelId) => {
2560
+ editDraft((current) => {
2561
+ const next = new Set(current.offIds);
2562
+ if (!next.delete(modelId)) next.add(modelId);
2563
+ return {
2564
+ ...current,
2565
+ offIds: next
2566
+ };
2567
+ });
2568
+ };
2545
2569
  const setContextBudget = (modelId, budget) => {
2546
2570
  editDraft((current) => ({
2547
2571
  ...current,
@@ -2622,6 +2646,7 @@ window.__ModuleLoader__.load({
2622
2646
  lastCatalog: visibleModels.map(toPersistedWorkBuddyModel),
2623
2647
  enabledModelIds: [...activeEnabledIds],
2624
2648
  imageModelIds: [...activeImageIds],
2649
+ offModelIds: [...activeOffIds],
2625
2650
  contextBudgets: activeContextBudgets
2626
2651
  });
2627
2652
  discardModels();
@@ -2887,6 +2912,18 @@ window.__ModuleLoader__.load({
2887
2912
  }
2888
2913
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
2889
2914
  }),
2915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2916
+ className: "dsm-workbuddy-model-off",
2917
+ title: t("row.modelOffHint"),
2918
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2919
+ type: "checkbox",
2920
+ checked: activeOffIds.has(model.id),
2921
+ disabled: !canWrite || saving,
2922
+ onChange: () => {
2923
+ toggleOff(model.id);
2924
+ }
2925
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelOff") })]
2926
+ }),
2890
2927
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
2891
2928
  className: "dsm-workbuddy-context-budget",
2892
2929
  "aria-label": t("row.contextBudget"),
@@ -3108,6 +3145,8 @@ window.__ModuleLoader__.load({
3108
3145
  "row.modelImage": "Image",
3109
3146
  "row.modelImageText": "Image input: the vendor documents this model as text-only, so a refresh leaves it unchecked. Tick it yourself only if you know it reads images.",
3110
3147
  "row.modelImageUnverified": "Image input unverified: no vendor source confirms this model reads images, so a refresh leaves it unchecked rather than guessing. Tick it yourself if you have verified it works.",
3148
+ "row.modelOff": "Can disable",
3149
+ "row.modelOffHint": "Offer the “off” level for this model",
3111
3150
  "row.modelReasoning": "Reasoning: {efforts}",
3112
3151
  "row.modelUnknown": "Unknown",
3113
3152
  "row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
@@ -3296,6 +3335,8 @@ window.__ModuleLoader__.load({
3296
3335
  "row.modelImage": "图片",
3297
3336
  "row.modelImageText": "图片输入:厂商文档标明该模型仅支持文本,因此刷新时不会自动勾选。只有你确认它能读图,才建议手动勾上。",
3298
3337
  "row.modelImageUnverified": "图片输入未核实:没有厂商资料确认该模型能读图,刷新时按「不猜」处理,不会自动勾选。如果你验证过确实可用,可以手动勾上。",
3338
+ "row.modelOff": "可关闭思考",
3339
+ "row.modelOffHint": "为这个模型提供「关闭思考」档位",
3299
3340
  "row.modelReasoning": "推理强度:{efforts}",
3300
3341
  "row.modelUnknown": "未知",
3301
3342
  "row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
@@ -2556,11 +2556,26 @@ const FALLBACK_WORKBUDDY_MODELS_GLOBAL = [
2556
2556
  function fallbackModelsFor(region) {
2557
2557
  return region === "global" ? FALLBACK_WORKBUDDY_MODELS_GLOBAL : FALLBACK_WORKBUDDY_MODELS;
2558
2558
  }
2559
- /** Apply the saved local DSH budget; models above 200K default to 200K. */
2559
+ /**
2560
+ * Apply the saved local DSH budget. A budget is an UPPER LIMIT the user sets,
2561
+ * never a default: a model without an explicit budget keeps its own window, and
2562
+ * a budget can only ever LOWER a window (`Math.min`), so a value above the
2563
+ * native one is not an error, just a no-op.
2564
+ *
2565
+ * There used to be a `?? 200_000` default here (issue #33), on the rationale
2566
+ * that a lower advertised window makes DSH compact sooner and so keeps a long
2567
+ * session under the upstream throttle. That rationale does not hold: the
2568
+ * measured throttle fires on a SINGLE request past 20k-30k input tokens (the
2569
+ * CHANGELOG's own wording calls 200K "远超上游 ~20–30k 的节流线"), so clamping to
2570
+ * 200K bought none of the safety it claimed while costing every >200K model its
2571
+ * real window — and contradicting the static roster in this very file, which
2572
+ * declares those models as 1M. A user who wants the conservative behaviour sets
2573
+ * a budget explicitly; the card offers that per model.
2574
+ */
2560
2575
  function applyContextBudgets(catalog, budgets = {}) {
2561
2576
  return catalog.map((model) => ({
2562
2577
  ...model,
2563
- contextWindow: model.contextWindow > 2e5 ? Math.min(model.contextWindow, budgets[model.id] ?? 2e5) : model.contextWindow
2578
+ contextWindow: budgets[model.id] === void 0 ? model.contextWindow : Math.min(model.contextWindow, budgets[model.id])
2564
2579
  }));
2565
2580
  }
2566
2581
  function deriveCatalog(catalog, enabled, budgets = {}) {
@@ -2791,7 +2806,7 @@ function generationOf(version) {
2791
2806
  //#endregion
2792
2807
  //#region src/version.ts
2793
2808
  /** The npm package version this build was produced from. */
2794
- const WORKBUDDY_CONNECT_VERSION = "3.4.4";
2809
+ const WORKBUDDY_CONNECT_VERSION = "3.5.0";
2795
2810
  //#endregion
2796
2811
  //#region src/host-heartbeat.ts
2797
2812
  /**
@@ -2949,4 +2964,4 @@ async function clearHostHeartbeat() {
2949
2964
  await rm(workbuddyHostHeartbeatPath(), { force: true });
2950
2965
  }
2951
2966
  //#endregion
2952
- export { openEncryptedField as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, findWorkbuddyAppExecutableWithSource as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, macosNestedAppBundles as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, isWorkbuddyBundle as X, isEncryptedFieldWrapper as Y, macosBundleExecutable as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, readAtRestKey as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, workbuddyAppExecutableCandidates as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
2967
+ export { macosNestedAppBundles as $, workbuddyOwnAuthPath as A, parseUpstreamModel as B, defaultDesktopAuthDirs as C, legacyWorkbuddyOwnAuthPath as D, isEncryptedCredentialError as E, classifyUpstreamError as F, deriveAtRestKey as G, regionOf as H, declaredTools as I, findWorkbuddyAppExecutable as J, deriveAtRestKeyId as K, isCredentialRejectedError as L, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as M, WorkBuddyCredentialRejectedError as N, parseWorkBuddyAuth as O, WorkBuddyUpstreamClient as P, macosBundleExecutable as Q, parseCreditMultiplier as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, WORKBUDDY_APP_EXECUTABLE_ENV as U, prepareChatBody as V, clearAtRestKeyCache as W, isEncryptedFieldWrapper as X, findWorkbuddyAppExecutableWithSource as Y, isWorkbuddyBundle as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, openEncryptedField as et, WorkBuddyCatalog as f, ENCRYPTED_CREDENTIAL_CODE as g, fallbackModelsFor as h, processStartTimeMs as i, CREDENTIAL_REJECTED_CODE as j, workbuddyAccountId as k, piAiRuntimeInfo as l, deriveCatalog as m, clearHostHeartbeat as n, workbuddyAppExecutableCandidates as nt, workbuddyHostHeartbeatPath as o, applyContextBudgets as p, fetchAtRestKeyPayload as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, readAtRestKey as tt, FALLBACK_WORKBUDDY_MODELS as u, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, parseReasoning as z };
package/lib/index.d.ts CHANGED
@@ -1050,6 +1050,68 @@ declare function imageDefaultFor(info: {
1050
1050
  supportsImages?: boolean;
1051
1051
  }): boolean;
1052
1052
  //#endregion
1053
+ //#region src/off-thinking.d.ts
1054
+ /**
1055
+ * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
1056
+ * declaring `canDisableThinking: true` (issue #34).
1057
+ *
1058
+ * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
1059
+ * refuses on BOTH gateways, and the split tracks the model family rather than
1060
+ * the gateway. The cost is asymmetric — offering a level that always fails
1061
+ * costs the user every request for that model, while hiding one that would have
1062
+ * worked costs a single option — so the table errs towards hiding. A user who
1063
+ * knows better can override any entry (see {@link effectiveOff}).
1064
+ *
1065
+ * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
1066
+ * its sibling does refuse on both, and the same asymmetry argues for hiding.
1067
+ */
1068
+ declare const REFUSES_OFF_MODEL_IDS: ReadonlySet<string>;
1069
+ /** The two fields this rule needs; both are declared by the model itself. */
1070
+ interface WorkBuddyOffCapability {
1071
+ id: string;
1072
+ reasoning?: WorkBuddyReasoning;
1073
+ }
1074
+ /**
1075
+ * Whether `off` should be offered for a model when the user has expressed no
1076
+ * opinion: the upstream's declaration, minus the ids known to refuse it.
1077
+ *
1078
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
1079
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1080
+ * undeclared capability.
1081
+ */
1082
+ declare function offDefaultFor(info: WorkBuddyOffCapability): boolean;
1083
+ /**
1084
+ * The model ids the `off` level should be offered for by default (issue #34).
1085
+ *
1086
+ * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
1087
+ * This is the seed — what the set is filled with the first time it is read —
1088
+ * so a profile that has never saved shows the correct default immediately,
1089
+ * without requiring a save.
1090
+ *
1091
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
1092
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1093
+ * undeclared capability.
1094
+ */
1095
+ declare function defaultOffModelIds(models: readonly WorkBuddyOffCapability[]): string[];
1096
+ /**
1097
+ * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
1098
+ * catalog.
1099
+ *
1100
+ * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
1101
+ * reads only the model's own declaration — so the corrected answer has to be
1102
+ * written onto the model before it reaches the adapter. Doing it here keeps the
1103
+ * adapter a pure "declared capability in, level map out" function and keeps the
1104
+ * table next to the rule it feeds.
1105
+ *
1106
+ * This OVERWRITES the declared field rather than adding a sibling, which is
1107
+ * safe because the declared value has exactly one consumer (that same level
1108
+ * map) and is never persisted where the card can see it: `toWebModel` drops
1109
+ * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
1110
+ * upstream discovery, never from this stamped list. A model that declares no
1111
+ * reasoning is returned untouched — there is no level to correct.
1112
+ */
1113
+ declare function withEffectiveOff<T extends WorkBuddyOffCapability>(models: readonly T[], selection: ReadonlySet<string>): T[];
1114
+ //#endregion
1053
1115
  //#region src/at-rest.d.ts
1054
1116
  /**
1055
1117
  * WorkBuddy desktop "at-rest" credential decryption.
@@ -1804,6 +1866,21 @@ interface WorkBuddyWebModel {
1804
1866
  reasoning?: {
1805
1867
  supportedEfforts?: readonly string[];
1806
1868
  defaultEffort?: string;
1869
+ /**
1870
+ * Upstream's own "thinking can be turned off" declaration, carried through
1871
+ * the card so it RIDES ALONG in the saved `lastCatalog` entry.
1872
+ *
1873
+ * It is a persistence detail, not the runtime answer: the adapter's level
1874
+ * map is built from the effective value stamped in `index.ts` (declaration
1875
+ * minus the models known to reject `off`, then the user's override). It has
1876
+ * to travel because the card writes the whole row back on save — omitting
1877
+ * it made one Save silently strip the declaration from every model, which
1878
+ * withdrew `off` from the models that accept it (the default is
1879
+ * conservative: an absent declaration never offers `off`, so `hy4-preview`
1880
+ * and friends keep their intended `false` but the rest lose a working
1881
+ * level). Same shape as `supportsImages` above.
1882
+ */
1883
+ canDisableThinking?: boolean;
1807
1884
  };
1808
1885
  description?: string;
1809
1886
  }
@@ -1915,6 +1992,11 @@ type WorkBuddyWebUsage =
1915
1992
  models: readonly WorkBuddyWebModel[];
1916
1993
  enabledModelIds: readonly string[];
1917
1994
  imageModelIds: readonly string[];
1995
+ /**
1996
+ * Model ids whose `off` thinking level is offered (issue #34): the saved
1997
+ * selection, seeded from the built-in rule the first time it is read.
1998
+ */
1999
+ offModelIds: readonly string[];
1918
2000
  credits?: WorkBuddyWebCredits;
1919
2001
  creditsError?: string;
1920
2002
  /**
@@ -2196,6 +2278,8 @@ interface WorkBuddyStatusRouteOptions {
2196
2278
  enabledModelIds(region: WorkBuddyRegion): readonly string[];
2197
2279
  /** Model ids the user opted into image input, for the requested region. */
2198
2280
  imageModelIds(region: WorkBuddyRegion): readonly string[];
2281
+ /** Model ids whose `off` thinking level is offered, for the requested region. */
2282
+ offModelIds(region: WorkBuddyRegion): readonly string[];
2199
2283
  /** Saved local DSH context budgets by model id, for the requested region. */
2200
2284
  contextBudgets(region: WorkBuddyRegion): Readonly<Record<string, number | undefined>>;
2201
2285
  /** Re-read the live catalog of one region from the upstream. */
@@ -2403,6 +2487,12 @@ interface WorkBuddyRegionState {
2403
2487
  enabledModelIds?: string[];
2404
2488
  /** Model ids the user explicitly opted into image input. */
2405
2489
  imageModelIds?: string[];
2490
+ /**
2491
+ * Model ids whose `off` thinking level is offered (issue #34). Seeded from
2492
+ * the built-in rule (upstream declaration minus the models known to reject
2493
+ * `off`) when first read; the card's checkbox toggles membership.
2494
+ */
2495
+ offModelIds?: string[];
2406
2496
  /** Local DSH context budget per model in this region. */
2407
2497
  contextBudgets?: Record<string, WorkBuddyContextBudget>;
2408
2498
  /** This region's account-pool preferences (opt-in; see {@link WorkBuddyPoolPreferences}). */
@@ -2604,4 +2694,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
2604
2694
  */
2605
2695
  declare function apply(ctx: Context, config: Config): void;
2606
2696
  //#endregion
2607
- export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
2697
+ export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as openEncryptedField, A as CREDENTIAL_REJECTED_CODE, B as prepareChatBody, C as defaultDesktopAuthPath, D as parseWorkBuddyAuth, E as legacyWorkbuddyOwnAuthPath, F as declaredTools, G as deriveAtRestKeyId, H as WORKBUDDY_APP_EXECUTABLE_ENV, I as isCredentialRejectedError, K as fetchAtRestKeyPayload, L as parseCreditMultiplier, M as WorkBuddyCredentialRejectedError, N as WorkBuddyUpstreamClient, O as workbuddyAccountId, P as classifyUpstreamError, Q as macosNestedAppBundles, R as parseReasoning, S as defaultDesktopAuthDirs, T as isEncryptedCredentialError, U as clearAtRestKeyCache, V as regionOf, W as deriveAtRestKey, X as isWorkbuddyBundle, Y as isEncryptedFieldWrapper, Z as macosBundleExecutable, _ as WORKBUDDY_AUTH_FILE_ENV, a as readHostHeartbeat, b as authFileName, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as readAtRestKey, f as WorkBuddyCatalog, g as WORKBUDDY_AUTH_FILENAME, h as ENCRYPTED_CREDENTIAL_CODE, i as processStartTimeMs, j as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, k as workbuddyOwnAuthPath, l as piAiRuntimeInfo, m as fallbackModelsFor, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as deriveCatalog, q as findWorkbuddyAppExecutable, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as workbuddyAppExecutableCandidates, u as FALLBACK_WORKBUDDY_MODELS, v as WorkBuddyCredentialStore, w as hasEncryptedCredentialFields, x as defaultDesktopAuthCandidates, y as WorkBuddyEncryptedCredentialError, z as parseUpstreamModel } from "./host-heartbeat-DTuwptkM.js";
1
+ import { $ as macosNestedAppBundles, A as workbuddyOwnAuthPath, B as parseUpstreamModel, C as defaultDesktopAuthDirs, D as legacyWorkbuddyOwnAuthPath, E as isEncryptedCredentialError, F as classifyUpstreamError, G as deriveAtRestKey, H as regionOf, I as declaredTools, J as findWorkbuddyAppExecutable, K as deriveAtRestKeyId, L as isCredentialRejectedError, M as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, N as WorkBuddyCredentialRejectedError, O as parseWorkBuddyAuth, P as WorkBuddyUpstreamClient, Q as macosBundleExecutable, R as parseCreditMultiplier, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as WORKBUDDY_APP_EXECUTABLE_ENV, V as prepareChatBody, W as clearAtRestKeyCache, X as isEncryptedFieldWrapper, Z as isWorkbuddyBundle, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as openEncryptedField, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, j as CREDENTIAL_REJECTED_CODE, k as workbuddyAccountId, l as piAiRuntimeInfo, m as deriveCatalog, n as clearHostHeartbeat, nt as workbuddyAppExecutableCandidates, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as fetchAtRestKeyPayload, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as readAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as parseReasoning } from "./host-heartbeat-C6G_goTD.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -3093,10 +3093,11 @@ function toCredits(answer) {
3093
3093
  }
3094
3094
  /** Project a model into the card's row, dropping empty optional fields. */
3095
3095
  function toWebModel(model, budgets) {
3096
+ const budgeted = applyContextBudgets([model], budgets)[0] ?? model;
3096
3097
  return {
3097
3098
  id: model.id,
3098
3099
  name: model.name,
3099
- contextWindow: model.contextWindow > 2e5 ? Math.min(model.contextWindow, budgets[model.id] ?? 2e5) : model.contextWindow,
3100
+ contextWindow: budgeted.contextWindow,
3100
3101
  nativeContextWindow: model.contextWindow,
3101
3102
  maxTokens: model.maxTokens,
3102
3103
  ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
@@ -3104,7 +3105,8 @@ function toWebModel(model, budgets) {
3104
3105
  ...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
3105
3106
  ...model.reasoning === void 0 ? {} : { reasoning: {
3106
3107
  ...model.reasoning.supportedEfforts === void 0 ? {} : { supportedEfforts: [...model.reasoning.supportedEfforts] },
3107
- ...model.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: model.reasoning.defaultEffort }
3108
+ ...model.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: model.reasoning.defaultEffort },
3109
+ ...model.reasoning.canDisableThinking === void 0 ? {} : { canDisableThinking: model.reasoning.canDisableThinking }
3108
3110
  } }
3109
3111
  };
3110
3112
  }
@@ -3203,7 +3205,8 @@ async function workBuddyWebStatus(deps, region) {
3203
3205
  accounts: accounts.map(toWebAccount),
3204
3206
  models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
3205
3207
  enabledModelIds: [...deps.enabledModelIds(region)],
3206
- imageModelIds: [...deps.imageModelIds(region)]
3208
+ imageModelIds: [...deps.imageModelIds(region)],
3209
+ offModelIds: [...deps.offModelIds(region)]
3207
3210
  };
3208
3211
  const [creditsResult, checkinResult] = await Promise.allSettled([deps.client.fetchCredits(credential), deps.client.fetchCheckinStatus(credential)]);
3209
3212
  const rejected = [creditsResult, checkinResult].some((result) => result.status === "rejected" && isCredentialRejectedError(result.reason));
@@ -3551,6 +3554,14 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
3551
3554
  ...slot
3552
3555
  } : slot;
3553
3556
  }
3557
+ if (field === "regions") for (const key of Object.keys(incoming)) {
3558
+ const slot = merged[key];
3559
+ if (!isPlainRecord(slot) || key !== "cn" && key !== "global") continue;
3560
+ if (Array.isArray(slot.lastCatalog) && slot.lastCatalog.length > 0) continue;
3561
+ const live = deps.displayModels(key);
3562
+ if (live.length === 0) continue;
3563
+ slot.lastCatalog = live;
3564
+ }
3554
3565
  await settings.mutate(row.ns, [{
3555
3566
  op: "set",
3556
3567
  path: [field],
@@ -3631,6 +3642,85 @@ function imageDefaultFor(info) {
3631
3642
  return NATIVE_MODALITY_BY_MODEL_ID[info.id] === "multimodal";
3632
3643
  }
3633
3644
  //#endregion
3645
+ //#region src/off-thinking.ts
3646
+ /**
3647
+ * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
3648
+ * declaring `canDisableThinking: true` (issue #34).
3649
+ *
3650
+ * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
3651
+ * refuses on BOTH gateways, and the split tracks the model family rather than
3652
+ * the gateway. The cost is asymmetric — offering a level that always fails
3653
+ * costs the user every request for that model, while hiding one that would have
3654
+ * worked costs a single option — so the table errs towards hiding. A user who
3655
+ * knows better can override any entry (see {@link effectiveOff}).
3656
+ *
3657
+ * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
3658
+ * its sibling does refuse on both, and the same asymmetry argues for hiding.
3659
+ */
3660
+ const REFUSES_OFF_MODEL_IDS = /* @__PURE__ */ new Set([
3661
+ "deepseek-v4.1-flash",
3662
+ "deepseek-v4.1-flash-sg",
3663
+ "deepseek-v4-pro",
3664
+ "primary-model",
3665
+ "gpt-6-astra",
3666
+ "gpt-5.6-sol",
3667
+ "gpt-5.6-terra",
3668
+ "gpt-5.6-luna",
3669
+ "gemini-3.5-flash"
3670
+ ]);
3671
+ /**
3672
+ * Whether `off` should be offered for a model when the user has expressed no
3673
+ * opinion: the upstream's declaration, minus the ids known to refuse it.
3674
+ *
3675
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
3676
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
3677
+ * undeclared capability.
3678
+ */
3679
+ function offDefaultFor(info) {
3680
+ return info.reasoning?.canDisableThinking === true && !REFUSES_OFF_MODEL_IDS.has(info.id);
3681
+ }
3682
+ /**
3683
+ * The model ids the `off` level should be offered for by default (issue #34).
3684
+ *
3685
+ * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
3686
+ * This is the seed — what the set is filled with the first time it is read —
3687
+ * so a profile that has never saved shows the correct default immediately,
3688
+ * without requiring a save.
3689
+ *
3690
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
3691
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
3692
+ * undeclared capability.
3693
+ */
3694
+ function defaultOffModelIds(models) {
3695
+ return models.filter(offDefaultFor).map((model) => model.id);
3696
+ }
3697
+ /**
3698
+ * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
3699
+ * catalog.
3700
+ *
3701
+ * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
3702
+ * reads only the model's own declaration — so the corrected answer has to be
3703
+ * written onto the model before it reaches the adapter. Doing it here keeps the
3704
+ * adapter a pure "declared capability in, level map out" function and keeps the
3705
+ * table next to the rule it feeds.
3706
+ *
3707
+ * This OVERWRITES the declared field rather than adding a sibling, which is
3708
+ * safe because the declared value has exactly one consumer (that same level
3709
+ * map) and is never persisted where the card can see it: `toWebModel` drops
3710
+ * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
3711
+ * upstream discovery, never from this stamped list. A model that declares no
3712
+ * reasoning is returned untouched — there is no level to correct.
3713
+ */
3714
+ function withEffectiveOff(models, selection) {
3715
+ return models.map((model) => model.reasoning === void 0 ? model : {
3716
+ ...model,
3717
+ reasoning: {
3718
+ ...model.reasoning,
3719
+ canDisableThinking: selection.has(model.id)
3720
+ }
3721
+ });
3722
+ }
3723
+ //#endregion
3634
3724
  //#region src/index.ts
3635
3725
  /** Stable Cordis plugin name. */
3636
3726
  const name = "dsh-connect-workbuddy";
@@ -3698,6 +3788,16 @@ const regionStateConfig = z.object({
3698
3788
  lastCatalog: z.array(modelConfig).default([]),
3699
3789
  enabledModelIds: z.array(z.string()).default([]),
3700
3790
  imageModelIds: z.array(z.string()).default([]),
3791
+ /**
3792
+ * Per-model override of whether thinking `off` is offered, keyed by model id
3793
+ * (issue #34). An ABSENT key means "use the built-in rule" — the upstream's
3794
+ * `canDisableThinking` declaration minus the ids known to refuse `off` (see
3795
+ * `src/off-thinking.ts`).
3796
+ *
3797
+ * Seeded from {@link defaultOffModelIds} on first read so the checkbox is
3798
+ * ticked correctly without the user having to save.
3799
+ */
3800
+ offModelIds: z.array(z.string()).default([]).description("Model ids whose \"off\" thinking level is offered; seeded from the built-in rule when first read."),
3701
3801
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}),
3702
3802
  pool: poolConfig.default({})
3703
3803
  });
@@ -3926,13 +4026,27 @@ function apply(ctx, config) {
3926
4026
  ...model,
3927
4027
  ...images.has(model.id) ? { multimodal: true } : { multimodal: false }
3928
4028
  }));
3929
- const configuredModels = (value, region) => {
4029
+ const configuredModels = (value, region, liveModels = []) => {
3930
4030
  const state = regionStateOf(value, region);
3931
- return withImageSelection(deriveCatalog(state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region), new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
4031
+ const roster = state.lastCatalog?.length ? state.lastCatalog : liveModels.length ? liveModels : fallbackModelsFor(region);
4032
+ const declared = withImageSelection(deriveCatalog(roster, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
4033
+ return withEffectiveOff(declared, new Set(savedOffModelIds(state, declared)));
3932
4034
  };
4035
+ /**
4036
+ * The region's `off` selection as a Set (issue #34).
4037
+ *
4038
+ * `undefined` means "the user has never saved" — seed from the built-in rule
4039
+ * so the checkbox is ticked correctly on first read. An explicit `[]` means
4040
+ * the user UNticked everything and must stay empty: `regionStateOf` reads the
4041
+ * raw stored slot, so `undefined` and `[]` are distinguishable here, and
4042
+ * collapsing them would silently re-enable every model on the next load.
4043
+ */
4044
+ const savedOffModelIds = (state, roster) => state.offModelIds ?? defaultOffModelIds(roster);
3933
4045
  const displayModels = (value, region) => {
3934
4046
  const state = regionStateOf(value, region);
3935
- return state.lastCatalog?.length ? state.lastCatalog : fallbackModelsFor(region);
4047
+ if (state.lastCatalog?.length) return state.lastCatalog;
4048
+ const live = stacks[region]?.catalog?.current();
4049
+ return live?.length ? live : fallbackModelsFor(region);
3936
4050
  };
3937
4051
  let current = () => config;
3938
4052
  let invalidateCatalog = () => {};
@@ -3955,7 +4069,7 @@ function apply(ctx, config) {
3955
4069
  for (const region of REGION_KEYS) {
3956
4070
  stacks[region].store.setDesktopPath(authFile);
3957
4071
  stacks[region].store.selectAccount(effectiveAccountFor(region, value));
3958
- stacks[region].catalog.set(configuredModels(value, region));
4072
+ stacks[region].catalog.set(configuredModels(value, region, stacks[region].catalog.current()));
3959
4073
  }
3960
4074
  invalidateCatalog();
3961
4075
  syncRegionRegistration(value);
@@ -4027,6 +4141,7 @@ function apply(ctx, config) {
4027
4141
  displayModels: (region) => displayModels(current(), region),
4028
4142
  enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
4029
4143
  imageModelIds: (region) => regionStateOf(current(), region).imageModelIds ?? [],
4144
+ offModelIds: (region) => savedOffModelIds(regionStateOf(current(), region), displayModels(current(), region)),
4030
4145
  contextBudgets: (region) => regionStateOf(current(), region).contextBudgets ?? {},
4031
4146
  discoverModels,
4032
4147
  regionEnabled: (region) => regionEnabled(current(), region),
@@ -4564,7 +4679,7 @@ function apply(ctx, config) {
4564
4679
  const models = await client.fetchModels(credential);
4565
4680
  if (stopped) return;
4566
4681
  const state = regionStateOf(current(), region);
4567
- stacks[region].catalog.set(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])));
4682
+ stacks[region].catalog.set(withEffectiveOff(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])), new Set(savedOffModelIds(state, models))));
4568
4683
  adapters[region].invalidate();
4569
4684
  } catch (error) {
4570
4685
  ctx.logger.warn(`dsh-connect-workbuddy: dynamic ${region} model catalog unavailable; serving the static fallback list`, error);
@@ -4576,4 +4691,4 @@ function apply(ctx, config) {
4576
4691
  });
4577
4692
  }
4578
4693
  //#endregion
4579
- export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
4694
+ export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, REFUSES_OFF_MODEL_IDS, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, WorkBuddyCatalog, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, WorkBuddyEncryptedCredentialError, WorkBuddyUpstreamClient, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, defaultOffModelIds, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, offDefaultFor, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withEffectiveOff, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
- "version": "3.4.4",
5
+ "version": "3.5.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",