dsh-grok-subscription 1.0.8 → 1.0.11

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.en.md CHANGED
@@ -25,7 +25,7 @@ Models, reasoning effort, and weekly quota all stay inside DSH.
25
25
 
26
26
  ## Three-step start
27
27
 
28
- 1. **Install the plugin.** Run the command below; to select a candidate version, enter the full `package@version`, for example `dsh-grok-subscription@1.0.8`.
28
+ 1. **Install the plugin.** Run the command below; to select a candidate version, enter the full `package@version`, for example `dsh-grok-subscription@1.0.10`.
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -91,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
91
91
  You can also install the version published on npm:
92
92
 
93
93
  ```sh
94
- dsh plugin --profile web add dsh-grok-subscription@1.0.8
94
+ dsh plugin --profile web add dsh-grok-subscription@1.0.10
95
95
  ```
96
96
 
97
97
  DSH handles target selection, the profile lock, dependency resolution, and bundle activation.
@@ -146,7 +146,8 @@ Restart DSH manually afterwards, then:
146
146
  - Settings can show the used and remaining percentages the backend reports, and says so plainly instead of guessing when the read fails;
147
147
  - Supports CLI login, device-code login, pull from the Grok CLI, and logout;
148
148
  - Renews the access token automatically: near the end of its 6-hour life, or on a 401, it runs the official `grok` CLI and retries once, so a session never lapses mid-use; the chat model list refreshes whenever sign-in state changes;
149
- - When subscription routing is unavailable it reports an error instead of silently switching to another paid route.
149
+ - When subscription routing is unavailable it reports an error instead of silently switching to another paid route;
150
+ - Prefix caching follows xAI's automatic prompt-cache semantics: one DSH session keeps a stable `prompt_cache_key`, tool definitions stay in a stable order, and the fallback adapter preserves and replays `reasoning.encrypted_content` unchanged. Server-side eviction can still miss, and the plugin never invents a hit rate.
150
151
 
151
152
  ### Composer quota
152
153
 
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
 
26
26
  ## 三步开始
27
27
 
28
- 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.8`。
28
+ 1. **安装插件**:在终端运行下面的命令;指定候选版本时填写完整的 `包名@版本`,例如 `dsh-grok-subscription@1.0.10`。
29
29
 
30
30
  ```sh
31
31
  dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
@@ -91,7 +91,7 @@ dsh plugin --profile web add BaronCyrus/dsh-grok-subscription
91
91
  也可以按 npm 上的已发布版本安装:
92
92
 
93
93
  ```sh
94
- dsh plugin --profile web add dsh-grok-subscription@1.0.8
94
+ dsh plugin --profile web add dsh-grok-subscription@1.0.10
95
95
  ```
96
96
 
97
97
  目标选择、profile 锁、依赖解析和 bundle 激活均由 DSH 负责。
@@ -146,7 +146,8 @@ dsh --profile web --dump-config
146
146
  - 设置页可查看服务端返回的用量与剩余百分比,失败时明确提示而不是显示猜测值;
147
147
  - 支持 CLI 登录、设备码登录、从 Grok CLI 拉取和登出;登录状态变化后 Chat 模型列表自动刷新;
148
148
  - access token 到期自动续期:临近 6 小时有效期或收到 401 时,调用官方 `grok` CLI 刷新会话并重试一次,不会中途要求手动重新登录;
149
- - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
149
+ - 订阅路由不可用时明确报错,不会静默切换到其他付费路由;
150
+ - 前缀缓存按 xAI 的自动缓存语义优化:同一 DSH 会话使用稳定的 `prompt_cache_key`,工具定义保持稳定顺序,回退适配器会保存并原样回放 `reasoning.encrypted_content`。缓存仍可能因服务端逐出而失效,插件不虚构命中率。
150
151
 
151
152
  ### 输入框额度
152
153
 
