dsh-codex-subscription 2.6.8 → 2.7.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/README.en.md CHANGED
@@ -106,7 +106,7 @@ This plugin supports the latest DeepSeek Harness release recorded in its package
106
106
  2. Paste this package name, with its version, into the **Package name or address** field:
107
107
 
108
108
  ```text
109
- dsh-codex-subscription@2.6.8
109
+ dsh-codex-subscription@2.7.0
110
110
  ```
111
111
 
112
112
  3. Click **Install** and wait for completion. Follow the page instructions; save your work before restarting if requested.
@@ -118,7 +118,7 @@ Why the version is included: DSH's plugin page only installs versions that are a
118
118
  <summary>Terminal installation (with an existing dsh command)</summary>
119
119
 
120
120
  ```sh
121
- dsh plugin --profile web add dsh-codex-subscription@2.6.8
121
+ dsh plugin --profile web add dsh-codex-subscription@2.7.0
122
122
  ```
123
123
 
124
124
  Follow the restart instructions, then sign in under **Settings → Codex**. Both the Plugins page and the terminal use DSH's installation management.
@@ -131,7 +131,7 @@ Follow the restart instructions, then sign in under **Settings → Codex**. Both
131
131
  After signing in and selecting a Codex model in Web, install the same plugin in the Headless profile:
132
132
 
133
133
  ```sh
134
- dsh plugin --profile headless add dsh-codex-subscription@2.6.8
134
+ dsh plugin --profile headless add dsh-codex-subscription@2.7.0
135
135
  dsh --profile headless "Reply with only the word: ok"
136
136
  ```
137
137
 
@@ -266,7 +266,7 @@ Find this plugin on the DSH **Plugins** page and use its update or uninstall act
266
266
  <summary>Terminal commands</summary>
267
267
 
268
268
  ```sh
269
- dsh plugin --profile web update dsh-codex-subscription@2.6.8
269
+ dsh plugin --profile web update dsh-codex-subscription@2.7.0
270
270
  ```
271
271
 
272
272
  Run only when you want to uninstall:
@@ -282,7 +282,7 @@ dsh plugin --profile web remove dsh-codex-subscription
282
282
  <details>
283
283
  <summary>DSH 0.2.0-rc.2 reports the plugin as incompatible?</summary>
284
284
 
285
- DSH 0.2.0-rc.2 did not exist when any release before 2.2.10 was published, and a published package's compatibility declaration cannot be changed. Install 2.2.10 or later (a bare package name can resolve to an older release during the first 24 hours, so enter `dsh-codex-subscription@2.2.10`) from the **Plugins** page, or run `dsh plugin --profile web update dsh-codex-subscription@2.6.8`, then restart DSH.
285
+ DSH 0.2.0-rc.2 did not exist when any release before 2.2.10 was published, and a published package's compatibility declaration cannot be changed. Install 2.2.10 or later (a bare package name can resolve to an older release during the first 24 hours, so enter `dsh-codex-subscription@2.2.10`) from the **Plugins** page, or run `dsh plugin --profile web update dsh-codex-subscription@2.7.0`, then restart DSH.
286
286
 
287
287
  </details>
288
288
 
package/README.md CHANGED
@@ -106,7 +106,7 @@
106
106
  2. 在 **包名或地址** 输入框中粘贴下面的包名(已带版本号):
107
107
 
108
108
  ```text
109
- dsh-codex-subscription@2.6.8
109
+ dsh-codex-subscription@2.7.0
110
110
  ```
111
111
 
112
112
  3. 点击 **安装**,等待安装完成;按页面提示操作,需要重启时先保存工作。
@@ -118,7 +118,7 @@
118
118
  <summary>终端安装(已能运行 dsh 命令)</summary>
119
119
 
120
120
  ```sh
121
- dsh plugin --profile web add dsh-codex-subscription@2.6.8
121
+ dsh plugin --profile web add dsh-codex-subscription@2.7.0
122
122
  ```
123
123
 
124
124
  安装完成后按提示重启 DSH,再到 **设置 → Codex 订阅** 登录。插件页面和终端均由 DSH 管理安装。
@@ -131,7 +131,7 @@ dsh plugin --profile web add dsh-codex-subscription@2.6.8
131
131
  先在 Web 中完成登录并选择一次 Codex 模型,再把同一个插件安装到 Headless profile:
132
132
 
133
133
  ```sh
134
- dsh plugin --profile headless add dsh-codex-subscription@2.6.8
134
+ dsh plugin --profile headless add dsh-codex-subscription@2.7.0
135
135
  dsh --profile headless "只回复:ok"
136
136
  ```
137
137
 
@@ -295,7 +295,7 @@ DSH 实机设置截图。
295
295
  <summary>终端方式</summary>
296
296
 
297
297
  ```sh
298
- dsh plugin --profile web update dsh-codex-subscription@2.6.8
298
+ dsh plugin --profile web update dsh-codex-subscription@2.7.0
299
299
  ```
300
300
 
301
301
  仅在需要卸载时运行:
@@ -311,7 +311,7 @@ dsh plugin --profile web remove dsh-codex-subscription
311
311
  <details>
312
312
  <summary>DSH 0.2.0-rc.2 提示插件不兼容?</summary>
313
313
 
314
- 2.2.10 之前的所有版本发布时 DSH 0.2.0-rc.2 还不存在,其兼容声明无法事后修改。请安装 2.2.10 或更高版本(只填包名时,发布满 24 小时前可能装到旧版,请直接填 `dsh-codex-subscription@2.2.10`):在 **插件** 页面更新,或运行 `dsh plugin --profile web update dsh-codex-subscription@2.6.8`,然后重启 DSH。
314
+ 2.2.10 之前的所有版本发布时 DSH 0.2.0-rc.2 还不存在,其兼容声明无法事后修改。请安装 2.2.10 或更高版本(只填包名时,发布满 24 小时前可能装到旧版,请直接填 `dsh-codex-subscription@2.2.10`):在 **插件** 页面更新,或运行 `dsh plugin --profile web update dsh-codex-subscription@2.7.0`,然后重启 DSH。
315
315
 
316
316
  </details>
317
317
 
