dsh-codex-subscription 2.1.9 → 2.2.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 +1 -0
- package/README.md +1 -0
- package/lib/client.js +59 -9
- package/lib/index.js +219 -2
- 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.
|
|
7293
|
+
const PACKAGE_VERSION = "2.2.0";
|
|
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",
|
|
@@ -2829,7 +2831,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
|
|
|
2829
2831
|
Object.freeze(["0.82.1", "0.85.1"]);
|
|
2830
2832
|
//#endregion
|
|
2831
2833
|
//#region src/version.js
|
|
2832
|
-
const PACKAGE_VERSION = "2.
|
|
2834
|
+
const PACKAGE_VERSION = "2.2.0";
|
|
2833
2835
|
const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
|
|
2834
2836
|
//#endregion
|
|
2835
2837
|
//#region src/model-catalog.js
|
|
@@ -3972,6 +3974,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
|
|
|
3972
3974
|
...gaps.length ? { unsupported: gaps } : {}
|
|
3973
3975
|
} } : {},
|
|
3974
3976
|
configuration: {
|
|
3977
|
+
...typeof preference.autoQuotaRetry === "boolean" ? { autoQuotaRetry: preference.autoQuotaRetry } : {},
|
|
3975
3978
|
contextMode: preference.contextMode,
|
|
3976
3979
|
quickQuotaMode: preference.quickQuotaMode,
|
|
3977
3980
|
...typeof preference.outputVerbosity === "string" ? { outputVerbosity: preference.outputVerbosity } : {},
|
|
@@ -4209,6 +4212,187 @@ function createCodexUsageReader(options) {
|
|
|
4209
4212
|
});
|
|
4210
4213
|
}
|
|
4211
4214
|
//#endregion
|
|
4215
|
+
//#region src/quota-retry.js
|
|
4216
|
+
const DEFAULT_MAX_WAIT_MS = 360 * 60 * 1e3;
|
|
4217
|
+
const DEFAULT_RESET_MARGIN_MS = 1e4;
|
|
4218
|
+
const EXHAUSTED_PERCENT = 99.9;
|
|
4219
|
+
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;
|
|
4220
|
+
const isCodexRateLimit = (failure) => failure?.code === "RATE_LIMIT" || failure?.code === "PI_AI_ERROR" && typeof failure.message === "string" && CODEX_USAGE_LIMIT_MESSAGE.test(failure.message);
|
|
4221
|
+
function cancellableDelay(delayMs, signal) {
|
|
4222
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
4223
|
+
return new Promise((resolve) => {
|
|
4224
|
+
const timer = setTimeout(() => {
|
|
4225
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4226
|
+
resolve(true);
|
|
4227
|
+
}, delayMs);
|
|
4228
|
+
function onAbort() {
|
|
4229
|
+
clearTimeout(timer);
|
|
4230
|
+
resolve(false);
|
|
4231
|
+
}
|
|
4232
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
function createWakeSignal() {
|
|
4236
|
+
let revision = 0;
|
|
4237
|
+
let reason;
|
|
4238
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
4239
|
+
const notify = (nextReason) => {
|
|
4240
|
+
revision += 1;
|
|
4241
|
+
reason = nextReason;
|
|
4242
|
+
for (const waiter of [...waiters]) waiter(nextReason);
|
|
4243
|
+
};
|
|
4244
|
+
const wait = (since, signal) => {
|
|
4245
|
+
if (revision !== since) return Promise.resolve(reason);
|
|
4246
|
+
if (signal?.aborted) return Promise.resolve("cancelled");
|
|
4247
|
+
return new Promise((resolve) => {
|
|
4248
|
+
const settle = (value) => {
|
|
4249
|
+
waiters.delete(settle);
|
|
4250
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4251
|
+
resolve(value);
|
|
4252
|
+
};
|
|
4253
|
+
const onAbort = () => settle("cancelled");
|
|
4254
|
+
waiters.add(settle);
|
|
4255
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4256
|
+
if (revision !== since) settle(reason);
|
|
4257
|
+
});
|
|
4258
|
+
};
|
|
4259
|
+
return {
|
|
4260
|
+
revision: () => revision,
|
|
4261
|
+
notify,
|
|
4262
|
+
wait
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
function shortResetDelay(usage, nowMs, maxWaitMs, resetMarginMs) {
|
|
4266
|
+
let latestDelayMs;
|
|
4267
|
+
for (const limit of usage?.rateLimits ?? []) {
|
|
4268
|
+
if (limit?.id !== "codex") continue;
|
|
4269
|
+
for (const window of limit?.windows ?? []) {
|
|
4270
|
+
if (!Number.isFinite(window?.usedPercent) || window.usedPercent < EXHAUSTED_PERCENT) continue;
|
|
4271
|
+
if (!Number.isFinite(window?.windowSeconds) || window.windowSeconds * 1e3 > maxWaitMs) return void 0;
|
|
4272
|
+
if (!Number.isFinite(window?.resetsAt)) return void 0;
|
|
4273
|
+
const delayMs = window.resetsAt * 1e3 - nowMs;
|
|
4274
|
+
if (delayMs <= 0 || delayMs + resetMarginMs > maxWaitMs) return void 0;
|
|
4275
|
+
latestDelayMs = Math.max(latestDelayMs ?? 0, delayMs);
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
return latestDelayMs;
|
|
4279
|
+
}
|
|
4280
|
+
function publishScheduledRetry({ agent, turn, step, provider, failure }, delayMs, resetAtMs) {
|
|
4281
|
+
if (typeof agent?.session?.append !== "function" || !Number.isSafeInteger(turn) || !Number.isSafeInteger(step)) return void 0;
|
|
4282
|
+
const retryId = `codex-quota-${randomUUID()}`;
|
|
4283
|
+
const message = `Codex usage limit exhausted. Waiting for quota reset at ${new Date(resetAtMs).toISOString()}; retrying shortly afterward.`;
|
|
4284
|
+
const visibleFailure = {
|
|
4285
|
+
code: failure.code,
|
|
4286
|
+
message,
|
|
4287
|
+
...Number.isInteger(failure.status) && failure.status >= 100 && failure.status <= 599 ? { status: failure.status } : {},
|
|
4288
|
+
...typeof failure.requestId === "string" && failure.requestId.length > 0 ? { requestId: failure.requestId } : {}
|
|
4289
|
+
};
|
|
4290
|
+
try {
|
|
4291
|
+
agent.session.append("llm/retry", {
|
|
4292
|
+
retryId,
|
|
4293
|
+
turn,
|
|
4294
|
+
step,
|
|
4295
|
+
provider,
|
|
4296
|
+
mode: "normal",
|
|
4297
|
+
policyKey: retryId,
|
|
4298
|
+
retry: 1,
|
|
4299
|
+
maxRetries: 1,
|
|
4300
|
+
delayMs,
|
|
4301
|
+
failure: visibleFailure
|
|
4302
|
+
});
|
|
4303
|
+
return {
|
|
4304
|
+
retryId,
|
|
4305
|
+
turn,
|
|
4306
|
+
step,
|
|
4307
|
+
retry: 1
|
|
4308
|
+
};
|
|
4309
|
+
} catch {
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
function publishRetryStarted(agent, scheduled) {
|
|
4314
|
+
try {
|
|
4315
|
+
agent.session.append("llm/retry-started", scheduled);
|
|
4316
|
+
return true;
|
|
4317
|
+
} catch {
|
|
4318
|
+
return false;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
async function waitForResetOrWake(delayMs, signal, wait, wakeSignal, revision) {
|
|
4322
|
+
const controller = new AbortController();
|
|
4323
|
+
const fusedSignal = signal === void 0 ? controller.signal : AbortSignal.any([signal, controller.signal]);
|
|
4324
|
+
const delayed = Promise.resolve(wait(delayMs, fusedSignal)).then((completed) => completed ? "reset" : "cancelled");
|
|
4325
|
+
const result = await Promise.race([delayed, wakeSignal.wait(revision, fusedSignal)]);
|
|
4326
|
+
controller.abort();
|
|
4327
|
+
return result;
|
|
4328
|
+
}
|
|
4329
|
+
/**
|
|
4330
|
+
* Recover a Codex subscription RATE_LIMIT only when the backend confirms an
|
|
4331
|
+
* exhausted short quota window whose reset is close enough to wait out.
|
|
4332
|
+
*
|
|
4333
|
+
* The listener owns the authoritative wait from `/wham/usage` and publishes
|
|
4334
|
+
* the host's standard model-retry surface event so the turn shows the reason,
|
|
4335
|
+
* countdown, and normal turn cancellation affordance while it is parked.
|
|
4336
|
+
*/
|
|
4337
|
+
function createCodexQuotaRetryHandler({ usageReader, provider = "openai-codex", enabled = () => true, now = Date.now, wait = cancellableDelay, maxWaitMs = DEFAULT_MAX_WAIT_MS, resetMarginMs = DEFAULT_RESET_MARGIN_MS } = {}) {
|
|
4338
|
+
if (usageReader === void 0 || typeof usageReader.read !== "function") throw new TypeError("quota retry requires a usage reader");
|
|
4339
|
+
if (typeof enabled !== "function" || typeof now !== "function" || typeof wait !== "function") throw new TypeError("quota retry requires enablement, clock, and wait functions");
|
|
4340
|
+
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");
|
|
4341
|
+
const wakeSignal = createWakeSignal();
|
|
4342
|
+
const clearCache = async () => {
|
|
4343
|
+
try {
|
|
4344
|
+
if (typeof usageReader.clearCache === "function") await usageReader.clearCache();
|
|
4345
|
+
else await usageReader.clear?.();
|
|
4346
|
+
} catch {}
|
|
4347
|
+
};
|
|
4348
|
+
const handler = async ({ agent, turn, step, provider: requestProvider, failure, signal }, next) => {
|
|
4349
|
+
if (requestProvider !== provider || !isCodexRateLimit(failure) || !enabled()) return next();
|
|
4350
|
+
if (signal?.aborted) return void 0;
|
|
4351
|
+
let wakeRevision = wakeSignal.revision();
|
|
4352
|
+
let usage;
|
|
4353
|
+
try {
|
|
4354
|
+
usage = await usageReader.read({
|
|
4355
|
+
force: true,
|
|
4356
|
+
signal
|
|
4357
|
+
});
|
|
4358
|
+
} catch {
|
|
4359
|
+
if (signal?.aborted) return void 0;
|
|
4360
|
+
return next();
|
|
4361
|
+
}
|
|
4362
|
+
if (signal?.aborted) return void 0;
|
|
4363
|
+
if (!enabled()) return next();
|
|
4364
|
+
const observedAtMs = now();
|
|
4365
|
+
let resetDelayMs = shortResetDelay(usage, observedAtMs, maxWaitMs, resetMarginMs);
|
|
4366
|
+
if (resetDelayMs === void 0) return next();
|
|
4367
|
+
const scheduled = publishScheduledRetry({
|
|
4368
|
+
agent,
|
|
4369
|
+
turn,
|
|
4370
|
+
step,
|
|
4371
|
+
provider: requestProvider,
|
|
4372
|
+
failure
|
|
4373
|
+
}, resetDelayMs + resetMarginMs, observedAtMs + resetDelayMs);
|
|
4374
|
+
if (scheduled === void 0) return next();
|
|
4375
|
+
while (true) {
|
|
4376
|
+
const reason = await waitForResetOrWake(resetDelayMs + resetMarginMs, signal, wait, wakeSignal, wakeRevision);
|
|
4377
|
+
if (reason === "cancelled" || signal?.aborted) return void 0;
|
|
4378
|
+
if (!enabled()) return next();
|
|
4379
|
+
if (reason !== "configuration") break;
|
|
4380
|
+
wakeRevision = wakeSignal.revision();
|
|
4381
|
+
resetDelayMs = shortResetDelay(usage, now(), maxWaitMs, resetMarginMs);
|
|
4382
|
+
if (resetDelayMs === void 0) return next();
|
|
4383
|
+
}
|
|
4384
|
+
await clearCache();
|
|
4385
|
+
if (signal?.aborted) return void 0;
|
|
4386
|
+
if (!publishRetryStarted(agent, scheduled)) return next();
|
|
4387
|
+
return { kind: "retry" };
|
|
4388
|
+
};
|
|
4389
|
+
Object.defineProperties(handler, {
|
|
4390
|
+
notifyAccountChanged: { value: () => wakeSignal.notify("account") },
|
|
4391
|
+
notifyConfigurationChanged: { value: () => wakeSignal.notify("configuration") }
|
|
4392
|
+
});
|
|
4393
|
+
return handler;
|
|
4394
|
+
}
|
|
4395
|
+
//#endregion
|
|
4212
4396
|
//#region src/quota-rate-interval.js
|
|
4213
4397
|
function quotaRateInterval(samples, { quantum = 1, rounding = "unknown" } = {}) {
|
|
4214
4398
|
if (!(quantum > 0) || !Number.isFinite(quantum)) throw new RangeError("Invalid quantum");
|
|
@@ -4879,7 +5063,7 @@ const publicError = (code, message) => ({
|
|
|
4879
5063
|
details: { issues: [] }
|
|
4880
5064
|
}
|
|
4881
5065
|
});
|
|
4882
|
-
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections }) {
|
|
5066
|
+
function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged }) {
|
|
4883
5067
|
return async (endpoint, payload, signal) => {
|
|
4884
5068
|
if ([
|
|
4885
5069
|
"runtime/status",
|
|
@@ -4950,6 +5134,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
4950
5134
|
signal.throwIfAborted();
|
|
4951
5135
|
if (endpoint === "preferences/update") {
|
|
4952
5136
|
const patch = capabilityPatch(payload);
|
|
5137
|
+
if (Object.hasOwn(payload ?? {}, "autoQuotaRetry")) {
|
|
5138
|
+
if (typeof payload["autoQuotaRetry"] !== "boolean") return publicError("internal", "Invalid automatic quota retry preference");
|
|
5139
|
+
patch[AUTO_QUOTA_RETRY_FIELD] = payload[AUTO_QUOTA_RETRY_FIELD];
|
|
5140
|
+
}
|
|
4953
5141
|
for (const [field, rule] of Object.entries(PREFERENCE_FIELDS)) {
|
|
4954
5142
|
if (!Object.hasOwn(payload ?? {}, field)) continue;
|
|
4955
5143
|
if (!rule.choices.includes(payload[field])) return publicError("internal", rule.error);
|
|
@@ -5032,10 +5220,12 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
|
|
|
5032
5220
|
await usageReader.clear();
|
|
5033
5221
|
resetCreditService.clear();
|
|
5034
5222
|
modelCatalog?.clear();
|
|
5223
|
+
onAccountChanged?.();
|
|
5035
5224
|
} else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
|
|
5036
5225
|
usageReader.clearCache();
|
|
5037
5226
|
resetCreditService.clear();
|
|
5038
5227
|
modelCatalog?.clear();
|
|
5228
|
+
onAccountChanged?.();
|
|
5039
5229
|
modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
5040
5230
|
} else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
|
|
5041
5231
|
return result;
|
|
@@ -5088,6 +5278,7 @@ function createSearchProviderSwitcher(loader) {
|
|
|
5088
5278
|
});
|
|
5089
5279
|
}
|
|
5090
5280
|
const settingsFields = {
|
|
5281
|
+
[AUTO_QUOTA_RETRY_FIELD]: z.boolean().default(false),
|
|
5091
5282
|
...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
5283
|
imageModel: z.union(Object.keys(IMAGE_MODELS)).default(DEFAULT_IMAGE_MODEL),
|
|
5093
5284
|
imageQuality: z.union([
|
|
@@ -5170,6 +5361,7 @@ function apply(ctx, config = {}) {
|
|
|
5170
5361
|
});
|
|
5171
5362
|
const preferences = {
|
|
5172
5363
|
status: () => ({
|
|
5364
|
+
[AUTO_QUOTA_RETRY_FIELD]: normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD]),
|
|
5173
5365
|
compactionMode: settings.get().compactionMode ?? "dsh",
|
|
5174
5366
|
connectionMode: settings.get().connectionMode ?? "sse",
|
|
5175
5367
|
subagentBackend: settings.get().subagentBackend ?? "dsh",
|
|
@@ -5380,6 +5572,30 @@ function apply(ctx, config = {}) {
|
|
|
5380
5572
|
usageReader.clearCache();
|
|
5381
5573
|
};
|
|
5382
5574
|
}, "codex-subscription: quota forecast warm-up");
|
|
5575
|
+
const quotaRetryLifetime = new AbortController();
|
|
5576
|
+
const activeQuotaRetries = /* @__PURE__ */ new Set();
|
|
5577
|
+
const quotaRetryHandler = createCodexQuotaRetryHandler({
|
|
5578
|
+
usageReader,
|
|
5579
|
+
enabled: () => normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD])
|
|
5580
|
+
});
|
|
5581
|
+
const disposeQuotaRetry = ctx.on("agent/request-error", (payload, next) => {
|
|
5582
|
+
if (quotaRetryLifetime.signal.aborted) return Promise.resolve(void 0);
|
|
5583
|
+
const signal = payload.signal === void 0 ? quotaRetryLifetime.signal : AbortSignal.any([payload.signal, quotaRetryLifetime.signal]);
|
|
5584
|
+
const tracked = quotaRetryHandler({
|
|
5585
|
+
...payload,
|
|
5586
|
+
signal
|
|
5587
|
+
}, next).finally(() => activeQuotaRetries.delete(tracked));
|
|
5588
|
+
activeQuotaRetries.add(tracked);
|
|
5589
|
+
return tracked;
|
|
5590
|
+
});
|
|
5591
|
+
ctx.effect(() => async () => {
|
|
5592
|
+
disposeQuotaRetry();
|
|
5593
|
+
quotaRetryLifetime.abort(/* @__PURE__ */ new Error("codex-subscription quota retry disposed"));
|
|
5594
|
+
await Promise.allSettled([...activeQuotaRetries]);
|
|
5595
|
+
}, "codex-subscription: abort and drain quota recovery");
|
|
5596
|
+
ctx.effect(() => settings.watch((value, previous) => {
|
|
5597
|
+
if (normalizeAutoQuotaRetry(value["autoQuotaRetry"]) !== normalizeAutoQuotaRetry(previous?.["autoQuotaRetry"])) quotaRetryHandler.notifyConfigurationChanged();
|
|
5598
|
+
}), "codex-subscription: update quota recovery preference");
|
|
5383
5599
|
const resetCreditService = createCodexResetCreditService({
|
|
5384
5600
|
getAuth: resolveAuth,
|
|
5385
5601
|
readCredential: (options) => store.read(PROVIDER, options),
|
|
@@ -5412,6 +5628,7 @@ function apply(ctx, config = {}) {
|
|
|
5412
5628
|
resetCreditService,
|
|
5413
5629
|
preferences,
|
|
5414
5630
|
runtimeManagement,
|
|
5631
|
+
onAccountChanged: quotaRetryHandler.notifyAccountChanged,
|
|
5415
5632
|
diagnosticsReader: () => createSubscriptionDiagnostics({
|
|
5416
5633
|
auth,
|
|
5417
5634
|
preferences,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-codex-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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",
|