package/lib/client.js CHANGED
@@ -26,6 +26,7 @@ __export(client_exports, {
26
26
  RPC_CALL_TIMEOUT_MS: () => RPC_CALL_TIMEOUT_MS,
27
27
  SectionBoundary: () => SectionBoundary,
28
28
  UsagePanel: () => UsagePanel,
29
+ VersionCard: () => VersionCard,
29
30
  apply: () => apply,
30
31
  callRpc: () => callRpc,
31
32
  inject: () => inject,
@@ -38,7 +39,7 @@ var import_react2 = require("react");
38
39
  var zh = {
39
40
  nav: "Grok \u8BA2\u9605",
40
41
  title: "Grok \u8BA2\u9605",
41
- subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.8",
42
+ subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 1.0.11",
42
43
  signedIn: "\u5DF2\u767B\u5F55",
43
44
  signedOut: "\u672A\u767B\u5F55",
44
45
  account: "\u8D26\u6237",
@@ -90,12 +91,31 @@ var zh = {
90
91
  composerQuotaDetails: "\u6BCF\u5468\u989D\u5EA6",
91
92
  composerQuotaRemaining: "\u6BCF\u5468\u989D\u5EA6 \u5269\u4F59 {remaining}%",
92
93
  composerQuotaResets: "\u91CD\u7F6E\u4E8E {reset}",
93
- composerQuotaResetUnknown: "\u91CD\u7F6E\u65F6\u95F4\u672A\u77E5"
94
+ composerQuotaResetUnknown: "\u91CD\u7F6E\u65F6\u95F4\u672A\u77E5",
95
+ versionTitle: "\u63D2\u4EF6\u7248\u672C",
96
+ versionCurrent: "\u5F53\u524D\u7248\u672C",
97
+ versionLatest: "\u6700\u65B0\u7248\u672C",
98
+ versionCheck: "\u68C0\u67E5\u66F4\u65B0",
99
+ versionChecking: "\u6B63\u5728\u68C0\u67E5\u66F4\u65B0\u2026",
100
+ versionFailed: "\u65E0\u6CD5\u68C0\u67E5\u6700\u65B0\u7248\u672C\u3002",
101
+ versionUpToDate: "\u5DF2\u662F\u6700\u65B0\u7248\u672C\u3002",
102
+ versionAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{version}\u3002",
103
+ versionLinked: "\u5F53\u524D\u4E3A\u672C\u5730\u5F00\u53D1\u5B89\u88C5\uFF0C\u8BF7\u5728\u63D2\u4EF6\u4ED3\u5E93\u62C9\u53D6\u6700\u65B0\u4EE3\u7801\u5E76\u91CD\u65B0\u6784\u5EFA\u3002",
104
+ versionManual: "\u8BF7\u5728\u7EC8\u7AEF\u8FD0\u884C dsh plugin --profile web add dsh-grok-subscription@latest \u5B8C\u6210\u66F4\u65B0\u3002",
105
+ versionUpdate: "\u66F4\u65B0\u63D2\u4EF6",
106
+ versionUpdating: "\u6B63\u5728\u66F4\u65B0\u2026",
107
+ versionUpdateFailed: "\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u6216\u5728\u7EC8\u7AEF\u624B\u52A8\u66F4\u65B0\u3002",
108
+ versionUpdated: "\u5DF2\u66F4\u65B0\u5230 v{version}\u3002",
109
+ versionUpdatedHint: "\u65B0\u7248\u672C\u9700\u8981\u91CD\u542F DSH \u670D\u52A1\u624D\u80FD\u5B8C\u5168\u751F\u6548\uFF1B\u4EC5\u5237\u65B0\u754C\u9762\u4E0D\u4F1A\u66F4\u65B0\u5BBF\u4E3B\u8FDB\u7A0B\u4E2D\u5DF2\u52A0\u8F7D\u7684\u63D2\u4EF6\u3002",
110
+ versionRestart: "\u91CD\u542F DSH \u670D\u52A1",
111
+ versionRestartHint: "\u63D2\u4EF6\u65E0\u6CD5\u5B89\u5168\u5730\u91CD\u542F\u5BBF\u4E3B\u8FDB\u7A0B\uFF1A\u8BF7\u5728\u8FD0\u884C DSH \u7684\u7EC8\u7AEF\u6309 Ctrl+C \u7ED3\u675F\u8FDB\u7A0B\uFF0C\u518D\u91CD\u65B0\u8FD0\u884C dsh web\u3002",
112
+ versionRefresh: "\u5237\u65B0\u754C\u9762",
113
+ versionLater: "\u7A0D\u540E"
94
114
  };
95
115
  var en = {
96
116
  nav: "Grok Subscription",
97
117
  title: "Grok Subscription",
98
- subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.8",
118
+ subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 1.0.11",
99
119
  signedIn: "Signed in",
100
120
  signedOut: "Signed out",
101
121
  account: "Account",
@@ -147,7 +167,26 @@ var en = {
147
167
  composerQuotaDetails: "Weekly quota",
148
168
  composerQuotaRemaining: "Weekly quota \xB7 {remaining}% left",
149
169
  composerQuotaResets: "Resets {reset}",
150
- composerQuotaResetUnknown: "Reset time unknown"
170
+ composerQuotaResetUnknown: "Reset time unknown",
171
+ versionTitle: "Plugin version",
172
+ versionCurrent: "Current version",
173
+ versionLatest: "Latest version",
174
+ versionCheck: "Check for updates",
175
+ versionChecking: "Checking for updates\u2026",
176
+ versionFailed: "Could not check the latest version.",
177
+ versionUpToDate: "You are on the latest version.",
178
+ versionAvailable: "A new version v{version} is available.",
179
+ versionLinked: "Installed from a local checkout; pull and rebuild the plugin repository to update.",
180
+ versionManual: "Run dsh plugin --profile web add dsh-grok-subscription@latest in a terminal to update.",
181
+ versionUpdate: "Update plugin",
182
+ versionUpdating: "Updating\u2026",
183
+ versionUpdateFailed: "The update failed. Try again or update manually in a terminal.",
184
+ versionUpdated: "Updated to v{version}.",
185
+ versionUpdatedHint: "The new version takes full effect after a DSH restart; refreshing the page alone does not reload the plugin inside the host process.",
186
+ versionRestart: "Restart DSH",
187
+ versionRestartHint: "The plugin cannot safely restart its host process: press Ctrl+C in the terminal running DSH, then run dsh web again.",
188
+ versionRefresh: "Refresh page",
189
+ versionLater: "Later"
151
190
  };
152
191
 
153
192
  // src/rpc-contract.js
@@ -160,7 +199,9 @@ var RPC_ENDPOINTS = Object.freeze([
160
199
  "logout",
161
200
  "catalog/refresh",
162
201
  "usage",
163
- "usage/refresh"
202
+ "usage/refresh",
203
+ "plugin/version",
204
+ "plugin/update"
164
205
  ]);
165
206
  function createRpcClient(transport) {
166
207
  return Object.freeze({
@@ -620,7 +661,7 @@ function formatPercent(value) {
620
661
  const rounded = Math.round(value * 10) / 10;
621
662
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
622
663
  }
623
- async function callRpc(rpc, endpoint, payload = {}) {
664
+ async function callRpc(rpc, endpoint, payload = {}, timeoutMs = RPC_CALL_TIMEOUT_MS) {
624
665
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
625
666
  let timer;
626
667
  try {
@@ -632,7 +673,7 @@ async function callRpc(rpc, endpoint, payload = {}) {
632
673
  } catch {
633
674
  }
634
675
  reject(new Error(`Request timed out after ${RPC_CALL_TIMEOUT_MS}ms`));
635
- }, RPC_CALL_TIMEOUT_MS);
676
+ }, timeoutMs);
636
677
  });
637
678
  return unwrap(await Promise.race([call, timeout]));
638
679
  } finally {
@@ -687,6 +728,90 @@ function DiagnosticsRows({ diagnostics, t }) {
687
728
  ] })
688
729
  ] });
