@juspay/neurolink 12.9.2 → 12.9.3
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 +382 -382
- package/dist/proxy/accountLedger.d.ts +6 -3
- package/dist/proxy/accountLedger.js +37 -10
- package/dist/server/routes/claudeProxyRoutes.d.ts +2 -0
- package/dist/server/routes/claudeProxyRoutes.js +155 -45
- package/dist/types/proxy.d.ts +21 -1
- package/dist/types/proxyClient.d.ts +20 -2
- package/package.json +1 -1
|
@@ -29,10 +29,13 @@ import type { CliAccountUsageTotals } from "../types/index.js";
|
|
|
29
29
|
/** UTC date stamp of the log file the totals cover. */
|
|
30
30
|
export declare function currentUsageDate(now?: Date): string;
|
|
31
31
|
/**
|
|
32
|
-
* Token and cost totals for one UTC day, keyed by
|
|
32
|
+
* Token and cost totals for one UTC day, keyed by provider-qualified account
|
|
33
|
+
* key ("anthropic:<label>" / "codex:<label>").
|
|
33
34
|
*
|
|
34
|
-
*
|
|
35
|
-
* with an Anthropic one
|
|
35
|
+
* Both engines' rows are attributed, each under its own key, so a Codex login
|
|
36
|
+
* sharing an email with an Anthropic one contributes to its own row and never
|
|
37
|
+
* to the other's. Keying by bare label used to force a choice between merging
|
|
38
|
+
* them and dropping Codex outright; it dropped Codex.
|
|
36
39
|
*/
|
|
37
40
|
export declare function readAccountUsage(date?: string): Promise<Map<string, CliAccountUsageTotals>>;
|
|
38
41
|
/** Drop all cached cursors. Exported for tests, which vary HOME per case. */
|
|
@@ -33,9 +33,29 @@ const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
|
33
33
|
/**
|
|
34
34
|
* Account types written by the Anthropic pool. The request log is shared with
|
|
35
35
|
* the Codex engine, and an operator can use the same email for both, so rows
|
|
36
|
-
*
|
|
36
|
+
* are attributed by provider-qualified key, never by bare label.
|
|
37
37
|
*/
|
|
38
38
|
const ANTHROPIC_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
|
|
39
|
+
const CODEX_ACCOUNT_TYPE = "codex-oauth";
|
|
40
|
+
/**
|
|
41
|
+
* The key a log row is attributed under. Current builds write `accountKey`;
|
|
42
|
+
* rows from before that field existed carry only the label and the engine's
|
|
43
|
+
* account type, which is enough to rebuild it — the two engines never share a
|
|
44
|
+
* type string.
|
|
45
|
+
*/
|
|
46
|
+
function ledgerAccountKey(record, account) {
|
|
47
|
+
if (typeof record.accountKey === "string" && record.accountKey.length > 0) {
|
|
48
|
+
return record.accountKey;
|
|
49
|
+
}
|
|
50
|
+
const type = typeof record.accountType === "string" ? record.accountType : "";
|
|
51
|
+
if (ANTHROPIC_ACCOUNT_TYPES.has(type)) {
|
|
52
|
+
return `anthropic:${account.trim().toLowerCase()}`;
|
|
53
|
+
}
|
|
54
|
+
if (type === CODEX_ACCOUNT_TYPE) {
|
|
55
|
+
return `codex:${account}`;
|
|
56
|
+
}
|
|
57
|
+
return account;
|
|
58
|
+
}
|
|
39
59
|
const cursors = new Map();
|
|
40
60
|
function getLogsDir() {
|
|
41
61
|
return join(homedir(), ".neurolink", "logs");
|
|
@@ -134,9 +154,11 @@ async function advanceCursor(fileName, cursor) {
|
|
|
134
154
|
// on the triple means two distinct calls that collide on id but differ in
|
|
135
155
|
// account or model are still counted separately; a true re-log of the same
|
|
136
156
|
// request keeps the same triple and merges.
|
|
137
|
-
const
|
|
157
|
+
const accountKey = ledgerAccountKey(record, account);
|
|
158
|
+
const entryKey = `${requestId}\u0000${accountKey}\u0000${typeof record.model === "string" ? record.model : ""}`;
|
|
138
159
|
const next = {
|
|
139
160
|
account,
|
|
161
|
+
accountKey,
|
|
140
162
|
accountType: typeof record.accountType === "string" ? record.accountType : "",
|
|
141
163
|
model: typeof record.model === "string" ? record.model : "",
|
|
142
164
|
provider: typeof record.provider === "string" ? record.provider : undefined,
|
|
@@ -254,10 +276,13 @@ function emptyTotals() {
|
|
|
254
276
|
};
|
|
255
277
|
}
|
|
256
278
|
/**
|
|
257
|
-
* Token and cost totals for one UTC day, keyed by
|
|
279
|
+
* Token and cost totals for one UTC day, keyed by provider-qualified account
|
|
280
|
+
* key ("anthropic:<label>" / "codex:<label>").
|
|
258
281
|
*
|
|
259
|
-
*
|
|
260
|
-
* with an Anthropic one
|
|
282
|
+
* Both engines' rows are attributed, each under its own key, so a Codex login
|
|
283
|
+
* sharing an email with an Anthropic one contributes to its own row and never
|
|
284
|
+
* to the other's. Keying by bare label used to force a choice between merging
|
|
285
|
+
* them and dropping Codex outright; it dropped Codex.
|
|
261
286
|
*/
|
|
262
287
|
export async function readAccountUsage(date = currentUsageDate()) {
|
|
263
288
|
const fileName = `proxy-${date}.jsonl`;
|
|
@@ -281,10 +306,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
|
|
|
281
306
|
const totals = new Map();
|
|
282
307
|
const unpriced = new Map();
|
|
283
308
|
for (const entry of cursor.entries.values()) {
|
|
284
|
-
if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType)
|
|
309
|
+
if (!ANTHROPIC_ACCOUNT_TYPES.has(entry.accountType) &&
|
|
310
|
+
entry.accountType !== CODEX_ACCOUNT_TYPE) {
|
|
311
|
+
// Translation and internal rows are proxy plumbing, not a login's spend.
|
|
285
312
|
continue;
|
|
286
313
|
}
|
|
287
|
-
const row = totals.get(entry.
|
|
314
|
+
const row = totals.get(entry.accountKey) ?? emptyTotals();
|
|
288
315
|
row.requests += 1;
|
|
289
316
|
row.inputTokens += entry.inputTokens;
|
|
290
317
|
row.outputTokens += entry.outputTokens;
|
|
@@ -318,12 +345,12 @@ export async function readAccountUsage(date = currentUsageDate()) {
|
|
|
318
345
|
}
|
|
319
346
|
else {
|
|
320
347
|
row.unpricedRequests += 1;
|
|
321
|
-
const seen = unpriced.get(entry.
|
|
348
|
+
const seen = unpriced.get(entry.accountKey) ?? new Set();
|
|
322
349
|
seen.add(entry.model);
|
|
323
|
-
unpriced.set(entry.
|
|
350
|
+
unpriced.set(entry.accountKey, seen);
|
|
324
351
|
}
|
|
325
352
|
}
|
|
326
|
-
totals.set(entry.
|
|
353
|
+
totals.set(entry.accountKey, row);
|
|
327
354
|
}
|
|
328
355
|
for (const [account, row] of totals) {
|
|
329
356
|
row.costUsd = Number(row.costUsd.toFixed(6));
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { buildTranslationOptions } from "../../proxy/proxyTranslationEngine.js";
|
|
13
13
|
import { ProxyTracer } from "../../proxy/proxyTracer.js";
|
|
14
14
|
import { isPermanentRefreshFailure } from "../../proxy/tokenRefresh.js";
|
|
15
|
+
import type { ProxyAccountDirectoryOverride } from "../../types/index.js";
|
|
15
16
|
import type { AccountAllowlist, AccountAdmissionLease, JsonObject, AccountCooldownPlan, AccountQuota, AccountQuotaWindow, AccountUsageFetchResult, AnthropicAttemptLogger, AnthropicAuthRetryResult, AnthropicEntitlementFailure, AnthropicInvalidRequestFailure, AnthropicLoopState, AnthropicScopedExhaustion, AnthropicNonOkResult, AnthropicSuccessResult, AnthropicUpstreamFetchResult, ClaudeFinalRequestLogger, ClaudeLoggedErrorBuilder, ClaudeRequest, ClaudeProxyRouteRuntimeOptions, ModelRouterInterface, ParsedClaudeError, ProxyAccountRoutingDecision, ProxyAccountSortMetrics, ProxyBodyCaptureLogger, ProxyLimitsRefreshResponse, ProxyQuotaCooldownUpdate, ProxyOveragePolicy, ProxyPassthroughAccount, QueuedAccountAdmission, RouteGroup, RuntimeAccountState, ServerContext, StreamTerminalOutcome } from "../../types/index.js";
|
|
16
17
|
declare function tryAcquireAccountAdmission(accountKey: string, capacity: number | undefined): AccountAdmissionLease | undefined;
|
|
17
18
|
declare function enqueueAccountAdmission(accountKey: string, capacity: number): QueuedAccountAdmission;
|
|
@@ -467,6 +468,7 @@ export declare const __testHooks: {
|
|
|
467
468
|
refreshAccountLimits: typeof refreshAccountLimits;
|
|
468
469
|
applyAccountUsageResult: typeof applyAccountUsageResult;
|
|
469
470
|
clearLimitsRefreshStateForTests: () => void;
|
|
471
|
+
setAccountDirectoryForTests: (override: ProxyAccountDirectoryOverride | null) => void;
|
|
470
472
|
isRetryableNetworkError: typeof isRetryableNetworkError;
|
|
471
473
|
isPermanentRefreshFailure: typeof isPermanentRefreshFailure;
|
|
472
474
|
getStreamFailureDetails: typeof getStreamFailureDetails;
|
|
@@ -18,6 +18,8 @@ import { clearAccountCooldown, loadAccountCooldowns, saveAccountCooldown, } from
|
|
|
18
18
|
import { anthropicAccountKeysEqual, ENV_ANTHROPIC_ACCOUNT_KEY, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
19
19
|
import { getUnifiedRateLimitStatus, isQuotaOverageAvailable, loadAccountQuotas, mergeQuotaSnapshot, modelFamilyToken, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
|
|
20
20
|
import { fetchAccountUsage, listAnthropicAccountsForUsage, usageToQuota, } from "../../proxy/accountUsage.js";
|
|
21
|
+
import { tokenStore } from "../../auth/tokenStore.js";
|
|
22
|
+
import { fetchCodexAccountUsage, listCodexAccountsForUsage, resolveProxyStatusAccountIdentity, } from "../../proxy/codexAccountUsage.js";
|
|
21
23
|
import { AccountQuotaRefreshCoordinator } from "../../proxy/accountQuotaRefreshCoordinator.js";
|
|
22
24
|
import { ProviderTransportCoordinator } from "../../proxy/providerTransportCoordinator.js";
|
|
23
25
|
import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
@@ -780,6 +782,45 @@ async function refreshAccountQuotaInBackground(account, trigger) {
|
|
|
780
782
|
}
|
|
781
783
|
await applyAccountUsageResult(account, refresh.result, refresh.startedAt);
|
|
782
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* The logins the account-exposing routes enumerate, per engine, plus every
|
|
787
|
+
* key the token store holds at all (disabled ones included). Overridable by
|
|
788
|
+
* the suite because the token store is a singleton bound to the real home at
|
|
789
|
+
* import — a case cannot point it elsewhere, and reading the operator's own
|
|
790
|
+
* logins inside a fixture test is exactly how a phantom would hide.
|
|
791
|
+
*/
|
|
792
|
+
let accountDirectoryOverride = null;
|
|
793
|
+
async function listRoutableAccountsByEngine(allowlist) {
|
|
794
|
+
if (accountDirectoryOverride) {
|
|
795
|
+
return {
|
|
796
|
+
anthropic: accountDirectoryOverride.anthropic,
|
|
797
|
+
codex: accountDirectoryOverride.codex,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
// The allowlist is an Anthropic routing concept, keyed by anthropic:
|
|
801
|
+
// prefixes; applying it to Codex keys would exclude every Codex login.
|
|
802
|
+
const [anthropic, codex] = await Promise.all([
|
|
803
|
+
listAnthropicAccountsForUsage(allowlist),
|
|
804
|
+
listCodexAccountsForUsage(),
|
|
805
|
+
]);
|
|
806
|
+
return { anthropic, codex };
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Every login the token store knows, routable or not. This is what separates
|
|
810
|
+
* a DISABLED login (still here, shown as unrouted) from a REMOVED one (gone,
|
|
811
|
+
* and not shown): usage counters and quota snapshots outlive a logout, and
|
|
812
|
+
* without this check a deleted login renders as an unrouted account forever.
|
|
813
|
+
*/
|
|
814
|
+
async function listKnownAccountKeys() {
|
|
815
|
+
if (accountDirectoryOverride) {
|
|
816
|
+
return accountDirectoryOverride.knownKeys;
|
|
817
|
+
}
|
|
818
|
+
const [anthropic, codex] = await Promise.all([
|
|
819
|
+
tokenStore.listByPrefix("anthropic:"),
|
|
820
|
+
tokenStore.listByPrefix("codex:"),
|
|
821
|
+
]);
|
|
822
|
+
return new Set([...anthropic.map(normalizeAnthropicAccountKey), ...codex]);
|
|
823
|
+
}
|
|
783
824
|
/**
|
|
784
825
|
* Fetch fresh limits from Anthropic's usage endpoint for every eligible OAuth
|
|
785
826
|
* account and write them through the exact same chain the passive header
|
|
@@ -789,24 +830,38 @@ async function refreshAccountQuotaInBackground(account, trigger) {
|
|
|
789
830
|
*/
|
|
790
831
|
async function refreshAccountLimits(options = {}) {
|
|
791
832
|
const fetchedAt = Date.now();
|
|
792
|
-
const
|
|
833
|
+
const directory = await listRoutableAccountsByEngine(options.accountAllowlist);
|
|
834
|
+
const allAccounts = [
|
|
835
|
+
...directory.anthropic.map((account) => ({
|
|
836
|
+
account,
|
|
837
|
+
provider: "anthropic",
|
|
838
|
+
})),
|
|
839
|
+
...directory.codex.map((account) => ({
|
|
840
|
+
account,
|
|
841
|
+
provider: "codex",
|
|
842
|
+
})),
|
|
843
|
+
];
|
|
793
844
|
const accounts = options.accountFilter
|
|
794
|
-
? allAccounts.filter((account) => account.label === options.accountFilter ||
|
|
845
|
+
? allAccounts.filter(({ account }) => account.label === options.accountFilter ||
|
|
795
846
|
account.key === options.accountFilter)
|
|
796
847
|
: allAccounts;
|
|
797
848
|
const persisted = await loadAccountQuotas().catch(() => ({}));
|
|
798
|
-
const buildResult = (account, status, quota, error) => {
|
|
849
|
+
const buildResult = (account, provider, status, quota, error) => {
|
|
799
850
|
const state = accountRuntimeState.get(account.key);
|
|
851
|
+
// The quota store keys Anthropic snapshots by bare label for historical
|
|
852
|
+
// reasons (see CLAUDE.md) and Codex snapshots by full key. A Codex login
|
|
853
|
+
// must never fall back to the bare label: with one email on both engines
|
|
854
|
+
// that label holds the ANTHROPIC account's windows.
|
|
855
|
+
const persistedQuota = provider === "anthropic"
|
|
856
|
+
? (persisted[account.key] ?? persisted[account.label] ?? null)
|
|
857
|
+
: (persisted[account.key] ?? null);
|
|
800
858
|
const result = {
|
|
801
859
|
account: account.label,
|
|
802
860
|
key: account.key,
|
|
861
|
+
provider,
|
|
803
862
|
type: account.type,
|
|
804
863
|
status,
|
|
805
|
-
quota: quota ??
|
|
806
|
-
state?.quota ??
|
|
807
|
-
persisted[account.key] ??
|
|
808
|
-
persisted[account.label] ??
|
|
809
|
-
null,
|
|
864
|
+
quota: quota ?? state?.quota ?? persistedQuota,
|
|
810
865
|
};
|
|
811
866
|
if (error !== undefined) {
|
|
812
867
|
result.error = error;
|
|
@@ -823,7 +878,7 @@ async function refreshAccountLimits(options = {}) {
|
|
|
823
878
|
return {
|
|
824
879
|
fetchedAt,
|
|
825
880
|
snapshot: true,
|
|
826
|
-
results: accounts.map((account) => buildResult(account, "snapshot", null)),
|
|
881
|
+
results: accounts.map(({ account, provider }) => buildResult(account, provider, "snapshot", null)),
|
|
827
882
|
refreshMetrics: accountQuotaRefreshCoordinator.getMetrics(),
|
|
828
883
|
};
|
|
829
884
|
}
|
|
@@ -835,23 +890,41 @@ async function refreshAccountLimits(options = {}) {
|
|
|
835
890
|
if (index >= accounts.length) {
|
|
836
891
|
return;
|
|
837
892
|
}
|
|
838
|
-
const account = accounts[index];
|
|
893
|
+
const { account, provider } = accounts[index];
|
|
839
894
|
if (account.type !== "oauth") {
|
|
840
|
-
results[index] = buildResult(account, "skipped_api_key", null);
|
|
895
|
+
results[index] = buildResult(account, provider, "skipped_api_key", null);
|
|
841
896
|
continue;
|
|
842
897
|
}
|
|
843
898
|
const lastFetch = lastUsageFetchAt.get(account.key) ?? 0;
|
|
844
899
|
if (Date.now() - lastFetch < MIN_USAGE_REFETCH_INTERVAL_MS) {
|
|
845
|
-
results[index] = buildResult(account, "throttled", null);
|
|
900
|
+
results[index] = buildResult(account, provider, "throttled", null);
|
|
846
901
|
continue;
|
|
847
902
|
}
|
|
848
903
|
lastUsageFetchAt.set(account.key, Date.now());
|
|
904
|
+
if (provider === "codex") {
|
|
905
|
+
// Codex has its own usage endpoint and no overage/cooldown
|
|
906
|
+
// reconciliation to run; the snapshot is written under the full key,
|
|
907
|
+
// which is the only key the Codex engine ever reads it back by.
|
|
908
|
+
try {
|
|
909
|
+
const fetched = await fetchCodexAccountUsage(account);
|
|
910
|
+
if (fetched.ok === false) {
|
|
911
|
+
results[index] = buildResult(account, provider, "error", null, `codex usage fetch failed: ${fetched.reason}`);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
await saveAccountQuota(account.key, fetched.quota);
|
|
915
|
+
results[index] = buildResult(account, provider, "refreshed", fetched.quota);
|
|
916
|
+
}
|
|
917
|
+
catch (err) {
|
|
918
|
+
results[index] = buildResult(account, provider, "error", null, err instanceof Error ? err.message : String(err));
|
|
919
|
+
}
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
849
922
|
// Isolate failures per account: an unexpected rejection must not abort
|
|
850
923
|
// the Promise.all sweep and turn the whole /limits response into a 502.
|
|
851
924
|
try {
|
|
852
925
|
const refresh = await accountQuotaRefreshCoordinator.run(account, `manual:${account.key}`, fetchValidatedAccountUsage, { force: true });
|
|
853
926
|
if (refresh.kind !== "completed") {
|
|
854
|
-
results[index] = buildResult(account, "throttled", null);
|
|
927
|
+
results[index] = buildResult(account, provider, "throttled", null);
|
|
855
928
|
continue;
|
|
856
929
|
}
|
|
857
930
|
const fetchResult = refresh.result;
|
|
@@ -859,18 +932,18 @@ async function refreshAccountLimits(options = {}) {
|
|
|
859
932
|
// file without strictNullChecks, where negated boolean-discriminant
|
|
860
933
|
// narrowing does not apply.
|
|
861
934
|
if (fetchResult.ok === false) {
|
|
862
|
-
results[index] = buildResult(account, "error", null, fetchResult.error);
|
|
935
|
+
results[index] = buildResult(account, provider, "error", null, fetchResult.error);
|
|
863
936
|
continue;
|
|
864
937
|
}
|
|
865
938
|
const quota = await applyAccountUsageResult(account, fetchResult, refresh.startedAt, persisted[account.key] ?? persisted[account.label] ?? null);
|
|
866
939
|
if (!quota) {
|
|
867
|
-
results[index] = buildResult(account, "error", null, "usage payload had no recognizable limit windows");
|
|
940
|
+
results[index] = buildResult(account, provider, "error", null, "usage payload had no recognizable limit windows");
|
|
868
941
|
continue;
|
|
869
942
|
}
|
|
870
|
-
results[index] = buildResult(account, "refreshed", quota);
|
|
943
|
+
results[index] = buildResult(account, provider, "refreshed", quota);
|
|
871
944
|
}
|
|
872
945
|
catch (err) {
|
|
873
|
-
results[index] = buildResult(account, "error", null, err instanceof Error ? err.message : String(err));
|
|
946
|
+
results[index] = buildResult(account, provider, "error", null, err instanceof Error ? err.message : String(err));
|
|
874
947
|
}
|
|
875
948
|
}
|
|
876
949
|
};
|
|
@@ -6769,11 +6842,6 @@ function buildEarlyClaudeRequestError(args) {
|
|
|
6769
6842
|
* millisecond fields tens of thousands of years into the future, so only the
|
|
6770
6843
|
* seconds fields are converted, and 0 becomes null rather than epoch zero.
|
|
6771
6844
|
*/
|
|
6772
|
-
/**
|
|
6773
|
-
* Account types that represent a real credential rather than proxy plumbing.
|
|
6774
|
-
* Mirrors the Anthropic pool's own account types.
|
|
6775
|
-
*/
|
|
6776
|
-
const REAL_ACCOUNT_TYPES = new Set(["oauth", "api_key"]);
|
|
6777
6845
|
function toMillis(value) {
|
|
6778
6846
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
6779
6847
|
? Math.round(value * 1000)
|
|
@@ -7498,9 +7566,31 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7498
7566
|
// Usage is the optional half; quota and status must still render.
|
|
7499
7567
|
usageError = error instanceof Error ? error.message : String(error);
|
|
7500
7568
|
}
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7569
|
+
// Joined by provider-qualified KEY, never by label. One email can
|
|
7570
|
+
// be logged in to both engines; joined by label, the Codex login was
|
|
7571
|
+
// swallowed by the Anthropic row (and its counters could land on
|
|
7572
|
+
// that row, last write winning). Legacy stats entries that predate
|
|
7573
|
+
// the key carry only a label and a type, which is enough to derive
|
|
7574
|
+
// it; a keyed entry for the same login wins over a legacy one.
|
|
7575
|
+
const statsByKey = new Map();
|
|
7576
|
+
for (const [mapKey, entry] of Object.entries(statsAccounts)) {
|
|
7577
|
+
const identity = resolveProxyStatusAccountIdentity(entry.label, entry.type, entry.key ?? mapKey);
|
|
7578
|
+
const identityKey = identity.key ?? mapKey;
|
|
7579
|
+
const existing = statsByKey.get(identityKey);
|
|
7580
|
+
if (!existing || (entry.key && !existing.key)) {
|
|
7581
|
+
statsByKey.set(identityKey, { ...entry, identityKey });
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
// Which logins exist at all, disabled ones included. A stats entry
|
|
7585
|
+
// with no login behind it is a REMOVED account, not an unrouted one.
|
|
7586
|
+
// If the store cannot be read, err towards showing rows: hiding a
|
|
7587
|
+
// real login is the worse mistake, and the old behaviour.
|
|
7588
|
+
let knownKeys;
|
|
7589
|
+
try {
|
|
7590
|
+
knownKeys = await listKnownAccountKeys();
|
|
7591
|
+
}
|
|
7592
|
+
catch {
|
|
7593
|
+
knownKeys = null;
|
|
7504
7594
|
}
|
|
7505
7595
|
const rows = [];
|
|
7506
7596
|
const claimed = new Set();
|
|
@@ -7509,13 +7599,14 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7509
7599
|
// today, which is exactly when an operator most wants to see it.
|
|
7510
7600
|
for (const result of limits.results) {
|
|
7511
7601
|
const label = result.account;
|
|
7512
|
-
claimed.add(
|
|
7513
|
-
const stat =
|
|
7602
|
+
claimed.add(result.key);
|
|
7603
|
+
const stat = statsByKey.get(result.key);
|
|
7514
7604
|
const quota = normalizeQuotaForAccounts(result.quota);
|
|
7515
7605
|
const cooling = isCooling(result.key ?? null);
|
|
7516
7606
|
rows.push({
|
|
7517
7607
|
label,
|
|
7518
7608
|
key: result.key ?? null,
|
|
7609
|
+
provider: result.provider,
|
|
7519
7610
|
kind: "account",
|
|
7520
7611
|
type: result.type ?? stat?.type ?? "oauth",
|
|
7521
7612
|
// result.status describes how the quota was obtained
|
|
@@ -7549,39 +7640,53 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7549
7640
|
weeklyHealth: quotaHealth(quota.weeklyStatus),
|
|
7550
7641
|
}
|
|
7551
7642
|
: null,
|
|
7552
|
-
usage: usageByAccount.get(
|
|
7643
|
+
usage: usageByAccount.get(result.key) ?? null,
|
|
7553
7644
|
});
|
|
7554
7645
|
}
|
|
7555
7646
|
// Plumbing rows are still reported, but tagged, so a consumer can
|
|
7556
7647
|
// show or hide them rather than rendering them as credentials.
|
|
7557
7648
|
//
|
|
7558
|
-
// A real login can land here too:
|
|
7559
|
-
//
|
|
7560
|
-
//
|
|
7561
|
-
//
|
|
7562
|
-
//
|
|
7563
|
-
//
|
|
7564
|
-
//
|
|
7565
|
-
//
|
|
7566
|
-
|
|
7567
|
-
|
|
7649
|
+
// A real login can land here too: the listers skip accounts the
|
|
7650
|
+
// token store has disabled or the allowlist excludes, so a login
|
|
7651
|
+
// with real usage history but no current route is absent from
|
|
7652
|
+
// limits.results. Tagging that as plumbing hid the one account an
|
|
7653
|
+
// operator is looking for when they ask why traffic stopped. It
|
|
7654
|
+
// stays kind "account", with a status saying why it has no quota
|
|
7655
|
+
// block — but only while the login still EXISTS. A stats entry whose
|
|
7656
|
+
// login has been removed from the store is history, not a
|
|
7657
|
+
// credential, and rendering it as "unrouted" put a phantom account
|
|
7658
|
+
// on every dashboard for as long as the counters file lived.
|
|
7659
|
+
for (const entry of statsByKey.values()) {
|
|
7660
|
+
const identity = resolveProxyStatusAccountIdentity(entry.label, entry.type, entry.identityKey);
|
|
7661
|
+
if (identity.key !== null && claimed.has(identity.key)) {
|
|
7662
|
+
continue;
|
|
7663
|
+
}
|
|
7664
|
+
const isLogin = identity.provider !== "other";
|
|
7665
|
+
if (isLogin &&
|
|
7666
|
+
identity.key !== null &&
|
|
7667
|
+
knownKeys !== null &&
|
|
7668
|
+
!knownKeys.has(identity.key)) {
|
|
7568
7669
|
continue;
|
|
7569
7670
|
}
|
|
7570
|
-
const isRealAccount = REAL_ACCOUNT_TYPES.has(entry.type);
|
|
7571
7671
|
rows.push({
|
|
7572
7672
|
label: entry.label,
|
|
7573
|
-
key: null,
|
|
7574
|
-
|
|
7673
|
+
key: isLogin ? identity.key : null,
|
|
7674
|
+
...(isLogin ? { provider: identity.provider } : {}),
|
|
7675
|
+
kind: isLogin
|
|
7575
7676
|
? "account"
|
|
7576
7677
|
: entry.type === "translation"
|
|
7577
7678
|
? "translation"
|
|
7578
7679
|
: "internal",
|
|
7579
|
-
type
|
|
7580
|
-
|
|
7680
|
+
// The row's type is the credential kind; the engine is
|
|
7681
|
+
// `provider`. Stats record Codex logins as "codex-oauth", which
|
|
7682
|
+
// consumers that only know the credential kinds read as
|
|
7683
|
+
// plumbing, so it is reported as the OAuth login it is.
|
|
7684
|
+
type: identity.provider === "codex" ? "oauth" : entry.type,
|
|
7685
|
+
status: isLogin ? "unrouted" : null,
|
|
7581
7686
|
cooling: false,
|
|
7582
7687
|
allowed: null,
|
|
7583
7688
|
expired: null,
|
|
7584
|
-
isPrimary: isPrimaryAccount(
|
|
7689
|
+
isPrimary: isLogin ? isPrimaryAccount(identity.key) : false,
|
|
7585
7690
|
requests: entry.successCount + entry.errorCount,
|
|
7586
7691
|
errors: entry.errorCount,
|
|
7587
7692
|
rateLimits: entry.rateLimitCount,
|
|
@@ -7593,7 +7698,9 @@ export function createClaudeProxyRoutes(modelRouter, basePath = "", accountStrat
|
|
|
7593
7698
|
// and cost in the ledger — hardcoding null here discarded
|
|
7594
7699
|
// exactly the usage an operator is looking for when they ask
|
|
7595
7700
|
// why traffic stopped.
|
|
7596
|
-
usage:
|
|
7701
|
+
usage: identity.key !== null
|
|
7702
|
+
? (usageByAccount.get(identity.key) ?? null)
|
|
7703
|
+
: null,
|
|
7597
7704
|
});
|
|
7598
7705
|
}
|
|
7599
7706
|
const response = {
|
|
@@ -8001,6 +8108,9 @@ export const __testHooks = {
|
|
|
8001
8108
|
limitsRefreshInFlight = null;
|
|
8002
8109
|
accountQuotaRefreshCoordinator.clear();
|
|
8003
8110
|
},
|
|
8111
|
+
setAccountDirectoryForTests: (override) => {
|
|
8112
|
+
accountDirectoryOverride = override;
|
|
8113
|
+
},
|
|
8004
8114
|
isRetryableNetworkError,
|
|
8005
8115
|
isPermanentRefreshFailure,
|
|
8006
8116
|
getStreamFailureDetails,
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -1209,8 +1209,14 @@ export type ProxyQuotaRefreshRunResult = {
|
|
|
1209
1209
|
export type ProxyLimitsAccountResult = {
|
|
1210
1210
|
/** Account label (quota-store key). */
|
|
1211
1211
|
account: string;
|
|
1212
|
-
/** Token-store key ("anthropic:<label>"). */
|
|
1212
|
+
/** Token-store key ("anthropic:<label>" or "codex:<label>"). */
|
|
1213
1213
|
key: string;
|
|
1214
|
+
/**
|
|
1215
|
+
* Which pool engine owns this login. Two logins can share a label — an
|
|
1216
|
+
* operator may use one email for both — so the key, not the label, is the
|
|
1217
|
+
* identity, and this names the engine without parsing the key's prefix.
|
|
1218
|
+
*/
|
|
1219
|
+
provider: ProxyAccountProvider;
|
|
1214
1220
|
type: ProxyAccountType;
|
|
1215
1221
|
status: "refreshed" | "throttled" | "skipped_api_key" | "snapshot" | "error";
|
|
1216
1222
|
/** Fresh quota on "refreshed"; last known snapshot otherwise (may be null). */
|
|
@@ -1219,6 +1225,20 @@ export type ProxyLimitsAccountResult = {
|
|
|
1219
1225
|
coolingUntil?: number;
|
|
1220
1226
|
coolingReason?: AccountCoolingReason;
|
|
1221
1227
|
};
|
|
1228
|
+
/** The pool engine a login belongs to, as named on limits and accounts rows. */
|
|
1229
|
+
export type ProxyAccountProvider = "anthropic" | "codex";
|
|
1230
|
+
/**
|
|
1231
|
+
* Test-only replacement for the token store behind the account-exposing
|
|
1232
|
+
* routes. The token store is a module singleton bound to the real home at
|
|
1233
|
+
* import, so a suite cannot redirect it; this lets a case state which logins
|
|
1234
|
+
* exist (`knownKeys`, including disabled ones) and which are routable per
|
|
1235
|
+
* engine, exactly as the real listers would answer.
|
|
1236
|
+
*/
|
|
1237
|
+
export type ProxyAccountDirectoryOverride = {
|
|
1238
|
+
knownKeys: Set<string>;
|
|
1239
|
+
anthropic: ProxyPassthroughAccount[];
|
|
1240
|
+
codex: ProxyPassthroughAccount[];
|
|
1241
|
+
};
|
|
1222
1242
|
/** Response body of the proxy's GET /limits endpoint. */
|
|
1223
1243
|
export type ProxyLimitsRefreshResponse = {
|
|
1224
1244
|
fetchedAt: number;
|
|
@@ -141,10 +141,21 @@ export type CliClientUsageTotals = {
|
|
|
141
141
|
};
|
|
142
142
|
/** One row of GET /accounts. */
|
|
143
143
|
export type CliAccountsRow = {
|
|
144
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Bare label, e.g. "someone@example.com". Display only: two rows can share
|
|
146
|
+
* it when one email is logged in to both engines. `key` is the identity.
|
|
147
|
+
*/
|
|
145
148
|
label: string;
|
|
146
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Full pool key, e.g. "anthropic:someone@example.com" or
|
|
151
|
+
* "codex:someone@example.com". Null only for plumbing rows.
|
|
152
|
+
*/
|
|
147
153
|
key: string | null;
|
|
154
|
+
/**
|
|
155
|
+
* Which pool engine owns this login. Absent on plumbing rows. Consumers
|
|
156
|
+
* that key a list by row must key by `key`, not `label` — see above.
|
|
157
|
+
*/
|
|
158
|
+
provider?: "anthropic" | "codex";
|
|
148
159
|
/**
|
|
149
160
|
* What this row actually is. Only "account" rows are real logins; the proxy
|
|
150
161
|
* also tracks internal and translation pseudo-accounts, which have no quota
|
|
@@ -183,6 +194,13 @@ export type CliAccountsResponse = {
|
|
|
183
194
|
/** One request as recorded in the proxy request log, reduced to what costing needs. */
|
|
184
195
|
export type ProxyLedgerEntry = {
|
|
185
196
|
account: string;
|
|
197
|
+
/**
|
|
198
|
+
* Provider-qualified identity, "anthropic:<label>" or "codex:<label>".
|
|
199
|
+
* Read from the log row when present; derived from `accountType` for rows
|
|
200
|
+
* written before the pool logged it. This, not `account`, is the join key:
|
|
201
|
+
* one email can be logged in to both engines.
|
|
202
|
+
*/
|
|
203
|
+
accountKey: string;
|
|
186
204
|
/** Derived calling CLI; see CliAccountUsageTotals.byClient. */
|
|
187
205
|
clientApp: string;
|
|
188
206
|
accountType: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.9.
|
|
3
|
+
"version": "12.9.3",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|