codex-multi-auth 2.22.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.
- package/.codex-plugin/plugin.json +9 -9
- package/LICENSE +21 -21
- package/README.md +159 -150
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1021 -1021
- package/config/codex-modern.json +347 -347
- package/config/minimal-codex.json +15 -15
- package/config/schema/config.schema.json +8 -0
- package/dist/index.js +33 -10
- package/dist/lib/account-policy.d.ts +2 -0
- package/dist/lib/account-policy.js +3 -1
- package/dist/lib/accounts/rate-limits.d.ts +6 -0
- package/dist/lib/accounts/rate-limits.js +14 -1
- package/dist/lib/accounts.d.ts +6 -1
- package/dist/lib/accounts.js +33 -8
- package/dist/lib/api-route-store.d.ts +1 -0
- package/dist/lib/api-route-store.js +2 -1
- package/dist/lib/auth/account-access.js +2 -1
- package/dist/lib/auth/auth.js +3 -2
- package/dist/lib/auth/device-auth.js +2 -1
- package/dist/lib/auth/http.d.ts +1 -0
- package/dist/lib/auth/http.js +1 -0
- package/dist/lib/auth/token-utils.d.ts +5 -0
- package/dist/lib/auth/token-utils.js +10 -0
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/commands/account.js +15 -0
- package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
- package/dist/lib/codex-manager/commands/locale.js +25 -0
- package/dist/lib/codex-manager/commands/resets.js +18 -5
- package/dist/lib/codex-manager/commands/status.d.ts +4 -0
- package/dist/lib/codex-manager/commands/status.js +476 -5
- package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
- package/dist/lib/codex-manager/commands/workspace.js +33 -2
- package/dist/lib/codex-manager/help.js +5 -3
- package/dist/lib/codex-manager.js +112 -30
- package/dist/lib/config.js +3 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/locale.d.ts +7 -0
- package/dist/lib/locale.js +35 -0
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy-cache.js +1 -1
- package/dist/lib/policy/runtime-policy.d.ts +1 -0
- package/dist/lib/policy/runtime-policy.js +4 -0
- package/dist/lib/preemptive-quota-scheduler.js +12 -3
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/quota-cache.d.ts +4 -0
- package/dist/lib/quota-probe.js +2 -1
- package/dist/lib/quota-readiness.d.ts +1 -1
- package/dist/lib/quota-readiness.js +9 -2
- package/dist/lib/request/proxy-fetch.d.ts +5 -0
- package/dist/lib/request/proxy-fetch.js +37 -0
- package/dist/lib/request/rate-limit-decision.js +1 -1
- package/dist/lib/request/url-rewriting.js +17 -0
- package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
- package/dist/lib/runtime/account-model-catalog.js +7 -2
- package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
- package/dist/lib/runtime/account-reset-credits.js +35 -12
- package/dist/lib/runtime/app-bind.d.ts +4 -1
- package/dist/lib/runtime/app-bind.js +63 -3
- package/dist/lib/runtime/automatic-account-checks.js +6 -5
- package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
- package/dist/lib/runtime/model-discovery-status.js +2 -2
- package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
- package/dist/lib/runtime/reset-credit-routing.js +66 -21
- package/dist/lib/runtime/reset-credits.d.ts +31 -7
- package/dist/lib/runtime/reset-credits.js +135 -28
- package/dist/lib/runtime/responses-websocket.d.ts +30 -0
- package/dist/lib/runtime/responses-websocket.js +243 -29
- package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
- package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
- package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
- package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
- package/dist/lib/runtime/subscription-quota-order.js +7 -3
- package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
- package/dist/lib/runtime/workspace-model-scopes.js +14 -4
- package/dist/lib/runtime-rotation-proxy.js +138 -38
- package/dist/lib/schemas.d.ts +5 -0
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/storage/snapshot-merge.js +15 -4
- package/dist/lib/web-dashboard-html.d.ts +1 -0
- package/dist/lib/web-dashboard-html.js +66 -0
- package/dist/lib/web-dashboard.d.ts +36 -0
- package/dist/lib/web-dashboard.js +322 -0
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +209 -207
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +446 -402
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +63 -62
- package/scripts/codex.js +7143 -7006
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -73
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -2,14 +2,15 @@ import { createAutomaticApiCheck, startAutomaticApiChecks } from "./runtime/auto
|
|
|
2
2
|
import { startAutomaticAccountChecks } from "./runtime/automatic-account-checks.js";
|
|
3
3
|
import { createAutomaticSubscriptionCheck } from "./runtime/automatic-subscription-checks.js";
|
|
4
4
|
import { ClientCancellationError } from "./request/client-cancellation.js";
|
|
5
|
+
import { fetchWithProxy } from "./request/proxy-fetch.js";
|
|
5
6
|
import { createNativeAccountStorageReader } from "./runtime/native-account-storage.js";
|
|
6
7
|
import { resetSnapshotQuota } from "./runtime/reset-credits.js";
|
|
7
8
|
import { createResetCreditService, loadResetCreditState } from "./runtime/account-reset-credits.js";
|
|
8
|
-
import { recoverResetQuota, applyConfirmedReset } from "./runtime/reset-credit-routing.js";
|
|
9
|
+
import { recoverResetQuota, applyConfirmedReset, primaryPoolFullyExhausted } from "./runtime/reset-credit-routing.js";
|
|
9
10
|
import { readSubscriptionQuotaEvent } from "./runtime/subscription-quota-event.js";
|
|
10
11
|
import { loadQuotaCache } from "./quota-cache.js";
|
|
11
12
|
import { findQuotaCacheEntryForAccount } from "./quota-readiness.js";
|
|
12
|
-
import { subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
|
|
13
|
+
import { hasFreshOrdinaryUsagePermission, subscriptionQuotaPreference, compareSubscriptionQuota } from "./runtime/subscription-quota-order.js";
|
|
13
14
|
import { inferenceAccountKey } from "./runtime/inference-activity.js";
|
|
14
15
|
import { modelScopeId, workspaceModelScopes } from "./runtime/workspace-model-scopes.js";
|
|
15
16
|
import { RuntimeCapabilityFailures, classifyCapabilityFailure } from "./runtime/runtime-capability-failures.js";
|
|
@@ -32,7 +33,7 @@ import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./a
|
|
|
32
33
|
import { withRoutingMutex } from "./routing-mutex.js";
|
|
33
34
|
import { getQuotaKey } from "./accounts/rate-limits.js";
|
|
34
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";
|
|
35
|
-
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";
|
|
36
37
|
import { getModelFamily } from "./prompts/codex.js";
|
|
37
38
|
import { CURRENT_CODEX_MODEL } from "./request/helpers/model-map.js";
|
|
38
39
|
import { recordRuntimeInferenceRequest, flushRuntimeInferenceActivity, mutateRuntimeObservabilitySnapshot, recordRuntimeAccountRecovery, recordRuntimePoolExhaustion, } from "./runtime/runtime-observability.js";
|
|
@@ -829,7 +830,8 @@ function writeUnauthorized(res) {
|
|
|
829
830
|
}
|
|
830
831
|
function writePoolExhausted(params) {
|
|
831
832
|
const { res, accountManager, family, model, reason } = params;
|
|
832
|
-
const waitMs = params.retryAfterMs ??
|
|
833
|
+
const waitMs = params.retryAfterMs ??
|
|
834
|
+
accountManager.getMinWaitTimeForFamily(family, model, params.ineligibleAccountIndexes);
|
|
833
835
|
const accountCount = accountManager.getAccountCount();
|
|
834
836
|
const accountSkipReasons = params.accountSkipReasons ?? {};
|
|
835
837
|
recordRuntimePoolExhaustion({
|
|
@@ -879,7 +881,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
879
881
|
const routingMutexMode = getRoutingMutexMode(pluginConfig);
|
|
880
882
|
activeAccountManager.setRoutingMutexMode(routingMutexMode);
|
|
881
883
|
const schedulingStrategy = getSchedulingStrategy(pluginConfig);
|
|
882
|
-
const fetchImpl = options.fetchImpl ??
|
|
884
|
+
const fetchImpl = options.fetchImpl ?? fetchWithProxy;
|
|
883
885
|
const host = options.host ?? DEFAULT_HOST;
|
|
884
886
|
// Defense in depth (runtime-proxy-01): the proxy presents managed OAuth tokens
|
|
885
887
|
// and must never be reachable off-box. It is loopback-only with NO opt-out —
|
|
@@ -1045,6 +1047,7 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1045
1047
|
await Promise.all([automaticChecks?.stop(), automaticApiChecks?.stop()]);
|
|
1046
1048
|
websocketGateway.close();
|
|
1047
1049
|
await closeServer(server, sockets);
|
|
1050
|
+
state.subscriptionQuotaRejections?.clear();
|
|
1048
1051
|
await flushRuntimeInferenceActivity();
|
|
1049
1052
|
await state.activeAccountManager.flushPendingSave();
|
|
1050
1053
|
},
|
|
@@ -1057,6 +1060,9 @@ export async function startRuntimeRotationProxy(options) {
|
|
|
1057
1060
|
schedulingStrategy: state.schedulingStrategy,
|
|
1058
1061
|
websocketConnections: websocketGateway.stats.connections,
|
|
1059
1062
|
websocketUpstreamRequests: websocketGateway.stats.upstreamRequests,
|
|
1063
|
+
websocketActiveConnections: websocketGateway.stats.activeConnections,
|
|
1064
|
+
websocketCapacityReclaims: websocketGateway.stats.capacityReclaims,
|
|
1065
|
+
lastWebsocketUpgradeRejection: websocketGateway.stats.lastUpgradeRejection,
|
|
1060
1066
|
lastWebsocketDisconnect: websocketGateway.stats.lastDisconnect,
|
|
1061
1067
|
// Redact any email/token material that leaked into a raw upstream or
|
|
1062
1068
|
// refresh error string before exposing it to status/report consumers
|
|
@@ -1603,7 +1609,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1603
1609
|
}
|
|
1604
1610
|
}
|
|
1605
1611
|
};
|
|
1606
|
-
|
|
1612
|
+
let resetCreditState = state.nativeOpenai && isResponsesRequest ? await cachedRead(state, "reset-credits", loadResetCreditState).catch(() => null) : null;
|
|
1607
1613
|
const quotaForScope = (account, scope) => {
|
|
1608
1614
|
// Scoped observations belong to this credential and workspace; legacy entries only describe the binding.
|
|
1609
1615
|
const cached = findQuotaCacheEntryForAccount(subscriptionQuotaCache, account, accountManager.getAccountsSnapshot(), undefined, scope.accountId);
|
|
@@ -1723,7 +1729,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1723
1729
|
if (retryAt > state.now())
|
|
1724
1730
|
throw new CatalogRetryError(retryAt - state.now());
|
|
1725
1731
|
backoff.delete(key);
|
|
1726
|
-
const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.
|
|
1732
|
+
const scope = state.activeAccountManager.getAccountsSnapshot().flatMap(workspaceModelScopes).find(s => s.id === key && s.routable);
|
|
1727
1733
|
const account = scope ? state.activeAccountManager.getAccountByIndex(scope.accountIndex) : null;
|
|
1728
1734
|
if (!account || account.enabled === false || account.authInvalidatedAt ||
|
|
1729
1735
|
(account.cooldownReason === "auth-failure" && (account.coolingDownUntil ?? 0) > state.now()))
|
|
@@ -1796,7 +1802,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1796
1802
|
a.email === reference.email &&
|
|
1797
1803
|
a.accountId === reference.accountId)));
|
|
1798
1804
|
const scopes = discovery.flatMap(workspaceModelScopes);
|
|
1799
|
-
const enabledKeys = scopes.filter(scope => scope.
|
|
1805
|
+
const enabledKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
|
|
1800
1806
|
const routableKeys = scopes.filter(scope => scope.routable).map(scope => scope.id);
|
|
1801
1807
|
const oauthRefresh = modelCatalog.list(enabledKeys);
|
|
1802
1808
|
// Explicit check waits for all workspaces; picker discovery runs API and OAuth in parallel.
|
|
@@ -1875,13 +1881,23 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1875
1881
|
models = await refresh;
|
|
1876
1882
|
else {
|
|
1877
1883
|
const cached = pickerSnapshot();
|
|
1878
|
-
|
|
1884
|
+
// An API cache can be warm while this client-version's OAuth
|
|
1885
|
+
// catalog is still cold. Give subscription discovery the same
|
|
1886
|
+
// bounded opportunity as an entirely empty picker.
|
|
1887
|
+
const oauthReady = modelCatalog.cachedList(routableKeys).length > 0 ||
|
|
1888
|
+
routableKeys.every(key => modelCatalog.hasUsablePickerSnapshot(key));
|
|
1889
|
+
if (cached.length && oauthReady)
|
|
1879
1890
|
models = cached;
|
|
1880
1891
|
else {
|
|
1881
1892
|
let timer;
|
|
1882
1893
|
try {
|
|
1883
1894
|
models = await Promise.race([refresh, new Promise(resolve => {
|
|
1884
|
-
timer = setTimeout(() =>
|
|
1895
|
+
timer = setTimeout(() => {
|
|
1896
|
+
// Do not let an incomplete API-only success replace the
|
|
1897
|
+
// native client's complete cached catalog for minutes.
|
|
1898
|
+
const missingSubscription = routableKeys.length > 0 && !modelCatalog.cachedList(routableKeys).length;
|
|
1899
|
+
resolve(missingSubscription && routableKeys.some(key => !modelCatalog.hasUsablePickerSnapshot(key)) ? [] : pickerSnapshot());
|
|
1900
|
+
}, 2000);
|
|
1885
1901
|
})]);
|
|
1886
1902
|
}
|
|
1887
1903
|
finally {
|
|
@@ -1890,6 +1906,12 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1890
1906
|
}
|
|
1891
1907
|
}
|
|
1892
1908
|
}
|
|
1909
|
+
// A completed failed fetch is still unknown, not a successful
|
|
1910
|
+
// empty catalog. Never replace native subscription entries with
|
|
1911
|
+
// an API-only success while a routable catalog is unknown.
|
|
1912
|
+
if (!forceCatalogRefresh && !models.some(model => !/^(api|zdr)\//.test(model.slug)) &&
|
|
1913
|
+
routableKeys.some(key => !modelCatalog.hasUsablePickerSnapshot(key)))
|
|
1914
|
+
models = [];
|
|
1893
1915
|
res.setHeader("etag", setCatalogEtag(state, models));
|
|
1894
1916
|
writeJson(res, models.length ? 200 : 503, models.length
|
|
1895
1917
|
? { models }
|
|
@@ -1948,20 +1970,45 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1948
1970
|
observations: state.subscriptionQuotaObservations ??= new Map(), now: state.now(), clearQuotaScheduler: a => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(a, context.family, context.model)) });
|
|
1949
1971
|
}
|
|
1950
1972
|
}
|
|
1951
|
-
|
|
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;
|
|
1952
1987
|
try {
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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,
|
|
1956
1992
|
clearQuotaScheduler: account => state.preemptiveQuotaScheduler.clear(buildQuotaScheduleKey(account, context.family, context.model)),
|
|
1957
1993
|
});
|
|
1994
|
+
try {
|
|
1995
|
+
resetCreditState = await loadResetCreditState();
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
resetCreditState = null;
|
|
1999
|
+
}
|
|
2000
|
+
return recovered;
|
|
1958
2001
|
}
|
|
1959
2002
|
catch {
|
|
2003
|
+
resetCreditState = null;
|
|
1960
2004
|
state.status.lastError = "Subscription reset recovery could not be confirmed; no further automatic redemption attempted.";
|
|
2005
|
+
return false;
|
|
1961
2006
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2007
|
+
finally {
|
|
2008
|
+
state.readCache?.delete("reset-credits");
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
await recoverPrimaryQuota();
|
|
1965
2012
|
// A pinned Luna request can arrive after an earlier turn already proved
|
|
1966
2013
|
// ordinary subscription quota exhausted. That persisted family marker
|
|
1967
2014
|
// would make chooseAccount reject the hard pin before any upstream call,
|
|
@@ -2012,8 +2059,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2012
2059
|
// alternative account, so exhausting that local heuristic can only reject a
|
|
2013
2060
|
// request that the pinned account could serve. Transcription is not inference
|
|
2014
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.
|
|
2015
2064
|
// Transcription uses credential-only admission below.
|
|
2016
|
-
const bypassPoolTokenBucket = isPinned || isTranscribeRequest;
|
|
2065
|
+
const bypassPoolTokenBucket = isPinned || isTranscribeRequest || (state.nativeOpenai && isResponsesRequest);
|
|
2017
2066
|
const refundConsumedPoolToken = (account) => {
|
|
2018
2067
|
if (!bypassPoolTokenBucket) {
|
|
2019
2068
|
accountManager.refundToken(account, context.family, context.model);
|
|
@@ -2105,8 +2154,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2105
2154
|
return "retry";
|
|
2106
2155
|
};
|
|
2107
2156
|
let runtimeSelectionIterations = 0;
|
|
2157
|
+
let recoveryRetriedAfterExhaustion = false;
|
|
2108
2158
|
while ((isPinned ||
|
|
2109
2159
|
attemptedIndexes.size < accountCount ||
|
|
2160
|
+
(!isPinned && !recoveryRetriedAfterExhaustion) ||
|
|
2110
2161
|
(isTranscribeRequest && !state.nativeOpenai && accountCount === 0 && !reloadedAfterNoAccount)) &&
|
|
2111
2162
|
transientAttempts < transientAttemptLimit &&
|
|
2112
2163
|
(!isPinned ||
|
|
@@ -2157,6 +2208,14 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2157
2208
|
}
|
|
2158
2209
|
}
|
|
2159
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
|
+
}
|
|
2160
2219
|
if (isTranscribeRequest) {
|
|
2161
2220
|
const candidate = chooseTranscriptionAccount({
|
|
2162
2221
|
accountManager,
|
|
@@ -2194,12 +2253,13 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2194
2253
|
stickyBoostByAccount: rotationStickyBoost,
|
|
2195
2254
|
pidOffsetEnabled: state.pidOffsetEnabled,
|
|
2196
2255
|
schedulingStrategy: state.schedulingStrategy,
|
|
2197
|
-
//
|
|
2198
|
-
//
|
|
2199
|
-
//
|
|
2200
|
-
//
|
|
2201
|
-
//
|
|
2202
|
-
allowPinnedCooldown: isPinned && transientAttempts > 0
|
|
2256
|
+
// A shared network cooldown is not evidence that the pinned
|
|
2257
|
+
// credentials cannot serve independent concurrent work. Let each
|
|
2258
|
+
// new request try; its own retries remain bounded and backed off.
|
|
2259
|
+
// Other initial cooldowns still block, as do quota/auth/policy
|
|
2260
|
+
// checks and the circuit admission immediately after selection.
|
|
2261
|
+
allowPinnedCooldown: isPinned && (transientAttempts > 0 ||
|
|
2262
|
+
(typeof pinnedIndex === "number" && accountManager.getAccountByIndex(pinnedIndex)?.cooldownReason === "network-error")),
|
|
2203
2263
|
});
|
|
2204
2264
|
return result;
|
|
2205
2265
|
};
|
|
@@ -2227,6 +2287,20 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2227
2287
|
for (const index of catalogExcludedIndexes())
|
|
2228
2288
|
accountSkipReasons.set(index, "model-not-supported");
|
|
2229
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
|
+
}
|
|
2230
2304
|
if (isTranscribeRequest &&
|
|
2231
2305
|
!state.nativeOpenai &&
|
|
2232
2306
|
!reloadedAfterNoAccount) {
|
|
@@ -2326,10 +2400,12 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2326
2400
|
}
|
|
2327
2401
|
attemptedIndexes.add(selected.index);
|
|
2328
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());
|
|
2329
2405
|
const preemptiveDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
2330
2406
|
if (!isTranscribeRequest &&
|
|
2331
2407
|
context.model !== LUNA_RESERVE_MODEL &&
|
|
2332
|
-
preemptiveDeferral.defer &&
|
|
2408
|
+
!nativePermission && preemptiveDeferral.defer &&
|
|
2333
2409
|
preemptiveDeferral.waitMs > 0) {
|
|
2334
2410
|
accountSkipReasons.set(selected.index, preemptiveDeferral.reason ?? "quota-near-exhaustion");
|
|
2335
2411
|
exhaustionReason = "rate-limit";
|
|
@@ -2474,7 +2550,8 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2474
2550
|
continue;
|
|
2475
2551
|
}
|
|
2476
2552
|
let pendingScopes = workspaceCandidates.get(refreshed.account.index);
|
|
2477
|
-
|
|
2553
|
+
// Routes without model-catalog preparation still use the selected enabled workspace.
|
|
2554
|
+
if (isImageRequest || isTranscribeRequest || isThreadGoalRequest) {
|
|
2478
2555
|
pendingScopes = workspaceModelScopes(refreshed.account)
|
|
2479
2556
|
.filter((scope) => scope.routable)
|
|
2480
2557
|
.sort((a, b) => Number(b.selected) - Number(a.selected) || Number(b.bound) - Number(a.bound));
|
|
@@ -2531,14 +2608,10 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2531
2608
|
: undefined;
|
|
2532
2609
|
const disk = latest?.accounts.find(item => item.recordId && item.recordId === refreshed.account.recordId)
|
|
2533
2610
|
?? latest?.accounts.find(item => item.accountId === refreshed.account.accountId && sanitizeEmail(item.email) === sanitizeEmail(refreshed.account.email)) ?? retained;
|
|
2534
|
-
const workspace = disk?.workspaces?.find(item => item.id.trim() === accountId);
|
|
2535
|
-
// Derive the stored binding exactly as workspaceModelScopes does: trimmed,
|
|
2536
|
-
// falling back to the token's workspace claim when no id was stored.
|
|
2537
|
-
// A stored record carries accessToken; the transient-grace fallback is a live account (access).
|
|
2538
2611
|
const diskToken = disk?.accessToken ?? disk?.access;
|
|
2539
|
-
const diskBoundId = disk ? disk.accountId?.trim() || extractAccountId(diskToken)?.trim() : undefined;
|
|
2540
2612
|
const stillEligible = disk && disk.enabled !== false && !disk.authInvalidatedAt &&
|
|
2541
|
-
(
|
|
2613
|
+
workspaceModelScopes({ ...disk, index: refreshed.account.index, access: diskToken })
|
|
2614
|
+
.some(scope => scope.routable && scope.accountId === accountId);
|
|
2542
2615
|
if (!stillEligible) {
|
|
2543
2616
|
writeJson(res, 503, { error: { code: "routing_configuration_changed", message: "Routing eligibility changed while preparing the request. Retry with the current configuration." } });
|
|
2544
2617
|
return;
|
|
@@ -2711,7 +2784,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2711
2784
|
const key = JSON.stringify([requestScope.id, context.model]);
|
|
2712
2785
|
if (observations.size >= 1000 && !observations.has(key))
|
|
2713
2786
|
observations.delete(observations.keys().next().value ?? "");
|
|
2714
|
-
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
|
+
});
|
|
2715
2790
|
}
|
|
2716
2791
|
};
|
|
2717
2792
|
const quotaSnapshot = readQuotaSchedulerSnapshot(upstream.headers, upstream.status, state.now());
|
|
@@ -2812,6 +2887,17 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2812
2887
|
if (outcome === "retry")
|
|
2813
2888
|
continue;
|
|
2814
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
|
+
}
|
|
2815
2901
|
if (context.model !== LUNA_RESERVE_MODEL && subscriptionQuotaExceeded) {
|
|
2816
2902
|
state.preemptiveQuotaScheduler.markRateLimited(quotaScheduleKey, retryAfterMs, state.now());
|
|
2817
2903
|
}
|
|
@@ -2824,7 +2910,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2824
2910
|
(context.model === "gpt-6-luna" || context.model === "gpt-5.6-luna")) {
|
|
2825
2911
|
const confirmedKey = confirmedQuotaExhaustionKey(context.family, context.model);
|
|
2826
2912
|
const confirmedResetAt = state.now() +
|
|
2827
|
-
Math.min(Math.max(0, Math.floor(retryAfterMs)),
|
|
2913
|
+
Math.min(Math.max(0, Math.floor(retryAfterMs)), RATE_LIMIT_CACHE_TTL_MS);
|
|
2828
2914
|
rateLimitState.account.rateLimitResetTimes[confirmedKey] = Math.max(rateLimitState.account.rateLimitResetTimes[confirmedKey] ?? 0, confirmedResetAt);
|
|
2829
2915
|
}
|
|
2830
2916
|
rateLimitState.accountManager.saveToDiskDebounced();
|
|
@@ -2864,7 +2950,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2864
2950
|
}
|
|
2865
2951
|
exhaustionReason = "rate-limit";
|
|
2866
2952
|
capabilityRejectionIsLatest = false;
|
|
2867
|
-
|
|
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;
|
|
2868
2958
|
transientExhaustionReason = "rate-limit";
|
|
2869
2959
|
state.status.retries += 1;
|
|
2870
2960
|
noteRotation();
|
|
@@ -2874,9 +2964,9 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
2874
2964
|
const bodyText = await readErrorBody(upstream, state.streamStallTimeoutMs);
|
|
2875
2965
|
const errorCode = extractErrorCodeFromBody(bodyText);
|
|
2876
2966
|
if (isWorkspaceDisabledError(upstream.status, errorCode, bodyText)) {
|
|
2877
|
-
if (requestScope
|
|
2967
|
+
if (requestScope?.storedWorkspaceId !== undefined) {
|
|
2878
2968
|
const workspaceState = responseStateTarget();
|
|
2879
|
-
if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.
|
|
2969
|
+
if (workspaceState.accountManager.disableWorkspace(workspaceState.account, requestScope.storedWorkspaceId))
|
|
2880
2970
|
workspaceState.accountManager.saveToDiskDebounced();
|
|
2881
2971
|
pendingScopes?.shift();
|
|
2882
2972
|
refundConsumedPoolToken(refreshed.account);
|
|
@@ -3111,7 +3201,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3111
3201
|
// No-op when no reason is recorded, so the hot path stays write-free.
|
|
3112
3202
|
if (!isTranscribeRequest) {
|
|
3113
3203
|
const quotaDeferral = state.preemptiveQuotaScheduler.getDeferral(quotaScheduleKey, state.now());
|
|
3114
|
-
const nearExhaustionWaitMs = context.model === LUNA_RESERVE_MODEL
|
|
3204
|
+
const nearExhaustionWaitMs = (context.model === LUNA_RESERVE_MODEL || (requestScope && hasFreshOrdinaryUsagePermission(quotaForScope(refreshed.account, requestScope), state.now())))
|
|
3115
3205
|
? 0
|
|
3116
3206
|
: quotaDeferral.defer
|
|
3117
3207
|
? quotaDeferral.waitMs
|
|
@@ -3382,9 +3472,13 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3382
3472
|
// Read live account state, not only selection skip reasons. The final
|
|
3383
3473
|
// refresh attempt can install an auth cooldown after the last selection
|
|
3384
3474
|
// pass, so that blocker may never have been written to accountSkipReasons.
|
|
3475
|
+
const backupsReserved = !isPinned && !emergencyBackupsAllowed();
|
|
3385
3476
|
for (const account of accountManager.getAccountsSnapshot()) {
|
|
3386
3477
|
if (policyDecision?.blockedAccountIndexes.has(account.index))
|
|
3387
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;
|
|
3388
3482
|
if (account.enabled === false)
|
|
3389
3483
|
continue;
|
|
3390
3484
|
if (!accountManager.hasEnabledWorkspaces(account))
|
|
@@ -3409,6 +3503,11 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3409
3503
|
return minWaitMs ?? 0;
|
|
3410
3504
|
})()
|
|
3411
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
|
+
}
|
|
3412
3511
|
writePoolExhausted({
|
|
3413
3512
|
res,
|
|
3414
3513
|
accountManager,
|
|
@@ -3416,6 +3515,7 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
3416
3515
|
model: context.model,
|
|
3417
3516
|
reason: exhaustionReason,
|
|
3418
3517
|
retryAfterMs: transcriptionRetryAfterMs,
|
|
3518
|
+
ineligibleAccountIndexes,
|
|
3419
3519
|
accountSkipReasons: Object.fromEntries([...accountSkipReasons.entries()].map(([index, reason]) => [
|
|
3420
3520
|
String(index),
|
|
3421
3521
|
reason,
|
package/dist/lib/schemas.d.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
export declare const PluginConfigSchema: z.ZodObject<{
|
|
8
|
+
locale: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
auto: "auto";
|
|
10
|
+
en: "en";
|
|
11
|
+
zh: "zh";
|
|
12
|
+
}>>;
|
|
8
13
|
codexMode: z.ZodOptional<z.ZodBoolean>;
|
|
9
14
|
codexRuntimeRotationProxy: z.ZodOptional<z.ZodBoolean>;
|
|
10
15
|
codexTuiV2: z.ZodOptional<z.ZodBoolean>;
|
package/dist/lib/schemas.js
CHANGED
|
@@ -22,6 +22,7 @@ function schemaLog() {
|
|
|
22
22
|
// Plugin Configuration Schema
|
|
23
23
|
// ============================================================================
|
|
24
24
|
export const PluginConfigSchema = z.object({
|
|
25
|
+
locale: z.enum(["auto", "en", "zh"]).optional(),
|
|
25
26
|
codexMode: z.boolean().optional(),
|
|
26
27
|
codexRuntimeRotationProxy: z.boolean().optional(),
|
|
27
28
|
codexTuiV2: z.boolean().optional(),
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderDashboardHtml(token: string | null, locale?: "en" | "zh"): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
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({"&":"&","<":"<",">":">",'"':""","'":"'"})[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
|
+
</html>`;
|
|
55
|
+
export function renderDashboardHtml(token, locale = "zh") {
|
|
56
|
+
const safeToken = JSON.stringify(token ?? "")
|
|
57
|
+
.slice(1, -1)
|
|
58
|
+
.replace(/</g, "\\u003c")
|
|
59
|
+
.replace(/>/g, "\\u003e")
|
|
60
|
+
.replace(/&/g, "\\u0026")
|
|
61
|
+
.replace(/\u2028/g, "\\u2028")
|
|
62
|
+
.replace(/\u2029/g, "\\u2029");
|
|
63
|
+
return DASHBOARD_HTML
|
|
64
|
+
.replace("__CMA_TOKEN__", safeToken)
|
|
65
|
+
.replace("__CMA_LANG__", locale === "zh" ? "zh" : "en");
|
|
66
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Server, type ServerResponse } from "node:http";
|
|
2
|
+
import { type OutputLocale } from "./locale.js";
|
|
3
|
+
export interface WebStatusSnapshot {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface WebServerOptions {
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
token?: string | null;
|
|
10
|
+
refreshMs?: number;
|
|
11
|
+
getStatus: () => Promise<WebStatusSnapshot>;
|
|
12
|
+
openBrowser?: (url: string) => void;
|
|
13
|
+
locale?: OutputLocale;
|
|
14
|
+
/** Called when an SSE client is dropped for falling behind on broadcasts. */
|
|
15
|
+
onSseClientDrop?: (client: ServerResponse) => void;
|
|
16
|
+
}
|
|
17
|
+
export interface WebServerHandle {
|
|
18
|
+
server: Server;
|
|
19
|
+
url: string;
|
|
20
|
+
token: string | null;
|
|
21
|
+
close: () => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function startWebServer(options: WebServerOptions): Promise<WebServerHandle>;
|
|
24
|
+
export declare function runWebCommand(args: string[], deps: {
|
|
25
|
+
getStatus: () => Promise<WebStatusSnapshot>;
|
|
26
|
+
logInfo?: (message: string) => void;
|
|
27
|
+
openBrowser?: (url: string) => void;
|
|
28
|
+
locale?: OutputLocale;
|
|
29
|
+
}): Promise<number>;
|
|
30
|
+
export declare function parseWebCommandArgs(args: string[]): {
|
|
31
|
+
host: string;
|
|
32
|
+
port: number;
|
|
33
|
+
token: string | null;
|
|
34
|
+
refreshMs: number;
|
|
35
|
+
openBrowser: boolean;
|
|
36
|
+
};
|