dsh-connect-workbuddy 4.10.1 → 4.11.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
@@ -20,6 +20,38 @@
20
20
  - **变异验证**:把修复回退后这两条断言分别报 **15 ≠ 3** 与 **9 ≠ 3**;三个用例在修复后全绿。写入后再读的用例在前修复/后修复两种状态下都绿——它守的是「共享窗口不得变成 TTL」,是回归护栏而非修复证据。
21
21
  - 文件里的凭据夹具用**真实的加密字段形状** `{$wbEncrypted: 1, envelope: "<base64>"}`;写成嵌套对象会被 `isEncryptedFieldWrapper()` 判为「未加密」,让整条用例静默走错分支(写测试时踩到过,已在夹具注释里记下)。
22
22
 
23
+ ## 4.11.0 (2026-10-20)
24
+
25
+ > 版本号是 **minor**:有行为变更(对实测拒绝 `off` 的那 9 个模型,卡片上的「可关闭思考」变为不可勾选)。发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。
26
+
27
+ ### Fixes
28
+
29
+ - **issue #83:给「实测拒绝 off」的模型勾上「可关闭思考」后,该模型的每一次 `/compact` 必定 400 / 11133**
30
+ - **报告链路**:用户在卡片上给 `deepseek-v4.1-flash` 之类**实测拒绝 `off`** 的模型勾了「可关闭思考」→ `withEffectiveOff()` 用保存的勾选覆盖 `canDisableThinking` → `workBuddyThinkingLevelMap()` 把 `off` 映成 `"off"` → pi-ai 的 `openai-completions` 在**调用方没传 `reasoningEffort`** 时用 `model.thinkingLevelMap?.off` 兜底 → 压缩摘要请求(`summarizeWithLlm`)恰好**不传**档位 → 每次 `/compact` 都带 `reasoning_effort:"off"` → 两个网关都回 400 / 11133,界面只说 `Compaction could not produce a useful summary`。
31
+ - **多写出来的那一条**:`REFUSES_OFF_MODEL_IDS` 一直被当作「种子」而不是「墙」,3.5.0 的说明写着「用户勾选即生效」。但它与同一条 changelog 里写下的取舍原则相反:**提供一个必定失败的档位,代价是该模型每一次请求;误藏一个本来可用的档位,代价只是一个选项**。而且这次失败的不只是「某个请求」——pi-ai 会把这个档位当成**所有未指定档位请求的默认值**。
32
+ - **三道防线(独立失效点,各自有测试)**:
33
+ 1. **层级映射墙**:`withEffectiveOff()` 对名单内 id 恒置 `canDisableThinking: false`(`offRefused()` 与 `offDefaultFor()` 同口径),`workBuddyThinkingLevelMap()` 再查一次名单,所以 `off` 根本不会被提供,也不依赖调用方先盖章。
34
+ 2. **请求体兜底**:`prepareChatBody()` 新增 `dropRefusedReasoning()`——名单内模型的 `reasoning_effort: "off"` 在发往上游前被摘掉。这一层守的是**目录建好之后才变**的情形:账号池换号会把 body 指向另一个账号的模型,`primary-model` 还是个由网关解析的别名。
35
+ 3. **显式默认档位**:`dsh-llm-pi-ai` 按 `options.reasoningEffort ?? profile.reasoning` 解析档位,此前 `profile.reasoning` 是 `undefined`,等于把默认值交给 pi-ai 的层级映射——也就是交给那个 `off`。现在 profile 显式声明 `reasoning: 'high'`(`WORKBUDDY_DEFAULT_EFFORT`),所有注册键一致。
36
+ - **只摘 `off`**:其他档位在 issue #34 的实测里全部 200;body 里**本来就没有**档位时不补任何值——插件不替模型决定该怎么思考。
37
+ - **列表外模型不受影响**:名单按**精确 id** 匹配,`grok-4.7` 这类未记录的模型仍然由用户勾选与自身声明决定,修复不会顺手拿走一个本来能用的档位。
38
+ - **错误信息可诊断**:`parseUpstreamErrorDetail()` 现在读 `extError.param` / `extError.value`;当被拒参数与推理档位有关时,回复直接点名「上游拒绝了推理档位「off」(reasoning_effort)」并给出「取消可关闭思考勾选」的处置建议,而不是只说「请求被上游拒绝」。`param` 为空时(issue #78 的那类 body)**不猜**,原有文案原样保留。
39
+ - **观测记录**:`reportRefusal` 回调把「某模型确实被拒了 off」记进一个有界环形缓冲(20 条,进程内、不落盘)。这是压缩路径唯一能把事实送到界面上的通路——它只把失败当布尔值,正文被丢掉。
40
+
41
+ ### Features
42
+
43
+ - **卡片上如实标注拒绝 `off` 的行**:名单内模型(以及进程内实测被拒的模型)的「可关闭思考」显示为**勾选但禁用**,旁边多一枚 `拒绝 off` 标记,tooltip 说明「实测两个网关都回 400 / code 11133,勾选后所有未指定档位的请求(尤其压缩摘要)都会失败」。**不禁用、不隐藏**这个勾选框:旧版本可能已经把勾存下来了,藏起来等于把用户保存的状态和失败原因一起藏掉。判定值 `offRefused` / `refusesOff` 由宿主给出,卡片不自行推断。
44
+
45
+ ### Tests
46
+
47
+ - **`tests/issue83-off-refusal.spec.ts`:钉请求体,而不是钉函数返回值**
48
+ - 用**真实 pi-ai `openai-completions`** 经**真实 adapter** 构建请求,注入 `fetch` 捕获 body,再断言 `reasoning_effort` 不在其中——「盖了章」不等于「没发出去」,只有 body 能证明。模型描述符取自 adapter 自己的目录(声明 `canDisableThinking: true`,与上游对这 9 个 id 的声明一致),所以手写描述符绕过的 profile 级修复在这里藏不住。
49
+ - 另外三条守 `prepareChatBody`:名单内摘除、名单外保留、其他档位一格不动、原本没有档位就不补。
50
+ - **`tests/issue83-profile-default.spec.ts`**:profile 的默认档位是 `'high'` 而不是 `undefined`/`'off'`,且**每一个注册键**都一致(issue #75 的身份键也算)。
51
+ - **`tests/off-thinking.spec.ts` / `off-thinking-runtime.spec.ts` / `off-thinking-card.spec.tsx` 改了三条断言**:此前它们断言「用户勾选优先于名单」,而那正是 issue #83 的成因。现在改为断言墙生效、名单外仍然尊重勾选、以及卡片对宿主标记的行渲染成「勾选但禁用 + 有标记」。
52
+ - **`tests/upstream-error-copy.spec.ts` 加了 4 条**:11133 且 `param` 指向档位时点名档位与处置;`param` 为空时**不**归因到档位;拒绝被上报(含被请求命名的 `modelId`,不是账号别名);非参数类失败不上报。
53
+ - **变异验证**:单独回退「层级映射墙」→ 1 条红;单独回退「adapter 名单判断」→ 2 条红;单独回退「显式默认档位」→ profile 那 2 条红。撤回其中任一层都有测试当场报错。全量 1536 passed / 2 skipped,`pnpm run typecheck` 与 `pnpm run build` 均干净。
54
+
23
55
  ## 4.10.0 (2026-10-19)
24
56
 
25
57
  > 版本号是 **minor**:4.9.5 / 4.9.6 两个 patch 从未单独发布到 npm(npm 上仍是 4.9.4),本次一次性带上它们,并新增福利活动的完整链路。**发往上游的请求体、路由 id、模型 id、凭据、计费与换号规则一律未变。**
package/README.en.md CHANGED
@@ -24,7 +24,7 @@ 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. **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.
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 model you do not need the level on. **The 9 tabled models cannot be ticked at all** (their row is marked "refuses off" with an explanatory tooltip): the level is not merely an extra option — pi-ai uses it as the DEFAULT for every request that names no tier, and a compaction summary is exactly such a request, so one tick made every `/compact` for that model fail with HTTP 400 / code 11133 (issue #83). Models outside the table are still decided entirely by your selection and their own declaration; 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
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 (200K / 500K / the model's native window).
package/README.md CHANGED
@@ -31,7 +31,7 @@
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` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于上游新出现的拒绝模型,**勾上**用于名单误伤、或你已确认可用的模型;首次保存即写入你的勾选,之后不再重算。
34
+ - **模型管理** —— 从上游刷新完整模型目录,逐项勾选启用或禁用;刷新是草稿操作,点保存才生效。上游同时给出积分倍率、上下文/输出上限与推理档位;**图片输入按厂商核验表判定**:刷新时只有厂商文档明确写为原生多模态的模型才自动勾选,文档写明纯文本的不勾,**没有厂商资料的一律按「未核实」不猜**(上游的 `supportsImages` 是平台侧声明,不等于模型能看图,不参与判定)。用户仍可随时手动勾选,勾选状态保存在本地。**每个模型行还有「可关闭思考」(`off`)勾选框**:决定要不要为该模型提供「完全关闭思考」这一档位。默认由内置复核名单给出(上游声称可关闭、实测却对它返回 HTTP 400 的 9 个模型默认不勾),你可以按模型改勾/取消——**取消**用于当前不需要该档位的模型;**名单内那 9 个模型不可勾选**(行上标「拒绝 off」,悬停有说明):该档位被勾上后不只是多一个选项,pi-ai 会把它当作**所有未指定档位请求的默认值**,压缩摘要请求正属于这一类,勾一次等于让该模型的每一次 `/compact` 都 400 / 11133(issue #83)。名单外的模型仍完全由你的勾选与自身声明决定;首次保存即写入你的勾选,之后不再重算。
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
37
  **两点需要知道**:① **它真的花钱**——实测一次报 `credit` 0.02(被限流时直接 429,几乎不花)。因此**只能按单个模型手工点**,没有「测试已勾选」这类批量按钮:一次覆盖整个列表就是一次不可控支出(服务端也据此**只接受单个模型**,多模型请求会被拒)。② 上游虽宣称 1M 上下文,但节流线远低于此;插件按模型的**原生窗口**向 DSH 宣称(3.5.0 起不再默认压到 200K),所以 DSH 很晚才压缩对话——长会话撞限流是这条机制的直接结果。想让 DSH 更早压缩,就在模型行上手动设一个「DSH 上下文预算」上限(可选 200K / 500K / 模型原生窗口)。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-Cq4_pJ_l.js";
2
+ import { $ as WORKBUDDY_APP_EXECUTABLE_ENV, C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, O as legacyWorkbuddyOwnAuthPath, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, at as findWorkbuddyAppExecutableWithSource, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, pt as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-BrIEnor4.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
@@ -3901,6 +3901,8 @@ window.__ModuleLoader__.load({
3901
3901
  .dsm-workbuddy-model-image input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
3902
3902
  .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}
3903
3903
  .dsm-workbuddy-model-off input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
3904
+ .dsm-workbuddy-model-off-blocked{cursor:not-allowed;opacity:.75}
3905
+ .dsm-workbuddy-model-off-flag{margin-left:2px;padding:0 4px;border-radius:4px;background:var(--dsw-alias-label-caution-background,#4a3a1f);color:var(--dsw-alias-label-caution,#e8b34b);font-size:10px;line-height:14px;white-space:nowrap}
3904
3906
  .dsm-workbuddy-model-copy{display:flex;align-items:baseline;gap:8px;min-width:0}
3905
3907
  .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}
3906
3908
  .dsm-workbuddy-model-name-rate{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:400;line-height:16px;flex:none}
@@ -5390,6 +5392,7 @@ window.__ModuleLoader__.load({
5390
5392
  * "reset" control: once an answer matches the rule, the two are
5391
5393
  * indistinguishable in effect, and the tooltip already names the override.
5392
5394
  */
5395
+ const offBlocked = (model) => model.reasoning?.offRefused === true || model.reasoning?.refusesOff === true;
5393
5396
  const toggleOff = (modelId) => {
5394
5397
  editDraft((current) => {
5395
5398
  const next = new Set(current.offIds);
@@ -5971,16 +5974,23 @@ window.__ModuleLoader__.load({
5971
5974
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
5972
5975
  }),
5973
5976
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5974
- className: "dsm-workbuddy-model-off",
5975
- title: t("row.modelOffHint"),
5976
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5977
- type: "checkbox",
5978
- checked: activeOffIds.has(model.id),
5979
- disabled: !canWrite || saving,
5980
- onChange: () => {
5981
- toggleOff(model.id);
5982
- }
5983
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelOff") })]
5977
+ className: `dsm-workbuddy-model-off${offBlocked(model) ? " dsm-workbuddy-model-off-blocked" : ""}`,
5978
+ title: offBlocked(model) ? t("row.modelOffRefusedHint") : t("row.modelOffHint"),
5979
+ children: [
5980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5981
+ type: "checkbox",
5982
+ checked: activeOffIds.has(model.id),
5983
+ disabled: !canWrite || saving || offBlocked(model),
5984
+ onChange: () => {
5985
+ toggleOff(model.id);
5986
+ }
5987
+ }),
5988
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelOff") }),
5989
+ offBlocked(model) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5990
+ className: "dsm-workbuddy-model-off-flag",
5991
+ children: t("row.modelOffRefused")
5992
+ }) : null
5993
+ ]
5984
5994
  }),
5985
5995
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
5986
5996
  className: "dsm-workbuddy-context-budget",
@@ -7190,6 +7200,8 @@ window.__ModuleLoader__.load({
7190
7200
  "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.",
7191
7201
  "row.modelOff": "Can disable",
7192
7202
  "row.modelOffHint": "Offer the “off” level for this model",
7203
+ "row.modelOffRefused": "refuses off",
7204
+ "row.modelOffRefusedHint": "Measured to reject the “off” level with HTTP 400 (code 11133) on both gateways. With this ticked, every request that carries no reasoning tier — compaction summaries above all — would fail, so it cannot be enabled here.",
7193
7205
  "row.modelReasoning": "Reasoning: {efforts}",
7194
7206
  "row.modelUnknown": "Unknown",
7195
7207
  "row.modelCapabilityPending": "Only capabilities advertised by WorkBuddy are shown.",
@@ -7512,6 +7524,8 @@ window.__ModuleLoader__.load({
7512
7524
  "row.modelImageUnverified": "图片输入未核实:没有厂商资料确认该模型能读图,刷新时按「不猜」处理,不会自动勾选。如果你验证过确实可用,可以手动勾上。",
7513
7525
  "row.modelOff": "可关闭思考",
7514
7526
  "row.modelOffHint": "为这个模型提供「关闭思考」档位",
7527
+ "row.modelOffRefused": "拒绝 off",
7528
+ "row.modelOffRefusedHint": "实测在两个网关上都拒绝「关闭思考」档位(HTTP 400 / code 11133)。勾选后,所有未指定推理档位的请求(尤其是压缩摘要)都会失败,因此这里不能启用。",
7515
7529
  "row.modelReasoning": "推理强度:{efforts}",
7516
7530
  "row.modelUnknown": "未知",
7517
7531
  "row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
@@ -962,6 +962,140 @@ function clearAtRestKeyCache() {
962
962
  cachedAppExecutable = void 0;
963
963
  }
964
964
  //#endregion
965
+ //#region src/off-thinking.ts
966
+ /**
967
+ * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
968
+ * declaring `canDisableThinking: true` (issue #34).
969
+ *
970
+ * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
971
+ * refuses on BOTH gateways, and the split tracks the model family rather than
972
+ * the gateway. The cost is asymmetric — offering a level that always fails
973
+ * costs the user every request for that model, while hiding one that would have
974
+ * worked costs a single option — so the table errs towards hiding, and
975
+ * {@link withEffectiveOff} enforces it as a HARD wall rather than a seed
976
+ * (issue #83: a tick used to reopen the failing level, and with no
977
+ * `reasoningEffort` passed the level became the default for compaction too).
978
+ * See {@link offRefused} for why the checkbox must not win.
979
+ *
980
+ * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
981
+ * its sibling does refuse on both, and the same asymmetry argues for hiding.
982
+ */
983
+ const REFUSES_OFF_MODEL_IDS = /* @__PURE__ */ new Set([
984
+ "deepseek-v4.1-flash",
985
+ "deepseek-v4.1-flash-sg",
986
+ "deepseek-v4-pro",
987
+ "primary-model",
988
+ "gpt-6-astra",
989
+ "gpt-5.6-sol",
990
+ "gpt-5.6-terra",
991
+ "gpt-5.6-luna",
992
+ "gemini-3.5-flash"
993
+ ]);
994
+ /**
995
+ * Whether `off` should be offered for a model when the user has expressed no
996
+ * opinion: the upstream's declaration, minus the ids known to refuse it.
997
+ *
998
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
999
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1000
+ * undeclared capability.
1001
+ */
1002
+ function offDefaultFor(info) {
1003
+ return info.reasoning?.canDisableThinking === true && !REFUSES_OFF_MODEL_IDS.has(info.id);
1004
+ }
1005
+ /**
1006
+ * The model ids the `off` level should be offered for by default (issue #34).
1007
+ *
1008
+ * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
1009
+ * This is the seed — what the set is filled with the first time it is read —
1010
+ * so a profile that has never saved shows the correct default immediately,
1011
+ * without requiring a save.
1012
+ *
1013
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
1014
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1015
+ * undeclared capability.
1016
+ */
1017
+ function defaultOffModelIds(models) {
1018
+ return models.filter(offDefaultFor).map((model) => model.id);
1019
+ }
1020
+ /** How many observations are kept, newest first. Bounded so a refusal storm cannot grow memory. */
1021
+ const REFUSAL_RING = 20;
1022
+ /**
1023
+ * The refusals this process has observed, newest first.
1024
+ *
1025
+ * Process-scoped rather than persisted: it is EVIDENCE ("what did the upstream
1026
+ * actually say"), and a stale record from another install would promote a model
1027
+ * into the refusal table on the strength of a request the current build never
1028
+ * sent. The card reads it; nothing sends from it.
1029
+ */
1030
+ const observedRefusals = [];
1031
+ /** Record one refusal, keeping the newest {@link REFUSAL_RING}. */
1032
+ function recordRefusal(report) {
1033
+ observedRefusals.unshift(report);
1034
+ if (observedRefusals.length > REFUSAL_RING) observedRefusals.length = REFUSAL_RING;
1035
+ }
1036
+ /** The most recent refusal naming `modelId`, or `undefined` when none did. */
1037
+ function lastRefusalFor(modelId) {
1038
+ return observedRefusals.find((report) => report.modelId === modelId);
1039
+ }
1040
+ /**
1041
+ * Whether a model may offer `off` AT ALL, no matter what the user asked for.
1042
+ *
1043
+ * This is the hard wall in front of {@link REFUSES_OFF_MODEL_IDS} (issue #83).
1044
+ * The table above used to be a seed only: the checkbox wrote the selection
1045
+ * straight into `canDisableThinking`, so ticking a model known to refuse `off`
1046
+ * reopened exactly the failure the table exists to prevent — and not once per
1047
+ * "off" turn either. pi-ai's `openai-completions` reads
1048
+ * `model.thinkingLevelMap?.off` whenever the caller passes NO
1049
+ * `reasoningEffort` at all, and a compaction summary is such a caller: it never
1050
+ * carries an effort. So one tick made every `/compact` for that model answer
1051
+ * HTTP 400 / code 11133, with the surface showing only "could not produce a
1052
+ * useful summary".
1053
+ *
1054
+ * The asymmetry that writes the table also writes this test: a level that
1055
+ * always fails costs the user EVERY request for that model, while a level that
1056
+ * was hidden needlessly costs one option. So the refusal list wins over the
1057
+ * checkbox, not the other way round.
1058
+ *
1059
+ * The table is keyed by EXACT id, so anything that pins a different id at
1060
+ * request time (aliases such as `primary-model`) is out of its reach; those are
1061
+ * covered where the request is actually built — see
1062
+ * {@link normalizePreparedReasoning}.
1063
+ */
1064
+ function offRefused(modelId) {
1065
+ return REFUSES_OFF_MODEL_IDS.has(modelId);
1066
+ }
1067
+ /**
1068
+ * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
1069
+ * catalog.
1070
+ *
1071
+ * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
1072
+ * reads only the model's own declaration — so the corrected answer has to be
1073
+ * written onto the model before it reaches the adapter. Doing it here keeps the
1074
+ * adapter a pure "declared capability in, level map out" function and keeps the
1075
+ * table next to the rule it feeds.
1076
+ *
1077
+ * This OVERWRITES the declared field rather than adding a sibling, which is
1078
+ * safe because the declared value has exactly one consumer (that same level
1079
+ * map) and is never persisted where the card can see it: `toWebModel` drops
1080
+ * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
1081
+ * upstream discovery, never from this stamped list. A model that declares no
1082
+ * reasoning is returned untouched — there is no level to correct.
1083
+ *
1084
+ * A refusal-table id is stamped `false` REGARDLESS of the selection
1085
+ * ({@link offRefused}): the tick stays in the saved set — the card shows it and
1086
+ * the hint explains it — but it can no longer be turned into a request that
1087
+ * fails.
1088
+ */
1089
+ function withEffectiveOff(models, selection) {
1090
+ return models.map((model) => model.reasoning === void 0 ? model : {
1091
+ ...model,
1092
+ reasoning: {
1093
+ ...model.reasoning,
1094
+ canDisableThinking: selection.has(model.id) && !offRefused(model.id)
1095
+ }
1096
+ });
1097
+ }
1098
+ //#endregion
965
1099
  //#region src/upstream.ts
966
1100
  /**
967
1101
  * WorkBuddy (CodeBuddy / copilot.tencent.com) upstream client: chat streaming,
@@ -1218,6 +1352,14 @@ function parseUpstreamErrorDetail(body) {
1218
1352
  if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
1219
1353
  if (displayMsg !== void 0) detail.displayMsg = displayMsg;
1220
1354
  if (tips !== void 0) detail.tips = tips;
1355
+ const ext = raw.extError;
1356
+ if (typeof ext === "object" && ext !== null && !Array.isArray(ext)) {
1357
+ const extRaw = ext;
1358
+ const param = extRaw["param"];
1359
+ if (typeof param === "string" && param !== "") detail.rejectedParam = param;
1360
+ const value = extRaw["value"];
1361
+ if (typeof value === "string" && value !== "") detail.rejectedTier = value;
1362
+ }
1221
1363
  return detail;
1222
1364
  }
1223
1365
  /**
@@ -1477,8 +1619,36 @@ function prepareChatBody(source) {
1477
1619
  }
1478
1620
  ensureSystemHead(obj);
1479
1621
  normalizeToolChoice(obj);
1622
+ dropRefusedReasoning(obj);
1480
1623
  return JSON.stringify(obj);
1481
1624
  }
1625
+ /**
1626
+ * Remove `reasoning_effort: "off"` when it would only ever fail (issue #83).
1627
+ *
1628
+ * The refusal table is keyed by exact model id, but the model a request
1629
+ * actually names can change AFTER the catalog was built: account-pool failover
1630
+ * re-points the body at another account's model, and `primary-model` is an
1631
+ * alias whose concrete id the gateway resolves. A tick on such a row therefore
1632
+ * reaches this plugin's prepared body as a bare `"off"` — exactly the value the
1633
+ * table says the upstream answers with HTTP 400 / code 11133.
1634
+ *
1635
+ * Stripped HERE because this is the last thing the plugin controls before the
1636
+ * upstream counts the body, and because the test is {@link offRefused} — the
1637
+ * same single source of truth the catalog stamping uses, so the two cannot
1638
+ * drift apart.
1639
+ *
1640
+ * Only the literal `"off"` is touched. Every other effort value returned 200 on
1641
+ * every model measured for issue #34, and a body naming no effort is left
1642
+ * alone: deleting a field the caller never sent would be inventing an opinion
1643
+ * about how the model should think.
1644
+ */
1645
+ function dropRefusedReasoning(obj) {
1646
+ const effort = obj["reasoning_effort"];
1647
+ if (typeof effort !== "string" || effort.trim().toLowerCase() !== "off") return;
1648
+ const model = obj["model"];
1649
+ if (typeof model !== "string" || !offRefused(model)) return;
1650
+ delete obj["reasoning_effort"];
1651
+ }
1482
1652
  /** Rewrite OpenAI `tool_choice` spellings into the upstream's string form. */
1483
1653
  function normalizeToolChoice(obj) {
1484
1654
  const suppress = () => {
@@ -4167,7 +4337,7 @@ function generationOf(version) {
4167
4337
  //#endregion
4168
4338
  //#region src/version.ts
4169
4339
  /** The npm package version this build was produced from. */
4170
- const WORKBUDDY_CONNECT_VERSION = "4.10.1";
4340
+ const WORKBUDDY_CONNECT_VERSION = "4.11.0";
4171
4341
  //#endregion
4172
4342
  //#region src/host-heartbeat.ts
4173
4343
  /**
@@ -4325,4 +4495,4 @@ async function clearHostHeartbeat() {
4325
4495
  await rm(workbuddyHostHeartbeatPath(), { force: true });
4326
4496
  }
4327
4497
  //#endregion
4328
- export { isEncryptedFieldWrapper as $, workbuddyAccountId as A, modelIdOfBody as B, defaultDesktopAuthDirs as C, isVaultAccountId as D, isEncryptedCredentialError as E, WorkBuddyUpstreamClient as F, regionOf as G, parseReasoning as H, classifyUpstreamError as I, deriveAtRestKey as J, WORKBUDDY_APP_EXECUTABLE_ENV as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, WorkBuddyCredentialRejectedError as P, findWorkbuddyAppExecutableWithSource as Q, isCredentialRejectedError as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, parseUpstreamModel as U, parseCreditMultiplier as V, prepareChatBody as W, fetchAtRestKeyPayload as X, deriveAtRestKeyId as Y, findWorkbuddyAppExecutable as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, workbuddyAppExecutableCandidates as at, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, withFileLock as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, isWorkbuddyBundle as et, WorkBuddyCatalog as f, ENCRYPTED_CREDENTIAL_CODE as g, fallbackModelsFor as h, processStartTimeMs as i, readAtRestKey as it, workbuddyOwnAuthPath as j, parseWorkBuddyAuth as k, piAiRuntimeInfo as l, writeFileAtomic as lt, deriveCatalog as m, clearHostHeartbeat as n, macosNestedAppBundles as nt, workbuddyHostHeartbeatPath as o, hostPeersInfo as ot, applyContextBudgets as p, clearAtRestKeyCache as q, isHeartbeatProcessAlive as r, openEncryptedField as rt, writeHostHeartbeat as s, resolveDshHome as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, macosBundleExecutable as tt, FALLBACK_WORKBUDDY_MODELS as u, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, knownRegionOf as z };
4498
+ export { WORKBUDDY_APP_EXECUTABLE_ENV as $, workbuddyAccountId as A, modelIdOfBody as B, defaultDesktopAuthDirs as C, isVaultAccountId as D, isEncryptedCredentialError as E, WorkBuddyUpstreamClient as F, regionOf as G, parseReasoning as H, classifyUpstreamError as I, lastRefusalFor as J, REFUSES_OFF_MODEL_IDS as K, declaredTools as L, CREDENTIAL_REJECTED_CODE as M, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as N, legacyWorkbuddyOwnAuthPath as O, WorkBuddyCredentialRejectedError as P, withEffectiveOff as Q, isCredentialRejectedError as R, defaultDesktopAuthCandidates as S, hasEncryptedCredentialFields as T, parseUpstreamModel as U, parseCreditMultiplier as V, prepareChatBody as W, offRefused as X, offDefaultFor as Y, recordRefusal as Z, WORKBUDDY_AUTH_FILENAME as _, readHostHeartbeat as a, findWorkbuddyAppExecutableWithSource as at, WorkBuddyEncryptedCredentialError as b, WORKBUDDY_CONNECT_VERSION as c, macosBundleExecutable as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, readAtRestKey as dt, clearAtRestKeyCache as et, WorkBuddyCatalog as f, workbuddyAppExecutableCandidates as ft, ENCRYPTED_CREDENTIAL_CODE as g, writeFileAtomic as gt, fallbackModelsFor as h, withFileLock as ht, processStartTimeMs as i, findWorkbuddyAppExecutable as it, workbuddyOwnAuthPath as j, parseWorkBuddyAuth as k, piAiRuntimeInfo as l, macosNestedAppBundles as lt, deriveCatalog as m, resolveDshHome as mt, clearHostHeartbeat as n, deriveAtRestKeyId as nt, workbuddyHostHeartbeatPath as o, isEncryptedFieldWrapper as ot, applyContextBudgets as p, hostPeersInfo as pt, defaultOffModelIds as q, isHeartbeatProcessAlive as r, fetchAtRestKeyPayload as rt, writeHostHeartbeat as s, isWorkbuddyBundle as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, deriveAtRestKey as tt, FALLBACK_WORKBUDDY_MODELS as u, openEncryptedField as ut, WORKBUDDY_AUTH_FILE_ENV as v, defaultDesktopAuthPath as w, authFileName as x, WorkBuddyCredentialStore as y, knownRegionOf as z };
package/lib/index.d.ts CHANGED
@@ -615,6 +615,15 @@ interface WorkBuddyReasoning {
615
615
  supportedEfforts?: readonly string[];
616
616
  defaultEffort?: string;
617
617
  canDisableThinking?: boolean;
618
+ /**
619
+ * An OBSERVED refusal of the `off` tier for this model (issue #83).
620
+ *
621
+ * Never declared by the upstream: it is stamped from this plugin's own
622
+ * refusal record, and it outlives the catalog that witnessed it so the card
623
+ * can keep showing the badge after a refresh. `undefined` means "never
624
+ * observed", which is not the same claim as "accepts".
625
+ */
626
+ refusesOff?: boolean;
618
627
  }
619
628
  /** One CLI-usable model, carrying everything the plugin card displays. */
620
629
  interface WorkBuddyUpstreamModel {
@@ -825,6 +834,17 @@ interface UpstreamErrorDetail {
825
834
  * clause losing its own meaning.
826
835
  */
827
836
  tips?: string;
837
+ /**
838
+ * The request parameter the upstream named as rejected, from
839
+ * `extError.param` (issue #83).
840
+ *
841
+ * Kept because the generic sentences cannot say WHICH parameter was at fault:
842
+ * a 11133 caused by `reasoning_effort` reads exactly like a model that is
843
+ * unavailable, and the fix (untick one checkbox) lives nowhere in the reply.
844
+ */
845
+ rejectedParam?: string;
846
+ /** The offending value of {@link rejectedParam}, when the body carries one. */
847
+ rejectedTier?: string;
828
848
  }
829
849
  /**
830
850
  * Region for a login domain; an empty domain means CN (matching upstream tooling).
@@ -1145,6 +1165,95 @@ declare class WorkBuddyCatalog {
1145
1165
  set(models: readonly WorkBuddyModelInfo[]): void;
1146
1166
  }
1147
1167
  //#endregion
1168
+ //#region src/off-thinking.d.ts
1169
+ /**
1170
+ * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
1171
+ * declaring `canDisableThinking: true` (issue #34).
1172
+ *
1173
+ * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
1174
+ * refuses on BOTH gateways, and the split tracks the model family rather than
1175
+ * the gateway. The cost is asymmetric — offering a level that always fails
1176
+ * costs the user every request for that model, while hiding one that would have
1177
+ * worked costs a single option — so the table errs towards hiding, and
1178
+ * {@link withEffectiveOff} enforces it as a HARD wall rather than a seed
1179
+ * (issue #83: a tick used to reopen the failing level, and with no
1180
+ * `reasoningEffort` passed the level became the default for compaction too).
1181
+ * See {@link offRefused} for why the checkbox must not win.
1182
+ *
1183
+ * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
1184
+ * its sibling does refuse on both, and the same asymmetry argues for hiding.
1185
+ */
1186
+ declare const REFUSES_OFF_MODEL_IDS: ReadonlySet<string>;
1187
+ /** The two fields this rule needs; both are declared by the model itself. */
1188
+ interface WorkBuddyOffCapability {
1189
+ id: string;
1190
+ reasoning?: WorkBuddyReasoning;
1191
+ }
1192
+ /**
1193
+ * Whether `off` should be offered for a model when the user has expressed no
1194
+ * opinion: the upstream's declaration, minus the ids known to refuse it.
1195
+ *
1196
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
1197
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1198
+ * undeclared capability.
1199
+ */
1200
+ declare function offDefaultFor(info: WorkBuddyOffCapability): boolean;
1201
+ /**
1202
+ * The model ids the `off` level should be offered for by default (issue #34).
1203
+ *
1204
+ * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
1205
+ * This is the seed — what the set is filled with the first time it is read —
1206
+ * so a profile that has never saved shows the correct default immediately,
1207
+ * without requiring a save.
1208
+ *
1209
+ * Only an explicit `canDisableThinking: true` enables the level; absence is
1210
+ * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1211
+ * undeclared capability.
1212
+ */
1213
+ declare function defaultOffModelIds(models: readonly WorkBuddyOffCapability[]): string[];
1214
+ /** One OBSERVED refusal of a reasoning tier, as the upstream reported it. */
1215
+ interface WorkBuddyRefusalReport {
1216
+ /** The rejected parameter (`extError.param`), e.g. `reasoning_effort`. */
1217
+ param: string;
1218
+ /** The rejected value (`extError.value`), when the body carried one. */
1219
+ tier?: string;
1220
+ /** Upstream business code, e.g. 11133. */
1221
+ code?: number;
1222
+ /** Epoch milliseconds of the observation. */
1223
+ at: number;
1224
+ /**
1225
+ * The model the refused REQUEST named — not the account's route alias.
1226
+ *
1227
+ * Taken from the body (`modelIdOfBody`), so it is the concrete id the card
1228
+ * lists; the badge would otherwise never attach, since failover re-points the
1229
+ * body at whatever the serving account routes.
1230
+ */
1231
+ modelId?: string;
1232
+ }
1233
+ /**
1234
+ * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
1235
+ * catalog.
1236
+ *
1237
+ * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
1238
+ * reads only the model's own declaration — so the corrected answer has to be
1239
+ * written onto the model before it reaches the adapter. Doing it here keeps the
1240
+ * adapter a pure "declared capability in, level map out" function and keeps the
1241
+ * table next to the rule it feeds.
1242
+ *
1243
+ * This OVERWRITES the declared field rather than adding a sibling, which is
1244
+ * safe because the declared value has exactly one consumer (that same level
1245
+ * map) and is never persisted where the card can see it: `toWebModel` drops
1246
+ * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
1247
+ * upstream discovery, never from this stamped list. A model that declares no
1248
+ * reasoning is returned untouched — there is no level to correct.
1249
+ *
1250
+ * A refusal-table id is stamped `false` REGARDLESS of the selection
1251
+ * ({@link offRefused}): the tick stays in the saved set — the card shows it and
1252
+ * the hint explains it — but it can no longer be turned into a request that
1253
+ * fails.
1254
+ */
1255
+ declare function withEffectiveOff<T extends WorkBuddyOffCapability>(models: readonly T[], selection: ReadonlySet<string>): T[];
1256
+ //#endregion
1148
1257
  //#region src/shim.d.ts
1149
1258
  /** Minimal logger surface the plugin context already provides. */
1150
1259
  interface ShimLogger {
@@ -1219,6 +1328,18 @@ interface WorkBuddyShimOptions {
1219
1328
  * it produces the same reply as before this option existed.
1220
1329
  */
1221
1330
  failoverShortfall?: WorkBuddyFailoverShortfall;
1331
+ /**
1332
+ * Told about every refused request the plugin can attribute to a MODEL, right
1333
+ * before the failure is rendered (issue #83).
1334
+ *
1335
+ * Separate from the rendered reply because the caller that hit the refusal is
1336
+ * not always the user: compaction reads the failure as a boolean and shows
1337
+ * "could not produce a useful summary", so the one fact that explains it (this
1338
+ * model refuses `off`) never reaches any surface. This callback is how the
1339
+ * card learns it happened. Must not throw: a bookkeeping failure must never
1340
+ * replace the upstream's own failure.
1341
+ */
1342
+ reportRefusal?: (refusal: WorkBuddyRefusalReport) => void;
1222
1343
  /**
1223
1344
  * Called before the FIRST attempt of every chat request, so the plugin can
1224
1345
  * point the store at the account the pool's ranking says should serve.
@@ -1365,7 +1486,6 @@ interface ContextRescue {
1365
1486
  }
1366
1487
  //#endregion
1367
1488
  //#region src/adapter.d.ts
1368
- /** Provider route this bundle owns for the domestic (CN) gateway. */
1369
1489
  declare const WORKBUDDY_PROVIDER = "workbuddy";
1370
1490
  /** Provider route this bundle owns for the international gateway. */
1371
1491
  declare const WORKBUDDY_GLOBAL_PROVIDER = "workbuddy-global";
@@ -1450,10 +1570,15 @@ declare function workBuddyDisplayName(info: WorkBuddyModelInfo): string;
1450
1570
  *
1451
1571
  * The value read here is already the EFFECTIVE one: the runtime catalog is
1452
1572
  * stamped by `withEffectiveOff()` before it reaches the adapter, so this is the
1453
- * user's `off` selection when they made one (and the upstream declaration,
1454
- * minus `REFUSES_OFF_MODEL_IDS`, when they never did). Absence is "unknown" and
1455
- * stays unoffered, matching how the rest of the plugin treats undeclared
1456
- * capability.
1573
+ * user's `off` selection when they made one — except for the ids on
1574
+ * `REFUSES_OFF_MODEL_IDS`, which the stamp forces to `false` no matter what the
1575
+ * user asked for (issue #83: this level, once ticked, also became the IMPLICIT
1576
+ * default for every request that carries no effort at all, which is how
1577
+ * compaction went to 100% failure). The table is re-checked here as well so the
1578
+ * wall does not depend on every caller remembering to stamp first.
1579
+ *
1580
+ * Absence is "unknown" and stays unoffered, matching how the rest of the plugin
1581
+ * treats undeclared capability.
1457
1582
  */
1458
1583
  declare function workBuddyThinkingLevelMap(info: WorkBuddyModelInfo): WorkBuddyThinkingLevelMap | undefined;
1459
1584
  /**
@@ -1520,68 +1645,6 @@ declare function imageDefaultFor(info: {
1520
1645
  supportsImages?: boolean;
1521
1646
  }): boolean;
1522
1647
  //#endregion
1523
- //#region src/off-thinking.d.ts
1524
- /**
1525
- * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
1526
- * declaring `canDisableThinking: true` (issue #34).
1527
- *
1528
- * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
1529
- * refuses on BOTH gateways, and the split tracks the model family rather than
1530
- * the gateway. The cost is asymmetric — offering a level that always fails
1531
- * costs the user every request for that model, while hiding one that would have
1532
- * worked costs a single option — so the table errs towards hiding. A user who
1533
- * knows better can override any entry (see {@link effectiveOff}).
1534
- *
1535
- * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
1536
- * its sibling does refuse on both, and the same asymmetry argues for hiding.
1537
- */
1538
- declare const REFUSES_OFF_MODEL_IDS: ReadonlySet<string>;
1539
- /** The two fields this rule needs; both are declared by the model itself. */
1540
- interface WorkBuddyOffCapability {
1541
- id: string;
1542
- reasoning?: WorkBuddyReasoning;
1543
- }
1544
- /**
1545
- * Whether `off` should be offered for a model when the user has expressed no
1546
- * opinion: the upstream's declaration, minus the ids known to refuse it.
1547
- *
1548
- * Only an explicit `canDisableThinking: true` enables the level; absence is
1549
- * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1550
- * undeclared capability.
1551
- */
1552
- declare function offDefaultFor(info: WorkBuddyOffCapability): boolean;
1553
- /**
1554
- * The model ids the `off` level should be offered for by default (issue #34).
1555
- *
1556
- * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
1557
- * This is the seed — what the set is filled with the first time it is read —
1558
- * so a profile that has never saved shows the correct default immediately,
1559
- * without requiring a save.
1560
- *
1561
- * Only an explicit `canDisableThinking: true` enables the level; absence is
1562
- * "unknown" and stays unoffered, matching how the rest of the plugin treats an
1563
- * undeclared capability.
1564
- */
1565
- declare function defaultOffModelIds(models: readonly WorkBuddyOffCapability[]): string[];
1566
- /**
1567
- * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
1568
- * catalog.
1569
- *
1570
- * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
1571
- * reads only the model's own declaration — so the corrected answer has to be
1572
- * written onto the model before it reaches the adapter. Doing it here keeps the
1573
- * adapter a pure "declared capability in, level map out" function and keeps the
1574
- * table next to the rule it feeds.
1575
- *
1576
- * This OVERWRITES the declared field rather than adding a sibling, which is
1577
- * safe because the declared value has exactly one consumer (that same level
1578
- * map) and is never persisted where the card can see it: `toWebModel` drops
1579
- * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
1580
- * upstream discovery, never from this stamped list. A model that declares no
1581
- * reasoning is returned untouched — there is no level to correct.
1582
- */
1583
- declare function withEffectiveOff<T extends WorkBuddyOffCapability>(models: readonly T[], selection: ReadonlySet<string>): T[];
1584
- //#endregion
1585
1648
  //#region src/at-rest.d.ts
1586
1649
  /**
1587
1650
  * WorkBuddy desktop "at-rest" credential decryption.
@@ -2514,6 +2577,28 @@ interface WorkBuddyWebModel {
2514
2577
  * level). Same shape as `supportsImages` above.
2515
2578
  */
2516
2579
  canDisableThinking?: boolean;
2580
+ /**
2581
+ * This model is on the reviewed `off`-refusal table (`src/off-thinking.ts`),
2582
+ * so ticking its checkbox cannot take effect (issue #83).
2583
+ *
2584
+ * The verdict, derived on the host — not a stored preference, and not
2585
+ * something the card has to recompute. It exists so the card can explain a
2586
+ * tick that did not stick instead of leaving the user staring at a checkbox
2587
+ * that looks saved. Like every other `reasoning` field it rides along in
2588
+ * the saved `lastCatalog` entry, where it is ignored on load: the verdict is
2589
+ * always recomputed from `id`.
2590
+ */
2591
+ offRefused?: boolean;
2592
+ /**
2593
+ * A REQUEST really carried `reasoning_effort: "off"` for this model and the
2594
+ * upstream answered HTTP 400 / code 11133 (issue #83).
2595
+ *
2596
+ * Row-level, DERIVED on write from the last refusal's `modelIdOfBody` —
2597
+ * never typed in by hand. Merged rather than replaced when the saved
2598
+ * catalog is refreshed, so the badge survives the full-table overwrite a
2599
+ * model refresh performs.
2600
+ */
2601
+ refusesOff?: boolean;
2517
2602
  };
2518
2603
  description?: string;
2519
2604
  }
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-Cq4_pJ_l.js";
1
+ import { $ as WORKBUDDY_APP_EXECUTABLE_ENV, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as lastRefusalFor, K as REFUSES_OFF_MODEL_IDS, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as withEffectiveOff, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as offRefused, Y as offDefaultFor, Z as recordRefusal, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as macosBundleExecutable, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as readAtRestKey, et as clearAtRestKeyCache, f as WorkBuddyCatalog, ft as workbuddyAppExecutableCandidates, g as ENCRYPTED_CREDENTIAL_CODE, gt as writeFileAtomic, h as fallbackModelsFor, ht as withFileLock, i as processStartTimeMs, it as findWorkbuddyAppExecutable, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as macosNestedAppBundles, m as deriveCatalog, mt as resolveDshHome, n as clearHostHeartbeat, nt as deriveAtRestKeyId, o as workbuddyHostHeartbeatPath, ot as isEncryptedFieldWrapper, p as applyContextBudgets, q as defaultOffModelIds, r as isHeartbeatProcessAlive, rt as fetchAtRestKeyPayload, s as writeHostHeartbeat, st as isWorkbuddyBundle, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as deriveAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, ut as openEncryptedField, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-BrIEnor4.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -138,6 +138,13 @@ function claimableBenefits(benefits, nowMs) {
138
138
  * @module dsh-connect-workbuddy/adapter
139
139
  */
140
140
  /** Provider route this bundle owns for the domestic (CN) gateway. */
141
+ /**
142
+ * The tier a request gets when it names none — compaction summaries, above all
143
+ * (issue #83). See the `reasoning` field in `profileFor` for why an absent
144
+ * default is not "no opinion" but "whatever pi-ai's level map says", which is
145
+ * how `off` became the silent default for `/compact`.
146
+ */
147
+ const WORKBUDDY_DEFAULT_EFFORT = "high";
141
148
  const WORKBUDDY_PROVIDER = "workbuddy";
142
149
  /** Provider route this bundle owns for the international gateway. */
143
150
  const WORKBUDDY_GLOBAL_PROVIDER = "workbuddy-global";
@@ -307,16 +314,21 @@ function workBuddyDisplayName(info) {
307
314
  *
308
315
  * The value read here is already the EFFECTIVE one: the runtime catalog is
309
316
  * stamped by `withEffectiveOff()` before it reaches the adapter, so this is the
310
- * user's `off` selection when they made one (and the upstream declaration,
311
- * minus `REFUSES_OFF_MODEL_IDS`, when they never did). Absence is "unknown" and
312
- * stays unoffered, matching how the rest of the plugin treats undeclared
313
- * capability.
317
+ * user's `off` selection when they made one — except for the ids on
318
+ * `REFUSES_OFF_MODEL_IDS`, which the stamp forces to `false` no matter what the
319
+ * user asked for (issue #83: this level, once ticked, also became the IMPLICIT
320
+ * default for every request that carries no effort at all, which is how
321
+ * compaction went to 100% failure). The table is re-checked here as well so the
322
+ * wall does not depend on every caller remembering to stamp first.
323
+ *
324
+ * Absence is "unknown" and stays unoffered, matching how the rest of the plugin
325
+ * treats undeclared capability.
314
326
  */
315
327
  function workBuddyThinkingLevelMap(info) {
316
328
  const supported = info.reasoning?.supportedEfforts?.filter((effort) => THINKING_LEVELS.includes(effort));
317
329
  if (supported === void 0 || supported.length === 0) return void 0;
318
330
  const map = Object.fromEntries(THINKING_LEVELS.map((level) => [level, supported.includes(level) ? level : null]));
319
- if (info.reasoning?.canDisableThinking === true) map.off = "off";
331
+ if (info.reasoning?.canDisableThinking === true && !offRefused(info.id)) map.off = "off";
320
332
  else map.off = null;
321
333
  return map;
322
334
  }
@@ -673,6 +685,20 @@ function createWorkBuddyAdapter(options) {
673
685
  retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-workbuddy retryPolicy"),
674
686
  configuredMaxTokens: /* @__PURE__ */ new Map(),
675
687
  modelErrors: /* @__PURE__ */ new Map(),
688
+ /**
689
+ * The default tier, pinned to the SAME value pi-ai's level map would fill in
690
+ * for an absent one (issue #83).
691
+ *
692
+ * `dsh-llm-pi-ai` resolves every request's tier as
693
+ * `options.reasoningEffort ?? profile.reasoning`, so an absent default here
694
+ * means "whatever pi-ai fills in" — and for a model whose map carried
695
+ * `off → "off"`, that was `"off"`, i.e. every compaction summary asked a
696
+ * model like `deepseek-v4.1-flash` to disable thinking, which it answers with
697
+ * 400 / 11133. Nothing structurally prevents a level or a catalog snapshot
698
+ * from disagreeing with the adapter's own map, so the default must never be
699
+ * that. This is the same value the host asks for explicitly on normal turns.
700
+ */
701
+ reasoning: WORKBUDDY_DEFAULT_EFFORT,
676
702
  ...REQUEST_IMAGE_BUDGETS,
677
703
  piProvider: key === providerId ? provider : {
678
704
  ...provider,
@@ -2721,6 +2747,46 @@ function describeAccount(credential) {
2721
2747
  * turn in this product reads Chinese, and an English clause inside Chinese prose
2722
2748
  * was the previous build's most visible rough edge.
2723
2749
  */
2750
+ /**
2751
+ * Name the rejected PARAMETER when the upstream refused a reasoning tier, or
2752
+ * `undefined` when it is any other failure (issue #83).
2753
+ *
2754
+ * The compaction path used to surface "Compaction could not produce a useful
2755
+ * summary" and nothing else, so a 11133 caused by `reasoning_effort: "off"` was
2756
+ * indistinguishable from a model that was simply unavailable — the reporter had
2757
+ * to read the raw body to learn that one parameter was at fault. When the body
2758
+ * names the offending parameter (`extError.param`) the reply says so; this is a
2759
+ * refusal the plugin can explain without guessing, because it is the union of
2760
+ * two facts the upstream stated in the response.
2761
+ */
2762
+ /**
2763
+ * Turn a failure into a refusal report for {@link WorkBuddyShimOptions.reportRefusal},
2764
+ * or `undefined` when it is not a tier refusal worth recording.
2765
+ *
2766
+ * Only refusals the upstream attributed to a PARAMETER are reported: a plain
2767
+ * 500 or a quota error says nothing about `off`, and recording it would pin a
2768
+ * "refuses off" badge on a model that never refused anything.
2769
+ */
2770
+ function refusedTierReport(failure, modelId) {
2771
+ if (failure.ok || failure.kind !== "client") return void 0;
2772
+ const param = failure.detail?.rejectedParam;
2773
+ if (param === void 0 || !param.includes("reasoning")) return void 0;
2774
+ const tier = failure.detail?.rejectedTier;
2775
+ return {
2776
+ param,
2777
+ ...tier === void 0 ? {} : { tier },
2778
+ ...failure.detail?.upstreamCode === void 0 ? {} : { code: failure.detail.upstreamCode },
2779
+ ...modelId === void 0 ? {} : { modelId },
2780
+ at: Date.now()
2781
+ };
2782
+ }
2783
+ function reasoningTierRefusal(failure) {
2784
+ if (failure.ok || failure.kind !== "client") return void 0;
2785
+ const param = failure.detail?.rejectedParam;
2786
+ if (param === void 0 || !param.includes("reasoning")) return void 0;
2787
+ const tier = failure.detail?.rejectedTier;
2788
+ return tier === void 0 ? `上游拒绝了推理档位参数(${param})` : `上游拒绝了推理档位「${tier}」(${param})`;
2789
+ }
2724
2790
  function failoverReasonText(failure) {
2725
2791
  if (failure.ok) return "不可用";
2726
2792
  switch (failure.kind) {
@@ -2948,6 +3014,7 @@ function createWorkBuddyShim(options) {
2948
3014
  const logger = options.logger;
2949
3015
  const failoverAccount = options.failoverAccount;
2950
3016
  const failoverShortfall = options.failoverShortfall;
3017
+ const reportRefusedTier = options.reportRefusal ?? (() => {});
2951
3018
  const prepareAccount = options.prepareAccount;
2952
3019
  const onAccountFailure = options.onAccountFailure;
2953
3020
  const onRequestBody = options.onRequestBody;
@@ -3165,6 +3232,8 @@ function createWorkBuddyShim(options) {
3165
3232
  const note = attempts > 1 ? `(已尝试 ${attempts} 个账号)` : "";
3166
3233
  const shortfall = failoverShortfall !== void 0 && failoverAccount !== void 0 && isFailoverWorthy(result.kind) ? await failoverShortfall().catch(() => void 0) : void 0;
3167
3234
  const why = shortfall === void 0 ? "" : `:${shortfall}`;
3235
+ const refusedReport = refusedTierReport(result, requestModelId);
3236
+ if (refusedReport !== void 0) reportRefusedTier(refusedReport);
3168
3237
  if (result.kind === "policy_reject") {
3169
3238
  const detail = result.detail;
3170
3239
  const lead = detail?.displayMsg ?? clippedUpstreamText(result.message, 200);
@@ -3190,8 +3259,9 @@ function createWorkBuddyShim(options) {
3190
3259
  if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
3191
3260
  if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
3192
3261
  const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
3193
- const receipt = detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
3194
- const advice = result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
3262
+ const refusedTier = reasoningTierRefusal(result);
3263
+ const receipt = refusedTier ?? detail?.tips ?? detail?.displayMsg ?? clippedUpstreamText(result.message, 400);
3264
+ const advice = refusedTier !== void 0 ? "这不是账号或网络问题:该模型实测拒绝「off」推理档位,请到模型卡片里取消该行的「可关闭思考」勾选后重试" : result.kind === "client" ? "该请求被上游按参数拒绝:与网络、账号、额度都无关,换账号也不会改变结果,请按上面的提示调整后重试" : shortfall === void 0 ? "" : "建议换一个模型继续,或稍后等待该账号的额度恢复";
3195
3265
  const body = advice === "" ? receipt : `${receipt.replace(/[。.]$/u, "")}。${advice}`;
3196
3266
  writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${why}${stamp}。${body}`, hostErrorCode(result.kind));
3197
3267
  return;
@@ -5185,7 +5255,9 @@ function toWebModel(model, budgets) {
5185
5255
  ...model.reasoning === void 0 ? {} : { reasoning: {
5186
5256
  ...model.reasoning.supportedEfforts === void 0 ? {} : { supportedEfforts: [...model.reasoning.supportedEfforts] },
5187
5257
  ...model.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: model.reasoning.defaultEffort },
5188
- ...model.reasoning.canDisableThinking === void 0 ? {} : { canDisableThinking: model.reasoning.canDisableThinking }
5258
+ ...model.reasoning.canDisableThinking === void 0 ? {} : { canDisableThinking: model.reasoning.canDisableThinking },
5259
+ ...offRefused(model.id) ? { offRefused: true } : {},
5260
+ ...model.reasoning.refusesOff === true ? { refusesOff: true } : {}
5189
5261
  } }
5190
5262
  };
5191
5263
  }
@@ -5352,7 +5424,13 @@ async function workBuddyWebStatus(deps, region, options = {}) {
5352
5424
  showCreditsInMainUi,
5353
5425
  creditPosition,
5354
5426
  accounts: accounts.map(toWebAccount),
5355
- models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))),
5427
+ models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
5428
+ ...model,
5429
+ reasoning: {
5430
+ ...model.reasoning,
5431
+ refusesOff: true
5432
+ }
5433
+ }),
5356
5434
  enabledModelIds: [...deps.enabledModelIds(region)],