689
730
  }
731
+ function fillTemplate2(template, values) {
732
+ return Object.entries(values).reduce(
733
+ (text, [key, value]) => text.replaceAll(`{${key}}`, value),
734
+ template
735
+ );
736
+ }
737
+ function VersionCard({ call, t }) {
738
+ const [info, setInfo] = (0, import_react2.useState)();
739
+ const [loading, setLoading] = (0, import_react2.useState)(true);
740
+ const [error, setError] = (0, import_react2.useState)(false);
741
+ const [updating, setUpdating] = (0, import_react2.useState)(false);
742
+ const [updateFailed, setUpdateFailed] = (0, import_react2.useState)(false);
743
+ const [updated, setUpdated] = (0, import_react2.useState)();
744
+ const [restartHint, setRestartHint] = (0, import_react2.useState)(false);
745
+ const generation = (0, import_react2.useRef)(0);
746
+ const load = (force) => {
747
+ const current = ++generation.current;
748
+ setLoading(true);
749
+ setError(false);
750
+ void call("plugin/version", { force }).then((value) => {
751
+ if (generation.current === current) setInfo(value);
752
+ }).catch(() => {
753
+ if (generation.current === current) setError(true);
754
+ }).finally(() => {
755
+ if (generation.current === current) setLoading(false);
756
+ });
757
+ };
758
+ (0, import_react2.useEffect)(() => {
759
+ load(false);
760
+ return () => {
761
+ generation.current += 1;
762
+ };
763
+ }, []);
764
+ const update = () => {
765
+ setUpdating(true);
766
+ setUpdateFailed(false);
767
+ void call("plugin/update").then((value) => {
768
+ setUpdated(value);
769
+ setRestartHint(false);
770
+ }).catch(() => setUpdateFailed(true)).finally(() => setUpdating(false));
771
+ };
772
+ const installKind = info?.install?.kind;
773
+ const showStatus = info !== void 0 && updated === void 0;
774
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsCard", children: [
775
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsCardHead", children: [
776
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { children: t("versionTitle") }),
777
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gsSpacer" }),
778
+ updated === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "gsBtn", type: "button", disabled: loading || updating, onClick: () => load(true), children: loading ? t("versionChecking") : t("versionCheck") }) : null
779
+ ] }),
780
+ loading && info === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--busy", role: "status", children: t("versionChecking") }) : null,
781
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--error", role: "alert", children: t("versionFailed") }) : null,
782
+ info !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRows", children: [
783
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRow", children: [
784
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("versionCurrent") }),
785
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gsRowValue", children: [
786
+ "v",
787
+ info.current
788
+ ] })
789
+ ] }),
790
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsRow", children: [
791
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("versionLatest") }),
792
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "gsRowValue", children: [
793
+ "v",
794
+ info.latest
795
+ ] })
796
+ ] })
797
+ ] }) : null,
798
+ showStatus ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--ok", role: "status", children: info.updateAvailable ? fillTemplate2(t("versionAvailable"), { version: info.latest }) : t("versionUpToDate") }) : null,
799
+ showStatus && info.updateAvailable && installKind === "npm" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "gsActions", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "gsBtn gsBtn--primary", type: "button", disabled: updating, onClick: update, children: updating ? t("versionUpdating") : t("versionUpdate") }) }) : null,
800
+ showStatus && installKind === "link" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsHint", children: t("versionLinked") }) : null,
801
+ showStatus && info.updateAvailable && installKind === "unknown" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsHint", children: t("versionManual") }) : null,
802
+ updateFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--error", role: "alert", children: t("versionUpdateFailed") }) : null,
803
+ updated !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "status", children: [
804
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsStatus gsStatus--ok", children: fillTemplate2(t("versionUpdated"), { version: updated.version }) }),
805
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsHint", children: t("versionUpdatedHint") }),
806
+ restartHint ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "gsHint", children: t("versionRestartHint") }) : null,
807
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gsActions", children: [
808
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "gsBtn", type: "button", onClick: () => setRestartHint(true), children: t("versionRestart") }),
809
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "gsBtn gsBtn--primary", type: "button", onClick: () => window.location.reload(), children: t("versionRefresh") }),
810
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { className: "gsBtn", type: "button", onClick: () => setUpdated(void 0), children: t("versionLater") })
811
+ ] })
812
+ ] }) : null
813
+ ] });
814
+ }
690
815
  function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
691
816
  const ok = usage?.status === "ok";
692
817
  const usedNumber = ok && Number.isFinite(usage.usedPercent) ? usage.usedPercent : void 0;
@@ -877,6 +1002,18 @@ function GrokSubscriptionSection({ rpc, t }) {
877
1002
  ] })
878
1003
  ] })
879
1004
  ] }),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1006
