dsh-workbuddy-bridge 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,10 +47,11 @@
47
47
 
48
48
  - **开箱即用** —— 装完启用,模型分组自己出现。
49
49
  - **两版隔离** —— 国内版与国际版各自分组,模型、账号、积分互不混用,只跟随各自 App 的登录状态。
50
- - **推理档位** —— 上游声明了档位的模型直接可选;没声明的可在插件页**手动检测**。
51
50
  - **推理档位** —— 上游声明了档位的模型直接可选;没声明的可在插件页**手动检测**。
52
51
  灯泡表示能切档位;上游已声明的模型也亮着,只是不需要检测。
53
52
  能切档位的模型会带一个**默认档位**(上游声明了就按声明,否则取 `high`),因此选择器里不再出现「Default」这一项。
53
+ 档位里的 **Off 表示「不发送档位参数」**:两个接口都不接受这个字面量,所以插件不会把它发出去;
54
+ 实际是否关闭思考由上游决定,插件不做保证。
54
55
  - **积分可见** —— 每条回复下方显示本次消耗,模型选择器旁常驻剩余积分。拿不到数字时不显示。
55
56
  - **模型显隐** —— 卡片「模型」标签里勾选哪些模型出现在选择器里,按登录账号分别保存。
56
57
  - **图片输入** —— 多数模型可直接粘贴或拖入图片;纯文字模型会明确说明。
package/README_en.md CHANGED
@@ -54,6 +54,8 @@ models appear directly in the DSH model picker — no separate provider to confi
54
54
  on the plugin page. The bulb means "this model can switch levels"; a declared model shows it too, it just
55
55
  has nothing to detect. A switchable model also carries a **default level** (the upstream's own declaration
56
56
  when it has one, otherwise `high`), so the picker no longer offers "Default".
57
+ **Off means "send no level parameter"**: neither endpoint accepts that literal, so the plugin never puts it
58
+ on the wire — whether thinking is then actually disabled is up to the upstream, and the plugin makes no claim.
57
59
  - **Credit in view** — each reply shows what it consumed, and remaining credit sits beside the model picker.
58
60
  Nothing renders when a figure is unavailable.