package/lib/client.js CHANGED
@@ -1074,6 +1074,7 @@ window.__ModuleLoader__.load({
1074
1074
  "account/select",
1075
1075
  "account/remove",
1076
1076
  "usage",
1077
+ "usage/accounts",
1077
1078
  "diagnostics",
1078
1079
  "preferences/status",
1079
1080
  "preferences/models",
@@ -6037,7 +6038,13 @@ window.__ModuleLoader__.load({
6037
6038
  resetNoCredit: "没有可用的额度重置。",
6038
6039
  resetAlready: "这次重置请求已处理。",
6039
6040
  resetFailed: "无法使用额度重置。",
6041
+ accountQuotaLeft: "{label}剩余 {value}%",
6042
+ accountQuotaResets: "{value} 重置",
6043
+ accountQuotaLoading: "正在读取额度…",
6044
+ accountQuotaError: "暂时读不到这个账号的额度",
6045
+ accountQuotaNone: "没有额度窗口信息",
6040
6046
  resetRenewLogin: "登录状态已失效,请重新登录。",
6047
+ resetBlocked: "ChatGPT 的网络防护拦截了这次请求(HTTP 403),登录状态没有问题。请稍后再试,或检查网络、代理或 VPN。",
6041
6048
  resetExpired: "本次确认已失效,请重新开始。",
6042
6049
  resetInProgress: "额度重置正在处理中。",
6043
6050
  resetTooEarly: "请等待冷静期结束后再确认。",
@@ -6643,7 +6650,13 @@ window.__ModuleLoader__.load({
6643
6650
  resetNoCredit: "No quota reset is available.",
6644
6651
  resetAlready: "This reset request was already processed.",
6645
6652
  resetFailed: "Could not use the quota reset.",
6653
+ accountQuotaLeft: "{label} {value}% left",
6654
+ accountQuotaResets: "resets {value}",
6655
+ accountQuotaLoading: "Reading quota…",
6656
+ accountQuotaError: "The quota of this account could not be read right now",
6657
+ accountQuotaNone: "No quota window reported",
6646
6658
  resetRenewLogin: "Your sign-in expired. Sign in again.",
6659
+ resetBlocked: "ChatGPT network protection blocked this request (HTTP 403). Your sign-in is fine; try again later or check your network, proxy or VPN.",
6647
6660
  resetExpired: "This confirmation expired. Start again.",
6648
6661
  resetInProgress: "A quota reset is already in progress.",
6649
6662
  resetTooEarly: "Wait for the cooldown before confirming.",
@@ -6849,6 +6862,9 @@ window.__ModuleLoader__.load({
6849
6862
  .codexSubscriptionAccounts{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
6850
6863
  .codexSubscriptionAccount{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:42px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px}
6851
6864
  .codexSubscriptionAccount:last-child{border-bottom:0}
6865
+ .codexSubscriptionAccountMain{display:flex;flex-direction:column;gap:2px;padding:6px 0;min-width:0}
6866
+ .codexSubscriptionAccountQuota{display:flex;flex-wrap:wrap;gap:2px 12px;font-size:12px;color:var(--dsw-alias-text-l2)}
6867
+ .codexSubscriptionAccountQuota[data-state=error]{color:var(--dsw-alias-text-danger,#c0392b)}
6852
6868
  .codexSubscriptionAccount[data-active=true] .codexSubscriptionEmail,.codexSubscriptionAccount[data-active=true]>span{font-weight:600}
6853
6869
  .codexSubscriptionEmail{max-width:100%;overflow:hidden;padding:2px 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
6854
6870
  .codexSubscriptionEmail:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -9078,7 +9094,9 @@ window.__ModuleLoader__.load({
9078
9094
  return `${local[0]}•••${local.at(-1)}@${domain}`;
9079
9095
  };
9080
9096
  const hours = (seconds) => Math.round(seconds / 3600 * 10) / 10;
9081
- const percent = (value) => Number(value).toLocaleString(void 0, { maximumFractionDigits: 1 });
9097
+ /** The language DSH shows its interface in, so dates and numbers match the text around them rather than the OS. */
9098
+ const uiLocale = () => typeof document === "undefined" ? void 0 : document.documentElement.lang || void 0;
9099
+ const percent = (value) => Number(value).toLocaleString(uiLocale(), { maximumFractionDigits: 1 });
9082
9100
  const isApproximateWindow = (seconds, expected) => seconds >= expected * .95 && seconds <= expected * 1.05;
9083
9101
  const windowLabel = (seconds, t) => {
9084
9102
  if (isApproximateWindow(seconds, 18e3)) return t("windowFiveHours");
@@ -9155,7 +9173,7 @@ window.__ModuleLoader__.load({
9155
9173
  }
9156
9174
  //#endregion
9157
9175
  //#region src/version.js
9158
- const PACKAGE_VERSION = "2.6.8";
9176
+ const PACKAGE_VERSION = "2.7.0";
9159
9177
  //#endregion
9160
9178
  //#region src/client-recovery.js
9161
9179
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -9383,7 +9401,7 @@ window.__ModuleLoader__.load({
9383
9401
  }
9384
9402
  function shortReset(quota, t) {
9385
9403
  if (!Number.isSafeInteger(quota.resetsAt)) return t("resetUnknown");
9386
- const value = (/* @__PURE__ */ new Date(quota.resetsAt * 1e3)).toLocaleString(void 0, {
9404
+ const value = (/* @__PURE__ */ new Date(quota.resetsAt * 1e3)).toLocaleString(uiLocale(), {
9387
9405
  month: "numeric",
9388
9406
  day: "numeric",
9389
9407
  hour: "2-digit",
@@ -11152,6 +11170,73 @@ window.__ModuleLoader__.load({
11152
11170
  }
11153
11171
  }
11154
11172
  //#endregion
11173
+ //#region src/client-account-quota.jsx
11174
+ const when = (seconds) => (/* @__PURE__ */ new Date(seconds * 1e3)).toLocaleString(uiLocale(), {
11175
+ month: "numeric",
11176
+ day: "numeric",
11177
+ hour: "2-digit",
11178
+ minute: "2-digit"
11179
+ });
11180
+ /** Quota of every signed-in account (read-only; the active account does not change). */
11181
+ function useAccountQuotas(rpc, ids) {
11182
+ const [entries, setEntries] = (0, react.useState)({});
11183
+ const [busy, setBusy] = (0, react.useState)(false);
11184
+ const request = (0, react.useRef)(0);
11185
+ const key = ids.join(",");
11186
+ const load = (force) => {
11187
+ if (ids.length < 2) {
11188
+ setEntries({});
11189
+ return;
11190
+ }
11191
+ const id = ++request.current;
11192
+ setBusy(true);
11193
+ recoveryCall(rpc, "usage/accounts", { force }).then((value) => {
11194
+ if (request.current === id) setEntries(Object.fromEntries((value?.accounts ?? []).map((entry) => [entry.id, entry])));
11195
+ }).catch(() => {
11196
+ if (request.current === id) setEntries(Object.fromEntries(ids.map((account) => [account, {
11197
+ id: account,
11198
+ error: "Could not read ChatGPT usage"
11199
+ }])));
11200
+ }).finally(() => {
11201
+ if (request.current === id) setBusy(false);
11202
+ });
11203
+ };
11204
+ (0, react.useEffect)(() => {
11205
+ load(false);
11206
+ return () => {
11207
+ request.current += 1;
11208
+ };
11209
+ }, [key]);
11210
+ return {
11211
+ entries,
11212
+ busy,
11213
+ refresh: () => load(true)
11214
+ };
11215
+ }
11216
+ function AccountQuota({ entry, loading, t }) {
11217
+ if (entry === void 0) return loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11218
+ className: "codexSubscriptionAccountQuota",
11219
+ children: t("accountQuotaLoading")
11220
+ }) : null;
11221
+ if (entry.error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11222
+ className: "codexSubscriptionAccountQuota",
11223
+ "data-state": "error",
11224
+ children: entry.error === "ChatGPT sign-in needs to be renewed" ? t("resetRenewLogin") : t("accountQuotaError")
11225
+ });
11226
+ const windows = entry.usage?.rateLimits?.find((limit) => limit.id === "codex")?.windows ?? [];
11227
+ if (windows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11228
+ className: "codexSubscriptionAccountQuota",
11229
+ children: t("accountQuotaNone")
11230
+ });
11231
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", {
11232
+ className: "codexSubscriptionAccountQuota",
11233
+ children: windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [fill(t("accountQuotaLeft"), {
11234
+ label: windowLabel(window.windowSeconds, t),
11235
+ value: percent(window.remainingPercent)
11236
+ }), window.resetsAt === void 0 ? "" : ` · ${fill(t("accountQuotaResets"), { value: when(window.resetsAt) })}`] }, window.windowSeconds))
11237
+ });
11238
+ }
11239
+ //#endregion
11155
11240
  //#region src/client-account.jsx
11156
11241
  const LOGIN_FAILURES = [
11157
11242
  "account-claim",
@@ -11173,6 +11258,7 @@ window.__ModuleLoader__.load({
11173
11258
  children: emailVisible ? candidate.email : maskEmail(candidate.email)
11174
11259
  });
11175
11260
  }
11261
+ const signedInIds = (account) => account?.authenticated === true ? (account.accounts ?? []).map((candidate) => candidate.id).filter((id) => typeof id === "string") : [];
11176
11262
  function AccountCard({ rpc, t, account, setAccount, onSignedOut }) {
11177
11263
  const [flow, setFlow] = (0, react.useState)();
11178
11264
  const flowGeneration = (0, react.useRef)(0);
@@ -11185,6 +11271,7 @@ window.__ModuleLoader__.load({
11185
11271
  const [emailVisibilityKey, setEmailVisibilityKey] = (0, react.useState)(accountVisibilityKey);
11186
11272
  const [busy, setBusy] = (0, react.useState)(false);
11187
11273
  const [error, setError] = (0, react.useState)();
11274
+ const quotas = useAccountQuotas(rpc, signedInIds(account));
11188
11275
  const call = (endpoint, payload = {}) => recoveryCall(rpc, endpoint, payload);
11189
11276
  (0, react.useEffect)(() => {
11190
11277
  if (emailVisibilityKey === accountVisibilityKey) return;
@@ -11380,12 +11467,19 @@ window.__ModuleLoader__.load({
11380
11467
  children: accounts.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11381
11468
  className: "codexSubscriptionAccount",
11382
11469
  "data-active": candidate.active,
11383
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountEmail, {
11384
- candidate,
11385
- fallback: candidate.label,
11386
- t,
11387
- emailVisible: emailVisibleForAccount,
11388
- onClick: toggleEmail
11470
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11471
+ className: "codexSubscriptionAccountMain",
11472
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountEmail, {
11473
+ candidate,
11474
+ fallback: candidate.label,
11475
+ t,
11476
+ emailVisible: emailVisibleForAccount,
11477
+ onClick: toggleEmail
11478
+ }), accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountQuota, {
11479
+ entry: quotas.entries[candidate.id],
11480
+ loading: quotas.busy,
11481
+ t
11482
+ }) : null]
11389
11483
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11390
11484
  className: "codexSubscriptionActions",
11391
11485
  children: [
@@ -11789,7 +11883,7 @@ window.__ModuleLoader__.load({
11789
11883
  children: [
11790
11884
  t("diagnosticsSnapshotTime"),
11791
11885
  " ",
11792
- new Date(report.generatedAt).toLocaleString()
11886
+ new Date(report.generatedAt).toLocaleString(uiLocale())
11793
11887
  ]
11794
11888
  }) : null,
11795
11889
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -11807,7 +11901,7 @@ window.__ModuleLoader__.load({
11807
11901
  " · ",
11808
11902
  t(`diagnosticSource_${event.source}`),
11809
11903
  " · ",
11810
- new Date(event.observedAt).toLocaleTimeString()
11904
+ new Date(event.observedAt).toLocaleTimeString(uiLocale())
11811
11905
  ] }, `${event.source}:${event.action}`)) }) : null
11812
11906
  ] }, item.code)) }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsNoRecordedFailure") })
11813
11907
  }),
@@ -11825,7 +11919,7 @@ window.__ModuleLoader__.load({
11825
11919
  ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: (report.inspection?.capabilities ?? []).map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
11826
11920
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t(`diagnosticCapability_${item.id}`) }),
11827
11921
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [t(`diagnosticReason_${item.reason}`), item.blockedBy ? ` · ${t(`diagnosticCapability_${item.blockedBy}`)}` : ""] }),
11828
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString()}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
11922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [item.latest ? `${item.latest.action} · ${t(`diagnosticSource_${item.latest.source}`)} · ${t(`diagnosticOutcome_${item.latest.status}`)} · ${new Date(item.latest.observedAt).toLocaleTimeString(uiLocale())}` : t("diagnosticsUnverified"), item.unresolved?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("diagnosticsUnresolved") }) : null] })
11829
11923
  ] }, item.id)) })]
