codex-multi-auth 2.23.0 → 2.24.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.
Files changed (84) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +159 -152
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/dist/index.js +33 -10
  11. package/dist/lib/account-policy.d.ts +2 -0
  12. package/dist/lib/account-policy.js +3 -1
  13. package/dist/lib/accounts/rate-limits.d.ts +6 -0
  14. package/dist/lib/accounts/rate-limits.js +14 -1
  15. package/dist/lib/accounts.d.ts +6 -1
  16. package/dist/lib/accounts.js +33 -8
  17. package/dist/lib/auth/token-utils.d.ts +5 -0
  18. package/dist/lib/auth/token-utils.js +10 -0
  19. package/dist/lib/codex-manager/commands/account.js +15 -0
  20. package/dist/lib/codex-manager/commands/resets.js +18 -5
  21. package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
  22. package/dist/lib/codex-manager/commands/workspace.js +33 -2
  23. package/dist/lib/codex-manager/help.js +2 -2
  24. package/dist/lib/codex-manager.js +14 -1
  25. package/dist/lib/constants.d.ts +2 -0
  26. package/dist/lib/constants.js +2 -0
  27. package/dist/lib/context-budget-response.js +8 -8
  28. package/dist/lib/context-overflow.js +10 -10
  29. package/dist/lib/oauth-success.html +125 -125
  30. package/dist/lib/policy/runtime-policy-cache.js +1 -1
  31. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  32. package/dist/lib/policy/runtime-policy.js +4 -0
  33. package/dist/lib/preemptive-quota-scheduler.js +12 -3
  34. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  35. package/dist/lib/prompts/codex.js +79 -79
  36. package/dist/lib/quota-cache.d.ts +4 -0
  37. package/dist/lib/quota-readiness.d.ts +1 -1
  38. package/dist/lib/quota-readiness.js +9 -2
  39. package/dist/lib/request/rate-limit-decision.js +1 -1
  40. package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
  41. package/dist/lib/runtime/account-reset-credits.js +35 -12
  42. package/dist/lib/runtime/automatic-account-checks.js +6 -5
  43. package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
  44. package/dist/lib/runtime/model-discovery-status.js +2 -2
  45. package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
  46. package/dist/lib/runtime/reset-credit-routing.js +66 -21
  47. package/dist/lib/runtime/reset-credits.d.ts +31 -7
  48. package/dist/lib/runtime/reset-credits.js +135 -28
  49. package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
  50. package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
  51. package/dist/lib/runtime/subscription-quota-order.js +7 -3
  52. package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
  53. package/dist/lib/runtime/workspace-model-scopes.js +14 -4
  54. package/dist/lib/runtime-rotation-proxy.js +108 -29
  55. package/dist/lib/storage/snapshot-merge.js +15 -4
  56. package/dist/lib/web-dashboard-html.js +53 -53
  57. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  58. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  59. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  60. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  61. package/node_modules/@codex-ai/plugin/package.json +9 -9
  62. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  63. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  64. package/node_modules/@codex-ai/sdk/package.json +8 -8
  65. package/package.json +209 -209
  66. package/scripts/codex-app-launcher.js +653 -653
  67. package/scripts/codex-app-router.js +446 -446
  68. package/scripts/codex-bin-resolver.js +332 -332
  69. package/scripts/codex-multi-auth.js +65 -65
  70. package/scripts/codex-routing.js +63 -63
  71. package/scripts/codex.js +7143 -7143
  72. package/scripts/install-codex-auth-utils.js +213 -213
  73. package/scripts/mcodex.js +367 -367
  74. package/scripts/postinstall.js +114 -114
  75. package/scripts/preuninstall.js +265 -265
  76. package/skills/codex-auth-setup/SKILL.md +73 -73
  77. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  78. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  79. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  80. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  81. package/vendor/codex-ai-plugin/package.json +9 -9
  82. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  83. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  84. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -6,11 +6,11 @@ import { fetchWithProxy } from "./request/proxy-fetch.js";
6
6
  import { createNativeAccountStorageReader } from "./runtime/native-account-storage.js";
7
7
  import { resetSnapshotQuota } from "./runtime/reset-credits.js";
8
8
  import { createResetCreditService, loadResetCreditState } from "./runtime/account-reset-credits.js";
9
- import { recoverResetQuota, applyConfirmedReset } from "./runtime/reset-credit-routing.js";
9
+ import { recoverResetQuota, applyConfirmedReset, primaryPoolFullyExhausted } from "./runtime/reset-credit-routing.js";
10
10
  import { readSubscriptionQuotaEvent } from "./runtime/subscription-quota-event.js";
11
11
  import { loadQuotaCache } from "./quota-cache.js";
12
12
  import { findQuotaCacheEntryForAccount } from "./quota-readiness.js";
13
- import { subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
13
+ import { hasFreshOrdinaryUsagePermission, subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
14
14
  import { inferenceAccountKey } from "./runtime/inference-activity.js";
15
15
  import { modelScopeId, workspaceModelScopes } from "./runtime/workspace-model-scopes.js";
16
16
  import { RuntimeCapabilityFailures, classifyCapabilityFailure } from "./runtime/runtime-capability-failures.js";
@@ -33,7 +33,7 @@ import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./a
33
33
  import { withRoutingMutex } from "./routing-mutex.js";
34
34
  import { getQuotaKey } from "./accounts/rate-limits.js";
35
35
  import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getContextBudgetGuardEnabled, getContextBudgetGuardSoftPercent, getContextBudgetGuardHardPercent, getContextBudgetGuardModelWindowOverrides, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
36
- import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
36
+ import { CODEX_BASE_URL, HTTP_STATUS, LUNA_RESERVE_MODEL, MAX_RATE_LIMIT_DELAY_MS, RATE_LIMIT_CACHE_TTL_MS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
37
37
  import { getModelFamily } from "./prompts/codex.js";
38
38
  import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
39
39
  import { recordRuntimeInferenceRequest, flushRuntimeInferenceActivity, mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
@@ -830,7 +830,8 @@ function writeUnauthorized(res) {
830
830
  }
831
831
  function writePoolExhausted(params) {
832
832
  const { res, accountManager, family, model, reason } = params;
833
- const waitMs = params.retryAfterMs ?? accountManager.getMinWaitTimeForFamily(family, model);
833
+ const waitMs = params.retryAfterMs ??
834
+ accountManager.getMinWaitTimeForFamily(family, model, params.ineligibleAccountIndexes);
834
835
  const accountCount = accountManager.getAccountCount();
835
836
  const accountSkipReasons = params.accountSkipReasons ?? {};
836
837
  recordRuntimePoolExhaustion({
@@ -1046,6 +1047,7 @@ export async function startRuntimeRotationProxy(options) {
1046
1047
  await Promise.all([automaticChecks?.stop(), automaticApiChecks?.stop()]);
1047
1048
  websocketGateway.close();
1048
1049
  await closeServer(server, sockets);
1050
+ state.subscriptionQuotaRejections?.clear();
1049
1051
  await flushRuntimeInferenceActivity();
1050
1052
  await state.activeAccountManager.flushPendingSave();
1051
1053
  },
@@ -1607,7 +1609,7 @@ async function handleRequestInner(state, req, res, traceId) {
1607
1609
  }
1608
1610
  }
1609
1611
  };
1610
- const resetCreditState = state.nativeOpenai && isResponsesRequest ? await cachedRead(state, "reset-credits", loadResetCreditState).catch(() => null) : null;
1612
+ let resetCreditState = state.nativeOpenai && isResponsesRequest ? await cachedRead(state, "reset-credits", loadResetCreditState).catch(() => null) : null;
1611
1613
  const quotaForScope = (account, scope) => {
1612
1614
  // Scoped observations belong to this credential and workspace; legacy entries only describe the binding.
1613
1615
  const cached = findQuotaCacheEntryForAccount(subscriptionQuotaCache, account, accountManager.getAccountsSnapshot(), undefined, scope.accountId);
@@ -1727,7 +1729,7 @@ async function handleRequestInner(state, req, res, traceId) {
1727
1729
  if (retryAt > state.now())
1728
1730
  throw new CatalogRetryError(retryAt - state.now());
1729
1731
  backoff.delete(key);
1730
- const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.enabled);
1732
+ const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.routable);
1731
1733
  const account = scope ? state.activeAccountManager.getAccountByIndex(scope.accountIndex) : null;
1732
1734
  if (!account || account.enabled === false || account.authInvalidatedAt ||
1733
1735
  (account.cooldownReason === "auth-failure" && (account.coolingDownUntil ?? 0) > state.now()))
@@ -1800,7 +1802,7 @@ async function handleRequestInner(state, req, res, traceId) {
1800
1802
  a.email === reference.email &&
1801
1803
  a.accountId === reference.accountId)));
1802
1804
  const scopes = discovery.flatMap(workspaceModelScopes);
1803
- const enabledKeys = scopes.filter(scope => scope.enabled).map(scope => scope.id);
1805
+ const enabledKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
1804
1806
  const routableKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
1805
1807
  const oauthRefresh = modelCatalog.list(enabledKeys);
1806
1808
  // Explicit check waits for all workspaces; picker discovery runs API and OAuth in parallel.
@@ -1968,20 +1970,45 @@ async function handleRequestInner(state, req, res, traceId) {
1968
1970
  observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now(), clearQuotaScheduler: a => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(a, context.family, context.model)) });
1969
1971
  }
