@juspay/neurolink 10.1.2 → 10.2.1
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 +12 -0
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/cli/commands/proxy.js +85 -14
- package/dist/lib/providers/azureOpenai.d.ts +6 -0
- package/dist/lib/providers/azureOpenai.js +8 -0
- package/dist/lib/providers/openAI.d.ts +6 -0
- package/dist/lib/providers/openAI.js +8 -0
- package/dist/lib/providers/openaiChatCompletionsBase.d.ts +15 -0
- package/dist/lib/providers/openaiChatCompletionsBase.js +21 -1
- package/dist/lib/proxy/proxyPaths.d.ts +1 -0
- package/dist/lib/proxy/proxyPaths.js +5 -0
- package/dist/lib/proxy/usageStats.d.ts +58 -4
- package/dist/lib/proxy/usageStats.js +639 -79
- package/dist/lib/types/proxy.d.ts +39 -0
- package/dist/providers/azureOpenai.d.ts +6 -0
- package/dist/providers/azureOpenai.js +8 -0
- package/dist/providers/openAI.d.ts +6 -0
- package/dist/providers/openAI.js +8 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +15 -0
- package/dist/providers/openaiChatCompletionsBase.js +21 -1
- package/dist/proxy/proxyPaths.d.ts +1 -0
- package/dist/proxy/proxyPaths.js +5 -0
- package/dist/proxy/usageStats.d.ts +58 -4
- package/dist/proxy/usageStats.js +639 -79
- package/dist/types/proxy.d.ts +39 -0
- package/package.json +3 -2
|
@@ -41,6 +41,8 @@ const _require = createRequire(import.meta.url);
|
|
|
41
41
|
const PROXY_VERSION = packageJson.version;
|
|
42
42
|
const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url));
|
|
43
43
|
const PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
44
|
+
const LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS = 5_000;
|
|
45
|
+
let legacyStatusAccountCache;
|
|
44
46
|
// Allowed drift between a pid's OS-reported start time and the persisted
|
|
45
47
|
// ProxySupervisorState.startTime before processLooksLikeProxySupervisor
|
|
46
48
|
// treats it as a confident mismatch (recycled pid). Generous on purpose:
|
|
@@ -323,6 +325,34 @@ async function resolveStatusPrimaryAccount(proxyConfig) {
|
|
|
323
325
|
source: "fallback",
|
|
324
326
|
};
|
|
325
327
|
}
|
|
328
|
+
async function resolveLegacyStatusAccountLabel(storedAnthropicAccountCount) {
|
|
329
|
+
if (storedAnthropicAccountCount !== 0) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
const credentialsPath = join(homedir(), ".neurolink", "anthropic-credentials.json");
|
|
333
|
+
const now = Date.now();
|
|
334
|
+
if (legacyStatusAccountCache?.credentialsPath === credentialsPath &&
|
|
335
|
+
legacyStatusAccountCache.expiresAt > now) {
|
|
336
|
+
return legacyStatusAccountCache.label;
|
|
337
|
+
}
|
|
338
|
+
let label = null;
|
|
339
|
+
try {
|
|
340
|
+
const { readFile } = await import("node:fs/promises");
|
|
341
|
+
const parsed = JSON.parse(await readFile(credentialsPath, "utf8"));
|
|
342
|
+
if (parsed.oauth?.accessToken) {
|
|
343
|
+
label = parsed.email?.trim() || "legacy-default";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
label = null;
|
|
348
|
+
}
|
|
349
|
+
legacyStatusAccountCache = {
|
|
350
|
+
credentialsPath,
|
|
351
|
+
expiresAt: now + LEGACY_STATUS_ACCOUNT_CACHE_TTL_MS,
|
|
352
|
+
label,
|
|
353
|
+
};
|
|
354
|
+
return label;
|
|
355
|
+
}
|
|
326
356
|
/**
|
|
327
357
|
* Check if the launchd service is loaded and actively managing the proxy.
|
|
328
358
|
* Returns true if launchctl reports the service as running.
|
|
@@ -1492,23 +1522,32 @@ export async function createProxyStartApp(params) {
|
|
|
1492
1522
|
const activeAccountAllowlist = runtimeConfig
|
|
1493
1523
|
? runtimeConfig.accountAllowlist
|
|
1494
1524
|
: params.accountAllowlist;
|
|
1495
|
-
const {
|
|
1525
|
+
const { getReconciledStats, getUsageStatsPersistenceStatus } = await import("../../lib/proxy/usageStats.js");
|
|
1496
1526
|
const { loadAccountCooldowns } = await import("../../lib/proxy/accountCooldown.js");
|
|
1497
|
-
const stats =
|
|
1527
|
+
const stats = await getReconciledStats();
|
|
1498
1528
|
const runtimeState = loadProxyState();
|
|
1499
1529
|
const supervisorState = loadProxySupervisorState();
|
|
1500
1530
|
const updateState = loadUpdateState();
|
|
1501
1531
|
const cooldowns = await loadAccountCooldowns();
|
|
1502
1532
|
const storedAccountKeys = new Set();
|
|
1533
|
+
const disabledAccountKeys = new Set();
|
|
1534
|
+
let accountInventoryLoaded = false;
|
|
1503
1535
|
try {
|
|
1504
1536
|
const { tokenStore } = await import("../../lib/auth/tokenStore.js");
|
|
1505
1537
|
for (const key of await tokenStore.listByPrefix("anthropic:")) {
|
|
1506
1538
|
storedAccountKeys.add(normalizeAnthropicAccountKey(key));
|
|
1507
1539
|
}
|
|
1540
|
+
for (const key of await tokenStore.listDisabled()) {
|
|
1541
|
+
disabledAccountKeys.add(normalizeAnthropicAccountKey(key));
|
|
1542
|
+
}
|
|
1543
|
+
accountInventoryLoaded = true;
|
|
1508
1544
|
}
|
|
1509
1545
|
catch (err) {
|
|
1510
1546
|
logger.debug(`[proxy] /status: failed to resolve account cooldown labels: ${err instanceof Error ? err.message : String(err)}`);
|
|
1511
1547
|
}
|
|
1548
|
+
const legacyAccountLabel = accountInventoryLoaded
|
|
1549
|
+
? await resolveLegacyStatusAccountLabel(storedAccountKeys.size)
|
|
1550
|
+
: null;
|
|
1512
1551
|
const now = Date.now();
|
|
1513
1552
|
const health = buildProxyHealthResponse(readiness, {
|
|
1514
1553
|
strategy: activeStrategy,
|
|
@@ -1530,6 +1569,7 @@ export async function createProxyStartApp(params) {
|
|
|
1530
1569
|
version: PROXY_VERSION,
|
|
1531
1570
|
health,
|
|
1532
1571
|
stats: {
|
|
1572
|
+
startedAt: stats.startedAt,
|
|
1533
1573
|
totalAttempts: stats.totalAttempts,
|
|
1534
1574
|
totalAttemptErrors: stats.totalAttemptErrors,
|
|
1535
1575
|
totalRequests: stats.totalRequests,
|
|
@@ -1540,13 +1580,25 @@ export async function createProxyStartApp(params) {
|
|
|
1540
1580
|
totalQuotaRateLimits: stats.totalQuotaRateLimits,
|
|
1541
1581
|
accounts: Object.values(stats.accounts).map((account) => {
|
|
1542
1582
|
const normalizedKey = normalizeAnthropicAccountKey(account.label);
|
|
1583
|
+
const isLegacyAccount = account.type === "oauth" && account.label === legacyAccountLabel;
|
|
1543
1584
|
const accountKey = storedAccountKeys.has(normalizedKey)
|
|
1544
1585
|
? normalizedKey
|
|
1545
|
-
:
|
|
1546
|
-
?
|
|
1547
|
-
: account.
|
|
1548
|
-
?
|
|
1586
|
+
: isLegacyAccount
|
|
1587
|
+
? LEGACY_ANTHROPIC_ACCOUNT_KEY
|
|
1588
|
+
: account.label === "env"
|
|
1589
|
+
? ENV_ANTHROPIC_ACCOUNT_KEY
|
|
1549
1590
|
: normalizedKey;
|
|
1591
|
+
const isStored = storedAccountKeys.has(accountKey) || isLegacyAccount;
|
|
1592
|
+
const cooling = (cooldowns[accountKey]?.coolingUntil ?? 0) > now;
|
|
1593
|
+
const accountStatus = disabledAccountKeys.has(accountKey)
|
|
1594
|
+
? "disabled"
|
|
1595
|
+
: !isAccountAllowed(accountKey, activeAccountAllowlist)
|
|
1596
|
+
? "excluded"
|
|
1597
|
+
: accountInventoryLoaded && account.type === "oauth" && !isStored
|
|
1598
|
+
? "removed"
|
|
1599
|
+
: cooling
|
|
1600
|
+
? "cooling"
|
|
1601
|
+
: "active";
|
|
1550
1602
|
return {
|
|
1551
1603
|
label: account.label,
|
|
1552
1604
|
type: account.type,
|
|
@@ -1558,10 +1610,12 @@ export async function createProxyStartApp(params) {
|
|
|
1558
1610
|
rateLimits: account.rateLimitCount,
|
|
1559
1611
|
transientRateLimits: account.transientRateLimitCount,
|
|
1560
1612
|
quotaRateLimits: account.quotaRateLimitCount,
|
|
1561
|
-
cooling
|
|
1613
|
+
cooling,
|
|
1614
|
+
status: accountStatus,
|
|
1562
1615
|
};
|
|
1563
1616
|
}),
|
|
1564
1617
|
primaryAccount,
|
|
1618
|
+
persistence: getUsageStatsPersistenceStatus(),
|
|
1565
1619
|
},
|
|
1566
1620
|
activity: (() => {
|
|
1567
1621
|
const activity = getProxyActivitySnapshot();
|
|
@@ -1875,10 +1929,17 @@ function registerProxyShutdownHandlers(params) {
|
|
|
1875
1929
|
logger.error(`[proxy] failed to drain server during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1876
1930
|
}
|
|
1877
1931
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1932
|
+
const usageStatsFlush = import("../../lib/proxy/usageStats.js").then(({ flushUsageStats }) => flushUsageStats());
|
|
1933
|
+
const [usageStatsFlushResult, lifecycleFlushResult] = await Promise.allSettled([
|
|
1934
|
+
withTimeout(usageStatsFlush, PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy usage statistics during shutdown"),
|
|
1935
|
+
withTimeout(flushProxyLifecycleEvents(), PROXY_LIFECYCLE_SHUTDOWN_TIMEOUT_MS, "Timed out flushing proxy lifecycle metadata during shutdown"),
|
|
1936
|
+
]);
|
|
1937
|
+
if (usageStatsFlushResult.status === "rejected") {
|
|
1938
|
+
const error = usageStatsFlushResult.reason;
|
|
1939
|
+
logger.warn(`[proxy] usage statistics flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1880
1940
|
}
|
|
1881
|
-
|
|
1941
|
+
if (lifecycleFlushResult.status === "rejected") {
|
|
1942
|
+
const error = lifecycleFlushResult.reason;
|
|
1882
1943
|
logger.debug(`[proxy] lifecycle metadata flush failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
1883
1944
|
}
|
|
1884
1945
|
try {
|
|
@@ -2284,9 +2345,10 @@ async function startProxyCommandHandler(argv) {
|
|
|
2284
2345
|
}
|
|
2285
2346
|
// In dev mode: redirect writable state to .neurolink-dev/ and skip singleton check
|
|
2286
2347
|
let devPaths;
|
|
2348
|
+
const { resolveProxyPaths, resolveProxyUsageStatsPath } = await import("../../lib/proxy/proxyPaths.js");
|
|
2349
|
+
const proxyPaths = resolveProxyPaths(isDev);
|
|
2287
2350
|
if (isDev) {
|
|
2288
|
-
|
|
2289
|
-
devPaths = resolveProxyPaths(true);
|
|
2351
|
+
devPaths = proxyPaths;
|
|
2290
2352
|
setProxyStateDir(devPaths.stateDir);
|
|
2291
2353
|
const { initAccountQuota } = await import("../../lib/proxy/accountQuota.js");
|
|
2292
2354
|
initAccountQuota(devPaths.quotaFile);
|
|
@@ -2301,6 +2363,15 @@ async function startProxyCommandHandler(argv) {
|
|
|
2301
2363
|
if (!isDev && !socketWorker) {
|
|
2302
2364
|
await ensureProxyStartAllowed(spinner);
|
|
2303
2365
|
}
|
|
2366
|
+
const { initUsageStats, getUsageStatsPersistenceStatus } = await import("../../lib/proxy/usageStats.js");
|
|
2367
|
+
await initUsageStats(resolveProxyUsageStatsPath(proxyPaths));
|
|
2368
|
+
const usageStatsPersistence = getUsageStatsPersistenceStatus();
|
|
2369
|
+
if (usageStatsPersistence.lastError) {
|
|
2370
|
+
logger.warn(`[proxy] usage statistics persistence unavailable: ${usageStatsPersistence.lastError}`);
|
|
2371
|
+
}
|
|
2372
|
+
else if (usageStatsPersistence.lastRecoveryAt) {
|
|
2373
|
+
logger.warn(`[proxy] recovered corrupt usage statistics state at ${new Date(usageStatsPersistence.lastRecoveryAt).toISOString()}`);
|
|
2374
|
+
}
|
|
2304
2375
|
const baseEnv = { ...process.env };
|
|
2305
2376
|
const envResolution = resolveProxyEnvFile({
|
|
2306
2377
|
explicitEnvFile: argv.envFile,
|
|
@@ -2485,7 +2556,7 @@ function printStatusStats(stats) {
|
|
|
2485
2556
|
String(account.success ?? 0),
|
|
2486
2557
|
String(account.errors ?? 0),
|
|
2487
2558
|
String(account.rateLimits ?? 0),
|
|
2488
|
-
account.cooling ? "cooling" : "active",
|
|
2559
|
+
account.status ?? (account.cooling ? "cooling" : "active"),
|
|
2489
2560
|
]);
|
|
2490
2561
|
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
|
|
2491
2562
|
const numericColumns = new Set([2, 3, 4, 5]);
|
|
@@ -2503,7 +2574,7 @@ function printStatusStats(stats) {
|
|
|
2503
2574
|
const statusStart = formatted.length - widths[6];
|
|
2504
2575
|
const prefix = formatted.slice(0, statusStart);
|
|
2505
2576
|
const paddedStatus = formatted.slice(statusStart);
|
|
2506
|
-
console.info(` ${chalk.cyan(prefix.slice(0, widths[0]))}${prefix.slice(widths[0])}${status === "cooling" ? chalk.red(paddedStatus) : chalk.
|
|
2577
|
+
console.info(` ${chalk.cyan(prefix.slice(0, widths[0]))}${prefix.slice(widths[0])}${status === "active" ? chalk.green(paddedStatus) : status === "cooling" ? chalk.red(paddedStatus) : chalk.yellow(paddedStatus)}`);
|
|
2507
2578
|
}
|
|
2508
2579
|
}
|
|
2509
2580
|
}
|
|
@@ -22,6 +22,12 @@ export declare class AzureOpenAIProvider extends OpenAIChatCompletionsProvider {
|
|
|
22
22
|
protected readonly azureDeploymentPathPrefix: string;
|
|
23
23
|
protected readonly useMaxCompletionTokensOverride?: boolean;
|
|
24
24
|
constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["azure"]);
|
|
25
|
+
/**
|
|
26
|
+
* Azure OpenAI natively supports `response_format: json_schema` together
|
|
27
|
+
* with tool calling in one request, so structured output stays
|
|
28
|
+
* wire-enforced mid-loop instead of deferring to post-hoc coercion.
|
|
29
|
+
*/
|
|
30
|
+
protected suppressResponseFormatWithTools(): boolean;
|
|
25
31
|
protected getProviderName(): AIProviderName;
|
|
26
32
|
/**
|
|
27
33
|
* The "default model" for Azure is the deployment name — it's the
|
|
@@ -139,6 +139,14 @@ export class AzureOpenAIProvider extends OpenAIChatCompletionsProvider {
|
|
|
139
139
|
// ===========================================================================
|
|
140
140
|
// Abstract-hook implementations
|
|
141
141
|
// ===========================================================================
|
|
142
|
+
/**
|
|
143
|
+
* Azure OpenAI natively supports `response_format: json_schema` together
|
|
144
|
+
* with tool calling in one request, so structured output stays
|
|
145
|
+
* wire-enforced mid-loop instead of deferring to post-hoc coercion.
|
|
146
|
+
*/
|
|
147
|
+
suppressResponseFormatWithTools() {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
142
150
|
getProviderName() {
|
|
143
151
|
return "azure";
|
|
144
152
|
}
|
|
@@ -16,6 +16,12 @@ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export declare class OpenAIProvider extends OpenAIChatCompletionsProvider {
|
|
18
18
|
constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["openai"]);
|
|
19
|
+
/**
|
|
20
|
+
* OpenAI natively supports `response_format: json_schema` together with
|
|
21
|
+
* tool calling in one request, so structured output stays wire-enforced
|
|
22
|
+
* mid-loop instead of deferring to post-hoc coercion.
|
|
23
|
+
*/
|
|
24
|
+
protected suppressResponseFormatWithTools(): boolean;
|
|
19
25
|
protected getProviderName(): AIProviderName;
|
|
20
26
|
protected getDefaultModel(): string;
|
|
21
27
|
formatProviderError(error: unknown): Error;
|
|
@@ -74,6 +74,14 @@ export class OpenAIProvider extends OpenAIChatCompletionsProvider {
|
|
|
74
74
|
// ===========================================================================
|
|
75
75
|
// Abstract hook implementations
|
|
76
76
|
// ===========================================================================
|
|
77
|
+
/**
|
|
78
|
+
* OpenAI natively supports `response_format: json_schema` together with
|
|
79
|
+
* tool calling in one request, so structured output stays wire-enforced
|
|
80
|
+
* mid-loop instead of deferring to post-hoc coercion.
|
|
81
|
+
*/
|
|
82
|
+
suppressResponseFormatWithTools() {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
77
85
|
getProviderName() {
|
|
78
86
|
return AIProviderNameEnum.OPENAI;
|
|
79
87
|
}
|
|
@@ -62,6 +62,21 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
|
|
|
62
62
|
* downgraded `json_schema` to `json_object`. Subclasses replicate that here.
|
|
63
63
|
*/
|
|
64
64
|
protected adjustResponseFormat(rf: OpenAICompatResponseFormat | undefined, _modelId: string): OpenAICompatResponseFormat | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* When true (default), `response_format` is NOT sent on requests that carry
|
|
67
|
+
* tools. The AI SDK sets responseFormat on EVERY step of a tool loop, and
|
|
68
|
+
* generic/proxy backends (LiteLLM→vllm/GLM, openai-compatible, local
|
|
69
|
+
* servers) may silently honor it over tool calling — answering with
|
|
70
|
+
* final-shape JSON on step 1 instead of running the agentic loop. No error
|
|
71
|
+
* is raised, so the runtime tools↔schema conflict detector cannot catch it.
|
|
72
|
+
* The schema is still enforced post-hoc (GenerationHandler coerces the final
|
|
73
|
+
* text against it) — the same contract as the Gemini tools↔schema exclusion.
|
|
74
|
+
* Mirrors the streaming path, which never sends response_format.
|
|
75
|
+
*
|
|
76
|
+
* Backends with first-party support for tools + json_schema in one request
|
|
77
|
+
* (OpenAI, Azure OpenAI) override this to false.
|
|
78
|
+
*/
|
|
79
|
+
protected suppressResponseFormatWithTools(): boolean;
|
|
65
80
|
/**
|
|
66
81
|
* Hook to adjust the fully-built wire request body before it is sent, on
|
|
67
82
|
* both the streaming and non-streaming paths. Default identity. Override for
|
|
@@ -76,6 +76,23 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
76
76
|
adjustResponseFormat(rf, _modelId) {
|
|
77
77
|
return rf;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* When true (default), `response_format` is NOT sent on requests that carry
|
|
81
|
+
* tools. The AI SDK sets responseFormat on EVERY step of a tool loop, and
|
|
82
|
+
* generic/proxy backends (LiteLLM→vllm/GLM, openai-compatible, local
|
|
83
|
+
* servers) may silently honor it over tool calling — answering with
|
|
84
|
+
* final-shape JSON on step 1 instead of running the agentic loop. No error
|
|
85
|
+
* is raised, so the runtime tools↔schema conflict detector cannot catch it.
|
|
86
|
+
* The schema is still enforced post-hoc (GenerationHandler coerces the final
|
|
87
|
+
* text against it) — the same contract as the Gemini tools↔schema exclusion.
|
|
88
|
+
* Mirrors the streaming path, which never sends response_format.
|
|
89
|
+
*
|
|
90
|
+
* Backends with first-party support for tools + json_schema in one request
|
|
91
|
+
* (OpenAI, Azure OpenAI) override this to false.
|
|
92
|
+
*/
|
|
93
|
+
suppressResponseFormatWithTools() {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
79
96
|
/**
|
|
80
97
|
* Hook to adjust the fully-built wire request body before it is sent, on
|
|
81
98
|
* both the streaming and non-streaming paths. Default identity. Override for
|
|
@@ -221,6 +238,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
221
238
|
const adjustResponseFormat = this.adjustResponseFormat.bind(this);
|
|
222
239
|
const adjustRequestBody = this.adjustRequestBody.bind(this);
|
|
223
240
|
const adjustBodyAfter400 = this.adjustBodyAfter400.bind(this);
|
|
241
|
+
const suppressResponseFormatWithTools = this.suppressResponseFormatWithTools.bind(this);
|
|
224
242
|
const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
|
|
225
243
|
return {
|
|
226
244
|
specificationVersion: "v3",
|
|
@@ -229,7 +247,9 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
229
247
|
supportedUrls: {},
|
|
230
248
|
doGenerate: async (options) => {
|
|
231
249
|
const baseMessages = messageBuilderToOpenAI(options.prompt);
|
|
232
|
-
const
|
|
250
|
+
const hasTools = Array.isArray(options.tools) && options.tools.length > 0;
|
|
251
|
+
const responseFormat = options.responseFormat &&
|
|
252
|
+
!(hasTools && suppressResponseFormatWithTools())
|
|
233
253
|
? adjustResponseFormat(v3ResponseFormatToOpenAI(options.responseFormat), modelId)
|
|
234
254
|
: undefined;
|
|
235
255
|
// ensureJsonWordInBody runs LAST — on the body after adjustRequestBody —
|
|
@@ -22,6 +22,7 @@ export function resolveProxyPaths(dev) {
|
|
|
22
22
|
logsDir: join(base, "logs"),
|
|
23
23
|
quotaFile: join(base, "account-quotas.json"),
|
|
24
24
|
cooldownFile: join(base, "account-cooldowns.json"),
|
|
25
|
+
statsFile: join(base, "proxy-usage-stats.json"),
|
|
25
26
|
isDev: true,
|
|
26
27
|
};
|
|
27
28
|
}
|
|
@@ -31,7 +32,11 @@ export function resolveProxyPaths(dev) {
|
|
|
31
32
|
logsDir: join(base, "logs"),
|
|
32
33
|
quotaFile: join(base, "account-quotas.json"),
|
|
33
34
|
cooldownFile: join(base, "account-cooldowns.json"),
|
|
35
|
+
statsFile: join(base, "proxy-usage-stats.json"),
|
|
34
36
|
isDev: false,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
39
|
+
export function resolveProxyUsageStatsPath(paths) {
|
|
40
|
+
return paths.statsFile ?? join(paths.stateDir, "proxy-usage-stats.json");
|
|
41
|
+
}
|
|
37
42
|
//# sourceMappingURL=proxyPaths.js.map
|
|
@@ -1,13 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Proxy
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Proxy usage statistics with restart- and handoff-safe persistence.
|
|
3
|
+
*
|
|
4
|
+
* Request handlers update memory synchronously. Small deltas are merged into a
|
|
5
|
+
* shared snapshot asynchronously, under a cross-process lock, so overlapping
|
|
6
|
+
* rolling workers cannot overwrite each other's counters.
|
|
5
7
|
*/
|
|
6
|
-
import type { AccountStats, ProxyStats } from "../types/index.js";
|
|
8
|
+
import type { AccountStats, ProxyStats, ProxyStatsPersistenceStatus, ProxyUsageStatsStoreOptions } from "../types/index.js";
|
|
9
|
+
export declare class ProxyUsageStatsStore {
|
|
10
|
+
private readonly now;
|
|
11
|
+
private readonly flushIntervalMs;
|
|
12
|
+
private readonly lockTimeoutMs;
|
|
13
|
+
private readonly staleLockMs;
|
|
14
|
+
private filePath?;
|
|
15
|
+
private stats;
|
|
16
|
+
private pending;
|
|
17
|
+
private pendingMutations;
|
|
18
|
+
private inFlightMutations;
|
|
19
|
+
private revision;
|
|
20
|
+
private flushTimer;
|
|
21
|
+
private readonly flushMutex;
|
|
22
|
+
private lastFlushedAt?;
|
|
23
|
+
private lastReconciledAt?;
|
|
24
|
+
private lastRecoveryAt?;
|
|
25
|
+
private lastError?;
|
|
26
|
+
constructor(options?: ProxyUsageStatsStoreOptions);
|
|
27
|
+
initialize(filePath?: string): Promise<void>;
|
|
28
|
+
recordAttempt(accountLabel: string, accountType: string): void;
|
|
29
|
+
recordFinalSuccess(accountLabel?: string, accountType?: string): void;
|
|
30
|
+
recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
|
|
31
|
+
recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
|
|
32
|
+
getStats(): ProxyStats;
|
|
33
|
+
getAccountStats(label: string): AccountStats | undefined;
|
|
34
|
+
getPersistenceStatus(): ProxyStatsPersistenceStatus;
|
|
35
|
+
flush(): Promise<void>;
|
|
36
|
+
reconcile(): Promise<ProxyStats>;
|
|
37
|
+
resetMemory(): void;
|
|
38
|
+
resetForTests(): Promise<void>;
|
|
39
|
+
private markMutation;
|
|
40
|
+
private applyAttempt;
|
|
41
|
+
private applyFinalSuccess;
|
|
42
|
+
private applyAttemptError;
|
|
43
|
+
private applyFinalError;
|
|
44
|
+
private ensureAccount;
|
|
45
|
+
private scheduleFlush;
|
|
46
|
+
private cancelFlushTimer;
|
|
47
|
+
private readSnapshot;
|
|
48
|
+
private applySnapshot;
|
|
49
|
+
private recoverCorruptSnapshot;
|
|
50
|
+
private quarantineCorruptSnapshot;
|
|
51
|
+
private pruneCorruptSnapshots;
|
|
52
|
+
private describeError;
|
|
53
|
+
}
|
|
54
|
+
export declare function initUsageStats(filePath: string): Promise<void>;
|
|
7
55
|
export declare function recordAttempt(accountLabel: string, accountType: string): void;
|
|
8
56
|
export declare function recordFinalSuccess(accountLabel?: string, accountType?: string): void;
|
|
9
57
|
export declare function recordAttemptError(accountLabel: string, accountType: string, status: number, rateLimitKind?: "transient" | "quota"): void;
|
|
10
58
|
export declare function recordFinalError(_status: number, accountLabel?: string, accountType?: string): void;
|
|
11
59
|
export declare function getStats(): ProxyStats;
|
|
60
|
+
export declare function getReconciledStats(): Promise<ProxyStats>;
|
|
12
61
|
export declare function getAccountStats(label: string): AccountStats | undefined;
|
|
62
|
+
export declare function getUsageStatsPersistenceStatus(): ProxyStatsPersistenceStatus;
|
|
63
|
+
export declare function flushUsageStats(): Promise<void>;
|
|
64
|
+
/** Reset process-local counters while intentionally preserving durable state. */
|
|
13
65
|
export declare function resetStats(): void;
|
|
66
|
+
/** Disconnect persistence and clear singleton state between isolated tests. */
|
|
67
|
+
export declare function resetUsageStatsForTests(): Promise<void>;
|