5357
5435
  imageModelIds: [...deps.imageModelIds(region)],
5358
5436
  offModelIds: [...deps.offModelIds(region)]
@@ -6011,85 +6089,6 @@ function imageDefaultFor(info) {
6011
6089
  return NATIVE_MODALITY_BY_MODEL_ID[info.id] === "multimodal";
6012
6090
  }
6013
6091
  //#endregion
6014
- //#region src/off-thinking.ts
6015
- /**
6016
- * Ids observed to REFUSE `reasoning_effort: "off"` with HTTP 400 despite
6017
- * declaring `canDisableThinking: true` (issue #34).
6018
- *
6019
- * Keyed by exact model id and deliberately NOT by region: `deepseek-v4.1-flash`
6020
- * refuses on BOTH gateways, and the split tracks the model family rather than
6021
- * the gateway. The cost is asymmetric — offering a level that always fails
6022
- * costs the user every request for that model, while hiding one that would have
6023
- * worked costs a single option — so the table errs towards hiding. A user who
6024
- * knows better can override any entry (see {@link effectiveOff}).
6025
- *
6026
- * `deepseek-v4-pro` was measured on the CN gateway only; it is listed because
6027
- * its sibling does refuse on both, and the same asymmetry argues for hiding.
6028
- */
6029
- const REFUSES_OFF_MODEL_IDS = /* @__PURE__ */ new Set([
6030
- "deepseek-v4.1-flash",
6031
- "deepseek-v4.1-flash-sg",
6032
- "deepseek-v4-pro",
6033
- "primary-model",
6034
- "gpt-6-astra",
6035
- "gpt-5.6-sol",
6036
- "gpt-5.6-terra",
6037
- "gpt-5.6-luna",
6038
- "gemini-3.5-flash"
6039
- ]);
6040
- /**
6041
- * Whether `off` should be offered for a model when the user has expressed no
6042
- * opinion: the upstream's declaration, minus the ids known to refuse it.
6043
- *
6044
- * Only an explicit `canDisableThinking: true` enables the level; absence is
6045
- * "unknown" and stays unoffered, matching how the rest of the plugin treats an
6046
- * undeclared capability.
6047
- */
6048
- function offDefaultFor(info) {
6049
- return info.reasoning?.canDisableThinking === true && !REFUSES_OFF_MODEL_IDS.has(info.id);
6050
- }
6051
- /**
6052
- * The model ids the `off` level should be offered for by default (issue #34).
6053
- *
6054
- * The user's checkbox is a plain set: ticked = offered, unticked = withdrawn.
6055
- * This is the seed — what the set is filled with the first time it is read —
6056
- * so a profile that has never saved shows the correct default immediately,
6057
- * without requiring a save.
6058
- *
6059
- * Only an explicit `canDisableThinking: true` enables the level; absence is
6060
- * "unknown" and stays unoffered, matching how the rest of the plugin treats an
6061
- * undeclared capability.
6062
- */
6063
- function defaultOffModelIds(models) {
6064
- return models.filter(offDefaultFor).map((model) => model.id);
6065
- }
6066
- /**
6067
- * Stamp the user's saved `off` selection onto a model list, for the RUNTIME
6068
- * catalog.
6069
- *
6070
- * The runtime model descriptor is built by `workBuddyThinkingLevelMap`, which
6071
- * reads only the model's own declaration — so the corrected answer has to be
6072
- * written onto the model before it reaches the adapter. Doing it here keeps the
6073
- * adapter a pure "declared capability in, level map out" function and keeps the
6074
- * table next to the rule it feeds.
6075
- *
6076
- * This OVERWRITES the declared field rather than adding a sibling, which is
6077
- * safe because the declared value has exactly one consumer (that same level
6078
- * map) and is never persisted where the card can see it: `toWebModel` drops
6079
- * `canDisableThinking`, and the card's `lastCatalog` is written from the raw
6080
- * upstream discovery, never from this stamped list. A model that declares no
6081
- * reasoning is returned untouched — there is no level to correct.
6082
- */
6083
- function withEffectiveOff(models, selection) {
6084
- return models.map((model) => model.reasoning === void 0 ? model : {
6085
- ...model,
6086
- reasoning: {
6087
- ...model.reasoning,
6088
- canDisableThinking: selection.has(model.id)
6089
- }
6090
- });
6091
- }
6092
- //#endregion
6093
6092
  //#region src/index.ts
