pi-antigravity-rotator 2.3.0 → 2.3.2
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 +25 -0
- package/README.md +18 -12
- package/package.json +12 -8
- package/src/cli.ts +27 -2
- package/src/compat/translators.ts +58 -8
- package/src/compat.ts +103 -15
- package/src/dashboard.ts +35 -0
- package/src/exposure.ts +17 -0
- package/src/index.ts +12 -0
- package/src/login.ts +4 -3
- package/src/notification-poller.ts +23 -10
- package/src/onboarding.ts +16 -1
- package/src/proxy.ts +422 -387
- package/src/rotator.ts +271 -6
- package/src/static/dashboard.js +1971 -928
- package/src/telemetry.ts +7 -5
- package/src/types.ts +4 -9
- package/src/version-check.ts +23 -10
- package/tools/telemetry-receiver/README.md +2 -1
- package/tools/telemetry-receiver/receiver.js +5 -0
package/src/proxy.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
createServer,
|
|
5
5
|
type IncomingMessage,
|
|
6
|
+
type Server,
|
|
6
7
|
type ServerResponse,
|
|
7
8
|
} from "node:http";
|
|
8
9
|
import { Readable } from "node:stream";
|
|
@@ -33,6 +34,8 @@ import {
|
|
|
33
34
|
serveAccountFreshWindowStartsApi,
|
|
34
35
|
serveClearInFlightApi,
|
|
35
36
|
serveClearBreakerApi,
|
|
37
|
+
serveKickstartApi,
|
|
38
|
+
serveAutoWarmupApi,
|
|
36
39
|
serveStaticCss,
|
|
37
40
|
serveStaticJs,
|
|
38
41
|
} from "./dashboard.js";
|
|
@@ -120,6 +123,8 @@ export type RotationOutcome<T> =
|
|
|
120
123
|
errorText: string;
|
|
121
124
|
retryAfterMs?: number;
|
|
122
125
|
endpoint?: string;
|
|
126
|
+
context?: RotationAttemptContext;
|
|
127
|
+
totalMs?: number;
|
|
123
128
|
};
|
|
124
129
|
|
|
125
130
|
/**
|
|
@@ -240,6 +245,266 @@ function capCooldown(ms: number): number {
|
|
|
240
245
|
return Math.min(ms, MAX_COOLDOWN_MS);
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
type UpstreamActionDecision =
|
|
249
|
+
| { kind: "retry" }
|
|
250
|
+
| {
|
|
251
|
+
kind: "fail";
|
|
252
|
+
status: number;
|
|
253
|
+
errorText: string;
|
|
254
|
+
retryAfterMs?: number;
|
|
255
|
+
endpoint?: string;
|
|
256
|
+
context: RotationAttemptContext;
|
|
257
|
+
totalMs: number;
|
|
258
|
+
actionKind: Exclude<UpstreamAction["kind"], "success">;
|
|
259
|
+
providerResourceExhausted?: boolean;
|
|
260
|
+
noReplacementReason?: string;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
type UpstreamFailureDecision = Extract<UpstreamActionDecision, { kind: "fail" }>;
|
|
264
|
+
type UpstreamFailureExtra = Partial<
|
|
265
|
+
Pick<
|
|
266
|
+
UpstreamFailureDecision,
|
|
267
|
+
| "retryAfterMs"
|
|
268
|
+
| "endpoint"
|
|
269
|
+
| "providerResourceExhausted"
|
|
270
|
+
| "noReplacementReason"
|
|
271
|
+
>
|
|
272
|
+
>;
|
|
273
|
+
|
|
274
|
+
type UpstreamActionHandlerOptions = {
|
|
275
|
+
action: Exclude<UpstreamAction, { kind: "success" }>;
|
|
276
|
+
rotator: AccountRotator;
|
|
277
|
+
account: AccountRuntime;
|
|
278
|
+
model: string;
|
|
279
|
+
modelKey: string;
|
|
280
|
+
label: string;
|
|
281
|
+
context: RotationAttemptContext;
|
|
282
|
+
logRequestEnd: (status: string | number, extra?: string) => void;
|
|
283
|
+
rotateAndRelease: () => Promise<AccountRuntime | null>;
|
|
284
|
+
writeLog: (message: string, level?: "info" | "warn" | "error") => void;
|
|
285
|
+
recordFailureAttempt?: (statusCode: number) => void;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
function buildFailureDecision(
|
|
289
|
+
options: UpstreamActionHandlerOptions,
|
|
290
|
+
status: number,
|
|
291
|
+
errorText: string,
|
|
292
|
+
extra: UpstreamFailureExtra = {},
|
|
293
|
+
): UpstreamFailureDecision {
|
|
294
|
+
return {
|
|
295
|
+
kind: "fail",
|
|
296
|
+
status,
|
|
297
|
+
errorText,
|
|
298
|
+
context: options.context,
|
|
299
|
+
totalMs: Date.now() - options.context.requestStartMs,
|
|
300
|
+
actionKind: options.action.kind,
|
|
301
|
+
endpoint: "endpoint" in options.action ? options.action.endpoint : undefined,
|
|
302
|
+
...extra,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildNoReplacementDecision(
|
|
307
|
+
options: UpstreamActionHandlerOptions,
|
|
308
|
+
reason: string,
|
|
309
|
+
): UpstreamFailureDecision {
|
|
310
|
+
const retryAfterMs = options.rotator.getRetryAfterMs(options.model);
|
|
311
|
+
if (retryAfterMs > 0) {
|
|
312
|
+
return buildFailureDecision(
|
|
313
|
+
options,
|
|
314
|
+
429,
|
|
315
|
+
`All accounts cooling down or model circuit breaker active: ${reason}`,
|
|
316
|
+
{ retryAfterMs, noReplacementReason: reason },
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return buildFailureDecision(
|
|
320
|
+
options,
|
|
321
|
+
503,
|
|
322
|
+
`All accounts exhausted or disabled: ${reason}`,
|
|
323
|
+
{ noReplacementReason: reason },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function handleUpstreamAccountAction(
|
|
328
|
+
options: UpstreamActionHandlerOptions,
|
|
329
|
+
): Promise<UpstreamActionDecision> {
|
|
330
|
+
const {
|
|
331
|
+
action,
|
|
332
|
+
rotator,
|
|
333
|
+
account,
|
|
334
|
+
model,
|
|
335
|
+
modelKey,
|
|
336
|
+
label,
|
|
337
|
+
logRequestEnd,
|
|
338
|
+
rotateAndRelease,
|
|
339
|
+
writeLog,
|
|
340
|
+
recordFailureAttempt,
|
|
341
|
+
} = options;
|
|
342
|
+
|
|
343
|
+
if (action.kind === "rate-limited") {
|
|
344
|
+
writeLog(
|
|
345
|
+
`[${label}] 429 rate limited${action.providerResourceExhausted ? " (RESOURCE_EXHAUSTED)" : ""}, cooldown ${Math.ceil(action.cooldownMs / 1000)}s. Error text: ${action.errorText.slice(0, 300)}`,
|
|
346
|
+
"warn",
|
|
347
|
+
);
|
|
348
|
+
recordFailureAttempt?.(429);
|
|
349
|
+
rotator.markExhausted(
|
|
350
|
+
account,
|
|
351
|
+
model,
|
|
352
|
+
action.cooldownMs,
|
|
353
|
+
action.errorText.slice(0, 300),
|
|
354
|
+
);
|
|
355
|
+
rotator.recordProvider429(account, model, action.cooldownMs);
|
|
356
|
+
logRequestEnd(
|
|
357
|
+
429,
|
|
358
|
+
`cooldownMs=${action.cooldownMs}${action.providerResourceExhausted ? " resourceExhausted=true" : ""} endpoint=${action.endpoint}`,
|
|
359
|
+
);
|
|
360
|
+
return buildFailureDecision(options, 429, action.errorText, {
|
|
361
|
+
retryAfterMs: action.cooldownMs,
|
|
362
|
+
providerResourceExhausted: action.providerResourceExhausted,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (action.kind === "flagged-401") {
|
|
367
|
+
writeLog(
|
|
368
|
+
`[${label}] BLOCKED (401): ${action.errorText.slice(0, 200)}`,
|
|
369
|
+
"error",
|
|
370
|
+
);
|
|
371
|
+
const lower401 = action.errorText.toLowerCase();
|
|
372
|
+
const matched401 = FLAG_PATTERNS.filter((p) => lower401.includes(p));
|
|
373
|
+
const ctx401 = rotator.getFlagContext(account, modelKey);
|
|
374
|
+
reportFlagEvent({
|
|
375
|
+
flagHttpStatus: 401,
|
|
376
|
+
flagPatternsMatched:
|
|
377
|
+
matched401.length > 0 ? matched401 : ["blocked_401" as FlagPattern],
|
|
378
|
+
model: modelKey,
|
|
379
|
+
timerType: ctx401.timerType as FlagEventData["timerType"],
|
|
380
|
+
accountQuotaPercent: ctx401.accountQuotaPercent,
|
|
381
|
+
wasProAccount: ctx401.wasProAccount,
|
|
382
|
+
accountTotalRequests: account.totalRequests,
|
|
383
|
+
accountRequestsLastHour: ctx401.accountRequestsLastHour,
|
|
384
|
+
accountConcurrentAtFlag: account.inFlightRequests,
|
|
385
|
+
poolSize: ctx401.poolSize,
|
|
386
|
+
poolHealthyCount: ctx401.poolHealthyCount,
|
|
387
|
+
protectivePauseTriggered: false,
|
|
388
|
+
uptimeSeconds: ctx401.uptimeSeconds,
|
|
389
|
+
timeSinceLastFlagSeconds: -1,
|
|
390
|
+
});
|
|
391
|
+
rotator.markFlagged(
|
|
392
|
+
account,
|
|
393
|
+
`Account blocked (401): ${action.errorText.slice(0, 300)}`,
|
|
394
|
+
);
|
|
395
|
+
logRequestEnd(401, `endpoint=${action.endpoint}`);
|
|
396
|
+
const nextAccount = await rotateAndRelease();
|
|
397
|
+
if (!nextAccount) {
|
|
398
|
+
return buildNoReplacementDecision(
|
|
399
|
+
options,
|
|
400
|
+
`no replacement account remained after ${label} was flagged with 401`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return { kind: "retry" };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (action.kind === "flagged-403") {
|
|
407
|
+
writeLog(`[${label}] FLAGGED: ${action.errorText.slice(0, 200)}`, "error");
|
|
408
|
+
recordFailureAttempt?.(403);
|
|
409
|
+
logRequestEnd(403, `endpoint=${action.endpoint}`);
|
|
410
|
+
const lower = action.errorText.toLowerCase();
|
|
411
|
+
const matchedPatterns = FLAG_PATTERNS.filter((p) => lower.includes(p));
|
|
412
|
+
const ctx403 = rotator.getFlagContext(account, modelKey);
|
|
413
|
+
reportFlagEvent({
|
|
414
|
+
flagHttpStatus: 403,
|
|
415
|
+
flagPatternsMatched: matchedPatterns,
|
|
416
|
+
model: modelKey,
|
|
417
|
+
timerType: ctx403.timerType as FlagEventData["timerType"],
|
|
418
|
+
accountQuotaPercent: ctx403.accountQuotaPercent,
|
|
419
|
+
wasProAccount: ctx403.wasProAccount,
|
|
420
|
+
accountTotalRequests: account.totalRequests,
|
|
421
|
+
accountRequestsLastHour: ctx403.accountRequestsLastHour,
|
|
422
|
+
accountConcurrentAtFlag: account.inFlightRequests,
|
|
423
|
+
poolSize: ctx403.poolSize,
|
|
424
|
+
poolHealthyCount: ctx403.poolHealthyCount,
|
|
425
|
+
protectivePauseTriggered: false,
|
|
426
|
+
uptimeSeconds: ctx403.uptimeSeconds,
|
|
427
|
+
timeSinceLastFlagSeconds: -1,
|
|
428
|
+
});
|
|
429
|
+
rotator.markFlagged(account, action.errorText.slice(0, 300));
|
|
430
|
+
const nextAccount = await rotateAndRelease();
|
|
431
|
+
if (!nextAccount) {
|
|
432
|
+
return buildNoReplacementDecision(
|
|
433
|
+
options,
|
|
434
|
+
`no replacement account remained after ${label} was flagged with 403`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return { kind: "retry" };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (action.kind === "forbidden") {
|
|
441
|
+
writeLog(`[${label}] 403: ${action.errorText.slice(0, 200)}`, "warn");
|
|
442
|
+
logRequestEnd(403, `endpoint=${action.endpoint}`);
|
|
443
|
+
return buildFailureDecision(options, 403, action.errorText);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (action.kind === "not-found") {
|
|
447
|
+
writeLog(
|
|
448
|
+
`[${label}] 404 from ${action.endpoint}: ${action.errorText.slice(0, 200)}`,
|
|
449
|
+
"warn",
|
|
450
|
+
);
|
|
451
|
+
logRequestEnd(404, `endpoint=${action.endpoint}`);
|
|
452
|
+
return buildFailureDecision(options, 404, action.errorText);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (action.kind === "bad-request") {
|
|
456
|
+
writeLog(
|
|
457
|
+
`[${label}] 400 Bad Request from ${action.endpoint}: ${action.errorText.slice(0, 500)}`,
|
|
458
|
+
"warn",
|
|
459
|
+
);
|
|
460
|
+
logRequestEnd(400, `endpoint=${action.endpoint}`);
|
|
461
|
+
return buildFailureDecision(options, 400, action.errorText);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (action.kind === "server-error-503") {
|
|
465
|
+
writeLog(
|
|
466
|
+
`[${label}] Server error 503: ${action.errorText.slice(0, 200)}`,
|
|
467
|
+
"warn",
|
|
468
|
+
);
|
|
469
|
+
recordFailureAttempt?.(503);
|
|
470
|
+
logRequestEnd(503, `endpoint=${action.endpoint}`);
|
|
471
|
+
return buildFailureDecision(options, 503, action.errorText);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
writeLog(
|
|
475
|
+
`[${label}] Server error ${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
476
|
+
"warn",
|
|
477
|
+
);
|
|
478
|
+
recordFailureAttempt?.(action.httpStatus);
|
|
479
|
+
logRequestEnd(action.httpStatus, `endpoint=${action.endpoint}`);
|
|
480
|
+
rotator.markError(
|
|
481
|
+
account,
|
|
482
|
+
`${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
483
|
+
);
|
|
484
|
+
const nextAccount = await rotateAndRelease();
|
|
485
|
+
if (!nextAccount) {
|
|
486
|
+
return buildNoReplacementDecision(
|
|
487
|
+
options,
|
|
488
|
+
`no replacement account remained after ${label} failed with ${action.httpStatus}`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return { kind: "retry" };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function failureDecisionToOutcome<T>(
|
|
495
|
+
decision: UpstreamFailureDecision,
|
|
496
|
+
): RotationOutcome<T> {
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
status: decision.status,
|
|
500
|
+
errorText: decision.errorText,
|
|
501
|
+
retryAfterMs: decision.retryAfterMs,
|
|
502
|
+
endpoint: decision.endpoint,
|
|
503
|
+
context: decision.context,
|
|
504
|
+
totalMs: decision.totalMs,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
243
508
|
function formatError(err: unknown): string {
|
|
244
509
|
if (!(err instanceof Error)) return String(err);
|
|
245
510
|
const cause = err.cause;
|
|
@@ -654,21 +919,29 @@ export async function withRotation<T>(
|
|
|
654
919
|
context: RotationAttemptContext,
|
|
655
920
|
) => Promise<T>,
|
|
656
921
|
): Promise<RotationOutcome<T>> {
|
|
657
|
-
const sendNoAccountsAvailable = (
|
|
922
|
+
const sendNoAccountsAvailable = (
|
|
923
|
+
reason: string,
|
|
924
|
+
context?: RotationAttemptContext,
|
|
925
|
+
): RotationOutcome<T> => {
|
|
658
926
|
log(`[${model}] No healthy account available: ${reason}`, rotator, "warn");
|
|
659
927
|
const retryAfterMs = rotator.getRetryAfterMs(model);
|
|
928
|
+
const contextFields = context
|
|
929
|
+
? { context, totalMs: Date.now() - context.requestStartMs }
|
|
930
|
+
: {};
|
|
660
931
|
if (retryAfterMs > 0) {
|
|
661
932
|
return {
|
|
662
933
|
ok: false,
|
|
663
934
|
status: 429,
|
|
664
935
|
errorText: `All accounts cooling down or model circuit breaker active: ${reason}`,
|
|
665
936
|
retryAfterMs,
|
|
937
|
+
...contextFields,
|
|
666
938
|
};
|
|
667
939
|
}
|
|
668
940
|
return {
|
|
669
941
|
ok: false,
|
|
670
942
|
status: 503,
|
|
671
943
|
errorText: `All accounts exhausted or disabled: ${reason}`,
|
|
944
|
+
...contextFields,
|
|
672
945
|
};
|
|
673
946
|
};
|
|
674
947
|
|
|
@@ -754,186 +1027,21 @@ export async function withRotation<T>(
|
|
|
754
1027
|
modelKey,
|
|
755
1028
|
);
|
|
756
1029
|
|
|
757
|
-
if (action.kind
|
|
758
|
-
|
|
759
|
-
|
|
1030
|
+
if (action.kind !== "success") {
|
|
1031
|
+
const decision = await handleUpstreamAccountAction({
|
|
1032
|
+
action,
|
|
760
1033
|
rotator,
|
|
761
|
-
"warn",
|
|
762
|
-
);
|
|
763
|
-
rotator.markExhausted(
|
|
764
1034
|
account,
|
|
765
1035
|
model,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
`cooldownMs=${action.cooldownMs}${action.providerResourceExhausted ? " resourceExhausted=true" : ""}`,
|
|
773
|
-
);
|
|
774
|
-
return {
|
|
775
|
-
ok: false,
|
|
776
|
-
status: 429,
|
|
777
|
-
errorText: action.errorText,
|
|
778
|
-
retryAfterMs: action.cooldownMs,
|
|
779
|
-
endpoint,
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (action.kind === "flagged-401") {
|
|
784
|
-
log(
|
|
785
|
-
`[${label}] BLOCKED (401): ${action.errorText.slice(0, 200)}`,
|
|
786
|
-
rotator,
|
|
787
|
-
"error",
|
|
788
|
-
);
|
|
789
|
-
const lower401 = action.errorText.toLowerCase();
|
|
790
|
-
const matched401 = FLAG_PATTERNS.filter((p) => lower401.includes(p));
|
|
791
|
-
const ctx401 = rotator.getFlagContext(account, modelKey);
|
|
792
|
-
reportFlagEvent({
|
|
793
|
-
flagHttpStatus: 401,
|
|
794
|
-
flagPatternsMatched:
|
|
795
|
-
matched401.length > 0 ? matched401 : ["blocked_401" as FlagPattern],
|
|
796
|
-
model: modelKey,
|
|
797
|
-
timerType: ctx401.timerType as FlagEventData["timerType"],
|
|
798
|
-
accountQuotaPercent: ctx401.accountQuotaPercent,
|
|
799
|
-
wasProAccount: ctx401.wasProAccount,
|
|
800
|
-
accountTotalRequests: account.totalRequests,
|
|
801
|
-
accountRequestsLastHour: ctx401.accountRequestsLastHour,
|
|
802
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
803
|
-
poolSize: ctx401.poolSize,
|
|
804
|
-
poolHealthyCount: ctx401.poolHealthyCount,
|
|
805
|
-
protectivePauseTriggered: false,
|
|
806
|
-
uptimeSeconds: ctx401.uptimeSeconds,
|
|
807
|
-
timeSinceLastFlagSeconds: -1,
|
|
808
|
-
});
|
|
809
|
-
rotator.markFlagged(
|
|
810
|
-
account,
|
|
811
|
-
`Account blocked (401): ${action.errorText.slice(0, 300)}`,
|
|
812
|
-
);
|
|
813
|
-
logRequestEnd(401);
|
|
814
|
-
const nextAccount = await rotateAndRelease();
|
|
815
|
-
if (!nextAccount) {
|
|
816
|
-
return sendNoAccountsAvailable(
|
|
817
|
-
`no replacement account remained after ${label} was flagged with 401`,
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
continue;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (action.kind === "flagged-403") {
|
|
824
|
-
log(
|
|
825
|
-
`[${label}] FLAGGED: ${action.errorText.slice(0, 200)}`,
|
|
826
|
-
rotator,
|
|
827
|
-
"error",
|
|
828
|
-
);
|
|
829
|
-
const lower = action.errorText.toLowerCase();
|
|
830
|
-
const matchedPatterns = FLAG_PATTERNS.filter((p) => lower.includes(p));
|
|
831
|
-
const ctx403 = rotator.getFlagContext(account, modelKey);
|
|
832
|
-
reportFlagEvent({
|
|
833
|
-
flagHttpStatus: 403,
|
|
834
|
-
flagPatternsMatched: matchedPatterns,
|
|
835
|
-
model: modelKey,
|
|
836
|
-
timerType: ctx403.timerType as FlagEventData["timerType"],
|
|
837
|
-
accountQuotaPercent: ctx403.accountQuotaPercent,
|
|
838
|
-
wasProAccount: ctx403.wasProAccount,
|
|
839
|
-
accountTotalRequests: account.totalRequests,
|
|
840
|
-
accountRequestsLastHour: ctx403.accountRequestsLastHour,
|
|
841
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
842
|
-
poolSize: ctx403.poolSize,
|
|
843
|
-
poolHealthyCount: ctx403.poolHealthyCount,
|
|
844
|
-
protectivePauseTriggered: false,
|
|
845
|
-
uptimeSeconds: ctx403.uptimeSeconds,
|
|
846
|
-
timeSinceLastFlagSeconds: -1,
|
|
1036
|
+
modelKey,
|
|
1037
|
+
label,
|
|
1038
|
+
context,
|
|
1039
|
+
logRequestEnd,
|
|
1040
|
+
rotateAndRelease,
|
|
1041
|
+
writeLog: (message, level = "info") => log(message, rotator, level),
|
|
847
1042
|
});
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const nextAccount = await rotateAndRelease();
|
|
851
|
-
if (!nextAccount) {
|
|
852
|
-
return sendNoAccountsAvailable(
|
|
853
|
-
`no replacement account remained after ${label} was flagged with 403`,
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (action.kind === "forbidden") {
|
|
860
|
-
log(
|
|
861
|
-
`[${label}] 403: ${action.errorText.slice(0, 200)}`,
|
|
862
|
-
rotator,
|
|
863
|
-
"warn",
|
|
864
|
-
);
|
|
865
|
-
logRequestEnd(403);
|
|
866
|
-
return {
|
|
867
|
-
ok: false,
|
|
868
|
-
status: 403,
|
|
869
|
-
errorText: action.errorText,
|
|
870
|
-
endpoint,
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
if (action.kind === "not-found") {
|
|
875
|
-
log(
|
|
876
|
-
`[${label}] 404 from ${action.endpoint}: ${action.errorText.slice(0, 200)}`,
|
|
877
|
-
rotator,
|
|
878
|
-
"warn",
|
|
879
|
-
);
|
|
880
|
-
logRequestEnd(404, `endpoint=${action.endpoint}`);
|
|
881
|
-
return {
|
|
882
|
-
ok: false,
|
|
883
|
-
status: 404,
|
|
884
|
-
errorText: action.errorText,
|
|
885
|
-
endpoint,
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (action.kind === "bad-request") {
|
|
890
|
-
log(
|
|
891
|
-
`[${label}] 400 Bad Request from ${action.endpoint}: ${action.errorText.slice(0, 500)}`,
|
|
892
|
-
rotator,
|
|
893
|
-
"warn",
|
|
894
|
-
);
|
|
895
|
-
logRequestEnd(400, `endpoint=${action.endpoint}`);
|
|
896
|
-
return {
|
|
897
|
-
ok: false,
|
|
898
|
-
status: 400,
|
|
899
|
-
errorText: action.errorText,
|
|
900
|
-
endpoint,
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
if (action.kind === "server-error-503") {
|
|
905
|
-
log(
|
|
906
|
-
`[${label}] Server error 503: ${action.errorText.slice(0, 200)}`,
|
|
907
|
-
rotator,
|
|
908
|
-
"warn",
|
|
909
|
-
);
|
|
910
|
-
logRequestEnd(503, `endpoint=${action.endpoint}`);
|
|
911
|
-
return {
|
|
912
|
-
ok: false,
|
|
913
|
-
status: 503,
|
|
914
|
-
errorText: action.errorText,
|
|
915
|
-
endpoint,
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (action.kind === "rotate-on-5xx") {
|
|
920
|
-
log(
|
|
921
|
-
`[${label}] Server error ${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
922
|
-
rotator,
|
|
923
|
-
"warn",
|
|
924
|
-
);
|
|
925
|
-
logRequestEnd(action.httpStatus, `endpoint=${action.endpoint}`);
|
|
926
|
-
rotator.markError(
|
|
927
|
-
account,
|
|
928
|
-
`${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
929
|
-
);
|
|
930
|
-
const nextAccount = await rotateAndRelease();
|
|
931
|
-
if (!nextAccount) {
|
|
932
|
-
return sendNoAccountsAvailable(
|
|
933
|
-
`no replacement account remained after ${label} failed with ${action.httpStatus}`,
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
continue;
|
|
1043
|
+
if (decision.kind === "retry") continue;
|
|
1044
|
+
return failureDecisionToOutcome(decision);
|
|
937
1045
|
}
|
|
938
1046
|
|
|
939
1047
|
// success
|
|
@@ -1079,6 +1187,69 @@ async function handleProxyRequest(
|
|
|
1079
1187
|
}
|
|
1080
1188
|
return nextAccount;
|
|
1081
1189
|
};
|
|
1190
|
+
const sendFailureDecision = (decision: UpstreamFailureDecision): void => {
|
|
1191
|
+
if (decision.noReplacementReason) {
|
|
1192
|
+
sendNoAccountsAvailable(decision.noReplacementReason);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
const label = decision.context.label;
|
|
1197
|
+
if (decision.actionKind === "rate-limited") {
|
|
1198
|
+
const retryAfterMs = decision.retryAfterMs ?? 0;
|
|
1199
|
+
res.writeHead(429, {
|
|
1200
|
+
"Content-Type": "application/json",
|
|
1201
|
+
"Retry-After": String(Math.ceil(retryAfterMs / 1000)),
|
|
1202
|
+
});
|
|
1203
|
+
res.end(
|
|
1204
|
+
JSON.stringify({
|
|
1205
|
+
error: decision.providerResourceExhausted
|
|
1206
|
+
? "Resource exhausted"
|
|
1207
|
+
: "Rate limited",
|
|
1208
|
+
reason: decision.providerResourceExhausted
|
|
1209
|
+
? `${label} hit provider RESOURCE_EXHAUSTED; not retrying another account to avoid pool-wide hammering`
|
|
1210
|
+
: `${label} was rate limited; not retrying another account for account-safety`,
|
|
1211
|
+
model: body.model,
|
|
1212
|
+
account: label,
|
|
1213
|
+
retryAfterMs,
|
|
1214
|
+
}),
|
|
1215
|
+
);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
if (decision.actionKind === "forbidden") {
|
|
1220
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1221
|
+
res.end(decision.errorText || JSON.stringify({ error: "Forbidden" }));
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (decision.actionKind === "not-found") {
|
|
1226
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1227
|
+
res.end(decision.errorText || JSON.stringify({ error: "Not found" }));
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
if (decision.actionKind === "bad-request") {
|
|
1232
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1233
|
+
res.end(decision.errorText || JSON.stringify({ error: "Bad request" }));
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (decision.actionKind === "server-error-503") {
|
|
1238
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1239
|
+
res.end(
|
|
1240
|
+
decision.errorText ||
|
|
1241
|
+
JSON.stringify({
|
|
1242
|
+
error: "Server unavailable",
|
|
1243
|
+
account: label,
|
|
1244
|
+
model: body.model,
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
res.writeHead(decision.status, { "Content-Type": "application/json" });
|
|
1251
|
+
res.end(decision.errorText || JSON.stringify({ error: "Upstream error" }));
|
|
1252
|
+
};
|
|
1082
1253
|
|
|
1083
1254
|
for (let attempt = 0; attempt < MAX_ENDPOINT_RETRIES; attempt++) {
|
|
1084
1255
|
const account = await rotator.getActiveAccount(body.model);
|
|
@@ -1154,197 +1325,34 @@ async function handleProxyRequest(
|
|
|
1154
1325
|
modelKey,
|
|
1155
1326
|
);
|
|
1156
1327
|
|
|
1157
|
-
if (action.kind
|
|
1158
|
-
|
|
1159
|
-
`[${label}] 429 rate limited${action.providerResourceExhausted ? " (RESOURCE_EXHAUSTED)" : ""}, cooldown ${Math.ceil(action.cooldownMs / 1000)}s. Error text: ${action.errorText.slice(0, 300)}`,
|
|
1160
|
-
"warn",
|
|
1161
|
-
);
|
|
1162
|
-
recordOutcome(429);
|
|
1163
|
-
logRequestEnd(
|
|
1164
|
-
429,
|
|
1165
|
-
`cooldownMs=${action.cooldownMs}${action.providerResourceExhausted ? " resourceExhausted=true" : ""} endpoint=${endpoint}`,
|
|
1166
|
-
);
|
|
1167
|
-
rotator.markExhausted(account, body.model, action.cooldownMs);
|
|
1168
|
-
rotator.recordProvider429(account, body.model, action.cooldownMs);
|
|
1169
|
-
|
|
1170
|
-
// Safety first: do NOT immediately retry another account on 429.
|
|
1171
|
-
// Provider-side 429s can represent daily/request buckets or shared project pressure;
|
|
1172
|
-
// cascading retries burn the full pool and increase ban/flag risk.
|
|
1173
|
-
res.writeHead(429, {
|
|
1174
|
-
"Content-Type": "application/json",
|
|
1175
|
-
"Retry-After": String(Math.ceil(action.cooldownMs / 1000)),
|
|
1176
|
-
});
|
|
1177
|
-
res.end(
|
|
1178
|
-
JSON.stringify({
|
|
1179
|
-
error: action.providerResourceExhausted
|
|
1180
|
-
? "Resource exhausted"
|
|
1181
|
-
: "Rate limited",
|
|
1182
|
-
reason: action.providerResourceExhausted
|
|
1183
|
-
? `${label} hit provider RESOURCE_EXHAUSTED; not retrying another account to avoid pool-wide hammering`
|
|
1184
|
-
: `${label} was rate limited; not retrying another account for account-safety`,
|
|
1185
|
-
model: body.model,
|
|
1186
|
-
account: label,
|
|
1187
|
-
retryAfterMs: action.cooldownMs,
|
|
1188
|
-
}),
|
|
1189
|
-
);
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
if (action.kind === "flagged-401") {
|
|
1194
|
-
proxyLog(
|
|
1195
|
-
`[${label}] BLOCKED (401): ${action.errorText.slice(0, 200)}`,
|
|
1196
|
-
"error",
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
// Telemetry: report flag event BEFORE markFlagged (which may trigger protective pause)
|
|
1200
|
-
const lower401 = action.errorText.toLowerCase();
|
|
1201
|
-
const matched401 = FLAG_PATTERNS.filter((p) => lower401.includes(p));
|
|
1202
|
-
const ctx401 = rotator.getFlagContext(account, modelKey);
|
|
1203
|
-
reportFlagEvent({
|
|
1204
|
-
flagHttpStatus: 401,
|
|
1205
|
-
flagPatternsMatched:
|
|
1206
|
-
matched401.length > 0 ? matched401 : ["blocked_401" as FlagPattern],
|
|
1207
|
-
model: modelKey,
|
|
1208
|
-
timerType: ctx401.timerType as FlagEventData["timerType"],
|
|
1209
|
-
accountQuotaPercent: ctx401.accountQuotaPercent,
|
|
1210
|
-
wasProAccount: ctx401.wasProAccount,
|
|
1211
|
-
accountTotalRequests: account.totalRequests,
|
|
1212
|
-
accountRequestsLastHour: ctx401.accountRequestsLastHour,
|
|
1213
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
1214
|
-
poolSize: ctx401.poolSize,
|
|
1215
|
-
poolHealthyCount: ctx401.poolHealthyCount,
|
|
1216
|
-
protectivePauseTriggered: false, // not yet — markFlagged decides
|
|
1217
|
-
uptimeSeconds: ctx401.uptimeSeconds,
|
|
1218
|
-
timeSinceLastFlagSeconds: -1, // filled by reporter
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
rotator.markFlagged(
|
|
1328
|
+
if (action.kind !== "success") {
|
|
1329
|
+
const context: RotationAttemptContext = {
|
|
1222
1330
|
account,
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
logRequestEnd(403, `endpoint=${endpoint}`);
|
|
1243
|
-
|
|
1244
|
-
const matchedPatterns = FLAG_PATTERNS.filter((p) =>
|
|
1245
|
-
action.errorText.toLowerCase().includes(p),
|
|
1246
|
-
);
|
|
1247
|
-
const ctx403 = rotator.getFlagContext(account, modelKey);
|
|
1248
|
-
reportFlagEvent({
|
|
1249
|
-
flagHttpStatus: 403,
|
|
1250
|
-
flagPatternsMatched: matchedPatterns,
|
|
1251
|
-
model: modelKey,
|
|
1252
|
-
timerType: ctx403.timerType as FlagEventData["timerType"],
|
|
1253
|
-
accountQuotaPercent: ctx403.accountQuotaPercent,
|
|
1254
|
-
wasProAccount: ctx403.wasProAccount,
|
|
1255
|
-
accountTotalRequests: account.totalRequests,
|
|
1256
|
-
accountRequestsLastHour: ctx403.accountRequestsLastHour,
|
|
1257
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
1258
|
-
poolSize: ctx403.poolSize,
|
|
1259
|
-
poolHealthyCount: ctx403.poolHealthyCount,
|
|
1260
|
-
protectivePauseTriggered: false, // not yet
|
|
1261
|
-
uptimeSeconds: ctx403.uptimeSeconds,
|
|
1262
|
-
timeSinceLastFlagSeconds: -1, // filled by reporter
|
|
1331
|
+
label,
|
|
1332
|
+
modelKey,
|
|
1333
|
+
displayModelKey,
|
|
1334
|
+
requestId,
|
|
1335
|
+
requestStartMs,
|
|
1336
|
+
endpoint,
|
|
1337
|
+
};
|
|
1338
|
+
const decision = await handleUpstreamAccountAction({
|
|
1339
|
+
action,
|
|
1340
|
+
rotator,
|
|
1341
|
+
account,
|
|
1342
|
+
model: body.model,
|
|
1343
|
+
modelKey,
|
|
1344
|
+
label,
|
|
1345
|
+
context,
|
|
1346
|
+
logRequestEnd,
|
|
1347
|
+
rotateAndRelease,
|
|
1348
|
+
writeLog: proxyLog,
|
|
1349
|
+
recordFailureAttempt: recordOutcome,
|
|
1263
1350
|
});
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const nextAccount = await rotateAndRelease();
|
|
1267
|
-
if (!nextAccount) {
|
|
1268
|
-
sendNoAccountsAvailable(
|
|
1269
|
-
`no replacement account remained after ${label} was flagged with 403`,
|
|
1270
|
-
);
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
if (action.kind === "forbidden") {
|
|
1277
|
-
proxyLog(`[${label}] 403: ${action.errorText.slice(0, 200)}`, "warn");
|
|
1278
|
-
logRequestEnd(403, `endpoint=${action.endpoint}`);
|
|
1279
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1280
|
-
res.end(action.errorText || JSON.stringify({ error: "Forbidden" }));
|
|
1351
|
+
if (decision.kind === "retry") continue;
|
|
1352
|
+
sendFailureDecision(decision);
|
|
1281
1353
|
return;
|
|
1282
1354
|
}
|
|
1283
1355
|
|
|
1284
|
-
if (action.kind === "not-found") {
|
|
1285
|
-
proxyLog(
|
|
1286
|
-
`[${label}] 404 from ${action.endpoint}: ${action.errorText.slice(0, 200)}`,
|
|
1287
|
-
"warn",
|
|
1288
|
-
);
|
|
1289
|
-
logRequestEnd(404, `endpoint=${action.endpoint}`);
|
|
1290
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1291
|
-
res.end(action.errorText || JSON.stringify({ error: "Not found" }));
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
if (action.kind === "bad-request") {
|
|
1296
|
-
proxyLog(
|
|
1297
|
-
`[${label}] 400 Bad Request from ${action.endpoint}: ${action.errorText.slice(0, 500)}`,
|
|
1298
|
-
"warn",
|
|
1299
|
-
);
|
|
1300
|
-
logRequestEnd(400, `endpoint=${action.endpoint}`);
|
|
1301
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1302
|
-
res.end(action.errorText || JSON.stringify({ error: "Bad request" }));
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (action.kind === "server-error-503") {
|
|
1307
|
-
proxyLog(
|
|
1308
|
-
`[${label}] Server error 503: ${action.errorText.slice(0, 200)}`,
|
|
1309
|
-
"warn",
|
|
1310
|
-
);
|
|
1311
|
-
recordOutcome(503);
|
|
1312
|
-
logRequestEnd(503, `endpoint=${action.endpoint}`);
|
|
1313
|
-
// Return 503 as-is. Capacity errors still consume quota upstream,
|
|
1314
|
-
// so retrying on another account would just burn more quota for nothing.
|
|
1315
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1316
|
-
res.end(
|
|
1317
|
-
action.errorText ||
|
|
1318
|
-
JSON.stringify({
|
|
1319
|
-
error: "Server unavailable",
|
|
1320
|
-
account: label,
|
|
1321
|
-
model: body.model,
|
|
1322
|
-
}),
|
|
1323
|
-
);
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
if (action.kind === "rotate-on-5xx") {
|
|
1328
|
-
proxyLog(
|
|
1329
|
-
`[${label}] Server error ${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
1330
|
-
"warn",
|
|
1331
|
-
);
|
|
1332
|
-
recordOutcome(action.httpStatus);
|
|
1333
|
-
logRequestEnd(action.httpStatus, `endpoint=${action.endpoint}`);
|
|
1334
|
-
rotator.markError(
|
|
1335
|
-
account,
|
|
1336
|
-
`${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
1337
|
-
);
|
|
1338
|
-
const nextAccount = await rotateAndRelease();
|
|
1339
|
-
if (!nextAccount) {
|
|
1340
|
-
sendNoAccountsAvailable(
|
|
1341
|
-
`no replacement account remained after ${label} failed with ${action.httpStatus}`,
|
|
1342
|
-
);
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
1356
|
// Success or non-error client response
|
|
1349
1357
|
const shouldRotate = rotator.recordRequest(account, body.model);
|
|
1350
1358
|
|
|
@@ -1454,7 +1462,7 @@ export function startProxy(
|
|
|
1454
1462
|
rotator: AccountRotator,
|
|
1455
1463
|
port: number,
|
|
1456
1464
|
bindHost = "0.0.0.0",
|
|
1457
|
-
):
|
|
1465
|
+
): Server {
|
|
1458
1466
|
startVersionChecker();
|
|
1459
1467
|
startNotificationPoller();
|
|
1460
1468
|
const sseClients = new Set<ServerResponse>();
|
|
@@ -1539,7 +1547,6 @@ export function startProxy(
|
|
|
1539
1547
|
}
|
|
1540
1548
|
|
|
1541
1549
|
if (method === "GET" && pathname === "/auth/antigravity/callback") {
|
|
1542
|
-
if (!requireAdmin(req, res)) return;
|
|
1543
1550
|
handleHostedCallback(req, res, rotator).catch((err) => {
|
|
1544
1551
|
log(`Hosted callback error: ${err}`, rotator, "error");
|
|
1545
1552
|
if (!res.headersSent) {
|
|
@@ -1622,46 +1629,46 @@ export function startProxy(
|
|
|
1622
1629
|
return;
|
|
1623
1630
|
}
|
|
1624
1631
|
|
|
1625
|
-
if (method === "POST" &&
|
|
1632
|
+
if (method === "POST" && pathname.startsWith("/api/enable/")) {
|
|
1626
1633
|
if (!requireAdmin(req, res)) return;
|
|
1627
|
-
const email = decodeURIComponent(
|
|
1634
|
+
const email = decodeURIComponent(pathname.slice("/api/enable/".length));
|
|
1628
1635
|
serveEnableApi(res, rotator, email);
|
|
1629
1636
|
return;
|
|
1630
1637
|
}
|
|
1631
1638
|
|
|
1632
|
-
if (method === "POST" &&
|
|
1639
|
+
if (method === "POST" && pathname.startsWith("/api/disable/")) {
|
|
1633
1640
|
if (!requireAdmin(req, res)) return;
|
|
1634
|
-
const email = decodeURIComponent(
|
|
1641
|
+
const email = decodeURIComponent(pathname.slice("/api/disable/".length));
|
|
1635
1642
|
serveDisableApi(res, rotator, email);
|
|
1636
1643
|
return;
|
|
1637
1644
|
}
|
|
1638
1645
|
|
|
1639
|
-
if (method === "POST" &&
|
|
1646
|
+
if (method === "POST" && pathname.startsWith("/api/quarantine/")) {
|
|
1640
1647
|
if (!requireAdmin(req, res)) return;
|
|
1641
|
-
const email = decodeURIComponent(
|
|
1648
|
+
const email = decodeURIComponent(pathname.slice("/api/quarantine/".length));
|
|
1642
1649
|
serveQuarantineApi(res, rotator, email);
|
|
1643
1650
|
return;
|
|
1644
1651
|
}
|
|
1645
1652
|
|
|
1646
|
-
if (method === "POST" &&
|
|
1653
|
+
if (method === "POST" && pathname.startsWith("/api/restore/")) {
|
|
1647
1654
|
if (!requireAdmin(req, res)) return;
|
|
1648
|
-
const email = decodeURIComponent(
|
|
1655
|
+
const email = decodeURIComponent(pathname.slice("/api/restore/".length));
|
|
1649
1656
|
serveRestoreApi(res, rotator, email);
|
|
1650
1657
|
return;
|
|
1651
1658
|
}
|
|
1652
1659
|
|
|
1653
|
-
if (method === "POST" &&
|
|
1660
|
+
if (method === "POST" && pathname.startsWith("/api/remove-account/")) {
|
|
1654
1661
|
if (!requireAdmin(req, res)) return;
|
|
1655
1662
|
const email = decodeURIComponent(
|
|
1656
|
-
|
|
1663
|
+
pathname.slice("/api/remove-account/".length),
|
|
1657
1664
|
);
|
|
1658
1665
|
serveRemoveAccountApi(res, rotator, email);
|
|
1659
1666
|
return;
|
|
1660
1667
|
}
|
|
1661
1668
|
|
|
1662
|
-
if (method === "POST" &&
|
|
1669
|
+
if (method === "POST" && pathname.startsWith("/api/set-tier/")) {
|
|
1663
1670
|
if (!requireAdmin(req, res)) return;
|
|
1664
|
-
const rest =
|
|
1671
|
+
const rest = pathname.slice("/api/set-tier/".length);
|
|
1665
1672
|
const lastSlash = rest.lastIndexOf("/");
|
|
1666
1673
|
if (lastSlash < 0) {
|
|
1667
1674
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -1679,9 +1686,9 @@ export function startProxy(
|
|
|
1679
1686
|
return;
|
|
1680
1687
|
}
|
|
1681
1688
|
|
|
1682
|
-
if (method === "POST" &&
|
|
1689
|
+
if (method === "POST" && pathname.startsWith("/api/clear-inflight/")) {
|
|
1683
1690
|
if (!requireAdmin(req, res)) return;
|
|
1684
|
-
const rest =
|
|
1691
|
+
const rest = pathname.slice("/api/clear-inflight/".length);
|
|
1685
1692
|
const firstSlash = rest.indexOf("/");
|
|
1686
1693
|
const email = decodeURIComponent(
|
|
1687
1694
|
firstSlash >= 0 ? rest.slice(0, firstSlash) : rest,
|
|
@@ -1694,9 +1701,9 @@ export function startProxy(
|
|
|
1694
1701
|
return;
|
|
1695
1702
|
}
|
|
1696
1703
|
|
|
1697
|
-
if (method === "POST" &&
|
|
1704
|
+
if (method === "POST" && pathname.startsWith("/api/clear-breaker/")) {
|
|
1698
1705
|
if (!requireAdmin(req, res)) return;
|
|
1699
|
-
const rest =
|
|
1706
|
+
const rest = pathname.slice("/api/clear-breaker/".length);
|
|
1700
1707
|
const modelKey =
|
|
1701
1708
|
rest && rest !== "all" ? decodeURIComponent(rest) : undefined;
|
|
1702
1709
|
serveClearBreakerApi(res, rotator, modelKey);
|
|
@@ -1705,22 +1712,22 @@ export function startProxy(
|
|
|
1705
1712
|
|
|
1706
1713
|
if (
|
|
1707
1714
|
method === "POST" &&
|
|
1708
|
-
(
|
|
1709
|
-
|
|
1715
|
+
(pathname === "/api/settings/fresh-window-starts/on" ||
|
|
1716
|
+
pathname === "/api/settings/fresh-window-starts/off")
|
|
1710
1717
|
) {
|
|
1711
1718
|
if (!requireAdmin(req, res)) return;
|
|
1712
1719
|
trackFeature("freshWindowToggle");
|
|
1713
|
-
serveFreshWindowStartsApi(res, rotator,
|
|
1720
|
+
serveFreshWindowStartsApi(res, rotator, pathname.endsWith("/on"));
|
|
1714
1721
|
return;
|
|
1715
1722
|
}
|
|
1716
1723
|
|
|
1717
1724
|
if (
|
|
1718
1725
|
method === "POST" &&
|
|
1719
|
-
|
|
1720
|
-
(
|
|
1726
|
+
pathname.startsWith("/api/account-fresh-window-starts/") &&
|
|
1727
|
+
(pathname.endsWith("/on") || pathname.endsWith("/off"))
|
|
1721
1728
|
) {
|
|
1722
1729
|
if (!requireAdmin(req, res)) return;
|
|
1723
|
-
const rest =
|
|
1730
|
+
const rest = pathname.slice("/api/account-fresh-window-starts/".length);
|
|
1724
1731
|
const lastSlash = rest.lastIndexOf("/");
|
|
1725
1732
|
const email = decodeURIComponent(rest.slice(0, lastSlash));
|
|
1726
1733
|
const enabled = rest.slice(lastSlash + 1) === "on";
|
|
@@ -1728,6 +1735,33 @@ export function startProxy(
|
|
|
1728
1735
|
return;
|
|
1729
1736
|
}
|
|
1730
1737
|
|
|
1738
|
+
if (method === "POST" && pathname.startsWith("/api/kickstart/")) {
|
|
1739
|
+
if (!requireAdmin(req, res)) return;
|
|
1740
|
+
const rest = pathname.slice("/api/kickstart/".length);
|
|
1741
|
+
const firstSlash = rest.indexOf("/");
|
|
1742
|
+
if (firstSlash >= 0) {
|
|
1743
|
+
// /api/kickstart/:email/:modelKey
|
|
1744
|
+
const email = decodeURIComponent(rest.slice(0, firstSlash));
|
|
1745
|
+
const modelKey = decodeURIComponent(rest.slice(firstSlash + 1));
|
|
1746
|
+
serveKickstartApi(res, rotator, email, modelKey);
|
|
1747
|
+
} else {
|
|
1748
|
+
// /api/kickstart/:email — kickstart all fresh timers
|
|
1749
|
+
const email = decodeURIComponent(rest);
|
|
1750
|
+
serveKickstartApi(res, rotator, email);
|
|
1751
|
+
}
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (
|
|
1756
|
+
method === "POST" &&
|
|
1757
|
+
(pathname === "/api/settings/auto-warmup/on" ||
|
|
1758
|
+
pathname === "/api/settings/auto-warmup/off")
|
|
1759
|
+
) {
|
|
1760
|
+
if (!requireAdmin(req, res)) return;
|
|
1761
|
+
serveAutoWarmupApi(res, rotator, pathname.endsWith("/on"));
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1731
1765
|
if (method === "POST" && pathname === "/api/self-update") {
|
|
1732
1766
|
if (!requireAdmin(req, res)) return;
|
|
1733
1767
|
trackFeature("selfUpdate");
|
|
@@ -1869,4 +1903,5 @@ export function startProxy(
|
|
|
1869
1903
|
log(`Dashboard: http://localhost:${port}/dashboard`, rotator);
|
|
1870
1904
|
log(`Hosted login: http://localhost:${port}/login`, rotator);
|
|
1871
1905
|
});
|
|
1906
|
+
return server;
|
|
1872
1907
|
}
|