@juspay/neurolink 9.92.2 → 9.93.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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +330 -330
- package/dist/cli/commands/proxy.d.ts +2 -1
- package/dist/cli/commands/proxy.js +157 -27
- package/dist/lib/proxy/proxyActivity.d.ts +2 -2
- package/dist/lib/proxy/proxyActivity.js +49 -9
- package/dist/lib/proxy/proxyLifecycle.d.ts +8 -0
- package/dist/lib/proxy/proxyLifecycle.js +333 -0
- package/dist/lib/proxy/requestLogger.js +6 -0
- package/dist/lib/server/routes/claudeProxyRoutes.d.ts +16 -18
- package/dist/lib/server/routes/claudeProxyRoutes.js +63 -71
- package/dist/lib/types/proxy.d.ts +82 -0
- package/dist/proxy/proxyActivity.d.ts +2 -2
- package/dist/proxy/proxyActivity.js +49 -9
- package/dist/proxy/proxyLifecycle.d.ts +8 -0
- package/dist/proxy/proxyLifecycle.js +332 -0
- package/dist/proxy/requestLogger.js +6 -0
- package/dist/server/routes/claudeProxyRoutes.d.ts +16 -18
- package/dist/server/routes/claudeProxyRoutes.js +63 -71
- package/dist/types/proxy.d.ts +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { appendFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { performance } from "node:perf_hooks";
|
|
6
|
+
import { withTimeout } from "../utils/async/withTimeout.js";
|
|
7
|
+
import { logger } from "../utils/logger.js";
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
const DEFAULT_QUEUE_CAPACITY = 10_000;
|
|
10
|
+
const DEFAULT_BATCH_SIZE = 256;
|
|
11
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 25;
|
|
12
|
+
const LIFECYCLE_APPEND_TIMEOUT_MS = 2_000;
|
|
13
|
+
const MAX_SHORT_FIELD_LENGTH = 256;
|
|
14
|
+
const SESSION_KEY_FILE = ".proxy-lifecycle-session-key";
|
|
15
|
+
let loggerEnabled = false;
|
|
16
|
+
let lifecycleLogDir;
|
|
17
|
+
let queueCapacity = DEFAULT_QUEUE_CAPACITY;
|
|
18
|
+
let batchSize = DEFAULT_BATCH_SIZE;
|
|
19
|
+
let flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS;
|
|
20
|
+
let processInstanceId = randomUUID();
|
|
21
|
+
let sessionHashKey = randomBytes(32);
|
|
22
|
+
let nextSequence = 1;
|
|
23
|
+
let attempted = 0;
|
|
24
|
+
let enqueued = 0;
|
|
25
|
+
let written = 0;
|
|
26
|
+
let dropped = 0;
|
|
27
|
+
let queueDrops = 0;
|
|
28
|
+
let invalidDrops = 0;
|
|
29
|
+
let writeDrops = 0;
|
|
30
|
+
let writeFailures = 0;
|
|
31
|
+
let inFlight = 0;
|
|
32
|
+
let queue = [];
|
|
33
|
+
let flushTimer;
|
|
34
|
+
let flushInFlight;
|
|
35
|
+
function positiveInteger(value, fallback) {
|
|
36
|
+
return Number.isInteger(value) && (value ?? 0) > 0
|
|
37
|
+
? value
|
|
38
|
+
: fallback;
|
|
39
|
+
}
|
|
40
|
+
function clip(value) {
|
|
41
|
+
if (typeof value !== "string") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return value.length <= MAX_SHORT_FIELD_LENGTH
|
|
45
|
+
? value
|
|
46
|
+
: value.slice(0, MAX_SHORT_FIELD_LENGTH);
|
|
47
|
+
}
|
|
48
|
+
function finiteNonNegative(value) {
|
|
49
|
+
return value !== undefined && Number.isFinite(value) && value >= 0
|
|
50
|
+
? value
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
53
|
+
function nonNegativeInteger(value) {
|
|
54
|
+
const finite = finiteNonNegative(value);
|
|
55
|
+
return finite === undefined ? undefined : Math.floor(finite);
|
|
56
|
+
}
|
|
57
|
+
function formatTimestamp(timestampMs) {
|
|
58
|
+
const candidate = typeof timestampMs === "number" && Number.isFinite(timestampMs)
|
|
59
|
+
? timestampMs
|
|
60
|
+
: Date.now();
|
|
61
|
+
const date = new Date(candidate);
|
|
62
|
+
return Number.isFinite(date.getTime())
|
|
63
|
+
? date.toISOString()
|
|
64
|
+
: new Date().toISOString();
|
|
65
|
+
}
|
|
66
|
+
function readPersistedSessionHashKey(path) {
|
|
67
|
+
try {
|
|
68
|
+
const key = Buffer.from(readFileSync(path, "utf8").trim(), "base64");
|
|
69
|
+
if (key.length !== 32) {
|
|
70
|
+
throw new Error("persisted lifecycle session key must contain 32 bytes");
|
|
71
|
+
}
|
|
72
|
+
chmodSync(path, 0o600);
|
|
73
|
+
return key;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code === "ENOENT") {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function resolveSessionHashKey(logDir) {
|
|
83
|
+
const configuredSecret = process.env.NEUROLINK_PROXY_SESSION_SECRET?.trim();
|
|
84
|
+
if (configuredSecret) {
|
|
85
|
+
return createHash("sha256").update(configuredSecret).digest();
|
|
86
|
+
}
|
|
87
|
+
const keyPath = join(logDir, SESSION_KEY_FILE);
|
|
88
|
+
const existing = readPersistedSessionHashKey(keyPath);
|
|
89
|
+
if (existing) {
|
|
90
|
+
return existing;
|
|
91
|
+
}
|
|
92
|
+
const generated = randomBytes(32);
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(keyPath, generated.toString("base64"), {
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
flag: "wx",
|
|
97
|
+
mode: 0o600,
|
|
98
|
+
});
|
|
99
|
+
return generated;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code === "EEXIST") {
|
|
103
|
+
const concurrentlyCreated = readPersistedSessionHashKey(keyPath);
|
|
104
|
+
if (concurrentlyCreated) {
|
|
105
|
+
return concurrentlyCreated;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function hashProxyLifecycleSessionId(sessionId) {
|
|
112
|
+
if (!sessionId) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
return createHmac("sha256", sessionHashKey)
|
|
116
|
+
.update(sessionId)
|
|
117
|
+
.digest("hex")
|
|
118
|
+
.slice(0, 24);
|
|
119
|
+
}
|
|
120
|
+
function clearScheduledFlush() {
|
|
121
|
+
if (flushTimer) {
|
|
122
|
+
clearTimeout(flushTimer);
|
|
123
|
+
flushTimer = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function scheduleFlush() {
|
|
127
|
+
if (flushTimer || flushInFlight || queue.length === 0) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
flushTimer = setTimeout(() => {
|
|
131
|
+
flushTimer = undefined;
|
|
132
|
+
void startFlush();
|
|
133
|
+
}, flushIntervalMs);
|
|
134
|
+
flushTimer.unref?.();
|
|
135
|
+
}
|
|
136
|
+
async function flushBatch() {
|
|
137
|
+
if (queue.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const batch = queue.splice(0, batchSize);
|
|
141
|
+
inFlight += batch.length;
|
|
142
|
+
try {
|
|
143
|
+
const byPath = new Map();
|
|
144
|
+
for (const item of batch) {
|
|
145
|
+
const path = join(item.logDir, `proxy-lifecycle-${item.date}.jsonl`);
|
|
146
|
+
const lines = byPath.get(path) ?? [];
|
|
147
|
+
lines.push(`${JSON.stringify(item.record)}\n`);
|
|
148
|
+
byPath.set(path, lines);
|
|
149
|
+
}
|
|
150
|
+
for (const [path, lines] of byPath) {
|
|
151
|
+
try {
|
|
152
|
+
// This best-effort telemetry sink intentionally avoids fsync so request
|
|
153
|
+
// throughput is not coupled to storage latency. Loss is surfaced by
|
|
154
|
+
// writeDrops/writeFailures rather than delaying proxy responses.
|
|
155
|
+
await withTimeout(appendFile(path, lines.join(""), { mode: 0o600 }), LIFECYCLE_APPEND_TIMEOUT_MS, "Timed out writing proxy lifecycle metadata");
|
|
156
|
+
written += lines.length;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
dropped += lines.length;
|
|
160
|
+
writeDrops += lines.length;
|
|
161
|
+
writeFailures += 1;
|
|
162
|
+
logger.warn("[proxy] lifecycle metadata write failed", {
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
inFlight = Math.max(0, inFlight - batch.length);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function startFlush() {
|
|
173
|
+
if (flushInFlight) {
|
|
174
|
+
return flushInFlight;
|
|
175
|
+
}
|
|
176
|
+
const currentFlush = flushBatch();
|
|
177
|
+
flushInFlight = currentFlush;
|
|
178
|
+
void currentFlush.then(() => {
|
|
179
|
+
if (flushInFlight === currentFlush) {
|
|
180
|
+
flushInFlight = undefined;
|
|
181
|
+
}
|
|
182
|
+
scheduleFlush();
|
|
183
|
+
}, () => {
|
|
184
|
+
if (flushInFlight === currentFlush) {
|
|
185
|
+
flushInFlight = undefined;
|
|
186
|
+
}
|
|
187
|
+
scheduleFlush();
|
|
188
|
+
});
|
|
189
|
+
return currentFlush;
|
|
190
|
+
}
|
|
191
|
+
export function configureProxyLifecycleLogger(options) {
|
|
192
|
+
clearScheduledFlush();
|
|
193
|
+
loggerEnabled = false;
|
|
194
|
+
lifecycleLogDir = undefined;
|
|
195
|
+
queueCapacity = positiveInteger(options.queueCapacity, DEFAULT_QUEUE_CAPACITY);
|
|
196
|
+
batchSize = positiveInteger(options.batchSize, DEFAULT_BATCH_SIZE);
|
|
197
|
+
flushIntervalMs = positiveInteger(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
|
|
198
|
+
if (options.enabled && options.logDir) {
|
|
199
|
+
try {
|
|
200
|
+
mkdirSync(options.logDir, { recursive: true, mode: 0o700 });
|
|
201
|
+
sessionHashKey = resolveSessionHashKey(options.logDir);
|
|
202
|
+
lifecycleLogDir = options.logDir;
|
|
203
|
+
loggerEnabled = true;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
logger.warn("[proxy] lifecycle metadata logging disabled", {
|
|
207
|
+
error: error instanceof Error ? error.message : String(error),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Queued records retain their enqueue-time destination. Reconfiguration is
|
|
212
|
+
// therefore atomic for new events while an in-flight or pending batch safely
|
|
213
|
+
// drains to its original directory.
|
|
214
|
+
scheduleFlush();
|
|
215
|
+
}
|
|
216
|
+
/** Enqueue fixed-size lifecycle metadata without awaiting filesystem work. */
|
|
217
|
+
export function logProxyLifecycleEvent(input) {
|
|
218
|
+
if (!loggerEnabled || !lifecycleLogDir) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
attempted += 1;
|
|
223
|
+
const sequence = nextSequence++;
|
|
224
|
+
if (queue.length >= queueCapacity) {
|
|
225
|
+
dropped += 1;
|
|
226
|
+
queueDrops += 1;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const timestamp = formatTimestamp(input.timestampMs);
|
|
230
|
+
const monotonicMs = finiteNonNegative(input.monotonicMs) ?? performance.now();
|
|
231
|
+
const toolCount = nonNegativeInteger(input.toolCount);
|
|
232
|
+
const requestBytes = nonNegativeInteger(input.requestBytes);
|
|
233
|
+
const responseStatus = nonNegativeInteger(input.responseStatus);
|
|
234
|
+
const observedBodyBytes = nonNegativeInteger(input.observedBodyBytes);
|
|
235
|
+
const responseChunks = nonNegativeInteger(input.responseChunks);
|
|
236
|
+
const elapsedMs = finiteNonNegative(input.elapsedMs);
|
|
237
|
+
const model = clip(input.model);
|
|
238
|
+
const sessionHash = clip(input.sessionHash);
|
|
239
|
+
const terminalOutcome = clip(input.terminalOutcome);
|
|
240
|
+
const errorType = clip(input.errorType);
|
|
241
|
+
const errorCode = clip(input.errorCode);
|
|
242
|
+
const record = {
|
|
243
|
+
schemaVersion: SCHEMA_VERSION,
|
|
244
|
+
timestamp,
|
|
245
|
+
monotonicMs: Number(monotonicMs.toFixed(3)),
|
|
246
|
+
processInstanceId,
|
|
247
|
+
sequence,
|
|
248
|
+
event: clip(input.event) ?? "unknown",
|
|
249
|
+
requestId: clip(input.requestId) ?? "unknown",
|
|
250
|
+
method: clip(input.method) ?? "unknown",
|
|
251
|
+
path: clip(input.path) ?? "unknown",
|
|
252
|
+
...(model !== undefined ? { model } : {}),
|
|
253
|
+
...(input.stream !== undefined ? { stream: input.stream } : {}),
|
|
254
|
+
...(toolCount !== undefined ? { toolCount } : {}),
|
|
255
|
+
...(sessionHash !== undefined ? { sessionHash } : {}),
|
|
256
|
+
...(requestBytes !== undefined ? { requestBytes } : {}),
|
|
257
|
+
...(responseStatus !== undefined ? { responseStatus } : {}),
|
|
258
|
+
...(observedBodyBytes !== undefined ? { observedBodyBytes } : {}),
|
|
259
|
+
...(responseChunks !== undefined ? { responseChunks } : {}),
|
|
260
|
+
...(elapsedMs !== undefined
|
|
261
|
+
? { elapsedMs: Number(elapsedMs.toFixed(3)) }
|
|
262
|
+
: {}),
|
|
263
|
+
...(terminalOutcome !== undefined ? { terminalOutcome } : {}),
|
|
264
|
+
...(errorType !== undefined ? { errorType } : {}),
|
|
265
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
266
|
+
};
|
|
267
|
+
queue.push({
|
|
268
|
+
logDir: lifecycleLogDir,
|
|
269
|
+
date: String(record.timestamp).slice(0, 10),
|
|
270
|
+
record,
|
|
271
|
+
});
|
|
272
|
+
enqueued += 1;
|
|
273
|
+
scheduleFlush();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
dropped += 1;
|
|
277
|
+
invalidDrops += 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export async function flushProxyLifecycleEvents() {
|
|
281
|
+
clearScheduledFlush();
|
|
282
|
+
while (queue.length > 0 || flushInFlight) {
|
|
283
|
+
if (flushInFlight) {
|
|
284
|
+
await flushInFlight;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
await startFlush();
|
|
288
|
+
}
|
|
289
|
+
clearScheduledFlush();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function getProxyLifecycleLoggerSnapshot() {
|
|
293
|
+
return {
|
|
294
|
+
enabled: loggerEnabled,
|
|
295
|
+
schemaVersion: SCHEMA_VERSION,
|
|
296
|
+
processInstanceId,
|
|
297
|
+
nextSequence,
|
|
298
|
+
attempted,
|
|
299
|
+
enqueued,
|
|
300
|
+
written,
|
|
301
|
+
dropped,
|
|
302
|
+
queueDrops,
|
|
303
|
+
invalidDrops,
|
|
304
|
+
writeDrops,
|
|
305
|
+
writeFailures,
|
|
306
|
+
pending: queue.length,
|
|
307
|
+
inFlight,
|
|
308
|
+
flushing: flushInFlight !== undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
export function resetProxyLifecycleLoggerForTests() {
|
|
312
|
+
clearScheduledFlush();
|
|
313
|
+
loggerEnabled = false;
|
|
314
|
+
lifecycleLogDir = undefined;
|
|
315
|
+
queueCapacity = DEFAULT_QUEUE_CAPACITY;
|
|
316
|
+
batchSize = DEFAULT_BATCH_SIZE;
|
|
317
|
+
flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS;
|
|
318
|
+
processInstanceId = randomUUID();
|
|
319
|
+
sessionHashKey = randomBytes(32);
|
|
320
|
+
nextSequence = 1;
|
|
321
|
+
attempted = 0;
|
|
322
|
+
enqueued = 0;
|
|
323
|
+
written = 0;
|
|
324
|
+
dropped = 0;
|
|
325
|
+
queueDrops = 0;
|
|
326
|
+
invalidDrops = 0;
|
|
327
|
+
writeDrops = 0;
|
|
328
|
+
writeFailures = 0;
|
|
329
|
+
inFlight = 0;
|
|
330
|
+
queue = [];
|
|
331
|
+
flushInFlight = undefined;
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=proxyLifecycle.js.map
|
|
@@ -15,6 +15,7 @@ import { promisify } from "util";
|
|
|
15
15
|
import { gzip as gzipCallback } from "zlib";
|
|
16
16
|
import { OtelBridge } from "../observability/otelBridge.js";
|
|
17
17
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
18
|
+
import { configureProxyLifecycleLogger } from "./proxyLifecycle.js";
|
|
18
19
|
let logDir = null;
|
|
19
20
|
let logEnabled = false;
|
|
20
21
|
/**
|
|
@@ -45,8 +46,11 @@ const SENSITIVE_HEADER_PATTERN = /token|secret|key|password|credential/i;
|
|
|
45
46
|
/** JSON keys whose values should be redacted in request/response bodies. */
|
|
46
47
|
const SENSITIVE_BODY_KEYS = /("(?:password|access_token|refresh_token|api_key|apiKey|secret|authorization|token|credential|x-api-key)"\s*:\s*)"(?:[^"\\]|\\.)*"/gi;
|
|
47
48
|
export function initRequestLogger(enabled = true, customLogsDir) {
|
|
49
|
+
// Lifecycle metadata deliberately shares the request logger's enablement,
|
|
50
|
+
// directory permissions, retention boundary, and operator privacy control.
|
|
48
51
|
logEnabled = enabled;
|
|
49
52
|
if (!enabled) {
|
|
53
|
+
configureProxyLifecycleLogger({ enabled: false });
|
|
50
54
|
return;
|
|
51
55
|
}
|
|
52
56
|
try {
|
|
@@ -55,10 +59,12 @@ export function initRequestLogger(enabled = true, customLogsDir) {
|
|
|
55
59
|
mkdirSync(logDir, { recursive: true, mode: 0o700 });
|
|
56
60
|
}
|
|
57
61
|
chmodSync(logDir, 0o700);
|
|
62
|
+
configureProxyLifecycleLogger({ enabled: true, logDir });
|
|
58
63
|
}
|
|
59
64
|
catch (err) {
|
|
60
65
|
logEnabled = false;
|
|
61
66
|
logDir = null;
|
|
67
|
+
configureProxyLifecycleLogger({ enabled: false });
|
|
62
68
|
logger.warn(`[proxy] Request logging disabled — failed to create log directory: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -18,12 +18,12 @@ import type { AccountAllowlist, AccountCooldownPlan, AccountQuota, AnthropicAtte
|
|
|
18
18
|
* no key is configured or the key cannot be matched (account disabled/
|
|
19
19
|
* removed). The resolution is per-request because enabledAccounts membership
|
|
20
20
|
* can shift between requests. */
|
|
21
|
-
declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey
|
|
21
|
+
declare function resolveHomeIndex(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): number;
|
|
22
22
|
/** If the configured home primary is no longer cooling, reset
|
|
23
23
|
* primaryAccountIndex back to its index so traffic returns to the preferred
|
|
24
24
|
* account once its rate limit window expires. Called at the start of each
|
|
25
25
|
* request. Home is resolved fresh per call via resolveHomeIndex. */
|
|
26
|
-
declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey
|
|
26
|
+
declare function maybeResetPrimaryToHome(enabledAccounts: ProxyPassthroughAccount[], primaryAccountKey: string | undefined): void;
|
|
27
27
|
declare function claimTransientRateLimitRetry(accountKey: string, coolingUntil: number, now?: number): number | undefined;
|
|
28
28
|
declare function claimTransientCooldownAdmission(accountKey: string, coolingUntil: number, now?: number): number | undefined;
|
|
29
29
|
declare function waitForTransientAccountAvailability(orderedAccounts: ProxyPassthroughAccount[]): Promise<ProxyPassthroughAccount[]>;
|
|
@@ -61,28 +61,30 @@ declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: nu
|
|
|
61
61
|
declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
|
|
62
62
|
/**
|
|
63
63
|
* Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
|
|
64
|
-
* spend the
|
|
65
|
-
*
|
|
64
|
+
* spend the overall weekly allowance that expires SOONEST first, so quota
|
|
65
|
+
* cannot disappear while traffic is consuming a newer weekly window. The 5h
|
|
66
|
+
* window remains an availability boundary: a session at its soft limit is
|
|
67
|
+
* temporarily demoted until that session resets.
|
|
66
68
|
*
|
|
67
69
|
* Priority among usable accounts:
|
|
68
70
|
* 1. no quota data yet — probe first: one request reveals its windows and
|
|
69
71
|
* self-corrects the ordering. (Ranking unknowns last would starve them
|
|
70
72
|
* forever: never picked → never observed → never comparable.)
|
|
71
73
|
* 2. session headroom before session-saturated (>= soft limit or
|
|
72
|
-
* "throttled") —
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* 5. highest weekly utilization — finish off the one closest to done
|
|
74
|
+
* "throttled") — do not re-hammer an urgent weekly account while its 5h
|
|
75
|
+
* capacity is temporarily unavailable.
|
|
76
|
+
* 3. soonest WEEKLY (7d) reset — consume the oldest overall allowance
|
|
77
|
+
* before it expires.
|
|
78
|
+
* 4. soonest SESSION (5h) reset — decides equal-weekly or fresh-weekly ties,
|
|
79
|
+
* using tolerance buckets for comparator stability.
|
|
80
|
+
* 5. highest weekly utilization — finish off the one closest to done.
|
|
79
81
|
* 6. configured primary account, then insertion order
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
+
* Saturated pairs are ordered by soonest 5h recovery first, then weekly reset,
|
|
83
|
+
* because neither can consume more quota until session capacity returns.
|
|
82
84
|
* Cooling/rejected accounts sort last, soonest-back-to-service first, as
|
|
83
85
|
* last resort.
|
|
84
86
|
*/
|
|
85
|
-
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey
|
|
87
|
+
declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number, primaryKey: string | undefined, sessionSoftLimit?: number, sessionResetToleranceMs?: number): ProxyPassthroughAccount[];
|
|
86
88
|
declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>): {
|
|
87
89
|
stream: ReadableStream<Uint8Array>;
|
|
88
90
|
outcome: Promise<StreamTerminalOutcome>;
|
|
@@ -266,10 +268,6 @@ export declare const __testHooks: {
|
|
|
266
268
|
resetEpochToMs: typeof resetEpochToMs;
|
|
267
269
|
seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
|
|
268
270
|
getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
|
|
269
|
-
setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
|
|
270
|
-
getConfiguredPrimaryAccountKey: () => string | undefined;
|
|
271
|
-
setConfiguredAccountAllowlist: (allowlist: AccountAllowlist | undefined) => void;
|
|
272
|
-
getConfiguredAccountAllowlist: () => string[] | undefined;
|
|
273
271
|
setPrimaryAccountIndex: (index: number) => void;
|
|
274
272
|
getPrimaryAccountIndex: () => number;
|
|
275
273
|
setAccountRuntimeState: (key: string, state: Partial<RuntimeAccountState>) => void;
|