6094
6093
  /** Stable Cordis plugin name. */
6095
6094
  const name = "dsh-connect-workbuddy";
@@ -6428,6 +6427,9 @@ function apply(ctx, config) {
6428
6427
  onUpstreamUsage: (usage) => {
6429
6428
  pendingUsage?.(usage);
6430
6429
  },
6430
+ reportRefusal: (report) => {
6431
+ recordRefusal(report);
6432
+ },
6431
6433
  prepareAccount: () => applyPoolSelection(region),
6432
6434
  failoverAccount: (triedAccountIds) => failoverAccountFor(region, triedAccountIds),
6433
6435
  failoverShortfall: () => explainPoolShortfall(region),
@@ -6449,7 +6451,13 @@ function apply(ctx, config) {
6449
6451
  const state = regionStateOf(value, region);
6450
6452
  const roster = state.lastCatalog?.length ? state.lastCatalog : liveModels.length ? liveModels : fallbackModelsFor(region);
6451
6453
  const declared = withImageSelection(deriveCatalog(roster, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? []));
6452
- return withEffectiveOff(declared, new Set(savedOffModelIds(state, declared)));
6454
+ return withEffectiveOff(declared, new Set(savedOffModelIds(state, declared))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
6455
+ ...model,
6456
+ reasoning: {
6457
+ ...model.reasoning,
6458
+ refusesOff: true
6459
+ }
6460
+ });
6453
6461
  };
6454
6462
  /**
6455
6463
  * The region's `off` selection as a Set (issue #34).
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": "4.10.1",
5
+ "version": "4.11.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",