pi-antigravity-rotator 2.3.1 → 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 +20 -0
- package/README.md +17 -11
- package/package.json +11 -7
- package/src/cli.ts +27 -2
- package/src/compat/translators.ts +58 -8
- package/src/compat.ts +103 -15
- 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 +398 -392
- package/src/rotator.ts +10 -6
- package/src/static/dashboard.js +7 -2
- package/src/telemetry.ts +7 -5
- package/src/types.ts +2 -9
- package/src/version-check.ts +23 -10
- package/tools/telemetry-receiver/README.md +2 -1
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";
|
|
@@ -122,6 +123,8 @@ export type RotationOutcome<T> =
|
|
|
122
123
|
errorText: string;
|
|
123
124
|
retryAfterMs?: number;
|
|
124
125
|
endpoint?: string;
|
|
126
|
+
context?: RotationAttemptContext;
|
|
127
|
+
totalMs?: number;
|
|
125
128
|
};
|
|
126
129
|
|
|
127
130
|
/**
|
|
@@ -242,6 +245,266 @@ function capCooldown(ms: number): number {
|
|
|
242
245
|
return Math.min(ms, MAX_COOLDOWN_MS);
|
|
243
246
|
}
|
|
244
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
|
+
|
|
245
508
|
function formatError(err: unknown): string {
|
|
246
509
|
if (!(err instanceof Error)) return String(err);
|
|
247
510
|
const cause = err.cause;
|
|
@@ -656,21 +919,29 @@ export async function withRotation<T>(
|
|
|
656
919
|
context: RotationAttemptContext,
|
|
657
920
|
) => Promise<T>,
|
|
658
921
|
): Promise<RotationOutcome<T>> {
|
|
659
|
-
const sendNoAccountsAvailable = (
|
|
922
|
+
const sendNoAccountsAvailable = (
|
|
923
|
+
reason: string,
|
|
924
|
+
context?: RotationAttemptContext,
|
|
925
|
+
): RotationOutcome<T> => {
|
|
660
926
|
log(`[${model}] No healthy account available: ${reason}`, rotator, "warn");
|
|
661
927
|
const retryAfterMs = rotator.getRetryAfterMs(model);
|
|
928
|
+
const contextFields = context
|
|
929
|
+
? { context, totalMs: Date.now() - context.requestStartMs }
|
|
930
|
+
: {};
|
|
662
931
|
if (retryAfterMs > 0) {
|
|
663
932
|
return {
|
|
664
933
|
ok: false,
|
|
665
934
|
status: 429,
|
|
666
935
|
errorText: `All accounts cooling down or model circuit breaker active: ${reason}`,
|
|
667
936
|
retryAfterMs,
|
|
937
|
+
...contextFields,
|
|
668
938
|
};
|
|
669
939
|
}
|
|
670
940
|
return {
|
|
671
941
|
ok: false,
|
|
672
942
|
status: 503,
|
|
673
943
|
errorText: `All accounts exhausted or disabled: ${reason}`,
|
|
944
|
+
...contextFields,
|
|
674
945
|
};
|
|
675
946
|
};
|
|
676
947
|
|
|
@@ -756,186 +1027,21 @@ export async function withRotation<T>(
|
|
|
756
1027
|
modelKey,
|
|
757
1028
|
);
|
|
758
1029
|
|
|
759
|
-
if (action.kind
|
|
760
|
-
|
|
761
|
-
|
|
1030
|
+
if (action.kind !== "success") {
|
|
1031
|
+
const decision = await handleUpstreamAccountAction({
|
|
1032
|
+
action,
|
|
762
1033
|
rotator,
|
|
763
|
-
"warn",
|
|
764
|
-
);
|
|
765
|
-
rotator.markExhausted(
|
|
766
1034
|
account,
|
|
767
1035
|
model,
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
`cooldownMs=${action.cooldownMs}${action.providerResourceExhausted ? " resourceExhausted=true" : ""}`,
|
|
775
|
-
);
|
|
776
|
-
return {
|
|
777
|
-
ok: false,
|
|
778
|
-
status: 429,
|
|
779
|
-
errorText: action.errorText,
|
|
780
|
-
retryAfterMs: action.cooldownMs,
|
|
781
|
-
endpoint,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (action.kind === "flagged-401") {
|
|
786
|
-
log(
|
|
787
|
-
`[${label}] BLOCKED (401): ${action.errorText.slice(0, 200)}`,
|
|
788
|
-
rotator,
|
|
789
|
-
"error",
|
|
790
|
-
);
|
|
791
|
-
const lower401 = action.errorText.toLowerCase();
|
|
792
|
-
const matched401 = FLAG_PATTERNS.filter((p) => lower401.includes(p));
|
|
793
|
-
const ctx401 = rotator.getFlagContext(account, modelKey);
|
|
794
|
-
reportFlagEvent({
|
|
795
|
-
flagHttpStatus: 401,
|
|
796
|
-
flagPatternsMatched:
|
|
797
|
-
matched401.length > 0 ? matched401 : ["blocked_401" as FlagPattern],
|
|
798
|
-
model: modelKey,
|
|
799
|
-
timerType: ctx401.timerType as FlagEventData["timerType"],
|
|
800
|
-
accountQuotaPercent: ctx401.accountQuotaPercent,
|
|
801
|
-
wasProAccount: ctx401.wasProAccount,
|
|
802
|
-
accountTotalRequests: account.totalRequests,
|
|
803
|
-
accountRequestsLastHour: ctx401.accountRequestsLastHour,
|
|
804
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
805
|
-
poolSize: ctx401.poolSize,
|
|
806
|
-
poolHealthyCount: ctx401.poolHealthyCount,
|
|
807
|
-
protectivePauseTriggered: false,
|
|
808
|
-
uptimeSeconds: ctx401.uptimeSeconds,
|
|
809
|
-
timeSinceLastFlagSeconds: -1,
|
|
810
|
-
});
|
|
811
|
-
rotator.markFlagged(
|
|
812
|
-
account,
|
|
813
|
-
`Account blocked (401): ${action.errorText.slice(0, 300)}`,
|
|
814
|
-
);
|
|
815
|
-
logRequestEnd(401);
|
|
816
|
-
const nextAccount = await rotateAndRelease();
|
|
817
|
-
if (!nextAccount) {
|
|
818
|
-
return sendNoAccountsAvailable(
|
|
819
|
-
`no replacement account remained after ${label} was flagged with 401`,
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
continue;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
if (action.kind === "flagged-403") {
|
|
826
|
-
log(
|
|
827
|
-
`[${label}] FLAGGED: ${action.errorText.slice(0, 200)}`,
|
|
828
|
-
rotator,
|
|
829
|
-
"error",
|
|
830
|
-
);
|
|
831
|
-
const lower = action.errorText.toLowerCase();
|
|
832
|
-
const matchedPatterns = FLAG_PATTERNS.filter((p) => lower.includes(p));
|
|
833
|
-
const ctx403 = rotator.getFlagContext(account, modelKey);
|
|
834
|
-
reportFlagEvent({
|
|
835
|
-
flagHttpStatus: 403,
|
|
836
|
-
flagPatternsMatched: matchedPatterns,
|
|
837
|
-
model: modelKey,
|
|
838
|
-
timerType: ctx403.timerType as FlagEventData["timerType"],
|
|
839
|
-
accountQuotaPercent: ctx403.accountQuotaPercent,
|
|
840
|
-
wasProAccount: ctx403.wasProAccount,
|
|
841
|
-
accountTotalRequests: account.totalRequests,
|
|
842
|
-
accountRequestsLastHour: ctx403.accountRequestsLastHour,
|
|
843
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
844
|
-
poolSize: ctx403.poolSize,
|
|
845
|
-
poolHealthyCount: ctx403.poolHealthyCount,
|
|
846
|
-
protectivePauseTriggered: false,
|
|
847
|
-
uptimeSeconds: ctx403.uptimeSeconds,
|
|
848
|
-
timeSinceLastFlagSeconds: -1,
|
|
1036
|
+
modelKey,
|
|
1037
|
+
label,
|
|
1038
|
+
context,
|
|
1039
|
+
logRequestEnd,
|
|
1040
|
+
rotateAndRelease,
|
|
1041
|
+
writeLog: (message, level = "info") => log(message, rotator, level),
|
|
849
1042
|
});
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const nextAccount = await rotateAndRelease();
|
|
853
|
-
if (!nextAccount) {
|
|
854
|
-
return sendNoAccountsAvailable(
|
|
855
|
-
`no replacement account remained after ${label} was flagged with 403`,
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (action.kind === "forbidden") {
|
|
862
|
-
log(
|
|
863
|
-
`[${label}] 403: ${action.errorText.slice(0, 200)}`,
|
|
864
|
-
rotator,
|
|
865
|
-
"warn",
|
|
866
|
-
);
|
|
867
|
-
logRequestEnd(403);
|
|
868
|
-
return {
|
|
869
|
-
ok: false,
|
|
870
|
-
status: 403,
|
|
871
|
-
errorText: action.errorText,
|
|
872
|
-
endpoint,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (action.kind === "not-found") {
|
|
877
|
-
log(
|
|
878
|
-
`[${label}] 404 from ${action.endpoint}: ${action.errorText.slice(0, 200)}`,
|
|
879
|
-
rotator,
|
|
880
|
-
"warn",
|
|
881
|
-
);
|
|
882
|
-
logRequestEnd(404, `endpoint=${action.endpoint}`);
|
|
883
|
-
return {
|
|
884
|
-
ok: false,
|
|
885
|
-
status: 404,
|
|
886
|
-
errorText: action.errorText,
|
|
887
|
-
endpoint,
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
if (action.kind === "bad-request") {
|
|
892
|
-
log(
|
|
893
|
-
`[${label}] 400 Bad Request from ${action.endpoint}: ${action.errorText.slice(0, 500)}`,
|
|
894
|
-
rotator,
|
|
895
|
-
"warn",
|
|
896
|
-
);
|
|
897
|
-
logRequestEnd(400, `endpoint=${action.endpoint}`);
|
|
898
|
-
return {
|
|
899
|
-
ok: false,
|
|
900
|
-
status: 400,
|
|
901
|
-
errorText: action.errorText,
|
|
902
|
-
endpoint,
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (action.kind === "server-error-503") {
|
|
907
|
-
log(
|
|
908
|
-
`[${label}] Server error 503: ${action.errorText.slice(0, 200)}`,
|
|
909
|
-
rotator,
|
|
910
|
-
"warn",
|
|
911
|
-
);
|
|
912
|
-
logRequestEnd(503, `endpoint=${action.endpoint}`);
|
|
913
|
-
return {
|
|
914
|
-
ok: false,
|
|
915
|
-
status: 503,
|
|
916
|
-
errorText: action.errorText,
|
|
917
|
-
endpoint,
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
if (action.kind === "rotate-on-5xx") {
|
|
922
|
-
log(
|
|
923
|
-
`[${label}] Server error ${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
924
|
-
rotator,
|
|
925
|
-
"warn",
|
|
926
|
-
);
|
|
927
|
-
logRequestEnd(action.httpStatus, `endpoint=${action.endpoint}`);
|
|
928
|
-
rotator.markError(
|
|
929
|
-
account,
|
|
930
|
-
`${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
931
|
-
);
|
|
932
|
-
const nextAccount = await rotateAndRelease();
|
|
933
|
-
if (!nextAccount) {
|
|
934
|
-
return sendNoAccountsAvailable(
|
|
935
|
-
`no replacement account remained after ${label} failed with ${action.httpStatus}`,
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
continue;
|
|
1043
|
+
if (decision.kind === "retry") continue;
|
|
1044
|
+
return failureDecisionToOutcome(decision);
|
|
939
1045
|
}
|
|
940
1046
|
|
|
941
1047
|
// success
|
|
@@ -1081,6 +1187,69 @@ async function handleProxyRequest(
|
|
|
1081
1187
|
}
|
|
1082
1188
|
return nextAccount;
|
|
1083
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
|
+
};
|
|
1084
1253
|
|
|
1085
1254
|
for (let attempt = 0; attempt < MAX_ENDPOINT_RETRIES; attempt++) {
|
|
1086
1255
|
const account = await rotator.getActiveAccount(body.model);
|
|
@@ -1156,197 +1325,34 @@ async function handleProxyRequest(
|
|
|
1156
1325
|
modelKey,
|
|
1157
1326
|
);
|
|
1158
1327
|
|
|
1159
|
-
if (action.kind
|
|
1160
|
-
|
|
1161
|
-
`[${label}] 429 rate limited${action.providerResourceExhausted ? " (RESOURCE_EXHAUSTED)" : ""}, cooldown ${Math.ceil(action.cooldownMs / 1000)}s. Error text: ${action.errorText.slice(0, 300)}`,
|
|
1162
|
-
"warn",
|
|
1163
|
-
);
|
|
1164
|
-
recordOutcome(429);
|
|
1165
|
-
logRequestEnd(
|
|
1166
|
-
429,
|
|
1167
|
-
`cooldownMs=${action.cooldownMs}${action.providerResourceExhausted ? " resourceExhausted=true" : ""} endpoint=${endpoint}`,
|
|
1168
|
-
);
|
|
1169
|
-
rotator.markExhausted(account, body.model, action.cooldownMs);
|
|
1170
|
-
rotator.recordProvider429(account, body.model, action.cooldownMs);
|
|
1171
|
-
|
|
1172
|
-
// Safety first: do NOT immediately retry another account on 429.
|
|
1173
|
-
// Provider-side 429s can represent daily/request buckets or shared project pressure;
|
|
1174
|
-
// cascading retries burn the full pool and increase ban/flag risk.
|
|
1175
|
-
res.writeHead(429, {
|
|
1176
|
-
"Content-Type": "application/json",
|
|
1177
|
-
"Retry-After": String(Math.ceil(action.cooldownMs / 1000)),
|
|
1178
|
-
});
|
|
1179
|
-
res.end(
|
|
1180
|
-
JSON.stringify({
|
|
1181
|
-
error: action.providerResourceExhausted
|
|
1182
|
-
? "Resource exhausted"
|
|
1183
|
-
: "Rate limited",
|
|
1184
|
-
reason: action.providerResourceExhausted
|
|
1185
|
-
? `${label} hit provider RESOURCE_EXHAUSTED; not retrying another account to avoid pool-wide hammering`
|
|
1186
|
-
: `${label} was rate limited; not retrying another account for account-safety`,
|
|
1187
|
-
model: body.model,
|
|
1188
|
-
account: label,
|
|
1189
|
-
retryAfterMs: action.cooldownMs,
|
|
1190
|
-
}),
|
|
1191
|
-
);
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (action.kind === "flagged-401") {
|
|
1196
|
-
proxyLog(
|
|
1197
|
-
`[${label}] BLOCKED (401): ${action.errorText.slice(0, 200)}`,
|
|
1198
|
-
"error",
|
|
1199
|
-
);
|
|
1200
|
-
|
|
1201
|
-
// Telemetry: report flag event BEFORE markFlagged (which may trigger protective pause)
|
|
1202
|
-
const lower401 = action.errorText.toLowerCase();
|
|
1203
|
-
const matched401 = FLAG_PATTERNS.filter((p) => lower401.includes(p));
|
|
1204
|
-
const ctx401 = rotator.getFlagContext(account, modelKey);
|
|
1205
|
-
reportFlagEvent({
|
|
1206
|
-
flagHttpStatus: 401,
|
|
1207
|
-
flagPatternsMatched:
|
|
1208
|
-
matched401.length > 0 ? matched401 : ["blocked_401" as FlagPattern],
|
|
1209
|
-
model: modelKey,
|
|
1210
|
-
timerType: ctx401.timerType as FlagEventData["timerType"],
|
|
1211
|
-
accountQuotaPercent: ctx401.accountQuotaPercent,
|
|
1212
|
-
wasProAccount: ctx401.wasProAccount,
|
|
1213
|
-
accountTotalRequests: account.totalRequests,
|
|
1214
|
-
accountRequestsLastHour: ctx401.accountRequestsLastHour,
|
|
1215
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
1216
|
-
poolSize: ctx401.poolSize,
|
|
1217
|
-
poolHealthyCount: ctx401.poolHealthyCount,
|
|
1218
|
-
protectivePauseTriggered: false, // not yet — markFlagged decides
|
|
1219
|
-
uptimeSeconds: ctx401.uptimeSeconds,
|
|
1220
|
-
timeSinceLastFlagSeconds: -1, // filled by reporter
|
|
1221
|
-
});
|
|
1222
|
-
|
|
1223
|
-
rotator.markFlagged(
|
|
1328
|
+
if (action.kind !== "success") {
|
|
1329
|
+
const context: RotationAttemptContext = {
|
|
1224
1330
|
account,
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
logRequestEnd(403, `endpoint=${endpoint}`);
|
|
1245
|
-
|
|
1246
|
-
const matchedPatterns = FLAG_PATTERNS.filter((p) =>
|
|
1247
|
-
action.errorText.toLowerCase().includes(p),
|
|
1248
|
-
);
|
|
1249
|
-
const ctx403 = rotator.getFlagContext(account, modelKey);
|
|
1250
|
-
reportFlagEvent({
|
|
1251
|
-
flagHttpStatus: 403,
|
|
1252
|
-
flagPatternsMatched: matchedPatterns,
|
|
1253
|
-
model: modelKey,
|
|
1254
|
-
timerType: ctx403.timerType as FlagEventData["timerType"],
|
|
1255
|
-
accountQuotaPercent: ctx403.accountQuotaPercent,
|
|
1256
|
-
wasProAccount: ctx403.wasProAccount,
|
|
1257
|
-
accountTotalRequests: account.totalRequests,
|
|
1258
|
-
accountRequestsLastHour: ctx403.accountRequestsLastHour,
|
|
1259
|
-
accountConcurrentAtFlag: account.inFlightRequests,
|
|
1260
|
-
poolSize: ctx403.poolSize,
|
|
1261
|
-
poolHealthyCount: ctx403.poolHealthyCount,
|
|
1262
|
-
protectivePauseTriggered: false, // not yet
|
|
1263
|
-
uptimeSeconds: ctx403.uptimeSeconds,
|
|
1264
|
-
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,
|
|
1265
1350
|
});
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
const nextAccount = await rotateAndRelease();
|
|
1269
|
-
if (!nextAccount) {
|
|
1270
|
-
sendNoAccountsAvailable(
|
|
1271
|
-
`no replacement account remained after ${label} was flagged with 403`,
|
|
1272
|
-
);
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
continue;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
if (action.kind === "forbidden") {
|
|
1279
|
-
proxyLog(`[${label}] 403: ${action.errorText.slice(0, 200)}`, "warn");
|
|
1280
|
-
logRequestEnd(403, `endpoint=${action.endpoint}`);
|
|
1281
|
-
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1282
|
-
res.end(action.errorText || JSON.stringify({ error: "Forbidden" }));
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
if (action.kind === "not-found") {
|
|
1287
|
-
proxyLog(
|
|
1288
|
-
`[${label}] 404 from ${action.endpoint}: ${action.errorText.slice(0, 200)}`,
|
|
1289
|
-
"warn",
|
|
1290
|
-
);
|
|
1291
|
-
logRequestEnd(404, `endpoint=${action.endpoint}`);
|
|
1292
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1293
|
-
res.end(action.errorText || JSON.stringify({ error: "Not found" }));
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if (action.kind === "bad-request") {
|
|
1298
|
-
proxyLog(
|
|
1299
|
-
`[${label}] 400 Bad Request from ${action.endpoint}: ${action.errorText.slice(0, 500)}`,
|
|
1300
|
-
"warn",
|
|
1301
|
-
);
|
|
1302
|
-
logRequestEnd(400, `endpoint=${action.endpoint}`);
|
|
1303
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1304
|
-
res.end(action.errorText || JSON.stringify({ error: "Bad request" }));
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (action.kind === "server-error-503") {
|
|
1309
|
-
proxyLog(
|
|
1310
|
-
`[${label}] Server error 503: ${action.errorText.slice(0, 200)}`,
|
|
1311
|
-
"warn",
|
|
1312
|
-
);
|
|
1313
|
-
recordOutcome(503);
|
|
1314
|
-
logRequestEnd(503, `endpoint=${action.endpoint}`);
|
|
1315
|
-
// Return 503 as-is. Capacity errors still consume quota upstream,
|
|
1316
|
-
// so retrying on another account would just burn more quota for nothing.
|
|
1317
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1318
|
-
res.end(
|
|
1319
|
-
action.errorText ||
|
|
1320
|
-
JSON.stringify({
|
|
1321
|
-
error: "Server unavailable",
|
|
1322
|
-
account: label,
|
|
1323
|
-
model: body.model,
|
|
1324
|
-
}),
|
|
1325
|
-
);
|
|
1351
|
+
if (decision.kind === "retry") continue;
|
|
1352
|
+
sendFailureDecision(decision);
|
|
1326
1353
|
return;
|
|
1327
1354
|
}
|
|
1328
1355
|
|
|
1329
|
-
if (action.kind === "rotate-on-5xx") {
|
|
1330
|
-
proxyLog(
|
|
1331
|
-
`[${label}] Server error ${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
1332
|
-
"warn",
|
|
1333
|
-
);
|
|
1334
|
-
recordOutcome(action.httpStatus);
|
|
1335
|
-
logRequestEnd(action.httpStatus, `endpoint=${action.endpoint}`);
|
|
1336
|
-
rotator.markError(
|
|
1337
|
-
account,
|
|
1338
|
-
`${action.httpStatus}: ${action.errorText.slice(0, 200)}`,
|
|
1339
|
-
);
|
|
1340
|
-
const nextAccount = await rotateAndRelease();
|
|
1341
|
-
if (!nextAccount) {
|
|
1342
|
-
sendNoAccountsAvailable(
|
|
1343
|
-
`no replacement account remained after ${label} failed with ${action.httpStatus}`,
|
|
1344
|
-
);
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
continue;
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
1356
|
// Success or non-error client response
|
|
1351
1357
|
const shouldRotate = rotator.recordRequest(account, body.model);
|
|
1352
1358
|
|
|
@@ -1456,7 +1462,7 @@ export function startProxy(
|
|
|
1456
1462
|
rotator: AccountRotator,
|
|
1457
1463
|
port: number,
|
|
1458
1464
|
bindHost = "0.0.0.0",
|
|
1459
|
-
):
|
|
1465
|
+
): Server {
|
|
1460
1466
|
startVersionChecker();
|
|
1461
1467
|
startNotificationPoller();
|
|
1462
1468
|
const sseClients = new Set<ServerResponse>();
|
|
@@ -1541,7 +1547,6 @@ export function startProxy(
|
|
|
1541
1547
|
}
|
|
1542
1548
|
|
|
1543
1549
|
if (method === "GET" && pathname === "/auth/antigravity/callback") {
|
|
1544
|
-
if (!requireAdmin(req, res)) return;
|
|
1545
1550
|
handleHostedCallback(req, res, rotator).catch((err) => {
|
|
1546
1551
|
log(`Hosted callback error: ${err}`, rotator, "error");
|
|
1547
1552
|
if (!res.headersSent) {
|
|
@@ -1624,46 +1629,46 @@ export function startProxy(
|
|
|
1624
1629
|
return;
|
|
1625
1630
|
}
|
|
1626
1631
|
|
|
1627
|
-
if (method === "POST" &&
|
|
1632
|
+
if (method === "POST" && pathname.startsWith("/api/enable/")) {
|
|
1628
1633
|
if (!requireAdmin(req, res)) return;
|
|
1629
|
-
const email = decodeURIComponent(
|
|
1634
|
+
const email = decodeURIComponent(pathname.slice("/api/enable/".length));
|
|
1630
1635
|
serveEnableApi(res, rotator, email);
|
|
1631
1636
|
return;
|
|
1632
1637
|
}
|
|
1633
1638
|
|
|
1634
|
-
if (method === "POST" &&
|
|
1639
|
+
if (method === "POST" && pathname.startsWith("/api/disable/")) {
|
|
1635
1640
|
if (!requireAdmin(req, res)) return;
|
|
1636
|
-
const email = decodeURIComponent(
|
|
1641
|
+
const email = decodeURIComponent(pathname.slice("/api/disable/".length));
|
|
1637
1642
|
serveDisableApi(res, rotator, email);
|
|
1638
1643
|
return;
|
|
1639
1644
|
}
|
|
1640
1645
|
|
|
1641
|
-
if (method === "POST" &&
|
|
1646
|
+
if (method === "POST" && pathname.startsWith("/api/quarantine/")) {
|
|
1642
1647
|
if (!requireAdmin(req, res)) return;
|
|
1643
|
-
const email = decodeURIComponent(
|
|
1648
|
+
const email = decodeURIComponent(pathname.slice("/api/quarantine/".length));
|
|
1644
1649
|
serveQuarantineApi(res, rotator, email);
|
|
1645
1650
|
return;
|
|
1646
1651
|
}
|
|
1647
1652
|
|
|
1648
|
-
if (method === "POST" &&
|
|
1653
|
+
if (method === "POST" && pathname.startsWith("/api/restore/")) {
|
|
1649
1654
|
if (!requireAdmin(req, res)) return;
|
|
1650
|
-
const email = decodeURIComponent(
|
|
1655
|
+
const email = decodeURIComponent(pathname.slice("/api/restore/".length));
|
|
1651
1656
|
serveRestoreApi(res, rotator, email);
|
|
1652
1657
|
return;
|
|
1653
1658
|
}
|
|
1654
1659
|
|
|
1655
|
-
if (method === "POST" &&
|
|
1660
|
+
if (method === "POST" && pathname.startsWith("/api/remove-account/")) {
|
|
1656
1661
|
if (!requireAdmin(req, res)) return;
|
|
1657
1662
|
const email = decodeURIComponent(
|
|
1658
|
-
|
|
1663
|
+
pathname.slice("/api/remove-account/".length),
|
|
1659
1664
|
);
|
|
1660
1665
|
serveRemoveAccountApi(res, rotator, email);
|
|
1661
1666
|
return;
|
|
1662
1667
|
}
|
|
1663
1668
|
|
|
1664
|
-
if (method === "POST" &&
|
|
1669
|
+
if (method === "POST" && pathname.startsWith("/api/set-tier/")) {
|
|
1665
1670
|
if (!requireAdmin(req, res)) return;
|
|
1666
|
-
const rest =
|
|
1671
|
+
const rest = pathname.slice("/api/set-tier/".length);
|
|
1667
1672
|
const lastSlash = rest.lastIndexOf("/");
|
|
1668
1673
|
if (lastSlash < 0) {
|
|
1669
1674
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -1681,9 +1686,9 @@ export function startProxy(
|
|
|
1681
1686
|
return;
|
|
1682
1687
|
}
|
|
1683
1688
|
|
|
1684
|
-
if (method === "POST" &&
|
|
1689
|
+
if (method === "POST" && pathname.startsWith("/api/clear-inflight/")) {
|
|
1685
1690
|
if (!requireAdmin(req, res)) return;
|
|
1686
|
-
const rest =
|
|
1691
|
+
const rest = pathname.slice("/api/clear-inflight/".length);
|
|
1687
1692
|
const firstSlash = rest.indexOf("/");
|
|
1688
1693
|
const email = decodeURIComponent(
|
|
1689
1694
|
firstSlash >= 0 ? rest.slice(0, firstSlash) : rest,
|
|
@@ -1696,9 +1701,9 @@ export function startProxy(
|
|
|
1696
1701
|
return;
|
|
1697
1702
|
}
|
|
1698
1703
|
|
|
1699
|
-
if (method === "POST" &&
|
|
1704
|
+
if (method === "POST" && pathname.startsWith("/api/clear-breaker/")) {
|
|
1700
1705
|
if (!requireAdmin(req, res)) return;
|
|
1701
|
-
const rest =
|
|
1706
|
+
const rest = pathname.slice("/api/clear-breaker/".length);
|
|
1702
1707
|
const modelKey =
|
|
1703
1708
|
rest && rest !== "all" ? decodeURIComponent(rest) : undefined;
|
|
1704
1709
|
serveClearBreakerApi(res, rotator, modelKey);
|
|
@@ -1707,22 +1712,22 @@ export function startProxy(
|
|
|
1707
1712
|
|
|
1708
1713
|
if (
|
|
1709
1714
|
method === "POST" &&
|
|
1710
|
-
(
|
|
1711
|
-
|
|
1715
|
+
(pathname === "/api/settings/fresh-window-starts/on" ||
|
|
1716
|
+
pathname === "/api/settings/fresh-window-starts/off")
|
|
1712
1717
|
) {
|
|
1713
1718
|
if (!requireAdmin(req, res)) return;
|
|
1714
1719
|
trackFeature("freshWindowToggle");
|
|
1715
|
-
serveFreshWindowStartsApi(res, rotator,
|
|
1720
|
+
serveFreshWindowStartsApi(res, rotator, pathname.endsWith("/on"));
|
|
1716
1721
|
return;
|
|
1717
1722
|
}
|
|
1718
1723
|
|
|
1719
1724
|
if (
|
|
1720
1725
|
method === "POST" &&
|
|
1721
|
-
|
|
1722
|
-
(
|
|
1726
|
+
pathname.startsWith("/api/account-fresh-window-starts/") &&
|
|
1727
|
+
(pathname.endsWith("/on") || pathname.endsWith("/off"))
|
|
1723
1728
|
) {
|
|
1724
1729
|
if (!requireAdmin(req, res)) return;
|
|
1725
|
-
const rest =
|
|
1730
|
+
const rest = pathname.slice("/api/account-fresh-window-starts/".length);
|
|
1726
1731
|
const lastSlash = rest.lastIndexOf("/");
|
|
1727
1732
|
const email = decodeURIComponent(rest.slice(0, lastSlash));
|
|
1728
1733
|
const enabled = rest.slice(lastSlash + 1) === "on";
|
|
@@ -1730,9 +1735,9 @@ export function startProxy(
|
|
|
1730
1735
|
return;
|
|
1731
1736
|
}
|
|
1732
1737
|
|
|
1733
|
-
if (method === "POST" &&
|
|
1738
|
+
if (method === "POST" && pathname.startsWith("/api/kickstart/")) {
|
|
1734
1739
|
if (!requireAdmin(req, res)) return;
|
|
1735
|
-
const rest =
|
|
1740
|
+
const rest = pathname.slice("/api/kickstart/".length);
|
|
1736
1741
|
const firstSlash = rest.indexOf("/");
|
|
1737
1742
|
if (firstSlash >= 0) {
|
|
1738
1743
|
// /api/kickstart/:email/:modelKey
|
|
@@ -1749,11 +1754,11 @@ export function startProxy(
|
|
|
1749
1754
|
|
|
1750
1755
|
if (
|
|
1751
1756
|
method === "POST" &&
|
|
1752
|
-
(
|
|
1753
|
-
|
|
1757
|
+
(pathname === "/api/settings/auto-warmup/on" ||
|
|
1758
|
+
pathname === "/api/settings/auto-warmup/off")
|
|
1754
1759
|
) {
|
|
1755
1760
|
if (!requireAdmin(req, res)) return;
|
|
1756
|
-
serveAutoWarmupApi(res, rotator,
|
|
1761
|
+
serveAutoWarmupApi(res, rotator, pathname.endsWith("/on"));
|
|
1757
1762
|
return;
|
|
1758
1763
|
}
|
|
1759
1764
|
|
|
@@ -1898,4 +1903,5 @@ export function startProxy(
|
|
|
1898
1903
|
log(`Dashboard: http://localhost:${port}/dashboard`, rotator);
|
|
1899
1904
|
log(`Hosted login: http://localhost:${port}/login`, rotator);
|
|
1900
1905
|
});
|
|
1906
|
+
return server;
|
|
1901
1907
|
}
|