11830
11924
  }),
11831
11925
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -11972,8 +12066,8 @@ window.__ModuleLoader__.load({
11972
12066
  }
11973
12067
  //#endregion
11974
12068
  //#region src/client-usage.jsx
11975
- const count = (value) => Number(value).toLocaleString();
11976
- const creditAmount = (value) => Number.isFinite(Number(value)) && String(value).trim() !== "" ? Number(value).toLocaleString(void 0, { maximumFractionDigits: 2 }) : value;
12069
+ const count = (value) => Number(value).toLocaleString(uiLocale());
12070
+ const creditAmount = (value) => Number.isFinite(Number(value)) && String(value).trim() !== "" ? Number(value).toLocaleString(uiLocale(), { maximumFractionDigits: 2 }) : value;
11977
12071
  function CreditExpiry({ credits, t }) {
11978
12072
  const date = validDate(credits.expiresAt);
11979
12073
  if (date === void 0 || credits.unlimited) return null;
@@ -11984,7 +12078,7 @@ window.__ModuleLoader__.load({
11984
12078
  "data-soon": status.soon,
11985
12079
  role: "status",
11986
12080
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [fill(t(status.daysLeft === 0 ? "creditsExpiredToday" : "creditsExpires"), {
11987
- date: date.toLocaleDateString(),
12081
+ date: date.toLocaleDateString(uiLocale()),
11988
12082
  days: status.daysLeft
11989
12083
  }), estimate ? ` ${t("creditsExpiryEstimate")}` : ""] })
11990
12084
  });
