dsh-codex-subscription 2.1.9 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +59 -9
- package/lib/index.js +224 -6
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -39,6 +39,7 @@ See below for detailed installation steps, terminal commands, updates, and remov
|
|
|
39
39
|
| **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
|
|
40
40
|
| **Recoverable and diagnosable** | Sign-in state reconciles automatically; failed reads can be retried in place, while timeouts and stale account responses cannot overwrite current state; Settings can create a support report without credentials or account identifiers |
|
|
41
41
|
| **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate, with reset times |
|
|
42
|
+
| **Optional quota recovery** | Opt in to keep a turn open until a confirmed short Codex quota resets; the turn shows the wait and remains cancellable, while switching accounts retries immediately |
|
|
42
43
|
| **Composer quota** | Choose a compact percentage, progress bar, Beta runway forecast, or no inline display |
|
|
43
44
|
| **Safe quota reset** | See each reset credit separately and deliberately try one with a cooldown and acknowledgement |
|
|
44
45
|
| **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
| **订阅模型直连** | 登录 ChatGPT 后直接使用 Codex,不需要 OpenAI API Key 或 Codex CLI |
|
|
41
41
|
| **可恢复、可诊断** | 登录状态会自动对账;读取失败时可在原处重试,超时和旧账号响应不会覆盖当前状态;设置页可生成不含凭据和账号标识的支持报告 |
|
|
42
42
|
| **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示,并显示重置时间 |
|
|
43
|
+
| **可选额度恢复** | 可选择让对话等待已确认的短时 Codex 额度重置;对话会显示等待原因、仍可停止,等待期间切换账号会立即重试 |
|
|
43
44
|
| **输入框额度** | 可选择紧凑百分比、进度条、Beta 续航预测或关闭显示 |
|
|
44
45
|
| **安全额度重置** | 每张重置卡单独显示,并通过冷静期和知情确认主动尝试重置 |
|
|
45
46
|
| **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
|
package/lib/client.js
CHANGED
|
@@ -4878,6 +4878,8 @@ window.__ModuleLoader__.load({
|
|
|
4878
4878
|
quickQuotaBar: "进度条",
|
|
4879
4879
|
quickQuotaForecast: "续航预测",
|
|
4880
4880
|
quickQuotaBeta: "Beta",
|
|
4881
|
+
autoQuotaRetry: "额度重置后自动重试",
|
|
4882
|
+
autoQuotaRetryHint: "选择开启后,将等待已确认的短时额度重置;对话会显示等待原因且可停止,等待期间切换账号会立即重试。",
|
|
4881
4883
|
quickQuotaForecastHint: "按最近两小时的消耗估算;消耗不足或速度突变时重新校准,详情显示估计范围。仅供参考,历史保存在本机。",
|
|
4882
4884
|
contextTitle: "上下文窗口",
|
|
4883
4885
|
contextStandard: "标准",
|
|
@@ -5332,6 +5334,8 @@ window.__ModuleLoader__.load({
|
|
|
5332
5334
|
quickQuotaBar: "Progress bar",
|
|
5333
5335
|
quickQuotaForecast: "Runway",
|
|
5334
5336
|
quickQuotaBeta: "Beta",
|
|
5337
|
+
autoQuotaRetry: "Retry after quota reset",
|
|
5338
|
+
autoQuotaRetryHint: "Opt in to wait for confirmed short quota resets. The turn shows why it is waiting and can be stopped; switching accounts retries immediately.",
|
|
5335
5339
|
quickQuotaForecastHint: "Estimates recent two-hour consumption; recalibrates with insufficient data or changing pace. Details show a range. Indicative only; history stays local.",
|
|
5336
5340
|
contextTitle: "Context window",
|
|
5337
5341
|
contextStandard: "Standard",
|
|
@@ -5470,6 +5474,12 @@ window.__ModuleLoader__.load({
|
|
|
5470
5474
|
.codexSubscriptionPreferenceCopy{display:flex;min-width:0;flex-direction:column;gap:2px}
|
|
5471
5475
|
.codexSubscriptionPreferenceLabel{display:flex;align-items:center;gap:6px}
|
|
5472
5476
|
.codexSubscriptionPreferenceHint{max-width:300px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
|
|
5477
|
+
.codexSubscriptionSwitch{position:relative;flex:none;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer;transition:background 120ms var(--ds-ease-in-out)}
|
|
5478
|
+
.codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-primary)}
|
|
5479
|
+
.codexSubscriptionSwitch:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
|
|
5480
|
+
.codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}
|
|
5481
|
+
.codexSubscriptionSwitchKnob{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);box-shadow:0 1px 2px var(--dsw-alias-border-l3);transition:transform 120ms var(--ds-ease-in-out)}
|
|
5482
|
+
.codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(16px)}
|
|
5473
5483
|
.codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}
|
|
5474
5484
|
.codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap}
|
|
5475
5485
|
.codexSubscriptionQuotaMode small{margin-left:3px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}
|
|
@@ -6687,6 +6697,7 @@ window.__ModuleLoader__.load({
|
|
|
6687
6697
|
limitId: limit.id
|
|
6688
6698
|
}))).filter((window) => Number.isFinite(window.remainingPercent) && window.remainingPercent >= 0 && window.remainingPercent <= (mode === "custom" ? readCapabilitySettings(thresholds)[window.windowSeconds > 0 && window.windowSeconds <= 6 * 3600 ? "quotaShortThreshold" : "quotaLongThreshold"] : mode === "early" && window.windowSeconds > 0 && window.windowSeconds <= 6 * 3600 ? 50 : 20) && (!Number.isFinite(window.resetsAt) || window.resetsAt * 1e3 > now)).sort((a, b) => a.remainingPercent - b.remainingPercent)[0];
|
|
6689
6699
|
}
|
|
6700
|
+
const AUTO_QUOTA_RETRY_FIELD = "autoQuotaRetry";
|
|
6690
6701
|
const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
|
|
6691
6702
|
const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
|
|
6692
6703
|
const QUICK_QUOTA_MODE_PERCENT = "percent";
|
|
@@ -6734,6 +6745,7 @@ window.__ModuleLoader__.load({
|
|
|
6734
6745
|
"gpt-5.6": 272e3,
|
|
6735
6746
|
"gpt-6-astra": 272e3
|
|
6736
6747
|
});
|
|
6748
|
+
const normalizeAutoQuotaRetry = (value) => typeof value === "boolean" ? value : false;
|
|
6737
6749
|
const normalizeSearchProvider = (value) => [
|
|
6738
6750
|
"auto",
|
|
6739
6751
|
"dsh",
|
|
@@ -6805,6 +6817,7 @@ window.__ModuleLoader__.load({
|
|
|
6805
6817
|
return Object.freeze({
|
|
6806
6818
|
status: current.status,
|
|
6807
6819
|
...capabilities,
|
|
6820
|
+
autoQuotaRetry: normalizeAutoQuotaRetry(value?.[AUTO_QUOTA_RETRY_FIELD]),
|
|
6808
6821
|
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6809
6822
|
compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
|
|
6810
6823
|
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
@@ -6855,6 +6868,7 @@ window.__ModuleLoader__.load({
|
|
|
6855
6868
|
fallback = {
|
|
6856
6869
|
status: "ready",
|
|
6857
6870
|
value: {
|
|
6871
|
+
[AUTO_QUOTA_RETRY_FIELD]: normalizeAutoQuotaRetry(value?.[AUTO_QUOTA_RETRY_FIELD]),
|
|
6858
6872
|
connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
|
|
6859
6873
|
compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
|
|
6860
6874
|
subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
|
|
@@ -7276,7 +7290,7 @@ window.__ModuleLoader__.load({
|
|
|
7276
7290
|
}
|
|
7277
7291
|
//#endregion
|
|
7278
7292
|
//#region src/version.js
|
|
7279
|
-
const PACKAGE_VERSION = "2.1
|
|
7293
|
+
const PACKAGE_VERSION = "2.2.1";
|
|
7280
7294
|
//#endregion
|
|
7281
7295
|
//#region src/client-recovery.js
|
|
7282
7296
|
async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
|
|
@@ -8353,6 +8367,34 @@ window.__ModuleLoader__.load({
|
|
|
8353
8367
|
})]
|
|
8354
8368
|
});
|
|
8355
8369
|
}
|
|
8370
|
+
function AutoQuotaRetryPreference({ preference, t }) {
|
|
8371
|
+
const snapshot = usePreferenceSnapshot(preference);
|
|
8372
|
+
const writable = snapshot.status === "ready" && snapshot.writable === true;
|
|
8373
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8374
|
+
className: "codexSubscriptionPreference",
|
|
8375
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8376
|
+
className: "codexSubscriptionPreferenceCopy",
|
|
8377
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8378
|
+
className: "codexSubscriptionPreferenceLabel",
|
|
8379
|
+
children: t("autoQuotaRetry")
|
|
8380
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8381
|
+
className: "codexSubscriptionPreferenceHint",
|
|
8382
|
+
children: t("autoQuotaRetryHint")
|
|
8383
|
+
})]
|
|
8384
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8385
|
+
className: "codexSubscriptionSwitch",
|
|
8386
|
+
type: "button",
|
|
8387
|
+
role: "switch",
|
|
8388
|
+
"aria-label": t("autoQuotaRetry"),
|
|
8389
|
+
"aria-checked": snapshot.autoQuotaRetry,
|
|
8390
|
+
disabled: !writable,
|
|
8391
|
+
onClick: () => {
|
|
8392
|
+
preference.set({ [AUTO_QUOTA_RETRY_FIELD]: !snapshot.autoQuotaRetry });
|
|
8393
|
+
},
|
|
8394
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "codexSubscriptionSwitchKnob" })
|
|
8395
|
+
})]
|
|
8396
|
+
});
|
|
8397
|
+
}
|
|
8356
8398
|
function SearchProviderPreference({ preference, t }) {
|
|
8357
8399
|
const snapshot = usePreferenceSnapshot(preference);
|
|
8358
8400
|
const writable = snapshot.status === "ready" && snapshot.writable === true;
|
|
@@ -8700,14 +8742,22 @@ window.__ModuleLoader__.load({
|
|
|
8700
8742
|
t
|
|
8701
8743
|
})]
|
|
8702
8744
|
})
|
|
8703
|
-
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8745
|
+
] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
8746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
|
|
8747
|
+
preference,
|
|
8748
|
+
t
|
|
8749
|
+
}),
|
|
8750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
|
|
8751
|
+
preference,
|
|
8752
|
+
t,
|
|
8753
|
+
section: "quota"
|
|
8754
|
+
}),
|
|
8755
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
|
|
8756
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoQuotaRetryPreference, {
|
|
8757
|
+
preference,
|
|
8758
|
+
t
|
|
8759
|
+
})
|
|
8760
|
+
] }), snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8711
8761
|
className: "codexSubscriptionRecover",
|
|
8712
8762
|
role: "alert",
|
|
8713
8763
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
package/lib/index.js
CHANGED
|
@@ -193,6 +193,7 @@ function capabilityPatch(payload) {
|
|
|
193
193
|
//#endregion
|
|
194
194
|
//#region src/settings-contract.js
|
|
195
195
|
const SETTINGS_NAMESPACE = "codex-subscription";
|
|
196
|
+
const AUTO_QUOTA_RETRY_FIELD = "autoQuotaRetry";
|
|
196
197
|
const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
|
|
197
198
|
const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
|
|
198
199
|
const QUICK_QUOTA_MODE_PERCENT = "percent";
|
|
@@ -240,6 +241,7 @@ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
|
|
|
240
241
|
"gpt-5.6": 272e3,
|
|
241
242
|
"gpt-6-astra": 272e3
|
|
242
243
|
});
|
|
244
|
+
const normalizeAutoQuotaRetry = (value) => typeof value === "boolean" ? value : false;
|
|
243
245
|
const normalizeOutputVerbosity = (value) => [
|
|
244
246
|
"default",
|
|
245
247
|
"low",
|
|
@@ -551,14 +553,15 @@ async function withCodexNetwork(run, options = {}) {
|
|
|
551
553
|
}
|
|
552
554
|
if (options.websocket) activeWebSocketScopes += 1;
|
|
553
555
|
if (activeScopes === 0) {
|
|
554
|
-
|
|
556
|
+
const fetchBeforeScope = globalThis.fetch;
|
|
557
|
+
baseFetch = fetchBeforeScope;
|
|
555
558
|
scopedFetch = async (input, init) => {
|
|
556
559
|
const scope = networkScope.getStore();
|
|
557
|
-
if (scope === void 0) return
|
|
560
|
+
if (scope === void 0) return fetchBeforeScope(input, init);
|
|
558
561
|
const { options: scopedOptions, allowedHosts, resolved } = scope;
|
|
559
562
|
const proxyFetch = scopedOptions.fetchThroughProxy ?? fetchThroughProxy;
|
|
560
563
|
const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
|
|
561
|
-
if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return
|
|
564
|
+
if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return fetchBeforeScope(input, init);
|
|
562
565
|
let proxy = resolved.get(target.hostname);
|
|
563
566
|
if (proxy === void 0) {
|
|
564
567
|
proxy = resolveCodexProxy({
|
|
@@ -569,7 +572,7 @@ async function withCodexNetwork(run, options = {}) {
|
|
|
569
572
|
}
|
|
570
573
|
const route = await proxy;
|
|
571
574
|
scopedOptions.onRoute?.(route.source);
|
|
572
|
-
const response = await (route.url === void 0 ?
|
|
575
|
+
const response = await (route.url === void 0 ? fetchBeforeScope(input, init) : proxyFetch(input, init, route.url));
|
|
573
576
|
return scopedOptions.transformResponse?.(response, target) ?? response;
|
|
574
577
|
};
|
|
575
578
|
globalThis.fetch = scopedFetch;
|
|
@@ -2829,7 +2832,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
2829
2832
|
Object.freeze(["0.82.1", "0.85.1"]);
|
|
2830
2833
|
//#endregion
|
|
2831
2834
|
//#region src/version.js
|
|
2832
|
-
const PACKAGE_VERSION = "2.1
|
|
2835
|
+
const PACKAGE_VERSION = "2.2.1";
|
|
2833
2836
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2834
2837
|
//#endregion
|
|
2835
2838
|
//#region src/model-catalog.js
|
|
@@ -3972,6 +3975,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
3972
3975
|
...gaps.length ? { unsupported: gaps } : {}
|
|
3973
3976
|
} } : {},
|
|
3974
3977
|
configuration: {
|
|
3978
|
+
...typeof preference.autoQuotaRetry === "boolean" ? { autoQuotaRetry: preference.autoQuotaRetry } : {},
|
|
3975
3979
|
contextMode: preference.contextMode,
|
|
3976
3980
|
quickQuotaMode: preference.quickQuotaMode,
|
|
3977
3981
|
...typeof preference.outputVerbosity === "string" ? { outputVerbosity: preference.outputVerbosity } : {},
|
|
@@ -4209,6 +4213,187 @@ function createCodexUsageReader(options) {
|
|
|
4209
4213
|
});
|
|
4210
4214
|
}
|
|
4211
4215
|
//#endregion
|
|
4216
|
+
//#region src/quota-retry.js
|
|
4217
|
+
const DEFAULT_MAX_WAIT_MS = 360 * 60 * 1e3;
|
|
4218
|
+
const DEFAULT_RESET_MARGIN_MS = 1e4;
|
|
4219
|
+
const EXHAUSTED_PERCENT = 99.9;
|
|
4220
|
+
const CODEX_USAGE_LIMIT_MESSAGE = /^You have hit your ChatGPT usage limit(?: \([a-z0-9][a-z0-9 ._-]{0,39} plan\))?\.(?: Try again in ~\d{1,6} min\.)?$/u;
|
|
4221
|
+
const isCodexRateLimit = (failure) => failure?.code === "RATE_LIMIT" || failure?.code === "PI_AI_ERROR" && typeof failure.message === "string" && CODEX_USAGE_LIMIT_MESSAGE.test(failure.message);
|
|
4222
|
+
function cancellableDelay(delayMs, signal) {
|
|
4223
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
4224
|
+
return new Promise((resolve) => {
|
|
4225
|
+
const timer = setTimeout(() => {
|
|
4226
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4227
|
+
resolve(true);
|
|
4228
|
+
}, delayMs);
|
|
4229
|
+
function onAbort() {
|
|
4230
|
+
clearTimeout(timer);
|
|
4231
|
+
resolve(false);
|
|
4232
|
+
}
|
|
4233
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
function createWakeSignal() {
|
|
4237
|
+
let revision = 0;
|
|
4238
|
+
let reason;
|
|
4239
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
4240
|
+
const notify = (nextReason) => {
|
|
4241
|
+
revision += 1;
|
|
4242
|
+
reason = nextReason;
|
|
4243
|
+
for (const waiter of [...waiters]) waiter(nextReason);
|
|
4244
|
+
};
|
|
4245
|
+
const wait = (since, signal) => {
|
|
4246
|
+
if (revision !== since) return Promise.resolve(reason);
|
|
4247
|
+
if (signal?.aborted) return Promise.resolve("cancelled");
|
|
4248
|
+
return new Promise((resolve) => {
|
|
4249
|
+
const settle = (value) => {
|
|
4250
|
+
waiters.delete(settle);
|
|
4251
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4252
|
+
resolve(value);
|
|
4253
|
+
};
|
|
4254
|
+
const onAbort = () => settle("cancelled");
|
|
4255
|
+
waiters.add(settle);
|
|
4256
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4257
|
+
if (revision !== since) settle(reason);
|
|
4258
|
+
});
|
|
4259
|
+
};
|
|
4260
|
+
return {
|
|
4261
|
+
revision: () => revision,
|
|
4262
|
+
notify,
|
|
4263
|
+
wait
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
function shortResetDelay(usage, nowMs, maxWaitMs, resetMarginMs) {
|
|
4267
|
+
let latestDelayMs;
|
|
4268
|
+
for (const limit of usage?.rateLimits ?? []) {
|
|
4269
|
+
if (limit?.id !== "codex") continue;
|
|
4270
|
+
for (const window of limit?.windows ?? []) {
|
|
4271
|
+
if (!Number.isFinite(window?.usedPercent) || window.usedPercent < EXHAUSTED_PERCENT) continue;
|
|
4272
|
+
if (!Number.isFinite(window?.windowSeconds) || window.windowSeconds * 1e3 > maxWaitMs) return void 0;
|
|
4273
|
+
if (!Number.isFinite(window?.resetsAt)) return void 0;
|
|
4274
|
+
const delayMs = window.resetsAt * 1e3 - nowMs;
|
|
4275
|
+
if (delayMs <= 0 || delayMs + resetMarginMs > maxWaitMs) return void 0;
|
|
4276
|
+
latestDelayMs = Math.max(latestDelayMs ?? 0, delayMs);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
return latestDelayMs;
|
|
4280
|
+
}
|
|
4281
|
+
function publishScheduledRetry({ agent, turn, step, provider, failure }, delayMs, resetAtMs) {
|
|
4282
|
+
if (typeof agent?.session?.append !== "function" || !Number.isSafeInteger(turn) || !Number.isSafeInteger(step)) return void 0;
|
|
4283
|
+
const retryId = `codex-quota-${randomUUID()}`;
|
|
4284
|
+
const message = `Codex usage limit exhausted. Waiting for quota reset at ${new Date(resetAtMs).toISOString()}; retrying shortly afterward.`;
|
|
4285
|
+
const visibleFailure = {
|
|
4286
|
+
code: failure.code,
|
|
4287
|
+
message,
|
|
4288
|
+
...Number.isInteger(failure.status) && failure.status >= 100 && failure.status <= 599 ? { status: failure.status } : {},
|
|
4289
|
+
...typeof failure.requestId === "string" && failure.requestId.length > 0 ? { requestId: failure.requestId } : {}
|
|
4290
|
+
};
|
|
4291
|
+
try {
|
|
4292
|
+
agent.session.append("llm/retry", {
|
|
4293
|
+
retryId,
|
|
4294
|
+
turn,
|
|
4295
|
+
step,
|
|
4296
|
+
provider,
|
|
4297
|
+
mode: "normal",
|
|
4298
|
+
policyKey: retryId,
|
|
4299
|
+
retry: 1,
|
|
4300
|
+
maxRetries: 1,
|
|
4301
|
+
delayMs,
|
|
4302
|
+
failure: visibleFailure
|
|
4303
|
+
});
|
|
4304
|
+
return {
|
|
4305
|
+
retryId,
|
|
4306
|
+
turn,
|
|
4307
|
+
step,
|
|
4308
|
+
retry: 1
|
|
4309
|
+
};
|
|
4310
|
+
} catch {
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function publishRetryStarted(agent, scheduled) {
|
|
4315
|
+
try {
|
|
4316
|
+
agent.session.append("llm/retry-started", scheduled);
|
|
4317
|
+
return true;
|
|
4318
|
+
} catch {
|
|
4319
|
+
return false;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
async function waitForResetOrWake(delayMs, signal, wait, wakeSignal, revision) {
|
|
4323
|
+
const controller = new AbortController();
|
|
4324
|
+
const fusedSignal = signal === void 0 ? controller.signal : AbortSignal.any([signal, controller.signal]);
|
|
4325
|
+
const delayed = Promise.resolve(wait(delayMs, fusedSignal)).then((completed) => completed ? "reset" : "cancelled");
|
|
4326
|
+
const result = await Promise.race([delayed, wakeSignal.wait(revision, fusedSignal)]);
|
|
4327
|
+
controller.abort();
|
|
4328
|
+
return result;
|
|
4329
|
+
}
|
|
4330
|
+
/**
|
|
4331
|
+
* Recover a Codex subscription RATE_LIMIT only when the backend confirms an
|
|
4332
|
+
* exhausted short quota window whose reset is close enough to wait out.
|
|
4333
|
+
*
|
|
4334
|
+
* The listener owns the authoritative wait from `/wham/usage` and publishes
|
|
4335
|
+
* the host's standard model-retry surface event so the turn shows the reason,
|
|
4336
|
+
* countdown, and normal turn cancellation affordance while it is parked.
|
|
4337
|
+
*/
|
|
4338
|
+
function createCodexQuotaRetryHandler({ usageReader, provider = "openai-codex", enabled = () => true, now = Date.now, wait = cancellableDelay, maxWaitMs = DEFAULT_MAX_WAIT_MS, resetMarginMs = DEFAULT_RESET_MARGIN_MS } = {}) {
|
|
4339
|
+
if (usageReader === void 0 || typeof usageReader.read !== "function") throw new TypeError("quota retry requires a usage reader");
|
|
4340
|
+
if (typeof enabled !== "function" || typeof now !== "function" || typeof wait !== "function") throw new TypeError("quota retry requires enablement, clock, and wait functions");
|
|
4341
|
+
if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0 || !Number.isFinite(resetMarginMs) || resetMarginMs < 0) throw new TypeError("quota retry requires a positive wait cap and non-negative reset margin");
|
|
4342
|
+
const wakeSignal = createWakeSignal();
|
|
4343
|
+
const clearCache = async () => {
|
|
4344
|
+
try {
|
|
4345
|
+
if (typeof usageReader.clearCache === "function") await usageReader.clearCache();
|
|
4346
|
+
else await usageReader.clear?.();
|
|
4347
|
+
} catch {}
|
|
4348
|
+
};
|
|
4349
|
+
const handler = async ({ agent, turn, step, provider: requestProvider, failure, signal }, next) => {
|
|
4350
|
+
if (requestProvider !== provider || !isCodexRateLimit(failure) || !enabled()) return next();
|
|
4351
|
+
if (signal?.aborted) return void 0;
|
|
4352
|
+
let wakeRevision = wakeSignal.revision();
|
|
4353
|
+
let usage;
|
|
4354
|
+
try {
|
|
4355
|
+
usage = await usageReader.read({
|
|
4356
|
+
force: true,
|
|
4357
|
+
signal
|
|
4358
|
+
});
|
|
4359
|
+
} catch {
|
|
4360
|
+
if (signal?.aborted) return void 0;
|
|
4361
|
+
return next();
|
|
4362
|
+
}
|
|
4363
|
+
if (signal?.aborted) return void 0;
|
|
4364
|
+
if (!enabled()) return next();
|
|
4365
|
+
const observedAtMs = now();
|
|
4366
|
+
let resetDelayMs = shortResetDelay(usage, observedAtMs, maxWaitMs, resetMarginMs);
|
|
4367
|
+
if (resetDelayMs === void 0) return next();
|
|
4368
|
+
const scheduled = publishScheduledRetry({
|
|
4369
|
+
agent,
|
|
4370
|
+
turn,
|
|
4371
|
+
step,
|
|
4372
|
+
provider: requestProvider,
|
|
4373
|
+
failure
|
|
4374
|
+
}, resetDelayMs + resetMarginMs, observedAtMs + resetDelayMs);
|
|
4375
|
+
if (scheduled === void 0) return next();
|
|
4376
|
+
while (true) {
|
|
4377
|
+
const reason = await waitForResetOrWake(resetDelayMs + resetMarginMs, signal, wait, wakeSignal, wakeRevision);
|
|
4378
|
+
if (reason === "cancelled" || signal?.aborted) return void 0;
|
|
4379
|
+
if (!enabled()) return next();
|
|
4380
|
+
if (reason !== "configuration") break;
|
|
4381
|
+
wakeRevision = wakeSignal.revision();
|
|
4382
|
+
resetDelayMs = shortResetDelay(usage, now(), maxWaitMs, resetMarginMs);
|
|
4383
|
+
if (resetDelayMs === void 0) return next();
|
|
4384
|
+
}
|
|
4385
|
+
await clearCache();
|
|
4386
|
+
if (signal?.aborted) return void 0;
|
|
4387
|
+
if (!publishRetryStarted(agent, scheduled)) return next();
|
|
4388
|
+
return { kind: "retry" };
|
|
4389
|
+
};
|
|
4390
|
+
Object.defineProperties(handler, {
|
|
4391
|
+
notifyAccountChanged: { value: () => wakeSignal.notify("account") },
|
|
4392
|
+
notifyConfigurationChanged: { value: () => wakeSignal.notify("configuration") }
|
|
4393
|
+
});
|
|
4394
|
+
return handler;
|
|
4395
|
+
}
|
|
4396
|
+
//#endregion
|
|
4212
4397
|
//#region src/quota-rate-interval.js
|
|
4213
4398
|
function quotaRateInterval(samples, { quantum = 1, rounding = "unknown" } = {}) {
|
|
4214
4399
|
if (!(quantum > 0) || !Number.isFinite(quantum)) throw new RangeError("Invalid quantum");
|
|
@@ -4879,7 +5064,7 @@ const publicError = (code, message) => ({
|
|
|
4879
5064
|
details: { issues: [] }
|
|
4880
5065
|
}
|
|
4881
5066
|
});
|
|
4882
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections }) {
|
|
5067
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged }) {
|
|
4883
5068
|
return async (endpoint, payload, signal) => {
|
|
4884
5069
|
if ([
|
|
4885
5070
|
"runtime/status",
|
|
@@ -4950,6 +5135,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
4950
5135
|
signal.throwIfAborted();
|
|
4951
5136
|
if (endpoint === "preferences/update") {
|
|
4952
5137
|
const patch = capabilityPatch(payload);
|
|
5138
|
+
if (Object.hasOwn(payload ?? {}, "autoQuotaRetry")) {
|
|
5139
|
+
if (typeof payload["autoQuotaRetry"] !== "boolean") return publicError("internal", "Invalid automatic quota retry preference");
|
|
5140
|
+
patch[AUTO_QUOTA_RETRY_FIELD] = payload[AUTO_QUOTA_RETRY_FIELD];
|
|
5141
|
+
}
|
|
4953
5142
|
for (const [field, rule] of Object.entries(PREFERENCE_FIELDS)) {
|
|
4954
5143
|
if (!Object.hasOwn(payload ?? {}, field)) continue;
|
|
4955
5144
|
if (!rule.choices.includes(payload[field])) return publicError("internal", rule.error);
|
|
@@ -5032,10 +5221,12 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
5032
5221
|
await usageReader.clear();
|
|
5033
5222
|
resetCreditService.clear();
|
|
5034
5223
|
modelCatalog?.clear();
|
|
5224
|
+
onAccountChanged?.();
|
|
5035
5225
|
} else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
|
|
5036
5226
|
usageReader.clearCache();
|
|
5037
5227
|
resetCreditService.clear();
|
|
5038
5228
|
modelCatalog?.clear();
|
|
5229
|
+
onAccountChanged?.();
|
|
5039
5230
|
modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
5040
5231
|
} else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
5041
5232
|
return result;
|
|
@@ -5088,6 +5279,7 @@ function createSearchProviderSwitcher(loader) {
|
|
|
5088
5279
|
});
|
|
5089
5280
|
}
|
|
5090
5281
|
const settingsFields = {
|
|
5282
|
+
[AUTO_QUOTA_RETRY_FIELD]: z.boolean().default(false),
|
|
5091
5283
|
...Object.fromEntries(Object.entries(PREFERENCE_FIELDS).map(([field, rule]) => [field, rule.default === void 0 ? z.union(rule.choices) : z.union(rule.choices).default(rule.default)])),
|
|
5092
5284
|
imageModel: z.union(Object.keys(IMAGE_MODELS)).default(DEFAULT_IMAGE_MODEL),
|
|
5093
5285
|
imageQuality: z.union([
|
|
@@ -5170,6 +5362,7 @@ function apply(ctx, config = {}) {
|
|
|
5170
5362
|
});
|
|
5171
5363
|
const preferences = {
|
|
5172
5364
|
status: () => ({
|
|
5365
|
+
[AUTO_QUOTA_RETRY_FIELD]: normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD]),
|
|
5173
5366
|
compactionMode: settings.get().compactionMode ?? "dsh",
|
|
5174
5367
|
connectionMode: settings.get().connectionMode ?? "sse",
|
|
5175
5368
|
subagentBackend: settings.get().subagentBackend ?? "dsh",
|
|
@@ -5380,6 +5573,30 @@ function apply(ctx, config = {}) {
|
|
|
5380
5573
|
usageReader.clearCache();
|
|
5381
5574
|
};
|
|
5382
5575
|
}, "codex-subscription: quota forecast warm-up");
|
|
5576
|
+
const quotaRetryLifetime = new AbortController();
|
|
5577
|
+
const activeQuotaRetries = /* @__PURE__ */ new Set();
|
|
5578
|
+
const quotaRetryHandler = createCodexQuotaRetryHandler({
|
|
5579
|
+
usageReader,
|
|
5580
|
+
enabled: () => normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD])
|
|
5581
|
+
});
|
|
5582
|
+
const disposeQuotaRetry = ctx.on("agent/request-error", (payload, next) => {
|
|
5583
|
+
if (quotaRetryLifetime.signal.aborted) return Promise.resolve(void 0);
|
|
5584
|
+
const signal = payload.signal === void 0 ? quotaRetryLifetime.signal : AbortSignal.any([payload.signal, quotaRetryLifetime.signal]);
|
|
5585
|
+
const tracked = quotaRetryHandler({
|
|
5586
|
+
...payload,
|
|
5587
|
+
signal
|
|
5588
|
+
}, next).finally(() => activeQuotaRetries.delete(tracked));
|
|
5589
|
+
activeQuotaRetries.add(tracked);
|
|
5590
|
+
return tracked;
|
|
5591
|
+
});
|
|
5592
|
+
ctx.effect(() => async () => {
|
|
5593
|
+
disposeQuotaRetry();
|
|
5594
|
+
quotaRetryLifetime.abort(/* @__PURE__ */ new Error("codex-subscription quota retry disposed"));
|
|
5595
|
+
await Promise.allSettled([...activeQuotaRetries]);
|
|
5596
|
+
}, "codex-subscription: abort and drain quota recovery");
|
|
5597
|
+
ctx.effect(() => settings.watch((value, previous) => {
|
|
5598
|
+
if (normalizeAutoQuotaRetry(value["autoQuotaRetry"]) !== normalizeAutoQuotaRetry(previous?.["autoQuotaRetry"])) quotaRetryHandler.notifyConfigurationChanged();
|
|
5599
|
+
}), "codex-subscription: update quota recovery preference");
|
|
5383
5600
|
const resetCreditService = createCodexResetCreditService({
|
|
5384
5601
|
getAuth: resolveAuth,
|
|
5385
5602
|
readCredential: (options) => store.read(PROVIDER, options),
|
|
@@ -5412,6 +5629,7 @@ function apply(ctx, config = {}) {
|
|
|
5412
5629
|
resetCreditService,
|
|
5413
5630
|
preferences,
|
|
5414
5631
|
runtimeManagement,
|
|
5632
|
+
onAccountChanged: quotaRetryHandler.notifyAccountChanged,
|
|
5415
5633
|
diagnosticsReader: () => createSubscriptionDiagnostics({
|
|
5416
5634
|
auth,
|
|
5417
5635
|
preferences,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-subscription",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
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",
|