+ VersionCard,
1007
+ {
1008
+ t,
1009
+ call: (endpoint, payload) => callRpc(
1010
+ rpc,
1011
+ endpoint,
1012
+ payload,
1013
+ endpoint === "plugin/update" ? 19e4 : RPC_CALL_TIMEOUT_MS
1014
+ )
1015
+ }
1016
+ ),
880
1017
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
881
1018
  UsagePanel,
882
1019
  {
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/constants.js
2
+ var PLUGIN_ID = "dsh-grok-subscription";
2
3
  var CORDIS_ID = "grok-subscription";
3
4
  var PROVIDER_ID = "grok-build";
4
5
  var DISPLAY_NAME = "Grok Build (subscription)";
@@ -936,6 +937,37 @@ function imageContentParts(blocks, imageParts) {
936
937
  }
937
938
  return parts;
938
939
  }
940
+ var PROMPT_CACHE_KEY_MAX_LENGTH = 64;
941
+ var REPLAY_KIND = "grok-build-responses";
942
+ var REPLAY_VERSION = 1;
943
+ function promptCacheKey(options) {
944
+ const sessionId = typeof options?.sessionId === "string" ? options.sessionId.trim() : "";
945
+ if (!sessionId) return void 0;
946
+ const suffix = options.purpose === "compaction" || options.purpose === "session-title" ? `:${options.purpose}` : "";
947
+ const key = `grok${suffix}:${sessionId}`;
948
+ return key.length <= PROMPT_CACHE_KEY_MAX_LENGTH ? key : key.slice(0, PROMPT_CACHE_KEY_MAX_LENGTH);
949
+ }
950
+ function cacheStableTools(tools) {
951
+ if (!Array.isArray(tools) || tools.length === 0) return [];
952
+ return tools.map((tool, index) => ({ tool, index })).sort((left, right) => {
953
+ const byName = String(left.tool?.name ?? "").localeCompare(String(right.tool?.name ?? ""));
954
+ return byName === 0 ? left.index - right.index : byName;
955
+ }).map(({ tool }) => ({
956
+ type: "function",
957
+ name: tool.name,
958
+ description: tool.description,
959
+ parameters: tool.parameters
960
+ }));
961
+ }
962
+ function replayEnvelope(message) {
963
+ const state = message?.source?.replayState;
964
+ return state?.response?.kind === REPLAY_KIND && state.response.version === REPLAY_VERSION && Array.isArray(state.blocks) ? state : void 0;
965
+ }
966
+ function replayReasoningItem(message, contentIndex) {
967
+ const entry = replayEnvelope(message)?.blocks[contentIndex];
968
+ const item = entry?.type === "reasoning" ? entry.item : void 0;
969
+ return item?.type === "reasoning" && typeof item.encrypted_content === "string" && item.encrypted_content ? item : void 0;
970
+ }
939
971
  function responsesInput(options, imageParts) {
940
972
  const input = [];
941
973
  const system = typeof options.system === "string" && options.system ? options.system : void 0;
@@ -955,7 +987,15 @@ function responsesInput(options, imageParts) {
955
987
  continue;
956
988
  }
957
989
  if (role === "assistant" && toolCalls.length) {
958
- for (const block of toolCalls) {
990
+ const content = message.content ?? [];
991
+ for (let index = 0; index < content.length; index++) {
992
+ const block = content[index];
993
+ if (block?.type === "reasoning") {
994
+ const item = replayReasoningItem(message, index);
995
+ if (item) input.push(item);
996
+ continue;
997
+ }
998
+ if (block?.type !== "tool-call") continue;
959
999
  input.push({
960
1000
  type: "function_call",
961
1001
  call_id: block.id,
@@ -973,6 +1013,23 @@ function responsesInput(options, imageParts) {
973
1013
  if (parts.length > 0) input.push({ role, content: parts });
974
1014
  continue;
975
1015
  }
1016
+ if (role === "assistant") {
1017
+ const content = message.content ?? [];
1018
+ const output = [];
1019
+ let text2 = "";
1020
+ for (let index = 0; index < content.length; index++) {
1021
+ const block = content[index];
1022
+ if (block?.type === "reasoning") {
1023
+ const item = replayReasoningItem(message, index);
1024
+ if (item) output.push(item);
1025
+ } else if (block?.type === "text" && block.text) {
1026
+ text2 += block.text;
1027
+ }
1028
+ }
1029
+ if (text2) output.push({ role: "assistant", content: text2 });
1030
+ input.push(...output);
1031
+ continue;
1032
+ }
976
1033
  const text = textOf(message.content);
977
1034
  if (!text) continue;
978
1035
  input.push({ role, content: text });
@@ -1022,22 +1079,18 @@ async function* streamResponsesUnsafe(options, token) {
1022
1079
  const body = {
1023
1080
  model: options.model,
1024
1081
  input: responsesInput(options, await collectImageParts(options, options.resolveAttachments)),
1025
- stream: true
1082
+ stream: true,
1083
+ // grok-4.7 returns ciphertext by default, but naming it keeps replay explicit
1084
+ // for every reasoning model on this proxy.
1085
+ include: ["reasoning.encrypted_content"]
1026
1086
  };
1087
+ const cacheKey = promptCacheKey(options);
1088
+ if (cacheKey) body.prompt_cache_key = cacheKey;
1027
1089
  if (typeof options.maxTokens === "number") body.max_output_tokens = options.maxTokens;
1028
1090
  if (typeof options.temperature === "number") body.temperature = options.temperature;
1029
- if (Array.isArray(options.tools) && options.tools.length) {
1030
- body.tools = options.tools.map((tool) => ({
1031
- type: "function",
1032
- name: tool.name,
1033
- description: tool.description,
1034
- parameters: tool.parameters
1035
- }));
1036
- }
1037
- if (options.reasoningEffort) {
1038
- body.reasoning = { effort: options.reasoningEffort };
1039
- body.include = ["reasoning.encrypted_content"];
1040
- }
1091
+ const tools = cacheStableTools(options.tools);
1092
+ if (tools.length) body.tools = tools;
1093
+ if (options.reasoningEffort) body.reasoning = { effort: options.reasoningEffort };
1041
1094
  const response = await fetch(RESPONSES_URL, {
1042
1095
  method: "POST",
1043
1096
  headers,
@@ -1058,6 +1111,7 @@ async function* streamResponsesUnsafe(options, token) {
1058
1111
  let textContent = "";
1059
1112
  let reasoningContent = "";
1060
1113
  let nextIndex = 0;
1114
+ const emittedBlocks = [];
1061
1115
  const toolBlocks = /* @__PURE__ */ new Map();
1062
1116
  const toolNames = /* @__PURE__ */ new Map();
1063
1117
  let usage;
@@ -1099,6 +1153,7 @@ async function* streamResponsesUnsafe(options, token) {
1099
1153
  if (!delta) return;
1100
1154
  if (textIndex === void 0) {
1101
1155
  textIndex = nextIndex++;
1156
+ emittedBlocks[textIndex] = { type: "text" };
1102
1157
  yield { type: "block-start", index: textIndex, blockType: "text" };
1103
1158
  }
1104
1159
  textContent += delta;
@@ -1110,6 +1165,7 @@ async function* streamResponsesUnsafe(options, token) {
1110
1165
  if (!delta) return;
1111
1166
  if (reasoningIndex === void 0) {
1112
1167
  reasoningIndex = nextIndex++;
1168
+ emittedBlocks[reasoningIndex] = { type: "reasoning" };
1113
1169
  yield { type: "block-start", index: reasoningIndex, blockType: "reasoning" };
1114
1170
  }
1115
1171
  reasoningContent += delta;
@@ -1118,6 +1174,14 @@ async function* streamResponsesUnsafe(options, token) {
1118
1174
  }
1119
1175
  if (type === "response.output_item.added" || type === "response.output_item.done") {
1120
1176
  const item = payload.item;
1177
+ if (item?.type === "reasoning" && typeof item.encrypted_content === "string" && item.encrypted_content) {
1178
+ if (reasoningIndex === void 0) {
1179
+ reasoningIndex = nextIndex++;
1180
+ emittedBlocks[reasoningIndex] = { type: "reasoning" };
1181
+ yield { type: "block-start", index: reasoningIndex, blockType: "reasoning" };
1182
+ }
1183
+ emittedBlocks[reasoningIndex] = { type: "reasoning", item };
1184
+ }
1121
1185
  if (item?.type === "function_call") {
1122
1186
  learnToolName(item.id, item.name);
1123
1187
  learnToolName(item.call_id, item.name);
@@ -1143,6 +1207,7 @@ async function* streamResponsesUnsafe(options, token) {
1143
1207
  name: toolNames.get(id) ?? "tool",
1144
1208
  arguments: ""
1145
1209
  };
1210
+ emittedBlocks[tool.index] = { type: "tool-call" };
1146
1211
  toolBlocks.set(id, tool);
1147
1212
  yield { type: "block-start", index: tool.index, blockType: "tool-call" };
1148
1213
  }
@@ -1160,17 +1225,38 @@ async function* streamResponsesUnsafe(options, token) {
1160
1225
  if (id && !toolBlocks.has(id)) {
1161
1226
  const tool = { index: nextIndex++, id, name: toolNames.get(id) ?? "tool", arguments: "" };
1162
1227
  toolBlocks.set(id, tool);
1228
+ emittedBlocks[tool.index] = { type: "tool-call" };
1163
1229
  yield { type: "block-start", index: tool.index, blockType: "tool-call" };
1164
1230
  }
1165
1231
  adoptToolArguments(id, item.arguments);
1166
1232
  }
1167
1233
  const responseUsage = payload.response?.usage ?? payload.usage;
1168
1234
  if (responseUsage) {
1235
+ const details = responseUsage.input_tokens_details ?? responseUsage.prompt_tokens_details ?? {};
1236
+ const cachedTokens = finiteOr(details.cached_tokens, 0);
1237
+ const cacheWriteTokens = finiteOr(details.cache_write_tokens, 0);
1238
+ const promptTotal = finiteOr(
1239
+ responseUsage.input_tokens ?? responseUsage.prompt_tokens,
1240
+ 0
1241
+ );
1242
+ const outputTokens = finiteOr(
1243
+ responseUsage.output_tokens ?? responseUsage.completion_tokens,
1244
+ 0
1245
+ );
1169
1246
  usage = {
1170
- inputTokens: finiteOr(responseUsage.input_tokens ?? responseUsage.prompt_tokens, 0),
1171
- outputTokens: finiteOr(responseUsage.output_tokens ?? responseUsage.completion_tokens, 0)
1247
+ // The provider folds cache hits into the prompt count, while DSH's
1248
+ // fields are disjoint: billed input = inputTokens + cacheRead +
1249
+ // cacheWrite. Subtracting is what makes the host's "cache hit" figure
1250
+ // real instead of a permanent 0%. Both cache fields stay numeric so
1251
+ // consumers can always add them up.
1252
+ inputTokens: Math.max(0, promptTotal - cachedTokens - cacheWriteTokens),
1253
+ outputTokens,
1254
+ cacheReadTokens: cachedTokens,
1255
+ cacheWriteTokens
1172
1256
  };
1173
- usage.totalTokens = finiteOr(responseUsage.total_tokens, usage.inputTokens + usage.outputTokens);
1257
+ const reasoningTokens = finiteOr(responseUsage.output_tokens_details?.reasoning_tokens, void 0);
1258
+ if (reasoningTokens !== void 0) usage.reasoningTokens = reasoningTokens;
1259
+ usage.totalTokens = finiteOr(responseUsage.total_tokens, promptTotal + outputTokens);
1174
1260
  }
1175
1261
  finish = mapFinish(payload.response?.status === "incomplete" ? "length" : "stop");
1176
1262
  if (toolBlocks.size) finish = { kind: "tool-calls" };
@@ -1210,7 +1296,14 @@ async function* streamResponsesUnsafe(options, token) {
1210
1296
  };
1211
1297
  }
1212
1298
  if (usage) yield { type: "usage", usage };
1213
- yield { type: "finish", reason: finish };
1299
+ const finishChunk = { type: "finish", reason: finish };
1300
+ if (finish.kind !== "error" && finish.kind !== "aborted" && emittedBlocks.length === nextIndex) {
1301
+ finishChunk.replayState = {
1302
+ response: { kind: REPLAY_KIND, version: REPLAY_VERSION },
1303
+ blocks: emittedBlocks
1304
+ };
1305
+ }
1306
+ yield finishChunk;
1214
1307
  }
1215
1308
  async function* streamResponses(options, token) {
1216
1309
  for await (const chunk of streamResponsesUnsafe(options, token)) {
@@ -1882,6 +1975,251 @@ function createSessionService({
1882
1975
  };
1883
1976
  }
1884
1977
 
1978
+ // src/plugin-version.js
1979
+ import { spawn as spawn2 } from "node:child_process";
1980
+ import { readdir, readFile, realpath } from "node:fs/promises";
1981
+ import { homedir as homedir3 } from "node:os";
1982
+ import { join as join4 } from "node:path";
1983
+ import { fileURLToPath } from "node:url";
1984
+ var PACKAGE_NAME = PLUGIN_ID;
1985
+ var NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1986
+ var DEFAULT_VERSION_TTL_MS = 5 * 6e4;
1987
+ var DEFAULT_VERSION_TIMEOUT_MS = 1e4;
1988
+ var DEFAULT_UPDATE_TIMEOUT_MS = 18e4;
1989
+ var clone = (value) => structuredClone(value);
1990
+ function parseSemver(value) {
1991
+ if (typeof value !== "string") return void 0;
1992
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
1993
+ if (match === null) return void 0;
1994
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), pre: match[4] };
1995
+ }
1996
+ function compareSemver(a, b) {
1997
+ const left = parseSemver(a);
1998
+ const right = parseSemver(b);
1999
+ if (left === void 0 || right === void 0) return void 0;
2000
+ for (const key of ["major", "minor", "patch"]) {
2001
+ if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
2002
+ }
2003
+ if (left.pre === right.pre) return 0;
2004
+ if (left.pre === void 0) return 1;
2005
+ if (right.pre === void 0) return -1;
2006
+ return left.pre < right.pre ? -1 : 1;
2007
+ }
2008
+ function classifySpec(spec) {
2009
+ if (typeof spec !== "string" || spec === "") return "unknown";
2010
+ if (/^(?:link|file):/u.test(spec)) return "link";
2011
+ if (/^(?:\d|[~^*])/u.test(spec)) return "npm";
2012
+ return "unknown";
2013
+ }
2014
+ var readManifest = async (path) => {
2015
+ try {
2016
+ const parsed = JSON.parse(await readFile(path, "utf8"));
2017
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
2018
+ } catch {
2019
+ return void 0;
2020
+ }
2021
+ };
2022
+ async function findInstall({ dshHome, ownPackageJsonUrl }) {
2023
+ let ownReal;
2024
+ try {
2025
+ ownReal = await realpath(fileURLToPath(ownPackageJsonUrl));
2026
+ } catch {
2027
+ ownReal = void 0;
2028
+ }
2029
+ let entries = [];
2030
+ try {
2031
+ entries = await readdir(join4(dshHome, "profiles"), { withFileTypes: true });
2032
+ } catch {
2033
+ entries = [];
2034
+ }
2035
+ const declared = [];
2036
+ for (const entry of entries) {
2037
+ if (!entry.isDirectory()) continue;
2038
+ const profileDir = join4(dshHome, "profiles", entry.name);
2039
+ const spec = (await readManifest(join4(profileDir, "package.json")))?.dependencies?.[PACKAGE_NAME];
2040
+ if (typeof spec !== "string") continue;
2041
+ declared.push({ profile: entry.name, spec });
2042
+ if (ownReal === void 0) continue;
2043
+ try {
2044
+ const installed = await realpath(join4(profileDir, "node_modules", PACKAGE_NAME, "package.json"));
2045
+ if (installed === ownReal) return { kind: classifySpec(spec), profile: entry.name };
2046
+ } catch {
2047
+ }
2048
+ }
2049
+ if (declared.length === 1) return { kind: classifySpec(declared[0].spec), profile: declared[0].profile };
2050
+ return { kind: "unknown" };
2051
+ }
2052
+ function spawnRunCommand(argv, { signal, timeoutMs }) {
2053
+ return new Promise((resolve, reject) => {
2054
+ if (signal?.aborted) {
2055
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Grok plugin update aborted"));
2056
+ return;
2057
+ }
2058
+ const controller = new AbortController();
2059
+ const onAbort = () => controller.abort(signal.reason);
2060
+ signal?.addEventListener("abort", onAbort, { once: true });
2061
+ const timer = setTimeout(() => controller.abort(new Error("Grok plugin update timed out")), timeoutMs);
2062
+ let settled = false;
2063
+ const settle = (callback, value) => {
2064
+ if (settled) return;
2065
+ settled = true;
2066
+ clearTimeout(timer);
2067
+ signal?.removeEventListener("abort", onAbort);
2068
+ callback(value);
2069
+ };
2070
+ let child;
2071
+ try {
2072
+ child = spawn2(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "pipe"], signal: controller.signal });
2073
+ } catch (error) {
2074
+ settle(reject, error);
2075
+ return;
2076
+ }
2077
+ let stdout = "";
2078
+ let stderr = "";
2079
+ child.stdout?.on("data", (chunk) => {
2080
+ stdout = (stdout + chunk).slice(-8192);
2081
+ });
2082
+ child.stderr?.on("data", (chunk) => {
2083
+ stderr = (stderr + chunk).slice(-8192);
2084
+ });
2085
+ child.on("error", (error) => settle(reject, error));
2086
+ child.on("close", (code) => {
2087
+ if (controller.signal.aborted) {
2088
+ const reason = controller.signal.reason;
2089
+ settle(reject, reason instanceof Error ? reason : new Error("Grok plugin update aborted"));
2090
+ return;
2091
+ }
2092
+ settle(resolve, { code: code ?? 1, stdout, stderr });
2093
+ });
2094
+ });
2095
+ }
2096
+ function createGrokPluginManager({
2097
+ fetchImpl = globalThis.fetch,
2098
+ runCommand = spawnRunCommand,
2099
+ execPath = process.execPath,
2100
+ binPath = process.argv[1],
2101
+ env = process.env,
2102
+ ownPackageJsonUrl = new URL("../package.json", import.meta.url),
2103
+ registryUrl = NPM_REGISTRY_LATEST_URL,
2104
+ now = Date.now,
2105
+ ttlMs = DEFAULT_VERSION_TTL_MS,
2106
+ timeoutMs = DEFAULT_VERSION_TIMEOUT_MS,
2107
+ updateTimeoutMs = DEFAULT_UPDATE_TIMEOUT_MS
2108
+ } = {}) {
2109
+ if (typeof fetchImpl !== "function") throw new Error("Grok plugin manager requires fetch");
2110
+ if (typeof runCommand !== "function") throw new Error("Grok plugin manager requires runCommand");
2111
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME !== "" ? env.DSH_HOME : join4(homedir3(), ".dsh");
2112
+ let cached;
2113
+ let inFlight;
2114
+ let generation = 0;
2115
+ let updating = false;
2116
+ const fetchLatest = async (signal) => {
2117
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
2118
+ const requestSignal = signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
2119
+ let response;
2120
+ try {
2121
+ response = await fetchImpl(registryUrl, {
2122
+ headers: { Accept: "application/json" },
2123
+ redirect: "error",
2124
+ signal: requestSignal
2125
+ });
2126
+ } catch (error) {
2127
+ if (signal?.aborted) throw error;
2128
+ throw new Error("Grok plugin version check failed");
2129
+ }
2130
+ if (!response.ok) throw new Error("Grok plugin version check failed");
2131
+ let version;
2132
+ try {
2133
+ version = (await response.json())?.version;
2134
+ } catch {
2135
+ version = void 0;
2136
+ }
2137
+ if (parseSemver(version) === void 0) throw new Error("Grok plugin version check failed");
2138
+ return version;
2139
+ };
2140
+ const readOwnVersion = async () => {
2141
+ const version = (await readManifest(fileURLToPath(ownPackageJsonUrl)))?.version;
2142
+ if (parseSemver(version) === void 0) throw new Error("Grok plugin version is unavailable");
2143
+ return version;
2144
+ };
2145
+ const load = async (signal) => {
2146
+ signal?.throwIfAborted();
2147
+ const [current, install, latest] = await Promise.all([
2148
+ readOwnVersion(),
2149
+ findInstall({ dshHome, ownPackageJsonUrl }),
2150
+ fetchLatest(signal)
2151
+ ]);
2152
+ return {
2153
+ current,
2154
+ latest,
2155
+ updateAvailable: compareSemver(latest, current) === 1,
2156
+ install,
2157
+ fetchedAt: now()
2158
+ };
2159
+ };
2160
+ return Object.freeze({
2161
+ async read({ force = false, signal } = {}) {
2162
+ signal?.throwIfAborted();
2163
+ if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return clone(cached);
2164
+ const createPending = (requestSignal) => {
2165
+ const observedGeneration = generation;
2166
+ return load(requestSignal).then((value) => {
2167
+ if (generation === observedGeneration) cached = value;
2168
+ return value;
2169
+ });
2170
+ };
2171
+ if (force) return clone(await createPending(signal));
2172
+ const pending = inFlight ?? createPending();
2173
+ if (inFlight === void 0) {
2174
+ inFlight = pending;
2175
+ const clear = () => {
2176
+ if (inFlight === pending) inFlight = void 0;
2177
+ };
2178
+ void pending.then(clear, clear);
2179
+ }
2180
+ return clone(await (signal === void 0 ? pending : Promise.race([
2181
+ pending,
2182
+ new Promise((_, reject) => {
2183
+ const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Grok plugin version check aborted"));
2184
+ if (signal.aborted) onAbort();
2185
+ else signal.addEventListener("abort", onAbort, { once: true });
2186
+ })
2187
+ ])));
2188
+ },
2189
+ async update({ signal } = {}) {
2190
+ signal?.throwIfAborted();
2191
+ if (updating) throw new Error("Grok plugin update is already running");
2192
+ updating = true;
2193
+ try {
2194
+ const install = await findInstall({ dshHome, ownPackageJsonUrl });
2195
+ if (install.kind === "link") throw new Error("Grok plugin is installed from a local checkout");
2196
+ if (install.kind !== "npm" || install.profile === void 0) {
2197
+ throw new Error("Grok plugin update could not find the owning profile");
2198
+ }
2199
+ const latest = await fetchLatest(signal);
2200
+ const argv = binPath === void 0 ? ["dsh", "plugin", "--profile", install.profile, "add", `${PACKAGE_NAME}@${latest}`] : [execPath, binPath, "plugin", "--profile", install.profile, "add", `${PACKAGE_NAME}@${latest}`];
2201
+ let result;
2202
+ try {
2203
+ result = await runCommand(argv, { signal, timeoutMs: updateTimeoutMs });
2204
+ } catch (error) {
2205
+ if (signal?.aborted) throw error;
2206
+ throw new Error("Grok plugin update failed");
2207
+ }
2208
+ if (result.code !== 0) throw new Error("Grok plugin update failed");
2209
+ generation += 1;
2210
+ cached = void 0;
2211
+ return { version: latest, profile: install.profile };
2212
+ } finally {
2213
+ updating = false;
2214
+ }
2215
+ },
2216
+ invalidate() {
2217
+ generation += 1;
2218
+ cached = void 0;
2219
+ }
2220
+ });
2221
+ }
2222
+
1885
2223
  // src/rpc-contract.js
1886
2224
  var RPC_ENDPOINTS = Object.freeze([
1887
2225
  "status",
@@ -1891,7 +2229,9 @@ var RPC_ENDPOINTS = Object.freeze([
1891
2229
  "logout",
1892
2230
  "catalog/refresh",
1893
2231
  "usage",
1894
- "usage/refresh"
2232
+ "usage/refresh",
2233
+ "plugin/version",
2234
+ "plugin/update"
1895
2235
  ]);
1896
2236
  function publicResult(value) {
1897
2237
  return { ok: true, value };
@@ -1965,7 +2305,18 @@ function sanitizeUsage(usage) {
1965
2305
  }
1966
2306
  return out;
1967
2307
  }
1968
- async function dispatch(session, endpoint, diagnostics) {
2308
+ function publicPluginVersion(value) {
2309
+ const installKind = value?.install?.kind;
2310
+ return {
2311
+ current: value.current,
2312
+ latest: value.latest,
2313
+ updateAvailable: value.updateAvailable === true,
2314
+ install: {
2315
+ kind: installKind === "npm" || installKind === "link" ? installKind : "unknown"
2316
+ }
2317
+ };
2318
+ }
2319
+ async function dispatch(session, endpoint, diagnostics, payload, signal, pluginManager) {
1969
2320
  if (endpoint === "status") {
1970
2321
  const status = await session.status();
1971
2322
  return publicResult(stripSecrets({
@@ -1991,14 +2342,27 @@ async function dispatch(session, endpoint, diagnostics) {
1991
2342
  }
1992
2343
  if (endpoint === "login/cli") return publicResult(stripSecrets(await session.login({ device: false })));
1993
2344
  if (endpoint === "login/device") return publicResult(stripSecrets(await session.login({ device: true })));
2345
+ if (endpoint === "plugin/version" || endpoint === "plugin/update") {
2346
+ if (!pluginManager) return publicError(new Error("Grok plugin update is unavailable"));
2347
+ if (endpoint === "plugin/version") {
2348
+ return publicResult(publicPluginVersion(await pluginManager.read({
2349
+ force: payload?.force === true,
2350
+ signal
2351
+ })));
2352
+ }
2353
+ const updated = await pluginManager.update({ signal });
2354
+ return publicResult({ version: updated.version });
2355
+ }
1994
2356
  return publicError(new Error(`Unknown Grok subscription RPC: ${endpoint}`));
1995
2357
  }
1996
2358
  function createRpcHandler(session, options = {}) {
1997
2359
  const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : RPC_HANDLER_TIMEOUT_MS;
1998
- return async function handle(endpoint, _payload, _signal) {
2360
+ return async function handle(endpoint, payload, signal) {
1999
2361
  try {
2362
+ const operation = dispatch(session, endpoint, options.diagnostics, payload, signal, options.pluginManager);
2363
+ if (endpoint === "plugin/update") return await operation;
2000
2364
  return await withTimeout(
2001
- dispatch(session, endpoint, options.diagnostics),
2365
+ operation,
2002
2366
  timeoutMs,
2003
2367
  `Grok subscription RPC "${endpoint}" timed out after ${timeoutMs}ms`,
2004
2368
  { unref: false }
@@ -2144,7 +2508,9 @@ function apply(ctx, options = {}) {
2144
2508
  onCatalogChange: notifyCatalogChange
2145
2509
  });
2146
2510
  const adapterState = { kind: "custom-mvp", upgraded: false };
2511
+ const pluginManager = options.pluginManager ?? createGrokPluginManager();
2147
2512
  const handler = createRpcHandler(session, {
2513
+ pluginManager,
2148
2514
  diagnostics: () => ({
2149
2515
  adapter: adapterState.upgraded ? "pi-ai" : "fallback",
2150
2516
  adapterKind: adapterState.kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-grok-subscription",
3
- "version": "1.0.8",
3
+ "version": "1.0.11",
4
4
  "description": "Use a SuperGrok or X Premium Grok Build subscription in DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",