@@ -11992,7 +12086,7 @@ window.__ModuleLoader__.load({
11992
12086
  function ResetTime({ resetsAt, t }) {
11993
12087
  const date = Number.isSafeInteger(resetsAt) ? validDate(resetsAt * 1e3) : void 0;
11994
12088
  if (date === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("resetUnknown") });
11995
- const value = date.toLocaleString(void 0, {
12089
+ const value = date.toLocaleString(uiLocale(), {
11996
12090
  month: "numeric",
11997
12091
  day: "numeric",
11998
12092
  hour: "2-digit",
@@ -12000,7 +12094,7 @@ window.__ModuleLoader__.load({
12000
12094
  });
12001
12095
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12002
12096
  dateTime: date.toISOString(),
12003
- title: date.toLocaleString(),
12097
+ title: date.toLocaleString(uiLocale()),
12004
12098
  children: fill(t("resets"), { value })
12005
12099
  });
12006
12100
  }
@@ -12010,8 +12104,8 @@ window.__ModuleLoader__.load({
12010
12104
  className: "codexSubscriptionResetExpiry",
12011
12105
  children: date === void 0 ? t("resetCreditExpiryUnknown") : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12012
12106
  dateTime: date.toISOString(),
12013
- title: date.toLocaleString(),
12014
- children: fill(t("resetCreditExpires"), { value: date.toLocaleDateString(void 0, {
12107
+ title: date.toLocaleString(uiLocale()),
12108
+ children: fill(t("resetCreditExpires"), { value: date.toLocaleDateString(uiLocale(), {
12015
12109
  month: "long",
12016
12110
  day: "numeric"
12017
12111
  }) })
@@ -12203,6 +12297,7 @@ window.__ModuleLoader__.load({
12203
12297
  return t((/* @__PURE__ */ new Map([
12204
12298
  ["ChatGPT subscription is not signed in", "resetRenewLogin"],
12205
12299
  ["ChatGPT sign-in needs to be renewed", "resetRenewLogin"],
12300
+ ["ChatGPT blocked this request before it reached your account (HTTP 403); your sign-in is fine, try again later or check your network, proxy or VPN", "resetBlocked"],
12206
12301
  ["No quota reset is available", "resetNoCredit"],
12207
12302
  ["No usable quota reset is available", "resetNoCredit"],
12208
12303
  ["The available quota reset expires too soon", "resetExpired"],
@@ -12265,7 +12360,7 @@ window.__ModuleLoader__.load({
12265
12360
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("usage") }), fetchedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
12266
12361
  className: "codexSubscriptionFreshness",
12267
12362
  dateTime: fetchedAt.toISOString(),
12268
- children: fill(t("usageUpdated"), { value: fetchedAt.toLocaleString() })
12363
+ children: fill(t("usageUpdated"), { value: fetchedAt.toLocaleString(uiLocale()) })
12269
12364
  })]
12270
12365
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
12271
12366
  className: "codexSubscriptionRefresh",
package/lib/index.js CHANGED
@@ -12,7 +12,7 @@ import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
12
12
  import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
13
13
  import { constants, readFileSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
- import { pathToFileURL } from "node:url";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
16
  import { defineTool } from "@deepseek-ai/dsh-tools";
17
17
  import * as dshLlm from "@deepseek-ai/dsh-llm";
18
18
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -1135,6 +1135,7 @@ const RPC_ENDPOINTS = Object.freeze([
1135
1135
  "account/select",
1136
1136
  "account/remove",
1137
1137
  "usage",
1138
+ "usage/accounts",
1138
1139
  "diagnostics",
1139
1140
  "preferences/status",
1140
1141
  "preferences/models",
@@ -2087,9 +2088,36 @@ function inheritedOriginalImageRef(session, assetId) {
2087
2088
  }
2088
2089
  //#endregion
2089
2090
  //#region src/version.js
2090
- const PACKAGE_VERSION = "2.6.8";
2091
+ const PACKAGE_VERSION = "2.7.0";
2091
2092
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2092
2093
  //#endregion
2094
+ //#region src/http-failure.js
2095
+ const SIGN_IN_RENEW = "ChatGPT sign-in needs to be renewed";
2096
+ const REQUEST_BLOCKED = "ChatGPT blocked this request before it reached your account (HTTP 403); your sign-in is fine, try again later or check your network, proxy or VPN";
2097
+ const CHALLENGE_MARKERS = /just a moment|cf-chl|_cf_chl_opt|attention required|enable javascript and cookies|challenge-platform/iu;
2098
+ /** Whether error text is a Cloudflare / network-protection page (pi-ai keeps only the body of a failed request, not its status). */
2099
+ const isChallengePage = (text) => typeof text === "string" && /^\s*<(?:!doctype|html)/iu.test(text) && CHALLENGE_MARKERS.test(text);
2100
+ /** Whether a 403 response is a Cloudflare / network-protection page rather than an answer from the ChatGPT backend. */
2101
+ async function isBlockedByNetwork(response) {
2102
+ if (response?.status !== 403) return false;
2103
+ try {
2104
+ const header = (name) => String(response.headers?.get?.(name) ?? "");
2105
+ if (/challenge/iu.test(header("cf-mitigated"))) return true;
2106
+ if (!/html/iu.test(header("content-type"))) return false;
2107
+ if (/cloudflare/iu.test(header("server"))) return true;
2108
+ const text = typeof response.clone === "function" ? (await response.clone().text()).slice(0, 8192) : "";
2109
+ return CHALLENGE_MARKERS.test(text);
2110
+ } catch {
2111
+ return false;
2112
+ }
2113
+ }
2114
+ /** The error message for a 401 or 403 answer, or undefined for any other status. */
2115
+ async function authFailureMessage(response) {
2116
+ if (response?.status === 401) return SIGN_IN_RENEW;
2117
+ if (response?.status !== 403) return void 0;
2118
+ return await isBlockedByNetwork(response) ? REQUEST_BLOCKED : SIGN_IN_RENEW;
2119
+ }
2120
+ //#endregion
2093
2121
  //#region src/codex-images.js
2094
2122
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
2095
2123
  const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
@@ -2592,7 +2620,8 @@ function createCodexImageTool(options) {
2592
2620
  throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
2593
2621
  }
2594
2622
  if (!response.ok) {
2595
- if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
2623
+ const authFailure = await authFailureMessage(response);
2624
+ if (authFailure !== void 0) throw new Error(authFailure);
2596
2625
  const requestId = supportId(response);
2597
2626
  if (response.status === 429) throw new Error(`${await limitMessage(response)}${requestId}`);
2598
2627
  throw new Error(`Codex image ${editing ? "edit" : "generation"} failed (HTTP ${response.status})${requestId}`);
@@ -2770,6 +2799,37 @@ function retryPolicyFor(choice, resolve) {
2770
2799
  }
2771
2800
  }
2772
2801
  //#endregion
2802
+ //#region src/request-observer.js
2803
+ const failureOf = (chunk) => chunk?.reason?.kind === "error" ? chunk.reason.failure : void 0;
2804
+ function observed(stream, options, provider) {
2805
+ return (async function* () {
2806
+ try {
2807
+ for await (const chunk of stream(options)) {
2808
+ const failure = failureOf(chunk);
2809
+ if (failure !== void 0 && !options?.signal?.aborted) provider.noteAdapterFailure?.("failure", options, failure);
2810
+ yield chunk;
2811
+ }
2812
+ } catch (error) {
2813
+ if (!options?.signal?.aborted) provider.noteAdapterFailure?.("thrown", options, error);
2814
+ throw error;
2815
+ }
2816
+ })();
2817
+ }
2818
+ function observeAdapter(adapter, provider) {
2819
+ return new Proxy(adapter, { get(target, property) {
2820
+ if (property === "stream") return (options) => observed((o) => target.stream(o), options, provider);
2821
+ if (property === "prepareCall") return async (...args) => {
2822
+ const call = await target.prepareCall(...args);
2823
+ return {
2824
+ ...call,
2825
+ stream: (options) => observed((o) => call.stream(o), options, provider)
2826
+ };
2827
+ };
2828
+ const value = Reflect.get(target, property, target);
2829
+ return typeof value === "function" ? value.bind(target) : value;
2830
+ } });
2831
+ }
2832
+ //#endregion
2773
2833
  //#region src/account-vault.js
2774
2834
  const VERSION = 1;
2775
2835
  const DEFAULT_LABEL = "Account 1";
@@ -2969,6 +3029,42 @@ var DshOAuthAccountVault = class {
2969
3029
  return clone$1(payload?.accounts.find((account) => account.id === payload.activeId)?.credential);
2970
3030
  });
2971
3031
  }
3032
+ /** One account's credential, whether or not it is the active one. */
3033
+ readAccount(id) {
3034
+ return this.#enqueue(async () => {
3035
+ const payload = await this.#ensurePayload();
3036
+ return clone$1(payload?.accounts.find((account) => account.id === id)?.credential);
3037
+ });
3038
+ }
3039
+ /** Update one account's credential (a token refresh) without changing which account is active. */
3040
+ modifyAccount(id, update) {
3041
+ return this.#enqueue(async () => {
3042
+ let result;
3043
+ await this.#modifyPayload(async (current) => {
3044
+ const index = current.accounts.findIndex((account) => account.id === id);
3045
+ if (index < 0) throw new Error("Unknown Codex account");
3046
+ const previous = clone$1(current.accounts[index].credential);
3047
+ const next = await update(previous);
3048
+ if (next === void 0) {
3049
+ result = previous;
3050
+ return current;
3051
+ }
3052
+ const credential = sanitizeOAuthCredential(next);
3053
+ if (credential.email === void 0 && previous.email !== void 0) credential.email = previous.email;
3054
+ const accounts = [...current.accounts];
3055
+ accounts[index] = {
3056
+ ...accounts[index],
3057
+ credential
3058
+ };
3059
+ result = clone$1(credential);
3060
+ return {
3061
+ ...current,
3062
+ accounts
3063
+ };
3064
+ });
3065
+ return result;
3066
+ });
3067
+ }
2972
3068
  activeId() {
2973
3069
  return this.#enqueue(async () => (await this.#ensurePayload())?.activeId);
2974
3070
  }
@@ -3068,11 +3164,11 @@ var DshOAuthAccountVault = class {
3068
3164
  };
3069
3165
  //#endregion
3070
3166
  //#region src/credential-store.js
3071
- const PROVIDER$1 = "openai-codex";
3167
+ const PROVIDER$2 = "openai-codex";
3072
3168
  const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
3073
3169
  const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
3074
- function assertProvider(providerId) {
3075
- if (providerId !== PROVIDER$1) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
3170
+ function assertProvider$1(providerId) {
3171
+ if (providerId !== PROVIDER$2) throw new Error(`Codex credential store does not own provider ${JSON.stringify(providerId)}`);
3076
3172
  }
3077
3173
  function assertOAuthCredential(value) {
3078
3174
  if (value === void 0) return void 0;
@@ -3105,7 +3201,7 @@ var DshOAuthCredentialStore = class {
3105
3201
  this.vault = options.vault;
3106
3202
  }
3107
3203
  #enqueue(providerId, operation, options) {
3108
- assertProvider(providerId);
3204
+ assertProvider$1(providerId);
3109
3205
  const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
3110
3206
  abortIfNeeded(options);
3111
3207
  return operation();
@@ -3118,7 +3214,7 @@ var DshOAuthCredentialStore = class {
3118
3214
  return current;
3119
3215
  }
3120
3216
  async #read(providerId, options) {
3121
- assertProvider(providerId);
3217
+ assertProvider$1(providerId);
3122
3218
  abortIfNeeded(options);
3123
3219
  if (this.vault !== void 0) {
3124
3220
  const current = await this.vault.readActive();
@@ -3151,8 +3247,8 @@ var DshOAuthCredentialStore = class {
3151
3247
  }
3152
3248
  async list(options) {
3153
3249
  abortIfNeeded(options);
3154
- return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
3155
- providerId: PROVIDER$1,
3250
+ return await this.read(PROVIDER$2, options) === void 0 ? [] : [{
3251
+ providerId: PROVIDER$2,
3156
3252
  type: "oauth"
3157
3253
  }];
3158
3254
  }
@@ -3202,16 +3298,16 @@ function createCodexAuthService(models, store, options = {}) {
3202
3298
  const createPendingStore = options.createPendingStore ?? (() => new PendingOAuthCredentialStore());
3203
3299
  return Object.freeze({
3204
3300
  async status(options) {
3205
- const current = await store.read(PROVIDER$1, options);
3301
+ const current = await store.read(PROVIDER$2, options);
3206
3302
  const accounts = await accountVault?.list();
3207
3303
  if (current === void 0) return {
3208
3304
  authenticated: false,
3209
- provider: PROVIDER$1,
3305
+ provider: PROVIDER$2,
3210
3306
  ...accounts === void 0 ? {} : { accounts }
3211
3307
  };
3212
3308
  return {
3213
3309
  authenticated: true,
3214
- provider: PROVIDER$1,
3310
+ provider: PROVIDER$2,
3215
3311
  type: "oauth",
3216
3312
  expiresAt: current.expires,
3217
3313
  ...accounts === void 0 ? {} : { accounts }
@@ -3222,13 +3318,13 @@ function createCodexAuthService(models, store, options = {}) {
3222
3318
  if (accountVault === void 0 || createLoginModels === void 0) throw new Error("Codex multi-account is unavailable");
3223
3319
  return runLogin(async () => {
3224
3320
  const pending = createPendingStore();
3225
- await createLoginModels(pending).login(PROVIDER$1, "oauth", interaction);
3321
+ await createLoginModels(pending).login(PROVIDER$2, "oauth", interaction);
3226
3322
  const credential = pending.credential();
3227
3323
  if (credential === void 0) throw new Error("Codex login did not return credentials");
3228
3324
  await accountVault.add(input.label, credential);
3229
3325
  });
3230
3326
  }
3231
- return runLogin(() => models.login(PROVIDER$1, "oauth", interaction));
3327
+ return runLogin(() => models.login(PROVIDER$2, "oauth", interaction));
3232
3328
  },
3233
3329
  async select(id) {
3234
3330
  if (accountVault === void 0) throw new Error("Codex multi-account is unavailable");
@@ -3241,7 +3337,7 @@ function createCodexAuthService(models, store, options = {}) {
3241
3337
  return this.status();
3242
3338
  },
3243
3339
  logout(options) {
3244
- return models.logout(PROVIDER$1, options);
3340
+ return models.logout(PROVIDER$2, options);
3245
3341
  }
3246
3342
  });
3247
3343
  }
@@ -3644,13 +3740,146 @@ function createCodexRpcHandler(coordinator, options = {}) {
3644
3740
  };
3645
3741
  }
3646
3742
  //#endregion
3743
+ //#region src/failure-capture.js
3744
+ const KEEP = /* @__PURE__ */ new Set([
3745
+ "method",
3746
+ "type",
3747
+ "role",
3748
+ "name",
3749
+ "api",
3750
+ "provider",
3751
+ "model",
3752
+ "stopReason",
3753
+ "reasoning",
3754
+ "reasoningEffort",
3755
+ "transport",
3756
+ "mimeType",
3757
+ "kind",
3758
+ "effort",
3759
+ "toolChoice",
3760
+ "cacheRetention",
3761
+ "textVerbosity",
3762
+ "serviceTier"
3763
+ ]);
3764
+ const DROP = /* @__PURE__ */ new Set([
3765
+ "apiKey",
3766
+ "headers",
3767
+ "env",
3768
+ "authorization",
3769
+ "token",
3770
+ "signal",
3771
+ "fetch",
3772
+ "cookie"
3773
+ ]);
3774
+ const MAX_DEPTH = 10;
3775
+ const MAX_ARRAY = 40;
3776
+ const MAX_KEYS = 80;
3777
+ const MAX_NODES = 4e3;
3778
+ const MAX_TEXT = 64;
3779
+ const UNDEFINED_MARKER = "$undefined";
3780
+ function redactRequest(value) {
3781
+ let nodes = 0;
3782
+ const walk = (item, key, depth) => {
3783
+ nodes += 1;
3784
+ if (nodes > MAX_NODES) return { $truncated: "nodes" };
3785
+ if (item === void 0) return { [UNDEFINED_MARKER]: true };
3786
+ if (item === null || typeof item === "boolean") return item;
3787
+ if (typeof item === "number") return Number.isFinite(item) ? item : { $number: String(item) };
3788
+ if (typeof item === "bigint" || typeof item === "symbol") return { $type: typeof item };
3789
+ if (typeof item === "function") return { $type: "function" };
3790
+ if (typeof item === "string") {
3791
+ if (KEEP.has(key) && item.length <= MAX_TEXT) return item;
3792
+ return item.length <= MAX_TEXT ? "x".repeat(item.length) : { $text: item.length };
3793
+ }
3794
+ if (depth >= MAX_DEPTH) return { $truncated: "depth" };
3795
+ if (Array.isArray(item)) {
3796
+ const list = [];
3797
+ for (let index = 0; index < Math.min(item.length, MAX_ARRAY); index += 1) list.push(index in item ? walk(item[index], key, depth + 1) : { $hole: true });
3798
+ return item.length > MAX_ARRAY ? [...list, { $more: item.length - MAX_ARRAY }] : list;
3799
+ }
3800
+ if (item instanceof Uint8Array) return { $bytes: item.byteLength };
3801
+ if (typeof item === "object") {
3802
+ const entries = Object.keys(item).filter((name) => !DROP.has(name));
3803
+ const out = {};
3804
+ for (const name of entries.slice(0, MAX_KEYS)) out[name] = walk(item[name], name, depth + 1);
3805
+ if (entries.length > MAX_KEYS) out.$moreKeys = entries.length - MAX_KEYS;
3806
+ return out;
3807
+ }
3808
+ return { $type: typeof item };
3809
+ };
3810
+ return walk(value, "", 0);
3811
+ }
3812
+ //#endregion
3813
+ //#region src/pi-ai-compat.js
3814
+ const versionOf = (specifier) => {
3815
+ try {
3816
+ let path = fileURLToPath(import.meta.resolve(specifier));
3817
+ for (let depth = 0; depth < 6; depth += 1) {
3818
+ path = path.replace(/[\\/][^\\/]+$/u, "");
3819
+ try {
3820
+ const manifest = JSON.parse(readFileSync(`${path}/package.json`, "utf8"));
3821
+ if (typeof manifest.version === "string") return manifest.version;
3822
+ } catch {}
3823
+ }
3824
+ } catch {}
3825
+ };
3826
+ let cached;
3827
+ /** Versions of the pi-ai library and of DSH's adapter this plugin runs against. */
3828
+ function runtimeVersions() {
3829
+ cached ??= {
3830
+ piAi: versionOf("@earendil-works/pi-ai"),
3831
+ adapter: versionOf("@deepseek-ai/dsh-llm-pi-ai")
3832
+ };
3833
+ return cached;
3834
+ }
3835
+ const atLeast087 = (version) => {
3836
+ const match = /^(\d+)\.(\d+)/u.exec(version ?? "");
3837
+ return match !== null && (Number(match[1]) > 0 || Number(match[2]) >= 87);
3838
+ };
3839
+ /** Whether the installed pi-ai understands system messages inside the transcript. */
3840
+ const understandsSystemMessages = (version) => atLeast087(version);
3841
+ const textOf = (content) => typeof content === "string" ? content : Array.isArray(content) ? content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n") : "";
3842
+ /**
3843
+ * Fold the transcript's leading system messages back into `systemPrompt` and `tools`,
3844
+ * the form every pi-ai release accepts. Messages after the first non-system one stay.
3845
+ */
3846
+ function hoistLeadingSystem(context) {
3847
+ const messages = context?.messages;
3848
+ if (!Array.isArray(messages) || messages[0]?.role !== "system") return context;
3849
+ let end = 0;
3850
+ const texts = context.systemPrompt ? [context.systemPrompt] : [];
3851
+ const tools = [...Array.isArray(context.tools) ? context.tools : []];
3852
+ while (messages[end]?.role === "system") {
3853
+ const message = messages[end];
3854
+ const text = [textOf(message.content), ...Object.values(message.sections ?? {}).filter((value) => typeof value === "string")].filter(Boolean).join("\n\n");
3855
+ if (text !== "") texts.push(text);
3856
+ if (Array.isArray(message.toolsAdded)) tools.push(...message.toolsAdded);
3857
+ end += 1;
3858
+ }
3859
+ const { systemPrompt: _systemPrompt, tools: _tools, ...rest } = context;
3860
+ const systemPrompt = texts.join("\n\n");
3861
+ return {
3862
+ ...rest,
3863
+ ...systemPrompt === "" ? {} : { systemPrompt },
3864
+ ...tools.length === 0 ? {} : { tools },
3865
+ messages: messages.slice(end)
3866
+ };
3867
+ }
3868
+ //#endregion
3647
3869
  //#region src/transport-failure.js
3648
- const TRANSIENT_WEBSOCKET_CLOSE = /^WebSocket closed (?:1006|1011|1012|1013)(?:\s|$)/;
3870
+ const TRANSIENT_WEBSOCKET_CLOSE = /^WebSocket closed (?:1000|1006|1011|1012|1013)(?:\s|$)/;
3649
3871
  const GATEWAY_REPLAY_BUFFER_EXCEEDED = /^\s*exceeded request buffer limit while retrying upstream\s*$/;
3650
3872
  function normalizeTransportEvent(event, signal) {
3651
3873
  if (signal?.aborted || event?.type !== "error" || event.reason !== "error") return event;
3652
3874
  const message = event.error?.errorMessage;
3653
3875
  if (typeof message !== "string") return event;
3876
+ if (isChallengePage(message)) return {
3877
+ ...event,
3878
+ error: {
3879
+ ...event.error,
3880
+ errorMessage: REQUEST_BLOCKED
3881
+ }
3882
+ };
3654
3883
  if (!TRANSIENT_WEBSOCKET_CLOSE.test(message) && !GATEWAY_REPLAY_BUFFER_EXCEEDED.test(message)) return event;
3655
3884
  return {
3656
3885
  ...event,
@@ -3806,6 +4035,36 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
3806
4035
  sessionModel: 0,
3807
4036
  retries: 0
3808
4037
  };
4038
+ const streamFailure = {
4039
+ count: 0,
4040
+ stage: void 0,
4041
+ frame: void 0,
4042
+ request: void 0,
4043
+ adapter: void 0
4044
+ };
4045
+ const noteAdapterFailure = (kind, adapterOptions, failure) => {
4046
+ try {
4047
+ streamFailure.adapter = {
4048
+ kind,
4049
+ ...typeof failure?.code === "string" ? { code: failure.code.slice(0, 40) } : {},
4050
+ message: String(failure?.message ?? "").slice(0, 160),
4051
+ request: redactRequest(adapterOptions)
4052
+ };
4053
+ } catch {
4054
+ streamFailure.adapter = void 0;
4055
+ }
4056
+ };
4057
+ const noteStreamFailure = (stage, error, source) => {
4058
+ streamFailure.count += 1;
4059
+ streamFailure.stage = stage;
4060
+ try {
4061
+ streamFailure.request = source === void 0 ? void 0 : redactRequest(source);
4062
+ } catch {
4063
+ streamFailure.request = void 0;
4064
+ }
4065
+ const frame = typeof error?.stack === "string" ? /([\w.@-]+[\\/][\w.-]+\.[cm]?[jt]sx?):(\d+):(\d+)/u.exec(error.stack.split("\n").slice(1).join("\n")) : null;
4066
+ streamFailure.frame = frame ? `${frame[1].replaceAll("\\", "/")}:${frame[2]}:${frame[3]}` : void 0;
4067
+ };
3809
4068
  const reviewRoute = (model, context) => {
3810
4069
  if (resolveReviewModel() !== "official" || !isDshAutoReview(context)) return void 0;
3811
4070
  review.requests += 1;
@@ -3860,22 +4119,40 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
3860
4119
  };
3861
4120
  return { [Symbol.asyncIterator]: () => replay ??= run() };
3862
4121
  };
3863
- const networkIterable = (factory, options, direct = false) => {
4122
+ const networkIterable = (factory, options, direct = false, source) => {
3864
4123
  let iterator;
3865
4124
  let prepared;
4125
+ let stage = "prepare";
3866
4126
  const step = async (method, value) => {
3867
- const request = await (prepared ??= (direct ? void 0 : connection?.prepare(options)) ?? Promise.resolve({ options }));
3868
- const result = await runNetwork("model", () => {
3869
- iterator ??= factory(direct ? request.options : compaction?.requestOptions(request.options) ?? request.options)[Symbol.asyncIterator]();
3870
- return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
3871
- done: true,
3872
- value
3873
- }));
3874
- }, direct ? request.network : compaction?.networkOptions(request.network) ?? request.network);
3875
- return result.done ? result : {
3876
- ...result,
3877
- value: normalizeTransportEvent(result.value, request.options?.signal)
3878
- };
4127
+ try {
4128
+ const request = await (prepared ??= (direct ? void 0 : connection?.prepare(options)) ?? Promise.resolve({ options }));
4129
+ if (stage === "prepare") stage = "request";
4130
+ const result = await runNetwork("model", () => {
4131
+ const next = direct ? request.options : compaction?.requestOptions(request.options) ?? request.options;
4132
+ iterator ??= factory({
4133
+ ...next,
4134
+ async onPayload(payload, requestModel) {
4135
+ stage = "payload";
4136
+ return next.onPayload?.(payload, requestModel);
4137
+ }
4138
+ })[Symbol.asyncIterator]();
4139
+ return iterator[method]?.(value) ?? (method === "throw" ? Promise.reject(value) : Promise.resolve({
4140
+ done: true,
4141
+ value
4142
+ }));
4143
+ }, direct ? request.network : compaction?.networkOptions(request.network) ?? request.network);
4144
+ if (!result.done) {
4145
+ if (result.value?.type === "error" && !request.options?.signal?.aborted) noteStreamFailure(stage, void 0, source);
4146
+ else if (stage === "payload") stage = "stream";
4147
+ }
4148
+ return result.done ? result : {
4149
+ ...result,
4150
+ value: normalizeTransportEvent(result.value, request.options?.signal)
4151
+ };
4152
+ } catch (error) {
4153
+ noteStreamFailure(stage, error, source);
4154
+ throw error;
4155
+ }
3879
4156
  };
3880
4157
  return {
3881
4158
  [Symbol.asyncIterator]() {
@@ -3889,9 +4166,16 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
3889
4166
  throw: (error) => iterator ? step("throw", error) : Promise.reject(error)
3890
4167
  };
3891
4168
  };
3892
- const routed = (method, model, context, options) => {
4169
+ const routed = (method, model, transcript, options) => {
4170
+ const context = understandsSystemMessages(runtimeVersions().piAi) ? transcript : hoistLeadingSystem(transcript);
3893
4171
  const route = reviewRoute(model, context);
3894
- const create = () => route === void 0 ? networkIterable((prepared) => provider[method](model, context, prepared), withPreferences(model, options)) : networkIterable((prepared) => provider[method](model, context, prepared), withReview(route, options), true);
4172
+ const source = {
4173
+ method,
4174
+ model,
4175
+ context,
4176
+ options
4177
+ };
4178
+ const create = () => route === void 0 ? networkIterable((prepared) => provider[method](model, context, prepared), withPreferences(model, options), false, source) : networkIterable((prepared) => provider[method](model, context, prepared), withReview(route, options), true, source);
3895
4179
  return isDshAutoReview(context) ? reviewIterable(create) : create();
3896
4180
  };
3897
4181
  return Object.freeze({
@@ -3903,7 +4187,9 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
3903
4187
  getModels,
3904
4188
  stream: (model, context, options) => routed("stream", model, context, options),
3905
4189
  streamSimple: (model, context, options) => routed("streamSimple", model, context, options),
3906
- reviewCounters: () => ({ ...review })
4190
+ reviewCounters: () => ({ ...review }),
4191
+ streamFailure: () => ({ ...streamFailure }),
4192
+ noteAdapterFailure
3907
4193
  });
3908
4194
  }
3909
4195
  Object.freeze([
@@ -4318,7 +4604,10 @@ function createCodexSearchProvider(options) {
4318
4604
  if (signal?.aborted || error?.name === "AbortError") throw new WebError("Codex search aborted", "WEB_ABORTED", { cause: error });
4319
4605
  throw new WebError("Codex search request failed", "WEB_PROVIDER_ERROR", { cause: error });
4320
4606
  }
4321
- if (!response.ok) throw response.status === 401 || response.status === 403 ? new WebError("ChatGPT sign-in needs to be renewed", "WEB_PROVIDER_CREDENTIAL_MISSING") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4607
+ if (!response.ok) {
4608
+ const authFailure = await authFailureMessage(response);
4609
+ throw authFailure === "ChatGPT sign-in needs to be renewed" ? new WebError(authFailure, "WEB_PROVIDER_CREDENTIAL_MISSING") : authFailure !== void 0 ? new WebError(authFailure, "WEB_PROVIDER_ERROR") : new WebError(`Codex search request failed (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
4610
+ }
4322
4611
  let value;
4323
4612
  try {
4324
4613
  value = await response.json();
@@ -4849,6 +5138,15 @@ function safeRequests(network) {
4849
5138
  }
4850
5139
  return result;
4851
5140
  }
5141
+ /** pi-ai and DSH's adapter come from the host; a profile can pair an adapter with an older library, so the report names both. */
5142
+ function libraryVersions() {
5143
+ const { piAi, adapter } = runtimeVersions();
5144
+ const valid = (value) => typeof value === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]{1,32})?$/u.test(value);
5145
+ return {
5146
+ ...valid(piAi) ? { piAi } : {},
5147
+ ...valid(adapter) ? { adapter } : {}
5148
+ };
5149
+ }
4852
5150
  /** Build a support report that deliberately excludes OAuth and account metadata. */
4853
5151
  async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network, modelCatalog, connection, compaction, review, inspectionOptions, operations, runtimeManagement, storage, tools }) {
4854
5152
  const collected = {};
@@ -4914,6 +5212,26 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
4914
5212
  "retries"
4915
5213
  ]) : void 0
4916
5214
  ],
5215
+ [
5216
+ "streamFailure",
5217
+ "transport",
5218
+ () => {
5219
+ const value = review?.streamFailure?.();
5220
+ if (!Number.isSafeInteger(value?.count)) return void 0;
5221
+ return {
5222
+ count: value.count,
5223
+ ...[
5224
+ "prepare",
5225
+ "request",
5226
+ "payload",
5227
+ "stream"
5228
+ ].includes(value.stage) ? { stage: value.stage } : {},
5229
+ ...typeof value.frame === "string" && /^[\w.@/-]{1,120}:\d+:\d+$/u.test(value.frame) ? { frame: value.frame } : {},
5230
+ ...value.request && typeof value.request === "object" && JSON.stringify(value.request).length <= 12e4 ? { request: value.request } : {},
5231
+ ...value.adapter && typeof value.adapter === "object" && JSON.stringify(value.adapter).length <= 12e4 ? { adapter: value.adapter } : {}
5232
+ };
5233
+ }
5234
+ ],
4917
5235
  [
4918
5236
  "operations",
4919
5237
  "transport",
@@ -5007,7 +5325,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
5007
5325
  node: process.version,
5008
5326
  platform: process.platform,
5009
5327
  arch: process.arch,
5010
- ...hostDshVersion() ? { dsh: hostDshVersion() } : {}
5328
+ ...hostDshVersion() ? { dsh: hostDshVersion() } : {},
5329
+ ...libraryVersions()
5011
5330
  },
5012
5331
  account,
5013
5332
  login,
@@ -5017,6 +5336,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
5017
5336
  ...collected.websocket ? { websocket: collected.websocket } : {},
5018
5337
  ...collected.compaction ? { compaction: collected.compaction } : {},
5019
5338
  ...collected.review ? { review: collected.review } : {},
5339
+ ...collected.streamFailure ? { streamFailure: collected.streamFailure } : {},
5020
5340
  ...catalog && ["unavailable", "online"].includes(catalog.source) && [
5021
5341
  "idle",
5022
5342
  "refreshing",
@@ -5286,7 +5606,7 @@ function createCodexUsageReader(options) {
5286
5606
  signal: requestSignal$1(signal, timeoutMs)
5287
5607
  });
5288
5608
  if (!response.ok) {
5289
- const error = /* @__PURE__ */ new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT usage request failed (HTTP ${response.status})`);
5609
+ const error = new Error(await authFailureMessage(response) ?? `ChatGPT usage request failed (HTTP ${response.status})`);
5290
5610
  Object.defineProperty(error, "retryAfterMs", { value: retryAfterMs(response, now, maxRetryAfterMs) });
5291
5611
  throw error;
5292
5612
  }
@@ -5337,6 +5657,102 @@ function createCodexUsageReader(options) {
5337
5657
  });
5338
5658
  }
5339
5659
  //#endregion
5660
+ //#region src/account-usage.js
5661
+ const PROVIDER$1 = "openai-codex";
5662
+ const MAX_PARALLEL = 4;
5663
+ const assertProvider = (providerId) => {
5664
+ if (providerId !== PROVIDER$1) throw new Error("Codex account usage received an unknown provider");
5665
+ };
5666
+ /** pi-ai's OAuth credential store, bound to one account of the vault. */
5667
+ function accountCredentialStore(vault, id, expirySkewMs = 0) {
5668
+ const shift = (credential) => credential === void 0 || expirySkewMs === 0 ? credential : {
5669
+ ...credential,
5670
+ expires: credential.expires - expirySkewMs
5671
+ };
5672
+ return {
5673
+ async read(providerId) {
5674
+ assertProvider(providerId);
5675
+ return shift(await vault.readAccount(id));
5676
+ },
5677
+ async list() {
5678
+ return await vault.readAccount(id) === void 0 ? [] : [{
5679
+ providerId: PROVIDER$1,
5680
+ type: "oauth"
5681
+ }];
5682
+ },
5683
+ async modify(providerId, update) {
5684
+ assertProvider(providerId);
5685
+ return vault.modifyAccount(id, async (current) => update(shift(current)));
5686
+ },
5687
+ async delete() {
5688
+ throw new Error("An account usage read cannot remove a sign-in");
5689
+ }
5690
+ };
5691
+ }
5692
+ /**
5693
+ * @param {object} options
5694
+ * @param {{ list(): Promise<object[]>, readAccount(id: string): Promise<object|undefined>, modifyAccount: Function }} options.vault
5695
+ * @param {(credentials: object) => object} options.createModels builds a pi-ai Models over a credential store, with the Codex provider set
5696
+ * @param {typeof fetch} options.fetch
5697
+ */
5698
+ function createAccountUsageReader({ vault, createModels, fetch, expirySkewMs = 0, now = Date.now, activeReader }) {
5699
+ const readers = /* @__PURE__ */ new Map();
5700
+ const readerFor = (id) => {
5701
+ let reader = readers.get(id);
5702
+ if (reader === void 0) {
5703
+ const models = createModels(accountCredentialStore(vault, id, expirySkewMs));
5704
+ reader = createCodexUsageReader({
5705
+ getAuth: () => models.getAuth(PROVIDER$1),
5706
+ readCredential: () => vault.readAccount(id).then((credential) => credential),
5707
+ fetch,
5708
+ now
5709
+ });
5710
+ readers.set(id, reader);
5711
+ }
5712
+ return reader;
5713
+ };
5714
+ const summarize = (error, signal) => {
5715
+ if (signal?.aborted) throw error;
5716
+ const message = error instanceof Error ? error.message : "";
5717
+ return ["ChatGPT subscription is not signed in", "ChatGPT sign-in needs to be renewed"].includes(message) || message.includes("blocked this request") ? message : "Could not read ChatGPT usage";
5718
+ };
5719
+ return Object.freeze({
5720
+ /** One entry per account: its usage reading, or the reason it could not be read. */
5721
+ async readAll({ force = false, signal } = {}) {
5722
+ const accounts = await vault.list();
5723
+ for (const id of [...readers.keys()]) if (!accounts.some((account) => account.id === id)) readers.delete(id);
5724
+ const entries = new Array(accounts.length);
5725
+ let next = 0;
5726
+ const worker = async () => {
5727
+ while (next < accounts.length) {
5728
+ const index = next++;
5729
+ const account = accounts[index];
5730
+ try {
5731
+ const reader = account.active && activeReader !== void 0 ? activeReader : readerFor(account.id);
5732
+ entries[index] = {
5733
+ id: account.id,
5734
+ usage: await reader.read({
5735
+ force,
5736
+ signal
5737
+ })
5738
+ };
5739
+ } catch (error) {
5740
+ entries[index] = {
5741
+ id: account.id,
5742
+ error: summarize(error, signal)
5743
+ };
5744
+ }
5745
+ }
5746
+ };
5747
+ await Promise.all(Array.from({ length: Math.min(MAX_PARALLEL, accounts.length) }, worker));
5748
+ return entries;
5749
+ },
5750
+ clear() {
5751
+ readers.clear();
5752
+ }
5753
+ });
5754
+ }
5755
+ //#endregion
5340
5756
  //#region src/quota-retry.js
5341
5757
  const DEFAULT_MAX_WAIT_MS = 360 * 60 * 1e3;
5342
5758
  const DEFAULT_RESET_MARGIN_MS = 1e4;
@@ -6057,7 +6473,7 @@ function createCodexResetCreditService(options) {
6057
6473
  },
6058
6474
  signal: requestSignal(signal, timeoutMs)
6059
6475
  });
6060
- if (!response.ok) throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
6476
+ if (!response.ok) throw new Error(await authFailureMessage(response) ?? `ChatGPT quota reset request failed (HTTP ${response.status})`);
6061
6477
  let raw;
6062
6478
  try {
6063
6479
  raw = await response.json();
@@ -6191,7 +6607,7 @@ function createCodexResetCreditService(options) {
6191
6607
  throw new Error(UNCERTAIN_RESET_RESULT);
6192
6608
  }
6193
6609
  if (response.status !== 401 && response.status !== 403) retryable = false;
6194
- throw new Error(response.status === 401 || response.status === 403 ? "ChatGPT sign-in needs to be renewed" : `ChatGPT quota reset request failed (HTTP ${response.status})`);
6610
+ throw new Error(await authFailureMessage(response) ?? `ChatGPT quota reset request failed (HTTP ${response.status})`);
6195
6611
  }
6196
6612
  let raw;
6197
6613
  try {
@@ -6233,7 +6649,7 @@ const publicError = (code, message) => ({
6233
6649
  details: { issues: [] }
6234
6650
  }
6235
6651
  });
6236
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
6652
+ function createSubscriptionRpcHandler({ authHandler, usageReader, accountUsageReader, resetCreditService, preferences, defaultModel, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged, onCleanupFailure }) {
6237
6653
  return async (endpoint, payload, signal) => {
6238
6654
  if (endpoint === "default-model/status" || endpoint === "default-model/select") try {
6239
6655
  signal.throwIfAborted();
@@ -6380,10 +6796,27 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6380
6796
  };
6381
6797
  } catch (error) {
6382
6798
  if (signal.aborted) throw error;
6383
- const known = /* @__PURE__ */ new Set(["ChatGPT subscription is not signed in", "ChatGPT sign-in needs to be renewed"]);
6799
+ const known = /* @__PURE__ */ new Set([
6800
+ "ChatGPT subscription is not signed in",
6801
+ "ChatGPT sign-in needs to be renewed",
6802
+ REQUEST_BLOCKED
6803
+ ]);
6384
6804
  const message = error instanceof Error && known.has(error.message) ? error.message : "Could not read ChatGPT usage";
6385
6805
  return publicError("internal", message);
6386
6806
  }
6807
+ if (endpoint === "usage/accounts") try {
6808
+ signal.throwIfAborted();
6809
+ return {
6810
+ ok: true,
6811
+ value: { accounts: await accountUsageReader.readAll({
6812
+ force: payload?.force === true,
6813
+ signal
6814
+ }) }
6815
+ };
6816
+ } catch (error) {
6817
+ if (signal.aborted) throw error;
6818
+ return publicError("internal", "Could not read the usage of the signed-in accounts");
6819
+ }
6387
6820
  if (endpoint === "reset-credit/inspect" || endpoint === "reset-credit/prepare" || endpoint === "reset-credit/consume") try {
6388
6821
  signal.throwIfAborted();
6389
6822
  return {
@@ -6402,6 +6835,7 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6402
6835
  const known = /* @__PURE__ */ new Set([
6403
6836
  "ChatGPT subscription is not signed in",
6404
6837
  "ChatGPT sign-in needs to be renewed",
6838
+ REQUEST_BLOCKED,
6405
6839
  "No quota reset is available",
6406
6840
  "No usable quota reset is available",
6407
6841
  "The available quota reset expires too soon",
@@ -6424,7 +6858,7 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
6424
6858
  () => modelCatalog?.clear(),
6425
6859
  () => onAccountChanged?.()
6426
6860
  ];
6427
- if (endpoint === "logout") tasks.push(() => usageReader.clear());
6861
+ if (endpoint === "logout") tasks.push(() => usageReader.clear(), () => accountUsageReader?.clear());
6428
6862
  if (endpoint === "account/remove" && typeof payload?.id === "string") tasks.push(() => usageReader.clearScope(payload.id));
6429
6863
  if ((await Promise.allSettled(tasks.map((task) => Promise.resolve().then(task)))).some((value) => value.status === "rejected")) try {
6430
6864
  onCleanupFailure?.();
@@ -6745,7 +7179,7 @@ function apply(ctx, config = {}) {
6745
7179
  auth: adapterAuth,
6746
7180
  resolveAttachments: () => ctx.get?.("attachments")
6747
7181
  });
6748
- ctx.llm.registerAdapter([PROVIDER], compaction.wrapAdapter(adapter));
7182
+ ctx.llm.registerAdapter([PROVIDER], observeAdapter(compaction.wrapAdapter(adapter), provider));
6749
7183
  const currentAgent = () => ctx.get?.("agents")?.currentInitiator?.();
6750
7184
  const codexSearch = createCodexSearchProvider({
6751
7185
  resolvePreferences: () => readCapabilitySettings(settings.get()),
@@ -6785,12 +7219,13 @@ function apply(ctx, config = {}) {
6785
7219
  }
6786
7220
  });
6787
7221
  const coordinator = new CodexLoginCoordinator(auth);
7222
+ const baseUsageReader = createCodexUsageReader({
7223
+ getAuth: resolveAuth,
7224
+ readCredential: (options) => store.read(PROVIDER, options),
7225
+ fetch: (input, init) => network.fetch("quota", input, init)
7226
+ });
6788
7227
  const usageReader = createQuotaForecastReader({
6789
- reader: createCodexUsageReader({
6790
- getAuth: resolveAuth,
6791
- readCredential: (options) => store.read(PROVIDER, options),
6792
- fetch: (input, init) => network.fetch("quota", input, init)
6793
- }),
7228
+ reader: baseUsageReader,
6794
7229
  enabled: () => normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]) === QUICK_QUOTA_MODE_FORECAST,
6795
7230
  scope: async () => await accountVault?.activeId() ?? "legacy",
6796
7231
  stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
@@ -6864,9 +7299,21 @@ function apply(ctx, config = {}) {
6864
7299
  sketchBridge.dispose();
6865
7300
  };
6866
7301
  }, "codex-subscription: native sketch tool");
7302
+ const accountUsageReader = createAccountUsageReader({
7303
+ vault: accountVault,
7304
+ createModels: (credentials) => {
7305
+ const accountModels = createModels({ credentials });
7306
+ accountModels.setProvider(provider);
7307
+ return accountModels;
7308
+ },
7309
+ fetch: (input, init) => network.fetch("quota", input, init),
7310
+ expirySkewMs: OAUTH_EXPIRY_SKEW_MS,
7311
+ activeReader: baseUsageReader
7312
+ });
6867
7313
  const subscriptionHandler = createSubscriptionRpcHandler({
6868
7314
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
6869
7315
  usageReader,
7316
+ accountUsageReader,
6870
7317
  resetCreditService,
6871
7318
  preferences,
6872
7319
  defaultModel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "2.6.8",
3
+ "version": "2.7.0",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",