59
61
  - **Model visibility** — tick which models appear in the picker on the card's "Models" tab, saved per
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as variantFor, D as WorkBuddyUpstreamClient, S as WORKBUDDY_VARIANTS, Y as resolveAppVersion, a as readHostHeartbeat, c as WORKBUDDY_BRIDGE_VERSION, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, x as CN_VARIANT } from "./heartbeat-BpW2mSgI.js";
2
+ import { C as variantFor, D as WorkBuddyUpstreamClient, S as WORKBUDDY_VARIANTS, Y as resolveAppVersion, a as readHostHeartbeat, c as WORKBUDDY_BRIDGE_VERSION, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, x as CN_VARIANT } from "./heartbeat-DR6QsZ9h.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/cli/bin.ts
@@ -828,13 +828,21 @@ function billingHeaders(credential) {
828
828
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
829
829
  * force `stream: true` (the upstream rejects non-streaming), flatten
830
830
  * `tool_choice` (the upstream's field is a string; object forms return 400),
831
- * and rewrite `developer` messages as `system`.
831
+ * rewrite `developer` messages as `system`, and drop the adapter's own `off`
832
+ * effort spelling (see {@link dropUnsupportedEffort}).
832
833
  *
833
834
  * The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
834
835
  * `role: "developer"` (the OpenAI convention it adopted), but the WorkBuddy
835
836
  * upstream rejects that role with HTTP 400 code 11128 ("Illegal API
836
837
  * invocation from an unapproved channel"). Rewriting to `system` is the
837
838
  * compatible spelling the upstream accepts.
839
+ *
840
+ * Every transformation here is region-agnostic on purpose. The two endpoints
841
+ * differ in what they *add* (the international one also needs a leading system
842
+ * prompt — see {@link prepareInternationalChatBody}), not in what they reject:
843
+ * both refuse the picker's `off` spelling, which is why that strip lives here
844
+ * rather than in the international layer (issue #13 reversed the earlier
845
+ * international-only scope decision, which rested on CN accepting it).
838
846
  */
839
847
  function prepareChatBody(source) {
840
848
  let body;
@@ -848,6 +856,7 @@ function prepareChatBody(source) {
848
856
  obj["stream"] = true;
849
857
  normalizeDeveloperRole(obj);
850
858
  normalizeToolChoice(obj);
859
+ dropUnsupportedEffort(obj);
851
860
  return JSON.stringify(obj);
852
861
  }
853
862
  /** Rewrite `role: "developer"` messages to `role: "system"` (upstream rejects developer). */
@@ -937,7 +946,20 @@ var WorkBuddyUpstreamClient = class {
937
946
  this.resolveAppVersion = options.resolveAppVersion ?? (() => resolveAppVersion());
938
947
  this.resolveChatIdentity = options.resolveChatIdentity ?? ((region) => resolveChatIdentity(region));
939
948
  }
940
- /** POST the chat endpoint; a successful answer is the raw SSE response. */
949
+ /**
950
+ * POST the chat endpoint; a successful answer is the raw SSE response.
951
+ *
952
+ * This is the one place every chat request converges on (the loopback shim,
953
+ * the host LLM route, and any direct caller all end up here), so it is also
954
+ * where the body is normalized for the upstream — in *both* regions. The
955
+ * region only decides which extra requirement applies on top: the
956
+ * international gateway additionally demands a leading system message. It
957
+ * used to decide whether the body was normalized at all, which left the CN
958
+ * path sending the picker's `off` spelling verbatim (issue #13).
959
+ *
960
+ * Normalization is idempotent, so a caller that already prepared the body
961
+ * (the shim does) sees no second change.
962
+ */
941
963
  async chatStream(credential, bodyJson, signal) {
942
964
  const region = regionOf(credential.domain);
943
965
  let userAgent;
@@ -946,6 +968,7 @@ var WorkBuddyUpstreamClient = class {
946
968
  } catch {
947
969
  userAgent = chatUserAgent(fallbackChatIdentity(region), region);
948
970
  }
971
+ const body = region === "global" ? prepareInternationalChatBody(bodyJson) : prepareChatBody(bodyJson);
949
972
  let response;
950
973
  try {
951
974
  response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
@@ -954,7 +977,7 @@ var WorkBuddyUpstreamClient = class {
954
977
  ...chatHeaders(credential, userAgent),
955
978
  Authorization: `Bearer ${credential.accessToken}`
956
979
  },
957
- body: region === "global" ? prepareInternationalChatBody(bodyJson) : bodyJson,
980
+ body,
958
981
  ...signal === void 0 ? {} : { signal }
959
982
  });
960
983
  } catch (error) {
@@ -1498,6 +1521,11 @@ function modelWithCurrentPromotion(model, now = Date.now()) {
1498
1521
  * merged: existing messages keep their order and wording. A body that is not a
1499
1522
  * JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
1500
1523
  * so this is safe to run over an already-prepared-or-not body.
1524
+ *
1525
+ * This layer now carries *only* that requirement. The `reasoning_effort` strip
1526
+ * used to live here, scoped to this region; issue #13 showed CN rejects the
1527
+ * same spelling, so it moved into the shared {@link prepareChatBody} and this
1528
+ * function no longer touches the effort field at all.
1501
1529
  */
1502
1530
  function prepareInternationalChatBody(source) {
1503
1531
  const prepared = prepareChatBody(source);
@@ -1508,7 +1536,6 @@ function prepareInternationalChatBody(source) {
1508
1536
  return prepared;
1509
1537
  }
1510
1538
  if (!isObject(body)) return prepared;
1511
- dropUnsupportedEffort(body);
1512
1539
  const messages = body["messages"];
1513
1540
  if (!Array.isArray(messages)) return JSON.stringify(body);
1514
1541
  const first = messages[0];
@@ -1520,25 +1547,35 @@ function prepareInternationalChatBody(source) {
1520
1547
  return JSON.stringify(body);
1521
1548
  }
1522
1549
  /**
1523
- * Remove the adapter's own `off` effort spelling from the **international** wire.
1524
- *
1525
- * `thinkingLevelMap.off` is pinned to the literal `'off'` for models that
1526
- * declare `canDisableThinking`, so that the level stays selectable. pi-ai
1527
- * sends that value for any request carrying no explicit level, and the
1528
- * international endpoint rejects it on the GPT family with HTTP 400 `11133` /
1529
- * `extError.param === 'reasoning.effort'` (issue #49). Omission is the only
1530
- * form measured good on every such model; a literal `'none'` is *not* a safe
1531
- * substitute — accepted by the GPT-5.6 family and GLM, rejected by
1532
- * `gpt-6-astra`.
1533
- *
1534
- * Consequences, stated honestly: the international picker still offers Off,
1535
- * but selecting it now means "the field is omitted" — the model's actual
1536
- * behaviour is decided upstream and is *not* guaranteed to disable thinking
1537
- * or to match the catalog's `defaultEffort`. Declared spellings
1538
- * (`low`/`medium`/`high`/`xhigh`/`max`) and an explicit `none` pass through
1539
- * untouched. The CN variant is deliberately unaffected: its endpoint has
1540
- * accepted this spelling in every measurement so far, and keeping its wire
1541
- * unchanged is a scope decision, not a claim about that endpoint's future.
1550
+ * Remove the adapter's own `off` effort spelling from the wire, in both regions.
1551
+ *
1552
+ * `thinkingLevelMap.off` is pinned to the literal `'off'` so the level stays
1553
+ * selectable in the picker — pi-ai exposes a level only when its entry is a
1554
+ * string — but that same entry is also the value pi-ai puts on the wire, both
1555
+ * for an explicit Off selection and for *every* request that carries no level
1556
+ * at all. `'off'` is a picker name, not an upstream spelling, and both
1557
+ * endpoints reject it:
1558
+ *
1559
+ * - international GPT family: HTTP 400 `11133` / `extError.param ===
1560
+ * 'reasoning.effort'` (issue #49);
1561
+ * - CN: the same 400 `11133` on every `canDisableThinking` model — measured
1562
+ * on `deepseek-v4.1-flash`, where the author also confirmed `'disabled'`
1563
+ * is rejected and `'none'` answers 200 (issue #13). This is what made the
1564
+ * earlier international-only scope decision obsolete.
1565
+ *
1566
+ * Omission is the only form measured good on every affected model, so it is
1567
+ * the only form this function can produce; a literal `'none'` is *not* a safe
1568
+ * substitute (accepted by the GPT-5.6 family and GLM, rejected by
1569
+ * `gpt-6-astra`, and silently ignored by `kimi-k2.8-preview`). Declared
1570
+ * spellings (`low`/`medium`/`high`/`xhigh`/`max`) and an explicit `none` pass
1571
+ * through untouched — the adapter does not decide a user's `none` for them.
1572
+ *
1573
+ * Consequences, stated honestly: the picker still offers Off in both regions,
1574
+ * but selecting it means "the field is omitted". The model's actual behaviour
1575
+ * is then decided upstream — *not* guaranteed to disable thinking and not
1576
+ * guaranteed to match the catalog's `defaultEffort`. The plugin used to claim
1577
+ * more than that on CN, where `'off'` was merely accepted (accepted never
1578
+ * meant effective); #13 is why the claim is withdrawn rather than reworded.
1542
1579
  */
1543
1580
  function dropUnsupportedEffort(obj) {
1544
1581
  if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
@@ -3688,7 +3725,7 @@ var WorkBuddyCatalog = class {
3688
3725
  };
3689
3726
  //#endregion
3690
3727
  //#region src/version.ts
3691
- const WORKBUDDY_BRIDGE_VERSION = "0.5.0";
3728
+ const WORKBUDDY_BRIDGE_VERSION = "0.5.1";
3692
3729
  //#endregion
3693
3730
  //#region src/web/heartbeat.ts
3694
3731
  /**
package/lib/index.d.ts CHANGED
@@ -613,13 +613,21 @@ declare function regionOf(domain: string): WorkBuddyRegion;
613
613
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
614
614
  * force `stream: true` (the upstream rejects non-streaming), flatten
615
615
  * `tool_choice` (the upstream's field is a string; object forms return 400),
616
- * and rewrite `developer` messages as `system`.
616
+ * rewrite `developer` messages as `system`, and drop the adapter's own `off`
617
+ * effort spelling (see {@link dropUnsupportedEffort}).
617
618
  *
618
619
  * The `developer` rewrite is load-bearing: pi-ai emits the system prompt as
619
620
  * `role: "developer"` (the OpenAI convention it adopted), but the WorkBuddy
620
621
  * upstream rejects that role with HTTP 400 code 11128 ("Illegal API
621
622
  * invocation from an unapproved channel"). Rewriting to `system` is the
622
623
  * compatible spelling the upstream accepts.
624
+ *
625
+ * Every transformation here is region-agnostic on purpose. The two endpoints
626
+ * differ in what they *add* (the international one also needs a leading system
627
+ * prompt — see {@link prepareInternationalChatBody}), not in what they reject:
628
+ * both refuse the picker's `off` spelling, which is why that strip lives here
629
+ * rather than in the international layer (issue #13 reversed the earlier
630
+ * international-only scope decision, which rested on CN accepting it).
623
631
  */
624
632
  declare function prepareChatBody(source: string): string;
625
633
  /** Provenance of one successful catalog fetch, surfaced by the status card. */
@@ -660,7 +668,20 @@ declare class WorkBuddyUpstreamClient {
660
668
  /** Provenance of the most recent successful catalog fetch, for the card. */
661
669
  lastCatalog: WorkBuddyCatalogFetch | undefined;
662
670
  constructor(options?: WorkBuddyUpstreamClientOptions);
663
- /** POST the chat endpoint; a successful answer is the raw SSE response. */
671
+ /**
672
+ * POST the chat endpoint; a successful answer is the raw SSE response.
673
+ *
674
+ * This is the one place every chat request converges on (the loopback shim,
675
+ * the host LLM route, and any direct caller all end up here), so it is also
676
+ * where the body is normalized for the upstream — in *both* regions. The
677
+ * region only decides which extra requirement applies on top: the
678
+ * international gateway additionally demands a leading system message. It
679
+ * used to decide whether the body was normalized at all, which left the CN
680
+ * path sending the picker's `off` spelling verbatim (issue #13).
681
+ *
682
+ * Normalization is idempotent, so a caller that already prepared the body
683
+ * (the shim does) sees no second change.
684
+ */
664
685
  chatStream(credential: WorkBuddyCredential, bodyJson: string, signal?: AbortSignal): Promise<WorkBuddyChatResult>;
665
686
  /** POST the token-refresh endpoint; the caller merges the outcome. */
666
687
  refreshToken(credential: WorkBuddyCredential): Promise<WorkBuddyRefreshOutcome>;
@@ -833,6 +854,11 @@ declare function modelWithCurrentPromotion(model: WorkBuddyUpstreamModel, now?:
833
854
  * merged: existing messages keep their order and wording. A body that is not a
834
855
  * JSON object is returned unchanged, exactly as {@link prepareChatBody} does,
835
856
  * so this is safe to run over an already-prepared-or-not body.
857
+ *
858
+ * This layer now carries *only* that requirement. The `reasoning_effort` strip
859
+ * used to live here, scoped to this region; issue #13 showed CN rejects the
860
+ * same spelling, so it moved into the shared {@link prepareChatBody} and this
861
+ * function no longer touches the effort field at all.
836
862
  */
837
863
  declare function prepareInternationalChatBody(source: string): string;
838
864
  //#endregion
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as parseModelCatalog, B as chatUserAgent, C as variantFor, D as WorkBuddyUpstreamClient, F as probeModel, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as randomSentinel, J as readBundleVersion, K as appUserAgent, L as classifyUpstreamError, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as CN_APP_VERSION_FILENAME, S as WORKBUDDY_VARIANTS, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as AI_VARIANT, c as WORKBUDDY_BRIDGE_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as prepareChatBody, k as normalizeCredits, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as installedAppVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as WORKBUDDY_CREDIT_PATH, x as CN_VARIANT, y as workbuddyOwnAuthPath, z as FALLBACK_CN_APP_VERSION } from "./heartbeat-BpW2mSgI.js";
1
+ import { A as parseModelCatalog, B as chatUserAgent, C as variantFor, D as WorkBuddyUpstreamClient, F as probeModel, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as randomSentinel, J as readBundleVersion, K as appUserAgent, L as classifyUpstreamError, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as CN_APP_VERSION_FILENAME, S as WORKBUDDY_VARIANTS, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ as desktopAuthCandidatesFor, a as readHostHeartbeat, b as AI_VARIANT, c as WORKBUDDY_BRIDGE_VERSION, d as WorkBuddyCatalog, f as WORKBUDDY_AUTH_FILENAME, g as defaultDesktopAuthPath, h as defaultDesktopAuthCandidates, i as processStartTimeMs, j as prepareChatBody, k as normalizeCredits, l as FALLBACK_WORKBUDDY_AI_MODELS, m as WorkBuddyCredentialStore, n as clearHostHeartbeat, o as workbuddyHostHeartbeatPath, p as WORKBUDDY_AUTH_FILE_ENV, q as installedAppVersion, r as isHeartbeatProcessAlive, s as writeHostHeartbeat, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, u as FALLBACK_WORKBUDDY_MODELS, v as parseWorkBuddyAuth, w as WORKBUDDY_CREDIT_PATH, x as CN_VARIANT, y as workbuddyOwnAuthPath, z as FALLBACK_CN_APP_VERSION } from "./heartbeat-DR6QsZ9h.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -446,6 +446,13 @@ function withCatalogDisplay(name, info) {
446
446
  * The offered set is described internally as "verified accepted", never as
447
447
  * "verified effective": acceptance proves the upstream did not reject the
448
448
  * spelling, not that it changes what the model does.
449
+ *
450
+ * `off` is a picker name, not a wire spelling. It stays pinned to the literal
451
+ * `'off'` (a `null` here would remove the level from the picker entirely), and
452
+ * the shared preparation in `src/protocol/client.ts` strips that exact value
453
+ * before the request leaves the plugin — in both regions, because both
454
+ * endpoints reject it (issues #49 and #13). Selecting Off therefore means "send
455
+ * no `reasoning_effort`", and what the model then does is upstream's call.
449
456
  */
450
457
  function reasoningFields(info, observed) {
451
458
  const resolved = resolveEfforts(info, observed);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-bridge",
3
3
  "displayName": "DSH WorkBuddy Bridge",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.5.0",
5
+ "version": "0.5.1",
6
6
  "author": "zlZayn",
7
7
  "keywords": [
8
8
  "dsh",
@@ -97,7 +97,7 @@
97
97
  "react-dom": "^18.2.0"
98
98
  },
99
99
  "devDependencies": {
100
- "@deepseek-ai/cordis": "4.0.4",
100
+ "@deepseek-ai/cordis": "^4.0.4",
101
101
  "@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
102
102
  "@deepseek-ai/dsh-atomic-write": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
103
103
  "@deepseek-ai/dsh-attachment": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
@@ -119,7 +119,7 @@
119
119
  "@deepseek-ai/dsh-settings": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
120
120
  "@deepseek-ai/dsh-util-code-language": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
121
121
  "@deepseek-ai/dsh-util-workspace-path": "^0.1.7-rc.1 || ^0.2.0-rc.1 || ^0.2.1-rc.1",
122
- "@deepseek-ai/schemastery": "3.18.5",
122
+ "@deepseek-ai/schemastery": "^3.18.4",
123
123
  "@earendil-works/pi-ai": "^1.1.0",
124
124
  "@eslint/js": "^10.0.1",
125
125
  "@shikijs/langs": "^4.4.3",
@@ -150,7 +150,7 @@
150
150
  "micromark-util-symbol": "^2.0.1",
151
151
  "prettier": "^3.9.9",
152
152
  "react": "^18.2.0",
153
- "react-dom": "^18.3.1",
153
+ "react-dom": "^18.2.0",
154
154
  "react-test-renderer": "18.3.1",
155
155
  "shiki": "^4.4.3",
156
156
  "simple-icons": "^16.32.0",