1970
1972
  }
1971
- if (state.nativeOpenai && isResponsesRequest && context.model && context.model !== LUNA_RESERVE_MODEL && !isPinned) {
1973
+ const primaryWorkspaceCandidates = () => new Map([...workspaceCandidates].filter(([index]) => {
1974
+ const account = accountManager.getAccountByIndex(index);
1975
+ // Catalog candidates can outlive credential invalidation while preparing a request.
1976
+ return account && account.enabled !== false && !account.authInvalidatedAt &&
1977
+ !policyDecision?.emergencyAccountIndexes?.has(index) && !policyDecision?.blockedAccountIndexes.has(index);
1978
+ }));
1979
+ const emergencyBackupsAllowed = () => state.nativeOpenai && isResponsesRequest &&
1980
+ context.model !== LUNA_RESERVE_MODEL && primaryPoolFullyExhausted(primaryWorkspaceCandidates(), resetCreditState, state.now(), (index, scope) => {
1981
+ const account = accountManager.getAccountByIndex(index);
1982
+ return account ? quotaForScope(account, scope) : undefined;
1983
+ });
1984
+ const recoverPrimaryQuota = async () => {
1985
+ if (!state.nativeOpenai || !isResponsesRequest || !context.model || context.model === LUNA_RESERVE_MODEL || isPinned)
1986
+ return false;
1972
1987
  try {
1973
- await recoverResetQuota({ model: context.model, family: context.family, native: true, pinned: false, manager: accountManager,
1974
- scopes: workspaceCandidates, quotaForScope, priorityByAccount: policyDecision?.priorityByAccount, preferredIndex: storageMeta.pinnedAccountIndex, service: createResetCreditService(accountManager),
1975
- observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now,
1988
+ const observations = state.subscriptionQuotaObservations ??= new Map();
1989
+ const recovered = await recoverResetQuota({ model: context.model, family: context.family, native: true, pinned: false, manager: accountManager,
1990
+ scopes: primaryWorkspaceCandidates(), quotaForScope, priorityByAccount: policyDecision?.priorityByAccount, preferredIndex: storageMeta.pinnedAccountIndex, service: createResetCreditService(accountManager),
1991
+ observations, quotaRejections: state.subscriptionQuotaRejections, now: state.now,
1976
1992
  clearQuotaScheduler: account => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(account, context.family, context.model)),
1977
1993
  });
1994
+ try {
1995
+ resetCreditState = await loadResetCreditState();
1996
+ }
1997
+ catch {
1998
+ resetCreditState = null;
1999
+ }
2000
+ return recovered;
1978
2001
  }
1979
2002
  catch {
2003
+ resetCreditState = null;
1980
2004
  state.status.lastError = "Subscription reset recovery could not be confirmed; no further automatic redemption attempted.";
2005
+ return false;
1981
2006
  }
1982
- // Recovery may have redeemed and written new reset state; the next request re-reads it.
1983
- state.readCache?.delete("reset-credits");
1984
- }
2007
+ finally {
2008
+ state.readCache?.delete("reset-credits");
2009
+ }
2010
+ };
2011
+ await recoverPrimaryQuota();
1985
2012
  // A pinned Luna request can arrive after an earlier turn already proved
1986
2013
  // ordinary subscription quota exhausted. That persisted family marker
1987
2014
  // would make chooseAccount reject the hard pin before any upstream call,
@@ -2032,8 +2059,10 @@ async function handleRequestInner(state, req, res, traceId) {
2032
2059
  // alternative account, so exhausting that local heuristic can only reject a
2033
2060
  // request that the pinned account could serve. Transcription is not inference
2034
2061
  // and must not debit the Codex model pool or claim its inference circuit probe.
2062
+ // Native subscriptions use upstream quota for admission; local scoring must
2063
+ // not synthesize a 429 while actual subscription capacity remains.
2035
2064
  // Transcription uses credential-only admission below.
2036
- const bypassPoolTokenBucket = isPinned || isTranscribeRequest;
2065
+ const bypassPoolTokenBucket = isPinned || isTranscribeRequest || (state.nativeOpenai && isResponsesRequest);
2037
2066
  const refundConsumedPoolToken = (account) => {
2038
2067
  if (!bypassPoolTokenBucket) {
2039
2068
  accountManager.refundToken(account, context.family, context.model);
@@ -2125,8 +2154,10 @@ async function handleRequestInner(state, req, res, traceId) {
2125
2154
  return "retry";
2126
2155
  };
2127
2156
  let runtimeSelectionIterations = 0;
2157
+ let recoveryRetriedAfterExhaustion = false;
2128
2158
  while ((isPinned ||
2129
2159
  attemptedIndexes.size < accountCount ||
2160
+ (!isPinned && !recoveryRetriedAfterExhaustion) ||
2130
2161
  (isTranscribeRequest && !state.nativeOpenai && accountCount === 0 && !reloadedAfterNoAccount)) &&
2131
2162
  transientAttempts < transientAttemptLimit &&
2132
2163
  (!isPinned ||
@@ -2177,6 +2208,14 @@ async function handleRequestInner(state, req, res, traceId) {
2177
2208
  }
2178
2209
  }
2179
2210
  const attemptedForSelection = new Set([...attemptedIndexes, ...catalogExcludedIndexes()]);
2211
+ if (!isPinned && policyDecision?.emergencyAccountIndexes?.size) {
2212
+ const backupsAllowed = emergencyBackupsAllowed();
2213
+ if (!backupsAllowed)
2214
+ for (const index of policyDecision.emergencyAccountIndexes) {
2215
+ attemptedForSelection.add(index);
2216
+ accountSkipReasons.set(index, "policy: emergency backup reserved");
2217
+ }
2218
+ }
2180
2219
  if (isTranscribeRequest) {
2181
2220
  const candidate = chooseTranscriptionAccount({
2182
2221
  accountManager,
@@ -2248,6 +2287,20 @@ async function handleRequestInner(state, req, res, traceId) {
2248
2287
  for (const index of catalogExcludedIndexes())
2249
2288
  accountSkipReasons.set(index, "model-not-supported");
2250
2289
  if (!selected) {
2290
+ // A 429 can be the first evidence that cached ordinary capacity is gone.
2291
+ // Recheck once before failing; only an explicit pre-inference rejection
2292
+ // reaches this path, never an ambiguous or already-streaming generation.
2293
+ if (!isPinned && !recoveryRetriedAfterExhaustion) {
2294
+ recoveryRetriedAfterExhaustion = true;
2295
+ const backupsWereAllowed = emergencyBackupsAllowed();
2296
+ if (await recoverPrimaryQuota()) {
2297
+ attemptedIndexes.clear();
2298
+ accountSkipReasons.clear();
2299
+ continue;
2300
+ }
2301
+ if (!backupsWereAllowed && emergencyBackupsAllowed())
2302
+ continue;
2303
+ }
2251
2304
  if (isTranscribeRequest &&
2252
2305
  !state.nativeOpenai &&
2253
2306
  !reloadedAfterNoAccount) {
@@ -2347,10 +2400,12 @@ async function handleRequestInner(state, req, res, traceId) {
2347
2400
  }
2348
2401
  attemptedIndexes.add(selected.index);
2349
2402
  const quotaScheduleKey = buildQuotaScheduleKey(selected, context.family, context.model);
2403
+ const selectedScope = workspaceCandidates.get(selected.index)?.[0];
2404
+ const nativePermission = selectedScope && hasFreshOrdinaryUsagePermission(quotaForScope(selected, selectedScope), state.now());
2350
2405
  const preemptiveDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
2351
2406
  if (!isTranscribeRequest &&
2352
2407
  context.model !== LUNA_RESERVE_MODEL &&
2353
- preemptiveDeferral.defer &&
2408
+ !nativePermission && preemptiveDeferral.defer &&
2354
2409
  preemptiveDeferral.waitMs > 0) {
2355
2410
  accountSkipReasons.set(selected.index, preemptiveDeferral.reason ?? "quota-near-exhaustion");
2356
2411
  exhaustionReason = "rate-limit";
@@ -2495,7 +2550,8 @@ async function handleRequestInner(state, req, res, traceId) {
2495
2550
  continue;
2496
2551
  }
2497
2552
  let pendingScopes = workspaceCandidates.get(refreshed.account.index);
2498
- if (isTranscribeRequest) {
2553
+ // Routes without model-catalog preparation still use the selected enabled workspace.
2554
+ if (isImageRequest || isTranscribeRequest || isThreadGoalRequest) {
2499
2555
  pendingScopes = workspaceModelScopes(refreshed.account)
2500
2556
  .filter((scope) => scope.routable)
2501
2557
  .sort((a, b) => Number(b.selected) - Number(a.selected) || Number(b.bound) - Number(a.bound));
@@ -2552,14 +2608,10 @@ async function handleRequestInner(state, req, res, traceId) {
2552
2608
  : undefined;
2553
2609
  const disk = latest?.accounts.find(item => item.recordId && item.recordId === refreshed.account.recordId)
2554
2610
  ?? latest?.accounts.find(item => item.accountId === refreshed.account.accountId && sanitizeEmail(item.email) === sanitizeEmail(refreshed.account.email)) ?? retained;
2555
- const workspace = disk?.workspaces?.find(item => item.id.trim() === accountId);
2556
- // Derive the stored binding exactly as workspaceModelScopes does: trimmed,
2557
- // falling back to the token's workspace claim when no id was stored.
2558
- // A stored record carries accessToken; the transient-grace fallback is a live account (access).
2559
2611
  const diskToken = disk?.accessToken ?? disk?.access;
2560
- const diskBoundId = disk ? disk.accountId?.trim() || extractAccountId(diskToken)?.trim() : undefined;
2561
2612
  const stillEligible = disk && disk.enabled !== false && !disk.authInvalidatedAt &&
2562
- (workspace ? workspace.enabled !== false : accountId === diskBoundId);
2613
+ workspaceModelScopes({ ...disk, index: refreshed.account.index, access: diskToken })
2614
+ .some(scope => scope.routable && scope.accountId === accountId);
2563
2615
  if (!stillEligible) {
2564
2616
  writeJson(res, 503, { error: { code: "routing_configuration_changed", message: "Routing eligibility changed while preparing the request. Retry with the current configuration." } });
2565
2617
  return;
@@ -2732,7 +2784,9 @@ async function handleRequestInner(state, req, res, traceId) {
2732
2784
  const key = JSON.stringify([requestScope.id, context.model]);
2733
2785
  if (observations.size >= 1000 && !observations.has(key))
2734
2786
  observations.delete(observations.keys().next().value ?? "");
2735
- observations.set(key, { ...merged, model: context.model ?? "unknown", planType: planType ?? previous?.planType });
2787
+ observations.set(key, { ...merged, model: context.model ?? "unknown", planType: planType ?? previous?.planType,
2788
+ ...(snapshot.status === 200 && hasFreshOrdinaryUsagePermission(previous, state.now()) ? { ordinaryUsageAllowed: true, ordinaryUsageCheckedAt: previous?.ordinaryUsageCheckedAt ?? previous?.updatedAt } : {}),
2789
+ });
2736
2790
  }
2737
2791
  };
2738
2792
  const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
@@ -2833,6 +2887,17 @@ async function handleRequestInner(state, req, res, traceId) {
2833
2887
  if (outcome === "retry")
2834
2888
  continue;
2835
2889
  }
2890
+ if (state.nativeOpenai && isResponsesRequest && requestScope && context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded && !modelAtCapacity) {
2891
+ // A body-only quota rejection is newer evidence than an earlier
2892
+ // native permission. Record no invented quota percentage or permission.
2893
+ // An older in-flight success cannot clear this hint; newer native
2894
+ // permission checks or its bounded lifetime invalidate it during recovery.
2895
+ const rejections = state.subscriptionQuotaRejections ??= new Map();
2896
+ const key = JSON.stringify([requestScope.id, context.model]);
2897
+ if (rejections.size >= 1000 && !rejections.has(key))
2898
+ rejections.delete(rejections.keys().next().value ?? "");
2899
+ rejections.set(key, { key: requestScope.id, accountId: requestScope.accountId, observedAt: state.now() });
2900
+ }
2836
2901
  if (context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded) {
2837
2902
  state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
2838
2903
  }
@@ -2845,7 +2910,7 @@ async function handleRequestInner(state, req, res, traceId) {
2845
2910
  (context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
2846
2911
  const confirmedKey = confirmedQuotaExhaustionKey(context.family, context.model);
2847
2912
  const confirmedResetAt = state.now() +
2848
- Math.min(Math.max(0, Math.floor(retryAfterMs)), MAX_RATE_LIMIT_DELAY_MS);
2913
+ Math.min(Math.max(0, Math.floor(retryAfterMs)), RATE_LIMIT_CACHE_TTL_MS);
2849
2914
  rateLimitState.account.rateLimitResetTimes[confirmedKey] = Math.max(rateLimitState.account.rateLimitResetTimes[confirmedKey] ?? 0, confirmedResetAt);
2850
2915
  }
2851
2916
  rateLimitState.accountManager.saveToDiskDebounced();
@@ -2885,7 +2950,11 @@ async function handleRequestInner(state, req, res, traceId) {
2885
2950
  }
2886
2951
  exhaustionReason = "rate-limit";
2887
2952
  capabilityRejectionIsLatest = false;
2888
- transientAttempts += 1;
2953
+ // Account-local throttles are bounded by distinct accounts/workspaces.
2954
+ // They must not spend the network/server budget before a usable sibling.
2955
+ // Pins and model-wide capacity failures retain the transient bound.
2956
+ if (isPinned || modelAtCapacity)
2957
+ transientAttempts += 1;
2889
2958
  transientExhaustionReason = "rate-limit";
2890
2959
  state.status.retries += 1;
2891
2960
  noteRotation();
@@ -2895,9 +2964,9 @@ async function handleRequestInner(state, req, res, traceId) {
2895
2964
  const bodyText = await readErrorBody(upstream, state.streamStallTimeoutMs);
2896
2965
  const errorCode = extractErrorCodeFromBody(bodyText);
2897
2966
  if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) {
2898
- if (requestScope && refreshed.account.workspaces?.length) {
2967
+ if (requestScope?.storedWorkspaceId !== undefined) {
2899
2968
  const workspaceState = responseStateTarget();
2900
- if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.accountId))
2969
+ if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.storedWorkspaceId))
2901
2970
  workspaceState.accountManager.saveToDiskDebounced();
2902
2971
  pendingScopes?.shift();
2903
2972
  refundConsumedPoolToken(refreshed.account);
@@ -3132,7 +3201,7 @@ async function handleRequestInner(state, req, res, traceId) {
3132
3201
  // No-op when no reason is recorded, so the hot path stays write-free.
3133
3202
  if (!isTranscribeRequest) {
3134
3203
  const quotaDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
3135
- const nearExhaustionWaitMs = context.model === LUNA_RESERVE_MODEL
3204
+ const nearExhaustionWaitMs = (context.model === LUNA_RESERVE_MODEL || (requestScope && hasFreshOrdinaryUsagePermission(quotaForScope(refreshed.account, requestScope), state.now())))
3136
3205
  ? 0
3137
3206
  : quotaDeferral.defer
3138
3207
  ? quotaDeferral.waitMs
@@ -3403,9 +3472,13 @@ async function handleRequestInner(state, req, res, traceId) {
3403
3472
  // Read live account state, not only selection skip reasons. The final
3404
3473
  // refresh attempt can install an auth cooldown after the last selection
3405
3474
  // pass, so that blocker may never have been written to accountSkipReasons.
3475
+ const backupsReserved = !isPinned && !emergencyBackupsAllowed();
3406
3476
  for (const account of accountManager.getAccountsSnapshot()) {
3407
3477
  if (policyDecision?.blockedAccountIndexes.has(account.index))
3408
3478
  continue;
3479
+ // Selection never offers a reserved backup, so it is not ready capacity.
3480
+ if (backupsReserved && policyDecision?.emergencyAccountIndexes?.has(account.index))
3481
+ continue;
3409
3482
  if (account.enabled === false)
3410
3483
  continue;
3411
3484
  if (!accountManager.hasEnabledWorkspaces(account))
@@ -3430,6 +3503,11 @@ async function handleRequestInner(state, req, res, traceId) {
3430
3503
  return minWaitMs ?? 0;
3431
3504
  })()
3432
3505
  : undefined;
3506
+ const ineligibleAccountIndexes = new Set(policyDecision?.blockedAccountIndexes ?? []);
3507
+ if (!isPinned && !emergencyBackupsAllowed()) {
3508
+ for (const index of policyDecision?.emergencyAccountIndexes ?? [])
3509
+ ineligibleAccountIndexes.add(index);
3510
+ }
3433
3511
  writePoolExhausted({
3434
3512
  res,
3435
3513
  accountManager,
@@ -3437,6 +3515,7 @@ async function handleRequestInner(state, req, res, traceId) {
3437
3515
  model: context.model,
3438
3516
  reason: exhaustionReason,
3439
3517
  retryAfterMs: transcriptionRetryAfterMs,
3518
+ ineligibleAccountIndexes,
3440
3519
  accountSkipReasons: Object.fromEntries([...accountSkipReasons.entries()].map(([index, reason]) => [
3441
3520
  String(index),
3442
3521
  reason,
@@ -26,7 +26,9 @@ const byId = (rows) => new Map((rows ?? []).map(row => [row.id, row]));
26
26
  /**
27
27
  * Merge workspaces by id, never by array position. Fields changed on one side
28
28
  * win; enabled/disabledAt move as one unit, and when both sides changed it an
29
- * intentional re-enable wins over a disable. Other two-sided edits conflict.
29
+ * intentional re-enable wins over a disable. Concurrent disables preserve an
30
+ * operator exclusion (no disabledAt) over a recoverable runtime health marker.
31
+ * Other two-sided edits conflict.
30
32
  * Removal follows the account rule: a record removed on either side stays
31
33
  * removed.
32
34
  */
@@ -44,7 +46,13 @@ function mergeWorkspaces(base, disk, local) {
44
46
  continue;
45
47
  }
46
48
  const state = (row) => ({ enabled: row?.enabled, disabledAt: row?.disabledAt });
47
- const stateSource = equal(state(b), state(l)) ? d : equal(state(b), state(d)) ? l : d.enabled !== false ? d : l.enabled !== false ? l : (d.disabledAt ?? 0) >= (l.disabledAt ?? 0) ? d : l;
49
+ const stateSource = equal(state(b), state(l)) ? d
50
+ : equal(state(b), state(d)) ? l
51
+ : d.enabled !== false ? d
52
+ : l.enabled !== false ? l
53
+ : d.disabledAt === undefined ? d
54
+ : l.disabledAt === undefined ? l
55
+ : d.disabledAt >= l.disabledAt ? d : l;
48
56
  const pick = (row) => ({ ...row, enabled: stateSource.enabled, disabledAt: stateSource.disabledAt });
49
57
  const merged = mergeValue(b ? pick(b) : undefined, pick(d), pick(l));
50
58
  if (merged.disabledAt === undefined)
@@ -54,7 +62,7 @@ function mergeWorkspaces(base, disk, local) {
54
62
  const currentId = (row) => row.workspaces?.[row.currentWorkspaceIndex ?? 0]?.id;
55
63
  const chosen = currentId(local) !== currentId(base) ? currentId(local) : currentId(disk);
56
64
  let index = workspaces.findIndex(row => row.id === chosen);
57
- if (index < 0 || workspaces[index]?.enabled === false) {
65
+ if (index < 0) {
58
66
  const enabled = workspaces.findIndex(row => row.enabled !== false);
59
67
  index = enabled >= 0 ? enabled : Math.max(index, 0);
60
68
  }
@@ -86,7 +94,10 @@ function mergeRuntimeAccount(base, disk, local) {
86
94
  const before = base.rateLimitResetTimes?.[key], a = disk.rateLimitResetTimes?.[key], b = local.rateLimitResetTimes?.[key];
87
95
  // An explicit clear is an operation, not a new observation.
88
96
  const clear = before !== undefined && ((a === undefined && b === before) || (b === undefined && a === before) || (a === undefined && b === undefined));
89
- const value = clear ? undefined : Math.max(a ?? 0, b ?? 0);
97
+ // A side that kept the baseline made no observation, so the other side's
98
+ // value wins even when lower (e.g. a load-time revalidation bound).
99
+ // Only two concurrent observations keep the later deadline.
100
+ const value = clear ? undefined : a === before ? b : b === before ? a : Math.max(a ?? 0, b ?? 0);
90
101
  if (value !== undefined)
91
102
  limits[key] = value;
92
103
  }
@@ -1,56 +1,56 @@
1
- const DASHBOARD_HTML = `
2
- <!doctype html>
3
- <html lang="zh-CN">
4
- <head>
5
- <meta charset="utf-8">
6
- <meta name="viewport" content="width=device-width,initial-scale=1">
7
- <title>Codex Multi-Auth · 号池监控</title>
8
- <style>
9
- :root{color-scheme:dark;--bg:#09111f;--panel:#101c2f;--panel2:#14243b;--line:#223653;--text:#edf5ff;--muted:#8ea5c2;--green:#36d399;--amber:#f7c95c;--red:#fb7185;--blue:#65a8ff;--shadow:0 14px 40px rgba(0,0,0,.25)}
10
- *{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#17345a 0,transparent 38%),var(--bg);color:var(--text);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
11
- .shell{max-width:1440px;margin:0 auto;padding:28px 26px 48px}.top{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:26px}.brand{display:flex;gap:14px;align-items:center}.mark{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,#4b8dff,#55d6ae);color:#07111f;font-weight:900;font-size:18px;box-shadow:0 8px 24px #3b82f655}.eyebrow{color:var(--blue);font-size:12px;letter-spacing:.12em;text-transform:uppercase}.title{font-size:28px;font-weight:800;letter-spacing:-.03em}.subtitle{color:var(--muted);margin-top:2px}.actions{display:flex;gap:10px;align-items:center}.button{border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:9px;padding:9px 13px;cursor:pointer}.button:hover{border-color:var(--blue);background:var(--panel2)}.live{display:flex;gap:7px;align-items:center;color:var(--muted);font-size:12px}.dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}.dot.off{background:var(--red);box-shadow:0 0 10px var(--red)}
12
- .summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-bottom:22px}.card,.panel{background:linear-gradient(145deg,#122139dd,#0e192add);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.card{padding:17px}.label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:800;margin-top:7px}.value.green{color:var(--green)}.value.amber{color:var(--amber)}.value.red{color:var(--red)}.value.blue{color:var(--blue)}
13
- .grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,.7fr);gap:18px;align-items:start}.panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panel-title{font-size:16px;font-weight:750}.panel-meta{color:var(--muted);font-size:12px}.accounts{display:grid;gap:10px}.account{padding:15px;border:1px solid var(--line);border-radius:11px;background:#0e1a2caa}.account-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.account-name{font-weight:700;overflow-wrap:anywhere}.account-meta{color:var(--muted);font-size:12px;margin-top:3px}.badge{border-radius:99px;padding:3px 9px;font-size:11px;white-space:nowrap;border:1px solid #ffffff18}.badge.ready{color:var(--green);background:#36d39918}.badge.login{color:var(--red);background:#fb718518}.badge.disabled{color:var(--muted);background:#8ea5c218}.badge.current{color:var(--blue);background:#65a8ff18}.account-stats{display:grid;grid-template-columns:1fr 1fr 120px;gap:15px;margin-top:14px}.quota{min-width:0}.quota-row{display:flex;justify-content:space-between;gap:7px;color:var(--muted);font-size:11px;margin-top:8px}.bar{height:8px;border-radius:99px;background:#243752;overflow:hidden;margin-top:5px}.fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--green),#7de2ab)}.fill.warn{background:linear-gradient(90deg,var(--amber),#ff9e5c)}.fill.danger{background:linear-gradient(90deg,var(--red),#ffadad)}.risk{text-align:right}.risk-num{font-size:23px;font-weight:800}.risk-low{color:var(--green)}.risk-medium{color:var(--amber)}.risk-high{color:var(--red)}.reason{color:var(--muted);font-size:12px;margin-top:8px;overflow-wrap:anywhere}.empty{padding:35px;text-align:center;color:var(--muted)}.subhead{margin:14px 0 4px;color:var(--muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase}.side{display:grid;gap:18px}.kv{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #ffffff0d}.kv:last-child{border-bottom:0}.kv b{font-weight:650;text-align:right;overflow-wrap:anywhere}.notice{padding:12px;border-radius:10px;background:#f7c95c13;border:1px solid #f7c95c3d;color:#fbe6a2;font-size:12px}.error{padding:12px;border-radius:10px;background:#fb718513;border:1px solid #fb71853d;color:#ffc2cb;font-size:12px}.foot{color:var(--muted);font-size:12px;margin-top:20px;text-align:center}
14
- @media(max-width:1050px){.summary{grid-template-columns:repeat(3,minmax(0,1fr))}.grid{grid-template-columns:1fr}.side{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.shell{padding:18px 13px 36px}.top{display:block}.actions{margin-top:15px;justify-content:space-between}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}.account-stats{grid-template-columns:1fr 1fr}.risk{text-align:left}.side{grid-template-columns:1fr}.title{font-size:23px}}
15
- </style>
16
- </head>
17
- <body>
18
- <main class="shell">
19
- <header class="top"><div class="brand"><div class="mark">MA</div><div><div class="eyebrow">Codex Multi-Auth</div><div class="title" data-i18n="title">号池实时监控</div><div class="subtitle" data-i18n="subtitle">账号、配额与轮换状态</div></div></div><div class="actions"><div class="live"><span id="dot" class="dot"></span><span id="connection" data-i18n="connecting">连接中…</span></div><button id="lang" class="button">EN</button><button id="refresh" class="button" data-i18n="refresh">刷新</button></div></header>
20
- <section id="summary" class="summary"></section>
21
- <div id="error" class="error" hidden></div>
22
- <section class="grid"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="accounts">账号池</div><div id="updated" class="panel-meta">—</div></div><div id="accounts" class="accounts"></div></section><aside class="side"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="routing">路由</div></div><div id="routing"></div></section><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="runtime">运行时</div></div><div id="runtime"></div></section></aside></section>
23
- <div class="foot" data-i18n="foot">数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。</div>
24
- </main>
25
- <script>
26
- (function(){
27
- var token="__CMA_TOKEN__";
28
- var lang="__CMA_LANG__";
29
- var text={zh:{title:"号池实时监控",subtitle:"账号、配额与轮换状态",connecting:"连接中…",connected:"实时连接",offline:"连接中断",refresh:"刷新",accounts:"账号池",apiAccounts:"API/ZDR 账号",routing:"路由",runtime:"运行时",total:"总账号",ready:"可用",login:"需重新登录",disabled:"已禁用",enabled:"已启用",recommend:"推荐账号",active:"当前账号",requests:"请求",success:"成功",failed:"失败",last:"最近请求",quota:"配额",risk:"风险",unknown:"未知",reason:"原因",primary:"主窗口",secondary:"次窗口",priority:"优先级",models:"可见模型",foot:"数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。"},en:{title:"Pool live monitor",subtitle:"Accounts, quota and routing",connecting:"Connecting…",connected:"Live",offline:"Disconnected",refresh:"Refresh",accounts:"Account pool",apiAccounts:"API/ZDR accounts",routing:"Routing",runtime:"Runtime",total:"Accounts",ready:"Ready",login:"Re-login needed",disabled:"Disabled",enabled:"Enabled",recommend:"Recommended",active:"Current",requests:"Requests",success:"Success",failed:"Failed",last:"Last request",quota:"Quota",risk:"Risk",unknown:"Unknown",reason:"Reason",primary:"primary",secondary:"secondary",priority:"priority",models:"visible models",foot:"Data comes from the local pool; the server listens on loopback by default. Ctrl-C stops it."}};
30
- function t(k){return text[lang][k]||k}
31
- function label(v){var s=String(v==null?"":v);return lang==="zh"?s.replace(/^Account /,"账号 ").replace(/Token account/g,"令牌账号").replace(/, id:/g,",ID:").replace(/, /g,","):s}
32
- function reasonText(v){var s=String(v==null?"—":v);if(lang!=="zh")return s;return s.replace("token-invalid — re-login needed","令牌无效——需要重新登录").replace("primary quota ","主窗口配额 ").replace(" secondary quota "," 次窗口配额 ").replace(" used","已使用").replace("account is disabled","账号已禁用").replace("quota cache exhausted","配额缓存已用尽")}
33
- function esc(v){return String(v==null?"":v).replace(/[&<>"']/g,function(c){return({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]})}
34
- function api(path){return path+(path.indexOf("?")>=0?"&":"?")+"token="+encodeURIComponent(token)}
35
- function ago(ms){if(typeof ms!=="number"||!isFinite(ms))return "—";var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+"s";if(s<3600)return Math.floor(s/60)+"m";if(s<86400)return Math.floor(s/3600)+"h";return Math.floor(s/86400)+"d"}
36
- function statusClass(a){var m=(a.markers||[]).join(" ");if(a.enabled===false)return "disabled";if(/invalid|sign-in|re-login/i.test(m))return "login";return "ready"}
37
- function winLabel(w,key){var m=w&&w.windowMinutes;if(typeof m==="number"&&isFinite(m)&&m>0){var zh=lang==="zh";if(m%1440===0)return (m/1440)+(zh?"天":"d");if(m%60===0)return (m/60)+(zh?"小时":"h");return m+(zh?"分钟":"m")}return t(key)}
38
- function bar(window,key){if(!window||typeof window.usedPercent!=="number")return '<div class="label">'+t("unknown")+"</div>";var p=Math.max(0,Math.min(100,Math.round(window.usedPercent)));var cls=p>=90?" danger":p>=75?" warn":"";return '<div class="quota-row"><span>'+esc(winLabel(window,key))+" "+t("quota")+'</span><span>'+p+"% / "+(100-p)+"%</span></div><div class="bar"><div class="fill'+cls+'" style="width:'+p+'%"></div></div>'}
39
- function draw(data){window.__data=data;var all=data.accounts||[];var ready=all.filter(function(a){return a.enabled!==false&&statusClass(a)==="ready"}).length;var login=all.filter(function(a){return statusClass(a)==="login"}).length;var disabled=all.filter(function(a){return a.enabled===false}).length;var rec=data.recommendedIndex==null?"—":"#"+(data.recommendedIndex+1);var rt=data.runtime||{};document.getElementById("summary").innerHTML='<div class="card"><div class="label">'+t("total")+'</div><div class="value blue">'+(data.totalAccountCount||all.length)+'</div></div><div class="card"><div class="label">'+t("ready")+'</div><div class="value green">'+ready+'</div></div><div class="card"><div class="label">'+t("login")+'</div><div class="value red">'+login+'</div></div><div class="card"><div class="label">'+t("disabled")+'</div><div class="value">'+disabled+'</div></div><div class="card"><div class="label">'+t("recommend")+'</div><div class="value amber">'+rec+'</div></div>';
40
- document.getElementById("accounts").innerHTML=all.length?all.map(function(a){var sub=a.subscription||{};var q=a.quota||{};var risk=a.forecastRiskScore==null?"—":a.forecastRiskScore+"/100";var rl=a.forecastRiskLevel||"low";var badges=(a.markers||[]).map(function(m){return '<span class="badge '+(/current/.test(m)?"current":statusClass(a))+'">'+esc(label(m))+'</span>'}).join(" ");return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||("Account "+(a.index+1))))+'</div><div class="account-meta">'+esc(sub.planLabel||t("unknown"))+" · "+esc(label(sub.selectedWorkspace||""))+'</div></div><div>'+badges+' <span class="badge '+statusClass(a)+'">'+(statusClass(a)==="ready"?t("ready"):statusClass(a)==="login"?t("login"):t("disabled"))+'</span></div></div><div class="account-stats"><div class="quota">'+bar(q.primary,"primary")+bar(q.secondary,"secondary")+'</div><div><div class="label">'+t("last")+'</div><div class="value" style="font-size:18px">'+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div><div class="reason">'+t("reason")+": "+esc(reasonText(a.reason||"—"))+'</div></div><div class="risk"><div class="label">'+t("risk")+'</div><div class="risk-num risk-'+esc(rl)+'">'+esc(risk)+"</div></div></div></article>"}).join(""):'<div class="empty">'+t("unknown")+"</div>";
41
- var apis=(data.apiAccounts||[]).map(function(a){return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||"API"))+'</div><div class="account-meta">'+esc(a.kind||"api")+" · "+t("priority")+" "+esc(a.priority==null?"—":a.priority)+'</div></div><div><span class="badge '+(a.enabled===false?"disabled":"ready")+'">'+(a.enabled===false?t("disabled"):t("enabled"))+'</span></div></div><div class="account-meta">'+t("models")+": "+esc((a.visibleModels||[]).length||"—")+" · "+t("last")+": "+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div></article>'}).join("");
42
- if(apis)document.getElementById("accounts").innerHTML+='<div class="subhead">'+t("apiAccounts")+"</div>"+apis;
43
- var recReason=data.recommendationReason||"—";document.getElementById("routing").innerHTML='<div class="kv"><span>'+t("active")+'</span><b>'+esc(data.activeIndex==null?"—":"#"+(data.activeIndex+1))+'</b></div><div class="kv"><span>'+t("recommend")+'</span><b>'+esc(rec)+"</b></div><div class="kv"><span>'+t("reason")+'</span><b>'+esc(recReason)+'</b></div>';
44
- document.getElementById("runtime").innerHTML='<div class="kv"><span>'+t("requests")+'</span><b>'+esc(rt.responsesRequests==null?"—":rt.responsesRequests)+'</b></div><div class="kv"><span>'+t("success")+'</span><b>'+esc(rt.successfulRequests==null?"—":rt.successfulRequests)+'</b></div><div class="kv"><span>'+t("failed")+'</span><b>'+esc(rt.failedRequests==null?"—":rt.failedRequests)+'</b></div><div class="kv"><span>'+t("last")+'</span><b>'+esc(data.runtimeInUseIndex==null?"—":"#"+(data.runtimeInUseIndex+1))+'</b></div>';
45
- document.getElementById("updated").textContent=new Date().toLocaleTimeString();document.getElementById("error").hidden=true;document.getElementById("dot").className="dot";document.getElementById("connection").textContent=t("connected")
46
- }
47
- function setLang(){document.documentElement.lang=lang==="zh"?"zh-CN":"en";document.querySelectorAll("[data-i18n]").forEach(function(e){e.textContent=t(e.getAttribute("data-i18n"))});document.getElementById("lang").textContent=lang==="zh"?"EN":"中文";if(window.__data)draw(window.__data)}
48
- function failed(e){document.getElementById("error").hidden=false;document.getElementById("error").textContent=e&&e.message?e.message:"Request failed";document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}
49
- function load(){fetch(api("/api/status"),{cache:"no-store"}).then(function(r){if(!r.ok)throw Error("HTTP "+r.status);return r.json()}).then(draw).catch(failed)}
50
- document.getElementById("refresh").onclick=load;document.getElementById("lang").onclick=function(){lang=lang==="zh"?"en":"zh";setLang()};setLang();load();try{var es=new EventSource(api("/events"));es.onmessage=function(e){try{draw(JSON.parse(e.data))}catch(_){}};es.onerror=function(){document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}}catch(_){setInterval(load,5000)}
51
- })();
52
- </script>
53
- </body>
1
+ const DASHBOARD_HTML = `
2
+ <!doctype html>
3
+ <html lang="zh-CN">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <title>Codex Multi-Auth · 号池监控</title>
8
+ <style>
9
+ :root{color-scheme:dark;--bg:#09111f;--panel:#101c2f;--panel2:#14243b;--line:#223653;--text:#edf5ff;--muted:#8ea5c2;--green:#36d399;--amber:#f7c95c;--red:#fb7185;--blue:#65a8ff;--shadow:0 14px 40px rgba(0,0,0,.25)}
10
+ *{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#17345a 0,transparent 38%),var(--bg);color:var(--text);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
11
+ .shell{max-width:1440px;margin:0 auto;padding:28px 26px 48px}.top{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin-bottom:26px}.brand{display:flex;gap:14px;align-items:center}.mark{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,#4b8dff,#55d6ae);color:#07111f;font-weight:900;font-size:18px;box-shadow:0 8px 24px #3b82f655}.eyebrow{color:var(--blue);font-size:12px;letter-spacing:.12em;text-transform:uppercase}.title{font-size:28px;font-weight:800;letter-spacing:-.03em}.subtitle{color:var(--muted);margin-top:2px}.actions{display:flex;gap:10px;align-items:center}.button{border:1px solid var(--line);background:var(--panel);color:var(--text);border-radius:9px;padding:9px 13px;cursor:pointer}.button:hover{border-color:var(--blue);background:var(--panel2)}.live{display:flex;gap:7px;align-items:center;color:var(--muted);font-size:12px}.dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}.dot.off{background:var(--red);box-shadow:0 0 10px var(--red)}
12
+ .summary{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin-bottom:22px}.card,.panel{background:linear-gradient(145deg,#122139dd,#0e192add);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.card{padding:17px}.label{color:var(--muted);font-size:12px}.value{font-size:26px;font-weight:800;margin-top:7px}.value.green{color:var(--green)}.value.amber{color:var(--amber)}.value.red{color:var(--red)}.value.blue{color:var(--blue)}
13
+ .grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(280px,.7fr);gap:18px;align-items:start}.panel{padding:18px}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.panel-title{font-size:16px;font-weight:750}.panel-meta{color:var(--muted);font-size:12px}.accounts{display:grid;gap:10px}.account{padding:15px;border:1px solid var(--line);border-radius:11px;background:#0e1a2caa}.account-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.account-name{font-weight:700;overflow-wrap:anywhere}.account-meta{color:var(--muted);font-size:12px;margin-top:3px}.badge{border-radius:99px;padding:3px 9px;font-size:11px;white-space:nowrap;border:1px solid #ffffff18}.badge.ready{color:var(--green);background:#36d39918}.badge.login{color:var(--red);background:#fb718518}.badge.disabled{color:var(--muted);background:#8ea5c218}.badge.current{color:var(--blue);background:#65a8ff18}.account-stats{display:grid;grid-template-columns:1fr 1fr 120px;gap:15px;margin-top:14px}.quota{min-width:0}.quota-row{display:flex;justify-content:space-between;gap:7px;color:var(--muted);font-size:11px;margin-top:8px}.bar{height:8px;border-radius:99px;background:#243752;overflow:hidden;margin-top:5px}.fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--green),#7de2ab)}.fill.warn{background:linear-gradient(90deg,var(--amber),#ff9e5c)}.fill.danger{background:linear-gradient(90deg,var(--red),#ffadad)}.risk{text-align:right}.risk-num{font-size:23px;font-weight:800}.risk-low{color:var(--green)}.risk-medium{color:var(--amber)}.risk-high{color:var(--red)}.reason{color:var(--muted);font-size:12px;margin-top:8px;overflow-wrap:anywhere}.empty{padding:35px;text-align:center;color:var(--muted)}.subhead{margin:14px 0 4px;color:var(--muted);font-size:11px;letter-spacing:.1em;text-transform:uppercase}.side{display:grid;gap:18px}.kv{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid #ffffff0d}.kv:last-child{border-bottom:0}.kv b{font-weight:650;text-align:right;overflow-wrap:anywhere}.notice{padding:12px;border-radius:10px;background:#f7c95c13;border:1px solid #f7c95c3d;color:#fbe6a2;font-size:12px}.error{padding:12px;border-radius:10px;background:#fb718513;border:1px solid #fb71853d;color:#ffc2cb;font-size:12px}.foot{color:var(--muted);font-size:12px;margin-top:20px;text-align:center}
14
+ @media(max-width:1050px){.summary{grid-template-columns:repeat(3,minmax(0,1fr))}.grid{grid-template-columns:1fr}.side{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:680px){.shell{padding:18px 13px 36px}.top{display:block}.actions{margin-top:15px;justify-content:space-between}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}.account-stats{grid-template-columns:1fr 1fr}.risk{text-align:left}.side{grid-template-columns:1fr}.title{font-size:23px}}
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <main class="shell">
19
+ <header class="top"><div class="brand"><div class="mark">MA</div><div><div class="eyebrow">Codex Multi-Auth</div><div class="title" data-i18n="title">号池实时监控</div><div class="subtitle" data-i18n="subtitle">账号、配额与轮换状态</div></div></div><div class="actions"><div class="live"><span id="dot" class="dot"></span><span id="connection" data-i18n="connecting">连接中…</span></div><button id="lang" class="button">EN</button><button id="refresh" class="button" data-i18n="refresh">刷新</button></div></header>
20
+ <section id="summary" class="summary"></section>
21
+ <div id="error" class="error" hidden></div>
22
+ <section class="grid"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="accounts">账号池</div><div id="updated" class="panel-meta">—</div></div><div id="accounts" class="accounts"></div></section><aside class="side"><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="routing">路由</div></div><div id="routing"></div></section><section class="panel"><div class="panel-head"><div class="panel-title" data-i18n="runtime">运行时</div></div><div id="runtime"></div></section></aside></section>
23
+ <div class="foot" data-i18n="foot">数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。</div>
24
+ </main>
25
+ <script>
26
+ (function(){
27
+ var token="__CMA_TOKEN__";
28
+ var lang="__CMA_LANG__";
29
+ var text={zh:{title:"号池实时监控",subtitle:"账号、配额与轮换状态",connecting:"连接中…",connected:"实时连接",offline:"连接中断",refresh:"刷新",accounts:"账号池",apiAccounts:"API/ZDR 账号",routing:"路由",runtime:"运行时",total:"总账号",ready:"可用",login:"需重新登录",disabled:"已禁用",enabled:"已启用",recommend:"推荐账号",active:"当前账号",requests:"请求",success:"成功",failed:"失败",last:"最近请求",quota:"配额",risk:"风险",unknown:"未知",reason:"原因",primary:"主窗口",secondary:"次窗口",priority:"优先级",models:"可见模型",foot:"数据来自本机号池;页面默认仅监听回环地址,按 Ctrl-C 停止服务。"},en:{title:"Pool live monitor",subtitle:"Accounts, quota and routing",connecting:"Connecting…",connected:"Live",offline:"Disconnected",refresh:"Refresh",accounts:"Account pool",apiAccounts:"API/ZDR accounts",routing:"Routing",runtime:"Runtime",total:"Accounts",ready:"Ready",login:"Re-login needed",disabled:"Disabled",enabled:"Enabled",recommend:"Recommended",active:"Current",requests:"Requests",success:"Success",failed:"Failed",last:"Last request",quota:"Quota",risk:"Risk",unknown:"Unknown",reason:"Reason",primary:"primary",secondary:"secondary",priority:"priority",models:"visible models",foot:"Data comes from the local pool; the server listens on loopback by default. Ctrl-C stops it."}};
30
+ function t(k){return text[lang][k]||k}
31
+ function label(v){var s=String(v==null?"":v);return lang==="zh"?s.replace(/^Account /,"账号 ").replace(/Token account/g,"令牌账号").replace(/, id:/g,",ID:").replace(/, /g,","):s}
32
+ function reasonText(v){var s=String(v==null?"—":v);if(lang!=="zh")return s;return s.replace("token-invalid — re-login needed","令牌无效——需要重新登录").replace("primary quota ","主窗口配额 ").replace(" secondary quota "," 次窗口配额 ").replace(" used","已使用").replace("account is disabled","账号已禁用").replace("quota cache exhausted","配额缓存已用尽")}
33
+ function esc(v){return String(v==null?"":v).replace(/[&<>"']/g,function(c){return({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]})}
34
+ function api(path){return path+(path.indexOf("?")>=0?"&":"?")+"token="+encodeURIComponent(token)}
35
+ function ago(ms){if(typeof ms!=="number"||!isFinite(ms))return "—";var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+"s";if(s<3600)return Math.floor(s/60)+"m";if(s<86400)return Math.floor(s/3600)+"h";return Math.floor(s/86400)+"d"}
36
+ function statusClass(a){var m=(a.markers||[]).join(" ");if(a.enabled===false)return "disabled";if(/invalid|sign-in|re-login/i.test(m))return "login";return "ready"}
37
+ function winLabel(w,key){var m=w&&w.windowMinutes;if(typeof m==="number"&&isFinite(m)&&m>0){var zh=lang==="zh";if(m%1440===0)return (m/1440)+(zh?"天":"d");if(m%60===0)return (m/60)+(zh?"小时":"h");return m+(zh?"分钟":"m")}return t(key)}
38
+ function bar(window,key){if(!window||typeof window.usedPercent!=="number")return '<div class="label">'+t("unknown")+"</div>";var p=Math.max(0,Math.min(100,Math.round(window.usedPercent)));var cls=p>=90?" danger":p>=75?" warn":"";return '<div class="quota-row"><span>'+esc(winLabel(window,key))+" "+t("quota")+'</span><span>'+p+"% / "+(100-p)+"%</span></div><div class="bar"><div class="fill'+cls+'" style="width:'+p+'%"></div></div>'}
39
+ function draw(data){window.__data=data;var all=data.accounts||[];var ready=all.filter(function(a){return a.enabled!==false&&statusClass(a)==="ready"}).length;var login=all.filter(function(a){return statusClass(a)==="login"}).length;var disabled=all.filter(function(a){return a.enabled===false}).length;var rec=data.recommendedIndex==null?"—":"#"+(data.recommendedIndex+1);var rt=data.runtime||{};document.getElementById("summary").innerHTML='<div class="card"><div class="label">'+t("total")+'</div><div class="value blue">'+(data.totalAccountCount||all.length)+'</div></div><div class="card"><div class="label">'+t("ready")+'</div><div class="value green">'+ready+'</div></div><div class="card"><div class="label">'+t("login")+'</div><div class="value red">'+login+'</div></div><div class="card"><div class="label">'+t("disabled")+'</div><div class="value">'+disabled+'</div></div><div class="card"><div class="label">'+t("recommend")+'</div><div class="value amber">'+rec+'</div></div>';
40
+ document.getElementById("accounts").innerHTML=all.length?all.map(function(a){var sub=a.subscription||{};var q=a.quota||{};var risk=a.forecastRiskScore==null?"—":a.forecastRiskScore+"/100";var rl=a.forecastRiskLevel||"low";var badges=(a.markers||[]).map(function(m){return '<span class="badge '+(/current/.test(m)?"current":statusClass(a))+'">'+esc(label(m))+'</span>'}).join(" ");return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||("Account "+(a.index+1))))+'</div><div class="account-meta">'+esc(sub.planLabel||t("unknown"))+" · "+esc(label(sub.selectedWorkspace||""))+'</div></div><div>'+badges+' <span class="badge '+statusClass(a)+'">'+(statusClass(a)==="ready"?t("ready"):statusClass(a)==="login"?t("login"):t("disabled"))+'</span></div></div><div class="account-stats"><div class="quota">'+bar(q.primary,"primary")+bar(q.secondary,"secondary")+'</div><div><div class="label">'+t("last")+'</div><div class="value" style="font-size:18px">'+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div><div class="reason">'+t("reason")+": "+esc(reasonText(a.reason||"—"))+'</div></div><div class="risk"><div class="label">'+t("risk")+'</div><div class="risk-num risk-'+esc(rl)+'">'+esc(risk)+"</div></div></div></article>"}).join(""):'<div class="empty">'+t("unknown")+"</div>";
41
+ var apis=(data.apiAccounts||[]).map(function(a){return '<article class="account"><div class="account-head"><div><div class="account-name">'+esc(label(a.label||"API"))+'</div><div class="account-meta">'+esc(a.kind||"api")+" · "+t("priority")+" "+esc(a.priority==null?"—":a.priority)+'</div></div><div><span class="badge '+(a.enabled===false?"disabled":"ready")+'">'+(a.enabled===false?t("disabled"):t("enabled"))+'</span></div></div><div class="account-meta">'+t("models")+": "+esc((a.visibleModels||[]).length||"—")+" · "+t("last")+": "+ago(a.lastInferenceRequestAt?Date.now()-a.lastInferenceRequestAt:null)+'</div></article>'}).join("");
42
+ if(apis)document.getElementById("accounts").innerHTML+='<div class="subhead">'+t("apiAccounts")+"</div>"+apis;
43
+ var recReason=data.recommendationReason||"—";document.getElementById("routing").innerHTML='<div class="kv"><span>'+t("active")+'</span><b>'+esc(data.activeIndex==null?"—":"#"+(data.activeIndex+1))+'</b></div><div class="kv"><span>'+t("recommend")+'</span><b>'+esc(rec)+"</b></div><div class="kv"><span>'+t("reason")+'</span><b>'+esc(recReason)+'</b></div>';
44
+ document.getElementById("runtime").innerHTML='<div class="kv"><span>'+t("requests")+'</span><b>'+esc(rt.responsesRequests==null?"—":rt.responsesRequests)+'</b></div><div class="kv"><span>'+t("success")+'</span><b>'+esc(rt.successfulRequests==null?"—":rt.successfulRequests)+'</b></div><div class="kv"><span>'+t("failed")+'</span><b>'+esc(rt.failedRequests==null?"—":rt.failedRequests)+'</b></div><div class="kv"><span>'+t("last")+'</span><b>'+esc(data.runtimeInUseIndex==null?"—":"#"+(data.runtimeInUseIndex+1))+'</b></div>';
45
+ document.getElementById("updated").textContent=new Date().toLocaleTimeString();document.getElementById("error").hidden=true;document.getElementById("dot").className="dot";document.getElementById("connection").textContent=t("connected")
46
+ }
47
+ function setLang(){document.documentElement.lang=lang==="zh"?"zh-CN":"en";document.querySelectorAll("[data-i18n]").forEach(function(e){e.textContent=t(e.getAttribute("data-i18n"))});document.getElementById("lang").textContent=lang==="zh"?"EN":"中文";if(window.__data)draw(window.__data)}
48
+ function failed(e){document.getElementById("error").hidden=false;document.getElementById("error").textContent=e&&e.message?e.message:"Request failed";document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}
49
+ function load(){fetch(api("/api/status"),{cache:"no-store"}).then(function(r){if(!r.ok)throw Error("HTTP "+r.status);return r.json()}).then(draw).catch(failed)}
50
+ document.getElementById("refresh").onclick=load;document.getElementById("lang").onclick=function(){lang=lang==="zh"?"en":"zh";setLang()};setLang();load();try{var es=new EventSource(api("/events"));es.onmessage=function(e){try{draw(JSON.parse(e.data))}catch(_){}};es.onerror=function(){document.getElementById("dot").className="dot off";document.getElementById("connection").textContent=t("offline")}}catch(_){setInterval(load,5000)}
51
+ })();
52
+ </script>
53
+ </body>
54
54
  </html>`;
55
55
  export function renderDashboardHtml(token, locale = "zh") {
56
56
  const safeToken = JSON.stringify(token ?? "")
@@ -1,2 +1,2 @@
1
- export type PluginInput = any;
2
- export type Plugin = any;
1
+ export type PluginInput = any;
2
+ export type Plugin = any;
@@ -1,2 +1,2 @@
1
- export {};
2
-
1
+ export {};
2
+