@juspay/neurolink 12.7.3 → 12.7.4
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/commands/auth.js +252 -125
- package/dist/cli/commands/proxy.js +110 -91
- package/dist/cli/factories/authCommandFactory.js +8 -11
- package/dist/proxy/codexAccountUsage.d.ts +1 -1
- package/dist/proxy/codexAccountUsage.js +13 -1
- package/dist/proxy/proxyActivity.js +12 -1
- package/dist/proxy/usageStats.d.ts +2 -2
- package/dist/proxy/usageStats.js +66 -10
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -1
- package/dist/server/routes/claudeProxyRoutes.js +62 -12
- package/dist/server/routes/codexProxyRoutes.d.ts +8 -2
- package/dist/server/routes/codexProxyRoutes.js +191 -58
- package/dist/types/cli.d.ts +37 -5
- package/dist/types/proxy.d.ts +37 -0
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serv
|
|
|
24
24
|
import { configureProxyKeepAliveDispatcher } from "../../proxy/proxyDispatcher.js";
|
|
25
25
|
import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
|
|
26
26
|
import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
|
|
27
|
-
import { anthropicAccountKeysEqual, createAccountAllowlist,
|
|
27
|
+
import { anthropicAccountKeysEqual, createAccountAllowlist, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
28
28
|
import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
|
|
29
29
|
import { beginProxyRequest, getProxyActivitySnapshot, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
30
30
|
import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
|
|
@@ -66,11 +66,9 @@ const gatedShareRequests = new WeakSet();
|
|
|
66
66
|
const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
67
67
|
/** How long shutdown waits on the share listener before moving on. */
|
|
68
68
|
const SHARE_LISTENER_CLOSE_TIMEOUT_MS = 10_000;
|
|
69
|
-
const LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS = 5_000;
|
|
70
69
|
const PROXY_STATUS_TOKEN_READ_TIMEOUT_MS = 2_000;
|
|
71
70
|
const PROXY_STATUS_RECONCILE_TIMEOUT_MS = 750;
|
|
72
71
|
const PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS = 750;
|
|
73
|
-
let legacyStatusAccountCache;
|
|
74
72
|
// Allowed drift between a pid's OS-reported start time and the persisted
|
|
75
73
|
// ProxySupervisorState.startTime before processLooksLikeProxySupervisor
|
|
76
74
|
// treats it as a confident mismatch (recycled pid). Generous on purpose:
|
|
@@ -386,34 +384,6 @@ async function resolveStatusPrimaryAccount(proxyConfig) {
|
|
|
386
384
|
source: "fallback",
|
|
387
385
|
};
|
|
388
386
|
}
|
|
389
|
-
async function resolveLegacyStatusAccountLabel(storedAnthropicAccountCount) {
|
|
390
|
-
if (storedAnthropicAccountCount !== 0) {
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
const credentialsPath = join(homedir(), ".neurolink", "anthropic-credentials.json");
|
|
394
|
-
const now = Date.now();
|
|
395
|
-
if (legacyStatusAccountCache?.credentialsPath === credentialsPath &&
|
|
396
|
-
legacyStatusAccountCache.expiresAt > now) {
|
|
397
|
-
return legacyStatusAccountCache.label;
|
|
398
|
-
}
|
|
399
|
-
let label = null;
|
|
400
|
-
try {
|
|
401
|
-
const { readFile } = await import("node:fs/promises");
|
|
402
|
-
const parsed = JSON.parse(await readFile(credentialsPath, "utf8"));
|
|
403
|
-
if (parsed.oauth?.accessToken) {
|
|
404
|
-
label = parsed.email?.trim() || "legacy-default";
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
catch {
|
|
408
|
-
label = null;
|
|
409
|
-
}
|
|
410
|
-
legacyStatusAccountCache = {
|
|
411
|
-
credentialsPath,
|
|
412
|
-
expiresAt: now + LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS,
|
|
413
|
-
label,
|
|
414
|
-
};
|
|
415
|
-
return label;
|
|
416
|
-
}
|
|
417
387
|
function deriveAccountAllowance(accountKey, now, allowlist, expirations, cooldowns) {
|
|
418
388
|
const allowed = isAccountAllowed(accountKey, allowlist);
|
|
419
389
|
const expiresAt = expirations.get(accountKey);
|
|
@@ -1067,6 +1037,7 @@ function redactStatusAccounts(rows, allowed) {
|
|
|
1067
1037
|
return rows.map((row, index) => ({
|
|
1068
1038
|
...row,
|
|
1069
1039
|
label: `account-${index + 1}`,
|
|
1040
|
+
...(row.key !== undefined ? { key: null } : {}),
|
|
1070
1041
|
...(row.email !== undefined ? { email: null } : {}),
|
|
1071
1042
|
}));
|
|
1072
1043
|
}
|
|
@@ -1686,21 +1657,28 @@ export async function createProxyStartApp(params) {
|
|
|
1686
1657
|
logger.debug(`[proxy] /status using empty cooldown snapshot: ${error instanceof Error ? error.message : String(error)}`);
|
|
1687
1658
|
return {};
|
|
1688
1659
|
});
|
|
1689
|
-
const
|
|
1660
|
+
const storedAnthropicAccountKeys = new Set();
|
|
1661
|
+
const storedCodexAccountKeys = new Set();
|
|
1690
1662
|
const storedAccountExpirations = new Map();
|
|
1691
|
-
const disabledAccountKeys = new Set();
|
|
1692
1663
|
const disabledProviderAccountKeys = new Set();
|
|
1693
1664
|
let accountInventoryLoaded = false;
|
|
1694
1665
|
try {
|
|
1695
1666
|
const { tokenStore } = await import("../../auth/tokenStore.js");
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1667
|
+
const [anthropicKeys, codexKeys] = await withTimeout(Promise.all([
|
|
1668
|
+
tokenStore.listByPrefix("anthropic:"),
|
|
1669
|
+
tokenStore.listByPrefix("codex:"),
|
|
1670
|
+
]), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account enumeration timed out");
|
|
1671
|
+
for (const key of anthropicKeys) {
|
|
1672
|
+
storedAnthropicAccountKeys.add(normalizeAnthropicAccountKey(key));
|
|
1673
|
+
}
|
|
1674
|
+
for (const key of codexKeys) {
|
|
1675
|
+
storedCodexAccountKeys.add(key);
|
|
1699
1676
|
}
|
|
1700
1677
|
// Once account names are known, preserve them even when optional token
|
|
1701
1678
|
// metadata is slow. That keeps the status table useful and avoids
|
|
1702
1679
|
// incorrectly presenting known accounts as removed.
|
|
1703
1680
|
accountInventoryLoaded = true;
|
|
1681
|
+
const storedKeys = [...anthropicKeys, ...codexKeys];
|
|
1704
1682
|
const inventory = await withTimeout((async () => {
|
|
1705
1683
|
const tokenExpirations = await Promise.all(storedKeys.map(async (key) => {
|
|
1706
1684
|
try {
|
|
@@ -1717,20 +1695,21 @@ export async function createProxyStartApp(params) {
|
|
|
1717
1695
|
})(), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status account metadata timed out");
|
|
1718
1696
|
for (const expiration of inventory.tokenExpirations) {
|
|
1719
1697
|
if (expiration) {
|
|
1720
|
-
|
|
1698
|
+
const key = expiration[0].startsWith("anthropic:")
|
|
1699
|
+
? normalizeAnthropicAccountKey(expiration[0])
|
|
1700
|
+
: expiration[0];
|
|
1701
|
+
storedAccountExpirations.set(key, expiration[1]);
|
|
1721
1702
|
}
|
|
1722
1703
|
}
|
|
1723
1704
|
for (const key of inventory.disabledKeys) {
|
|
1724
|
-
|
|
1725
|
-
|
|
1705
|
+
disabledProviderAccountKeys.add(key.startsWith("anthropic:")
|
|
1706
|
+
? normalizeAnthropicAccountKey(key)
|
|
1707
|
+
: key);
|
|
1726
1708
|
}
|
|
1727
1709
|
}
|
|
1728
1710
|
catch (err) {
|
|
1729
1711
|
logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
|
|
1730
1712
|
}
|
|
1731
|
-
const legacyAccountLabel = accountInventoryLoaded
|
|
1732
|
-
? await withTimeout(resolveLegacyStatusAccountLabel(storedAccountKeys.size), PROXY_STATUS_ACCOUNT_INVENTORY_TIMEOUT_MS, "[proxy] /status legacy account inspection timed out").catch(() => null)
|
|
1733
|
-
: null;
|
|
1734
1713
|
const now = Date.now();
|
|
1735
1714
|
const health = buildProxyHealthResponse(readiness, {
|
|
1736
1715
|
strategy: activeStrategy,
|
|
@@ -1744,52 +1723,54 @@ export async function createProxyStartApp(params) {
|
|
|
1744
1723
|
source: "fallback",
|
|
1745
1724
|
}));
|
|
1746
1725
|
const activeUpdaterPid = supervisorState?.updaterPid ?? runtimeState?.updaterPid;
|
|
1747
|
-
const accountRows = Object.
|
|
1748
|
-
|
|
1726
|
+
const accountRows = Object.entries(stats.accounts).map(([persistedMapKey, account]) => {
|
|
1727
|
+
// Legacy snapshots used their bare map key as an inferred identity. Do
|
|
1728
|
+
// not map those old rows to a provider based on an email: the same email
|
|
1729
|
+
// can be present in both pools. New records carry account.key.
|
|
1730
|
+
const persistedKey = account.key ?? null;
|
|
1731
|
+
const identity = persistedKey
|
|
1732
|
+
? resolveProxyStatusAccountIdentity(account.label, account.type, persistedKey)
|
|
1733
|
+
: { provider: "other", key: null };
|
|
1734
|
+
const isLegacyUnattributed = persistedKey === null &&
|
|
1735
|
+
(account.type === "oauth" ||
|
|
1736
|
+
account.type === "api_key" ||
|
|
1737
|
+
account.type === "codex-oauth");
|
|
1749
1738
|
const isAnthropicAccount = identity.provider === "anthropic";
|
|
1750
1739
|
const isCodexAccount = identity.provider === "codex";
|
|
1751
|
-
const
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
? normalizedKey
|
|
1756
|
-
: isLegacyAccount
|
|
1757
|
-
? LEGACY_ANTHROPIC_ACCOUNT_KEY
|
|
1758
|
-
: account.label === "env"
|
|
1759
|
-
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1760
|
-
: normalizedKey
|
|
1761
|
-
: identity.key;
|
|
1762
|
-
const isStored = isAnthropicAccount &&
|
|
1763
|
-
accountKey !== null &&
|
|
1764
|
-
(storedAccountKeys.has(accountKey) || isLegacyAccount);
|
|
1765
|
-
const { allowed, expired, cooling } = isAnthropicAccount && accountKey !== null
|
|
1740
|
+
const accountKey = identity.key ?? (persistedKey || persistedMapKey);
|
|
1741
|
+
const isStored = (isAnthropicAccount && storedAnthropicAccountKeys.has(accountKey)) ||
|
|
1742
|
+
(isCodexAccount && storedCodexAccountKeys.has(accountKey));
|
|
1743
|
+
const providerState = isAnthropicAccount
|
|
1766
1744
|
? deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns)
|
|
1767
|
-
:
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
(isCodexAccount &&
|
|
1777
|
-
accountKey !== null &&
|
|
1778
|
-
disabledProviderAccountKeys.has(accountKey));
|
|
1745
|
+
: isCodexAccount
|
|
1746
|
+
? {
|
|
1747
|
+
allowed: true,
|
|
1748
|
+
expired: (storedAccountExpirations.get(accountKey) ?? 0) > 0 &&
|
|
1749
|
+
(storedAccountExpirations.get(accountKey) ?? 0) <= now,
|
|
1750
|
+
cooling: (cooldowns[accountKey]?.coolingUntil ?? 0) > now,
|
|
1751
|
+
}
|
|
1752
|
+
: { allowed: true, expired: false, cooling: false };
|
|
1753
|
+
const isDisabled = !isLegacyUnattributed && disabledProviderAccountKeys.has(accountKey);
|
|
1779
1754
|
const accountStatus = account.type === "internal"
|
|
1780
1755
|
? "internal"
|
|
1781
|
-
:
|
|
1782
|
-
? "
|
|
1783
|
-
:
|
|
1784
|
-
? "
|
|
1785
|
-
:
|
|
1786
|
-
? "
|
|
1787
|
-
:
|
|
1788
|
-
? "
|
|
1789
|
-
:
|
|
1790
|
-
|
|
1791
|
-
|
|
1756
|
+
: isLegacyUnattributed
|
|
1757
|
+
? "unattributed"
|
|
1758
|
+
: isDisabled
|
|
1759
|
+
? "disabled"
|
|
1760
|
+
: providerState.expired
|
|
1761
|
+
? "expired"
|
|
1762
|
+
: isAnthropicAccount && !providerState.allowed
|
|
1763
|
+
? "excluded"
|
|
1764
|
+
: accountInventoryLoaded &&
|
|
1765
|
+
(isAnthropicAccount || isCodexAccount) &&
|
|
1766
|
+
!isStored
|
|
1767
|
+
? "removed"
|
|
1768
|
+
: providerState.cooling
|
|
1769
|
+
? "cooling"
|
|
1770
|
+
: "active";
|
|
1792
1771
|
return {
|
|
1772
|
+
key: isLegacyUnattributed ? null : accountKey,
|
|
1773
|
+
provider: isLegacyUnattributed ? "unknown" : identity.provider,
|
|
1793
1774
|
label: account.label,
|
|
1794
1775
|
type: account.type,
|
|
1795
1776
|
attempts: account.attemptCount,
|
|
@@ -1800,21 +1781,26 @@ export async function createProxyStartApp(params) {
|
|
|
1800
1781
|
rateLimits: account.rateLimitCount,
|
|
1801
1782
|
transientRateLimits: account.transientRateLimitCount,
|
|
1802
1783
|
quotaRateLimits: account.quotaRateLimitCount,
|
|
1803
|
-
cooling,
|
|
1784
|
+
cooling: providerState.cooling,
|
|
1804
1785
|
status: accountStatus,
|
|
1805
|
-
allowed: account.type === "internal" ? undefined : allowed,
|
|
1806
|
-
expired: isAnthropicAccount
|
|
1786
|
+
allowed: account.type === "internal" ? undefined : providerState.allowed,
|
|
1787
|
+
expired: isAnthropicAccount || isCodexAccount
|
|
1788
|
+
? providerState.expired
|
|
1789
|
+
: undefined,
|
|
1807
1790
|
};
|
|
1808
1791
|
});
|
|
1809
|
-
const representedAccountKeys = new Set(accountRows
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1792
|
+
const representedAccountKeys = new Set(accountRows.flatMap((account) => account.key &&
|
|
1793
|
+
(account.provider === "anthropic" || account.provider === "codex")
|
|
1794
|
+
? [account.key]
|
|
1795
|
+
: []));
|
|
1796
|
+
for (const accountKey of storedAnthropicAccountKeys) {
|
|
1813
1797
|
if (representedAccountKeys.has(accountKey)) {
|
|
1814
1798
|
continue;
|
|
1815
1799
|
}
|
|
1816
1800
|
const { allowed, expired, cooling } = deriveAccountAllowance(accountKey, now, activeAccountAllowlist, storedAccountExpirations, cooldowns);
|
|
1817
1801
|
accountRows.push({
|
|
1802
|
+
key: accountKey,
|
|
1803
|
+
provider: "anthropic",
|
|
1818
1804
|
label: accountKey.slice("anthropic:".length),
|
|
1819
1805
|
type: "oauth",
|
|
1820
1806
|
attempts: 0,
|
|
@@ -1826,7 +1812,7 @@ export async function createProxyStartApp(params) {
|
|
|
1826
1812
|
transientRateLimits: 0,
|
|
1827
1813
|
quotaRateLimits: 0,
|
|
1828
1814
|
cooling,
|
|
1829
|
-
status:
|
|
1815
|
+
status: disabledProviderAccountKeys.has(accountKey)
|
|
1830
1816
|
? "disabled"
|
|
1831
1817
|
: expired
|
|
1832
1818
|
? "expired"
|
|
@@ -1839,6 +1825,38 @@ export async function createProxyStartApp(params) {
|
|
|
1839
1825
|
expired,
|
|
1840
1826
|
});
|
|
1841
1827
|
}
|
|
1828
|
+
for (const accountKey of storedCodexAccountKeys) {
|
|
1829
|
+
if (representedAccountKeys.has(accountKey)) {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const expiresAt = storedAccountExpirations.get(accountKey);
|
|
1833
|
+
const expired = expiresAt !== undefined && expiresAt > 0 && expiresAt <= now;
|
|
1834
|
+
const cooling = (cooldowns[accountKey]?.coolingUntil ?? 0) > now;
|
|
1835
|
+
accountRows.push({
|
|
1836
|
+
key: accountKey,
|
|
1837
|
+
provider: "codex",
|
|
1838
|
+
label: accountKey.slice("codex:".length),
|
|
1839
|
+
type: "codex-oauth",
|
|
1840
|
+
attempts: 0,
|
|
1841
|
+
requests: 0,
|
|
1842
|
+
success: 0,
|
|
1843
|
+
errors: 0,
|
|
1844
|
+
attemptErrors: 0,
|
|
1845
|
+
rateLimits: 0,
|
|
1846
|
+
transientRateLimits: 0,
|
|
1847
|
+
quotaRateLimits: 0,
|
|
1848
|
+
cooling,
|
|
1849
|
+
status: disabledProviderAccountKeys.has(accountKey)
|
|
1850
|
+
? "disabled"
|
|
1851
|
+
: expired
|
|
1852
|
+
? "expired"
|
|
1853
|
+
: cooling
|
|
1854
|
+
? "cooling"
|
|
1855
|
+
: "active",
|
|
1856
|
+
allowed: true,
|
|
1857
|
+
expired,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1842
1860
|
const attributed = accountRows.reduce((total, account) => ({
|
|
1843
1861
|
attempts: total.attempts + (account.attempts ?? 0),
|
|
1844
1862
|
requests: total.requests + (account.requests ?? 0),
|
|
@@ -3031,7 +3049,8 @@ function printStatusStats(stats) {
|
|
|
3031
3049
|
if (lastError) {
|
|
3032
3050
|
const cause = lastError.errorType ?? lastError.category;
|
|
3033
3051
|
const code = lastError.errorCode ? `/${lastError.errorCode}` : "";
|
|
3034
|
-
const
|
|
3052
|
+
const accountIdentity = lastError.accountKey ?? lastError.account;
|
|
3053
|
+
const account = accountIdentity ? ` account=${accountIdentity}` : "";
|
|
3035
3054
|
console.info(` Last error: ${new Date(lastError.at).toISOString()} ${cause}${code} status=${lastError.status}${account}`);
|
|
3036
3055
|
if (lastError.message) {
|
|
3037
3056
|
console.info(` Last cause: ${sanitizeProxyStatusTerminalErrorMessage(lastError.message)}`);
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* - login, logout, status, refresh: Anthropic OAuth (API key + OAuth)
|
|
7
7
|
* - providers, validate, health: Multi-provider auth management
|
|
8
8
|
*/
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
const SUPPORTED_PROVIDERS = ["anthropic", "codex"];
|
|
9
|
+
/** Providers with first-class credential flows in `neurolink auth`. */
|
|
10
|
+
const AUTH_LOGIN_PROVIDERS = ["anthropic", "codex"];
|
|
13
11
|
/**
|
|
14
12
|
* Auth Command Factory
|
|
15
13
|
*
|
|
@@ -167,7 +165,7 @@ export class AuthCommandFactory {
|
|
|
167
165
|
.positional("provider", {
|
|
168
166
|
type: "string",
|
|
169
167
|
description: "AI provider to authenticate with",
|
|
170
|
-
choices:
|
|
168
|
+
choices: AUTH_LOGIN_PROVIDERS,
|
|
171
169
|
demandOption: true,
|
|
172
170
|
})
|
|
173
171
|
.option("method", {
|
|
@@ -204,7 +202,7 @@ export class AuthCommandFactory {
|
|
|
204
202
|
.positional("provider", {
|
|
205
203
|
type: "string",
|
|
206
204
|
description: "AI provider to log out from",
|
|
207
|
-
choices:
|
|
205
|
+
choices: AUTH_LOGIN_PROVIDERS,
|
|
208
206
|
demandOption: true,
|
|
209
207
|
})
|
|
210
208
|
.example("$0 auth logout anthropic", "Clear all Anthropic credentials");
|
|
@@ -217,7 +215,7 @@ export class AuthCommandFactory {
|
|
|
217
215
|
.positional("provider", {
|
|
218
216
|
type: "string",
|
|
219
217
|
description: "AI provider to check (optional, shows all if not specified)",
|
|
220
|
-
choices:
|
|
218
|
+
choices: AUTH_LOGIN_PROVIDERS,
|
|
221
219
|
})
|
|
222
220
|
.example("$0 auth status", "Show status for all configured providers")
|
|
223
221
|
.example("$0 auth status anthropic", "Show Anthropic authentication status")
|
|
@@ -231,7 +229,7 @@ export class AuthCommandFactory {
|
|
|
231
229
|
.positional("provider", {
|
|
232
230
|
type: "string",
|
|
233
231
|
description: "AI provider to refresh tokens for",
|
|
234
|
-
choices:
|
|
232
|
+
choices: AUTH_LOGIN_PROVIDERS,
|
|
235
233
|
demandOption: true,
|
|
236
234
|
})
|
|
237
235
|
.example("$0 auth refresh anthropic", "Refresh Anthropic OAuth tokens");
|
|
@@ -244,7 +242,7 @@ export class AuthCommandFactory {
|
|
|
244
242
|
.option("refresh", {
|
|
245
243
|
type: "boolean",
|
|
246
244
|
default: false,
|
|
247
|
-
description: "
|
|
245
|
+
description: "Refresh available provider limits for authenticated accounts; show unsupported providers explicitly",
|
|
248
246
|
})
|
|
249
247
|
.example("$0 auth list", "List all authenticated accounts")
|
|
250
248
|
.example("$0 auth list --format json", "List accounts in JSON format")
|
|
@@ -257,8 +255,7 @@ export class AuthCommandFactory {
|
|
|
257
255
|
return yargs
|
|
258
256
|
.positional("provider", {
|
|
259
257
|
type: "string",
|
|
260
|
-
description: "
|
|
261
|
-
choices: SUPPORTED_PROVIDERS,
|
|
258
|
+
description: "Provider namespace to remove an account from",
|
|
262
259
|
demandOption: true,
|
|
263
260
|
})
|
|
264
261
|
.option("label", {
|
|
@@ -12,7 +12,7 @@ export declare const CODEX_ACCOUNT_PREFIX = "codex:";
|
|
|
12
12
|
/** Keep an account label in the Codex namespace used by its token store. */
|
|
13
13
|
export declare function normalizeCodexAccountKey(value: string): string;
|
|
14
14
|
/** Resolve the provider-qualified key used to render a proxy status row. */
|
|
15
|
-
export declare function resolveProxyStatusAccountIdentity(label: string, type: string): CodexProxyStatusAccountIdentity;
|
|
15
|
+
export declare function resolveProxyStatusAccountIdentity(label: string, type: string, persistedKey?: string): CodexProxyStatusAccountIdentity;
|
|
16
16
|
/** Enumerate Codex OAuth accounts from the token store for usage refresh. */
|
|
17
17
|
export declare function listCodexAccountsForUsage(): Promise<ProxyPassthroughAccount[]>;
|
|
18
18
|
/** Normalise a Codex rate-limit block into the shared AccountQuota shape. */
|
|
@@ -19,7 +19,19 @@ export function normalizeCodexAccountKey(value) {
|
|
|
19
19
|
: `${CODEX_ACCOUNT_PREFIX}${trimmed}`;
|
|
20
20
|
}
|
|
21
21
|
/** Resolve the provider-qualified key used to render a proxy status row. */
|
|
22
|
-
export function resolveProxyStatusAccountIdentity(label, type) {
|
|
22
|
+
export function resolveProxyStatusAccountIdentity(label, type, persistedKey) {
|
|
23
|
+
// Current counter snapshots persist the provider-qualified key. Prefer it
|
|
24
|
+
// over the display type so a future type rename cannot move an account into
|
|
25
|
+
// the wrong cooldown/disabled namespace.
|
|
26
|
+
if (persistedKey?.toLowerCase().startsWith("anthropic:")) {
|
|
27
|
+
return {
|
|
28
|
+
provider: "anthropic",
|
|
29
|
+
key: normalizeAnthropicAccountKey(persistedKey),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (persistedKey?.startsWith(CODEX_ACCOUNT_PREFIX)) {
|
|
33
|
+
return { provider: "codex", key: persistedKey };
|
|
34
|
+
}
|
|
23
35
|
if (type === "oauth" || type === "api_key") {
|
|
24
36
|
return { provider: "anthropic", key: normalizeAnthropicAccountKey(label) };
|
|
25
37
|
}
|
|
@@ -63,6 +63,13 @@ export function trackProxyResponse(response, finishRequest, observer) {
|
|
|
63
63
|
let observedBodyBytes = 0;
|
|
64
64
|
let responseChunks = 0;
|
|
65
65
|
let settled = false;
|
|
66
|
+
let sourceClosed = false;
|
|
67
|
+
// A framework can cancel its adapter after the upstream stream has already
|
|
68
|
+
// closed. Snapshot reader.closed before cancel() so that normal cleanup is
|
|
69
|
+
// not recorded as a client-aborted request.
|
|
70
|
+
void reader.closed.then(() => {
|
|
71
|
+
sourceClosed = true;
|
|
72
|
+
}, () => undefined);
|
|
66
73
|
const settle = (outcome) => {
|
|
67
74
|
if (settled) {
|
|
68
75
|
return;
|
|
@@ -100,7 +107,11 @@ export function trackProxyResponse(response, finishRequest, observer) {
|
|
|
100
107
|
}
|
|
101
108
|
},
|
|
102
109
|
async cancel(reason) {
|
|
103
|
-
|
|
110
|
+
// Read the state before cancelling: reader.cancel() itself rejects the
|
|
111
|
+
// closed promise for a genuinely active source, which is too late to
|
|
112
|
+
// distinguish it from a source that had already ended normally.
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
settle(sourceClosed ? "completed" : "client_cancelled");
|
|
104
115
|
await withTimeout(reader.cancel(reason), PROXY_RESPONSE_CANCEL_TIMEOUT_MS, "Timed out cancelling the upstream proxy response");
|
|
105
116
|
},
|
|
106
117
|
});
|
|
@@ -40,7 +40,7 @@ export declare class ProxyUsageStatsStore {
|
|
|
40
40
|
recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
|
|
41
41
|
recordFinalError(status: number, accountLabel?: string, accountType?: string, details?: ProxyTerminalErrorDetails): void;
|
|
42
42
|
getStats(): ProxyStats;
|
|
43
|
-
getAccountStats(label: string): AccountStats | undefined;
|
|
43
|
+
getAccountStats(label: string, type?: string): AccountStats | undefined;
|
|
44
44
|
getTerminalErrors(): ProxyTerminalErrorJournal;
|
|
45
45
|
getUsageSnapshot(): ProxyUsageStatsSnapshot;
|
|
46
46
|
getPersistenceStatus(): ProxyStatsPersistenceStatus;
|
|
@@ -81,7 +81,7 @@ export declare function getStats(): ProxyStats;
|
|
|
81
81
|
export declare function getUsageSnapshot(): ProxyUsageStatsSnapshot;
|
|
82
82
|
export declare function getReconciledStats(): Promise<ProxyStats>;
|
|
83
83
|
export declare function getReconciledUsageSnapshot(): Promise<ProxyUsageStatsSnapshot>;
|
|
84
|
-
export declare function getAccountStats(label: string): AccountStats | undefined;
|
|
84
|
+
export declare function getAccountStats(label: string, type?: string): AccountStats | undefined;
|
|
85
85
|
export declare function getTerminalErrors(): ProxyTerminalErrorJournal;
|
|
86
86
|
export declare function getUsageStatsPersistenceStatus(): ProxyStatsPersistenceStatus;
|
|
87
87
|
export declare function flushUsageStats(): Promise<void>;
|
package/dist/proxy/usageStats.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
10
10
|
import { basename, dirname, join } from "node:path";
|
|
11
|
+
import { normalizeAnthropicAccountKey } from "./accountSelection.js";
|
|
11
12
|
import { AsyncMutex } from "../utils/asyncMutex.js";
|
|
12
13
|
import { redactUrlsInText, sanitizeForLog } from "../utils/logSanitize.js";
|
|
13
14
|
import { writeJsonSnapshotAtomically } from "./snapshotPersistence.js";
|
|
@@ -63,6 +64,34 @@ function emptyTerminalErrorJournal(startedAt) {
|
|
|
63
64
|
recent: [],
|
|
64
65
|
};
|
|
65
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Statistics must never use an email/label as their primary map key. One
|
|
69
|
+
* person can authenticate both provider pools with the same email, and a
|
|
70
|
+
* bare key silently merged their attempts, limits, and failures into one row.
|
|
71
|
+
*
|
|
72
|
+
* Keep non-account pseudo rows (passthrough, peer, internal) untouched. The
|
|
73
|
+
* display label remains separate so the CLI stays readable while persisted
|
|
74
|
+
* counters retain a collision-proof identity.
|
|
75
|
+
*/
|
|
76
|
+
function resolveStatsAccountIdentity(label, type) {
|
|
77
|
+
if (type === "codex-oauth") {
|
|
78
|
+
const key = label.startsWith("codex:") ? label : `codex:${label}`;
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
label: label.startsWith("codex:") ? label.slice("codex:".length) : label,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (type === "oauth" || type === "api_key") {
|
|
85
|
+
const key = normalizeAnthropicAccountKey(label);
|
|
86
|
+
return {
|
|
87
|
+
key,
|
|
88
|
+
label: label.startsWith("anthropic:")
|
|
89
|
+
? label.slice("anthropic:".length)
|
|
90
|
+
: label,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { key: label, label };
|
|
94
|
+
}
|
|
66
95
|
function cloneTerminalErrorSummary(summary) {
|
|
67
96
|
return { ...summary };
|
|
68
97
|
}
|
|
@@ -152,6 +181,9 @@ function terminalErrorCategory(status, errorType) {
|
|
|
152
181
|
}
|
|
153
182
|
function createTerminalErrorSummary(args) {
|
|
154
183
|
const { now, status, accountLabel, accountType, details } = args;
|
|
184
|
+
const accountIdentity = accountLabel && accountType
|
|
185
|
+
? resolveStatsAccountIdentity(accountLabel, accountType)
|
|
186
|
+
: undefined;
|
|
155
187
|
const errorType = clipTerminalErrorField(details?.errorType);
|
|
156
188
|
const message = details?.message
|
|
157
189
|
? stripTerminalErrorControlCharacters(sanitizeForLog(redactUrlsInText(details.message), MAX_TERMINAL_ERROR_MESSAGE_LENGTH + MAX_TERMINAL_ERROR_FIELD_LENGTH))
|
|
@@ -170,6 +202,11 @@ function createTerminalErrorSummary(args) {
|
|
|
170
202
|
...(clipTerminalErrorField(accountLabel)
|
|
171
203
|
? { account: clipTerminalErrorField(accountLabel) }
|
|
172
204
|
: {}),
|
|
205
|
+
...(clipTerminalErrorField(details?.accountKey ?? accountIdentity?.key)
|
|
206
|
+
? {
|
|
207
|
+
accountKey: clipTerminalErrorField(details?.accountKey ?? accountIdentity?.key),
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
173
210
|
...(clipTerminalErrorField(accountType)
|
|
174
211
|
? { accountType: clipTerminalErrorField(accountType) }
|
|
175
212
|
: {}),
|
|
@@ -201,7 +238,9 @@ function mergeAccountStats(left, right) {
|
|
|
201
238
|
if (!left) {
|
|
202
239
|
return cloneAccount(right);
|
|
203
240
|
}
|
|
241
|
+
const key = right.key ?? left.key;
|
|
204
242
|
return {
|
|
243
|
+
...(key ? { key } : {}),
|
|
205
244
|
label: right.label || left.label,
|
|
206
245
|
type: right.type || left.type,
|
|
207
246
|
attemptCount: left.attemptCount + right.attemptCount,
|
|
@@ -248,7 +287,9 @@ function validAccountStats(value) {
|
|
|
248
287
|
return false;
|
|
249
288
|
}
|
|
250
289
|
const candidate = value;
|
|
251
|
-
return (
|
|
290
|
+
return ((candidate.key === undefined ||
|
|
291
|
+
(typeof candidate.key === "string" && candidate.key.length > 0)) &&
|
|
292
|
+
typeof candidate.label === "string" &&
|
|
252
293
|
typeof candidate.type === "string" &&
|
|
253
294
|
finiteNonNegativeInteger(candidate.attemptCount) &&
|
|
254
295
|
finiteNonNegativeInteger(candidate.attemptErrorCount) &&
|
|
@@ -282,7 +323,7 @@ function validStats(value) {
|
|
|
282
323
|
typeof candidate.accounts !== "object") {
|
|
283
324
|
return false;
|
|
284
325
|
}
|
|
285
|
-
return Object.entries(candidate.accounts).every(([
|
|
326
|
+
return Object.entries(candidate.accounts).every(([key, account]) => validAccountStats(account) && key === (account.key ?? account.label));
|
|
286
327
|
}
|
|
287
328
|
function validOptionalString(value, maxLength) {
|
|
288
329
|
return (value === undefined ||
|
|
@@ -302,6 +343,7 @@ function validTerminalErrorSummary(value) {
|
|
|
302
343
|
TERMINAL_ERROR_CATEGORIES.includes(candidate.category) &&
|
|
303
344
|
validOptionalString(candidate.requestId, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
304
345
|
validOptionalString(candidate.account, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
346
|
+
validOptionalString(candidate.accountKey, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
305
347
|
validOptionalString(candidate.accountType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
306
348
|
validOptionalString(candidate.errorType, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
307
349
|
validOptionalString(candidate.errorCode, MAX_TERMINAL_ERROR_FIELD_LENGTH) &&
|
|
@@ -615,8 +657,20 @@ export class ProxyUsageStatsStore {
|
|
|
615
657
|
getStats() {
|
|
616
658
|
return cloneStats(this.stats);
|
|
617
659
|
}
|
|
618
|
-
getAccountStats(label) {
|
|
619
|
-
const
|
|
660
|
+
getAccountStats(label, type) {
|
|
661
|
+
const direct = this.stats.accounts[label];
|
|
662
|
+
if (direct) {
|
|
663
|
+
return cloneAccount(direct);
|
|
664
|
+
}
|
|
665
|
+
const identity = type
|
|
666
|
+
? resolveStatsAccountIdentity(label, type)
|
|
667
|
+
: undefined;
|
|
668
|
+
const account = identity
|
|
669
|
+
? this.stats.accounts[identity.key]
|
|
670
|
+
: (() => {
|
|
671
|
+
const matches = Object.values(this.stats.accounts).filter((candidate) => candidate.label === label);
|
|
672
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
673
|
+
})();
|
|
620
674
|
return account ? cloneAccount(account) : undefined;
|
|
621
675
|
}
|
|
622
676
|
getTerminalErrors() {
|
|
@@ -920,9 +974,11 @@ export class ProxyUsageStatsStore {
|
|
|
920
974
|
}
|
|
921
975
|
}
|
|
922
976
|
ensureAccount(target, label, type) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
977
|
+
const identity = resolveStatsAccountIdentity(label, type);
|
|
978
|
+
if (!target.accounts[identity.key]) {
|
|
979
|
+
target.accounts[identity.key] = {
|
|
980
|
+
key: identity.key,
|
|
981
|
+
label: identity.label,
|
|
926
982
|
type,
|
|
927
983
|
attemptCount: 0,
|
|
928
984
|
attemptErrorCount: 0,
|
|
@@ -934,7 +990,7 @@ export class ProxyUsageStatsStore {
|
|
|
934
990
|
lastAttemptAt: 0,
|
|
935
991
|
};
|
|
936
992
|
}
|
|
937
|
-
return target.accounts[
|
|
993
|
+
return target.accounts[identity.key];
|
|
938
994
|
}
|
|
939
995
|
scheduleFlush() {
|
|
940
996
|
if (!this.filePath || this.flushTimer) {
|
|
@@ -1131,8 +1187,8 @@ export async function getReconciledStats() {
|
|
|
1131
1187
|
export async function getReconciledUsageSnapshot() {
|
|
1132
1188
|
return defaultStore.reconcileUsageSnapshot();
|
|
1133
1189
|
}
|
|
1134
|
-
export function getAccountStats(label) {
|
|
1135
|
-
return defaultStore.getAccountStats(label);
|
|
1190
|
+
export function getAccountStats(label, type) {
|
|
1191
|
+
return defaultStore.getAccountStats(label, type);
|
|
1136
1192
|
}
|
|
1137
1193
|
export function getTerminalErrors() {
|
|
1138
1194
|
return defaultStore.getTerminalErrors();
|
|
@@ -65,7 +65,8 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
|
|
|
65
65
|
declare function reconcileCooldownFromQuota(state: RuntimeAccountState, quota: AccountQuota, now: number, policy?: ProxyOveragePolicy): ProxyQuotaCooldownUpdate;
|
|
66
66
|
/**
|
|
67
67
|
* Seed each account's runtime quota from the persisted snapshots in
|
|
68
|
-
* ~/.neurolink/account-quotas.json (keyed by
|
|
68
|
+
* ~/.neurolink/account-quotas.json (keyed by provider-qualified account key).
|
|
69
|
+
* Runtime state is
|
|
69
70
|
* in-memory only, so without this the quota-aware ordering is blind after a
|
|
70
71
|
* proxy restart: all accounts tie, selection falls back to token-store
|
|
71
72
|
* enumeration order, and the first account served becomes self-reinforcing
|