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/rotator.ts
CHANGED
|
@@ -21,6 +21,9 @@ import {
|
|
|
21
21
|
TOKEN_URL,
|
|
22
22
|
QUOTA_API_URL,
|
|
23
23
|
QUOTA_USER_AGENT,
|
|
24
|
+
REQUEST_GOOG_API_CLIENT,
|
|
25
|
+
REQUEST_CLIENT_METADATA,
|
|
26
|
+
ANTIGRAVITY_ENDPOINTS,
|
|
24
27
|
QUOTA_MODEL_KEYS,
|
|
25
28
|
resolveQuotaModelKey,
|
|
26
29
|
resolveDisplayModelKey,
|
|
@@ -41,6 +44,7 @@ import { logger } from "./logger.js";
|
|
|
41
44
|
import { getUpdateInfo } from "./version-check.js";
|
|
42
45
|
import { getNotifications } from "./notification-poller.js";
|
|
43
46
|
import { getConfiguredAdminToken } from "./admin-auth.js";
|
|
47
|
+
import { getProxyExposureWarning } from "./exposure.js";
|
|
44
48
|
import {
|
|
45
49
|
getCachedState,
|
|
46
50
|
setCachedState,
|
|
@@ -67,6 +71,20 @@ function projectModelKey(projectId: string, modelKey: string): string {
|
|
|
67
71
|
return `${projectId}::${modelKey}`;
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
// Maps each quota pool key to the cheapest upstream model to use for kickstart warmup requests.
|
|
75
|
+
// gemini-3.5-flash and gemini-3.1-pro share the same upstream pool, so both map to gemini-3-flash.
|
|
76
|
+
const KICKSTART_MODEL_FOR_QUOTA_POOL: Record<string, string> = {
|
|
77
|
+
"claude-opus-4-6-thinking": "gpt-oss-120b-medium",
|
|
78
|
+
"gemini-3.5-flash": "gemini-3-flash",
|
|
79
|
+
"gemini-3.1-pro": "gemini-3-flash",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Reverse map: upstream model → the quota pool key it primarily represents (for deduplication).
|
|
83
|
+
const QUOTA_POOL_FOR_KICKSTART_MODEL: Record<string, string> = {
|
|
84
|
+
"gpt-oss-120b-medium": "claude-opus-4-6-thinking",
|
|
85
|
+
"gemini-3-flash": "gemini-3.5-flash",
|
|
86
|
+
};
|
|
87
|
+
|
|
70
88
|
export class AccountRotator {
|
|
71
89
|
private accounts: AccountRuntime[] = [];
|
|
72
90
|
// Per-model active account tracking
|
|
@@ -102,6 +120,10 @@ export class AccountRotator {
|
|
|
102
120
|
account: string;
|
|
103
121
|
}> = [];
|
|
104
122
|
private routingDiagnostics: Record<string, RoutingModelDiagnostics> = {};
|
|
123
|
+
private autoWarmupEnabled = false;
|
|
124
|
+
// Tracks upstream models already warmed-up this poll cycle per account (email → Set<upstreamModel>).
|
|
125
|
+
// Cleared at the start of each pollAllQuotas() to enforce the one-per-cycle guarantee.
|
|
126
|
+
private warmupSentThisCycle = new Map<string, Set<string>>();
|
|
105
127
|
// Debounced state writer: batches multiple saveState() calls within a 1s window
|
|
106
128
|
// to a single disk write. Hot paths (markError, recordRequest, etc.) call
|
|
107
129
|
// scheduleStateSave() instead of saveState() to avoid blocking the event loop.
|
|
@@ -267,6 +289,7 @@ export class AccountRotator {
|
|
|
267
289
|
this.protectivePauseUntil = state.protectivePauseUntil ?? 0;
|
|
268
290
|
this.protectivePauseReason = state.protectivePauseReason ?? null;
|
|
269
291
|
this.allowFreshWindowStarts = state.allowFreshWindowStarts ?? true;
|
|
292
|
+
this.autoWarmupEnabled = state.autoWarmupEnabled ?? false;
|
|
270
293
|
this.safetyDay = state.safety?.day ?? currentUtcDay();
|
|
271
294
|
this.projectRequests = state.safety?.projectRequests ?? {};
|
|
272
295
|
this.projectModelBreakers = state.safety?.projectModelBreakers ?? {};
|
|
@@ -372,6 +395,7 @@ export class AccountRotator {
|
|
|
372
395
|
protectivePauseUntil: this.protectivePauseUntil,
|
|
373
396
|
protectivePauseReason: this.protectivePauseReason,
|
|
374
397
|
allowFreshWindowStarts: this.allowFreshWindowStarts,
|
|
398
|
+
autoWarmupEnabled: this.autoWarmupEnabled,
|
|
375
399
|
safety: {
|
|
376
400
|
day: this.safetyDay,
|
|
377
401
|
projectRequests: { ...this.projectRequests },
|
|
@@ -474,6 +498,9 @@ export class AccountRotator {
|
|
|
474
498
|
}
|
|
475
499
|
|
|
476
500
|
private async pollAllQuotas(): Promise<void> {
|
|
501
|
+
// Reset per-cycle warmup tracking so each poll cycle allows at most one warmup per upstream model per account.
|
|
502
|
+
this.warmupSentThisCycle.clear();
|
|
503
|
+
|
|
477
504
|
const available = this.accounts.filter((a) => !a.disabled && !a.flagged);
|
|
478
505
|
for (const account of available) {
|
|
479
506
|
try {
|
|
@@ -482,6 +509,38 @@ export class AccountRotator {
|
|
|
482
509
|
} catch {
|
|
483
510
|
// Token refresh or quota fetch failed, skip this account
|
|
484
511
|
}
|
|
512
|
+
|
|
513
|
+
// Auto-warmup: send minimal kickstart requests for idle pools on accounts that have opted
|
|
514
|
+
// in via allowFreshWindowStartsOverride, but only if the operator has enabled the global
|
|
515
|
+
// auto-warmup toggle. "Idle" includes both fresh pools (no active timer) and rolling idle
|
|
516
|
+
// pools (100% quota with resetTime very close to a full 5h or 7d window — timer exists but
|
|
517
|
+
// untouched). Deduplicates by upstream model within this cycle.
|
|
518
|
+
if (this.autoWarmupEnabled && account.allowFreshWindowStartsOverride) {
|
|
519
|
+
const idlePools = account.quota.filter((q) => this.isQuotaIdleForKickstart(q));
|
|
520
|
+
if (idlePools.length > 0) {
|
|
521
|
+
const alreadySent =
|
|
522
|
+
this.warmupSentThisCycle.get(account.config.email) ?? new Set<string>();
|
|
523
|
+
// Build deduplicated upstream model list
|
|
524
|
+
const upstreamToQuotaKey = new Map<string, string>();
|
|
525
|
+
for (const q of idlePools) {
|
|
526
|
+
const upstream = KICKSTART_MODEL_FOR_QUOTA_POOL[q.modelKey] ?? q.modelKey;
|
|
527
|
+
if (!upstreamToQuotaKey.has(upstream)) {
|
|
528
|
+
const primaryKey = QUOTA_POOL_FOR_KICKSTART_MODEL[upstream] ?? q.modelKey;
|
|
529
|
+
upstreamToQuotaKey.set(upstream, primaryKey);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const [upstream, quotaKey] of upstreamToQuotaKey) {
|
|
533
|
+
if (!alreadySent.has(upstream)) {
|
|
534
|
+
alreadySent.add(upstream);
|
|
535
|
+
// Fire-and-forget — errors are handled internally by kickstartTimerForAccount
|
|
536
|
+
void this.kickstartTimerForAccount(account.config.email, quotaKey).catch(
|
|
537
|
+
() => {},
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
this.warmupSentThisCycle.set(account.config.email, alreadySent);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
485
544
|
}
|
|
486
545
|
|
|
487
546
|
if (this.isProtectivePauseActive(Date.now())) {
|
|
@@ -683,6 +742,19 @@ export class AccountRotator {
|
|
|
683
742
|
return q?.timerType ?? "fresh";
|
|
684
743
|
}
|
|
685
744
|
|
|
745
|
+
// A pool is "idle for kickstart" when it has a fresh pool (no active timer)
|
|
746
|
+
// OR a rolling idle timer (100% quota with resetTime very close to a full 5h or 7d
|
|
747
|
+
// window — the timer exists but the quota is completely untouched). Sending a
|
|
748
|
+
// minimal kickstart request against an idle pool starts the consumption clock.
|
|
749
|
+
private isQuotaIdleForKickstart(q: ModelQuota): boolean {
|
|
750
|
+
if (q.timerType === "fresh") return true;
|
|
751
|
+
if (!q.resetTime || q.percentRemaining !== 100) return false;
|
|
752
|
+
const remaining = new Date(q.resetTime).getTime() - Date.now();
|
|
753
|
+
if (remaining <= 0) return false;
|
|
754
|
+
const within = (target: number) => Math.abs(remaining - target) < 600_000;
|
|
755
|
+
return within(5 * 3600 * 1000) || within(7 * 24 * 3600 * 1000);
|
|
756
|
+
}
|
|
757
|
+
|
|
686
758
|
// Timer priority for a specific model:
|
|
687
759
|
// 1 (highest) = 5h timer -> only class with hard quota loss if underused before reset
|
|
688
760
|
// 2 = 7d timer -> already ticking, keep those long windows moving
|
|
@@ -805,7 +877,7 @@ export class AccountRotator {
|
|
|
805
877
|
const state = this.modelState.get(modelKey);
|
|
806
878
|
if (state) {
|
|
807
879
|
state.requestsOnActiveAccount++;
|
|
808
|
-
this.
|
|
880
|
+
this.scheduleStateSave();
|
|
809
881
|
}
|
|
810
882
|
}
|
|
811
883
|
|
|
@@ -2094,7 +2166,7 @@ export class AccountRotator {
|
|
|
2094
2166
|
account.dailyRequestCount++;
|
|
2095
2167
|
this.projectRequests[account.config.projectId] =
|
|
2096
2168
|
(this.projectRequests[account.config.projectId] ?? 0) + 1;
|
|
2097
|
-
this.
|
|
2169
|
+
this.scheduleStateSave();
|
|
2098
2170
|
}
|
|
2099
2171
|
|
|
2100
2172
|
getSafetyJitterMs(account: AccountRuntime): number {
|
|
@@ -2228,6 +2300,19 @@ export class AccountRotator {
|
|
|
2228
2300
|
return true;
|
|
2229
2301
|
}
|
|
2230
2302
|
|
|
2303
|
+
setAutoWarmup(enabled: boolean): boolean {
|
|
2304
|
+
if (this.autoWarmupEnabled === enabled) return false;
|
|
2305
|
+
this.autoWarmupEnabled = enabled;
|
|
2306
|
+
this.saveState();
|
|
2307
|
+
this.log(
|
|
2308
|
+
enabled
|
|
2309
|
+
? "Operator enabled auto-warmup; accounts with fresh-window override will automatically receive minimal kickstart requests on each quota poll"
|
|
2310
|
+
: "Operator disabled auto-warmup; no automatic kickstart requests will be sent",
|
|
2311
|
+
"warn",
|
|
2312
|
+
);
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2231
2316
|
clearModelBreaker(modelKey: string): boolean {
|
|
2232
2317
|
const now = Date.now();
|
|
2233
2318
|
const hasModelBreaker = (this.modelBreakers[modelKey] ?? 0) > now;
|
|
@@ -2571,6 +2656,11 @@ export class AccountRotator {
|
|
|
2571
2656
|
}
|
|
2572
2657
|
}
|
|
2573
2658
|
|
|
2659
|
+
const adminWarning = getConfiguredAdminToken()
|
|
2660
|
+
? null
|
|
2661
|
+
: `Admin routes are exposed on ${this.config.bindHost}:${this.config.proxyPort} because PI_ROTATOR_ADMIN_TOKEN is not configured.`;
|
|
2662
|
+
const proxyWarning = getProxyExposureWarning(this.config);
|
|
2663
|
+
|
|
2574
2664
|
return {
|
|
2575
2665
|
version: updateInfo.currentVersion,
|
|
2576
2666
|
proxyPort: this.config.proxyPort,
|
|
@@ -2588,12 +2678,11 @@ export class AccountRotator {
|
|
|
2588
2678
|
: null,
|
|
2589
2679
|
operatorControls: {
|
|
2590
2680
|
allowFreshWindowStarts: this.allowFreshWindowStarts,
|
|
2681
|
+
autoWarmupEnabled: this.autoWarmupEnabled,
|
|
2591
2682
|
},
|
|
2592
2683
|
security: {
|
|
2593
2684
|
adminTokenConfigured: !!getConfiguredAdminToken(),
|
|
2594
|
-
warning:
|
|
2595
|
-
? null
|
|
2596
|
-
: `Admin routes are exposed on ${this.config.bindHost}:${this.config.proxyPort} because PI_ROTATOR_ADMIN_TOKEN is not configured.`,
|
|
2685
|
+
warning: [adminWarning, proxyWarning].filter(Boolean).join(" ") || null,
|
|
2597
2686
|
bindHost: this.config.bindHost || "0.0.0.0",
|
|
2598
2687
|
},
|
|
2599
2688
|
routingDiagnostics,
|
|
@@ -2820,7 +2909,7 @@ export class AccountRotator {
|
|
|
2820
2909
|
}
|
|
2821
2910
|
|
|
2822
2911
|
setAccountTier(email: string, tier: string): boolean {
|
|
2823
|
-
const validTiers = ["unknown", "free", "pro", "ultra"];
|
|
2912
|
+
const validTiers = ["unknown", "free", "plus", "pro", "ultra"];
|
|
2824
2913
|
if (!validTiers.includes(tier)) return false;
|
|
2825
2914
|
const account = this.accounts.find((a) => a.config.email === email);
|
|
2826
2915
|
if (!account) return false;
|
|
@@ -3061,4 +3150,180 @@ export class AccountRotator {
|
|
|
3061
3150
|
errorCount,
|
|
3062
3151
|
};
|
|
3063
3152
|
}
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* Send a minimal single-token request to the upstream Antigravity endpoint for a specific
|
|
3156
|
+
* quota pool key on a given account. Uses the cheapest model in that pool to minimise cost.
|
|
3157
|
+
* Applies normal error handling (markExhausted, markFlagged, markError) so the account state
|
|
3158
|
+
* stays consistent with regular traffic.
|
|
3159
|
+
*/
|
|
3160
|
+
async kickstartTimerForAccount(
|
|
3161
|
+
email: string,
|
|
3162
|
+
quotaModelKey: string,
|
|
3163
|
+
): Promise<{ ok: boolean; status: number; upstreamModel: string; error?: string }> {
|
|
3164
|
+
const account = this.accounts.find((a) => a.config.email === email);
|
|
3165
|
+
if (!account) {
|
|
3166
|
+
return { ok: false, status: 404, upstreamModel: "", error: "account not found" };
|
|
3167
|
+
}
|
|
3168
|
+
if (account.disabled || account.flagged) {
|
|
3169
|
+
return {
|
|
3170
|
+
ok: false,
|
|
3171
|
+
status: 409,
|
|
3172
|
+
upstreamModel: "",
|
|
3173
|
+
error: account.disabled ? "account disabled" : "account flagged",
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
try {
|
|
3178
|
+
await this.ensureValidToken(account);
|
|
3179
|
+
} catch (err) {
|
|
3180
|
+
return {
|
|
3181
|
+
ok: false,
|
|
3182
|
+
status: 401,
|
|
3183
|
+
upstreamModel: "",
|
|
3184
|
+
error: `token refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
if (!account.accessToken) {
|
|
3189
|
+
return { ok: false, status: 401, upstreamModel: "", error: "no access token" };
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
const upstreamModel =
|
|
3193
|
+
KICKSTART_MODEL_FOR_QUOTA_POOL[quotaModelKey] ?? quotaModelKey;
|
|
3194
|
+
|
|
3195
|
+
const body = JSON.stringify({
|
|
3196
|
+
project: account.config.projectId,
|
|
3197
|
+
model: upstreamModel,
|
|
3198
|
+
request: {
|
|
3199
|
+
contents: [{ role: "user", parts: [{ text: "." }] }],
|
|
3200
|
+
generationConfig: { maxOutputTokens: 1 },
|
|
3201
|
+
},
|
|
3202
|
+
});
|
|
3203
|
+
|
|
3204
|
+
const endpoint = ANTIGRAVITY_ENDPOINTS[ANTIGRAVITY_ENDPOINTS.length - 1];
|
|
3205
|
+
const url = `${endpoint}/v1internal:streamGenerateContent?alt=sse`;
|
|
3206
|
+
|
|
3207
|
+
const controller = new AbortController();
|
|
3208
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
3209
|
+
|
|
3210
|
+
let response: Response;
|
|
3211
|
+
try {
|
|
3212
|
+
response = await fetch(url, {
|
|
3213
|
+
method: "POST",
|
|
3214
|
+
headers: {
|
|
3215
|
+
"Content-Type": "application/json",
|
|
3216
|
+
Authorization: `Bearer ${account.accessToken}`,
|
|
3217
|
+
"User-Agent": QUOTA_USER_AGENT,
|
|
3218
|
+
"X-Goog-Api-Client": REQUEST_GOOG_API_CLIENT,
|
|
3219
|
+
"Client-Metadata": REQUEST_CLIENT_METADATA,
|
|
3220
|
+
},
|
|
3221
|
+
body,
|
|
3222
|
+
signal: controller.signal,
|
|
3223
|
+
});
|
|
3224
|
+
} catch (err) {
|
|
3225
|
+
clearTimeout(timeout);
|
|
3226
|
+
const msg = `kickstart network error: ${err instanceof Error ? err.message : String(err)}`;
|
|
3227
|
+
this.markError(account, msg);
|
|
3228
|
+
return { ok: false, status: 0, upstreamModel, error: msg };
|
|
3229
|
+
}
|
|
3230
|
+
clearTimeout(timeout);
|
|
3231
|
+
|
|
3232
|
+
// Consume and discard the response body to free the connection
|
|
3233
|
+
try {
|
|
3234
|
+
await response.body?.cancel();
|
|
3235
|
+
} catch {
|
|
3236
|
+
// ignore
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
const label = account.config.label || account.config.email;
|
|
3240
|
+
|
|
3241
|
+
if (response.status === 429) {
|
|
3242
|
+
const cooldownMs = 60_000;
|
|
3243
|
+
this.markExhausted(account, quotaModelKey, cooldownMs, "kickstart 429");
|
|
3244
|
+
this.recordProvider429(account, quotaModelKey, cooldownMs);
|
|
3245
|
+
this.log(
|
|
3246
|
+
`${label} [${quotaModelKey}]: kickstart 429 — cooldown ${cooldownMs / 1000}s`,
|
|
3247
|
+
"warn",
|
|
3248
|
+
);
|
|
3249
|
+
return { ok: false, status: 429, upstreamModel };
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
if (response.status === 401 || response.status === 403) {
|
|
3253
|
+
this.markFlagged(
|
|
3254
|
+
account,
|
|
3255
|
+
`kickstart ${response.status} on ${upstreamModel}`,
|
|
3256
|
+
{ triggerProtectivePause: false },
|
|
3257
|
+
);
|
|
3258
|
+
return { ok: false, status: response.status, upstreamModel };
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
if (response.status >= 500) {
|
|
3262
|
+
this.markError(account, `kickstart ${response.status} on ${upstreamModel}`);
|
|
3263
|
+
return { ok: false, status: response.status, upstreamModel };
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
if (response.ok) {
|
|
3267
|
+
this.recordRequest(account, quotaModelKey);
|
|
3268
|
+
this.log(
|
|
3269
|
+
`${label} [${quotaModelKey}]: kickstart sent via ${upstreamModel} — timer should start within the next quota poll`,
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
return { ok: response.ok, status: response.status, upstreamModel };
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
/**
|
|
3277
|
+
* Kickstart all quota pools that are currently idle (no active timer, or rolling
|
|
3278
|
+
* idle with 100% quota and a fresh resetTime) for a given account. Deduplicates by
|
|
3279
|
+
* upstream model so that pools sharing the same upstream (e.g. gemini-3.5-flash and
|
|
3280
|
+
* gemini-3.1-pro → gemini-3-flash) only receive one request.
|
|
3281
|
+
*/
|
|
3282
|
+
async kickstartAllFreshTimers(email: string): Promise<{
|
|
3283
|
+
ok: boolean;
|
|
3284
|
+
error?: string;
|
|
3285
|
+
results: Array<{
|
|
3286
|
+
quotaPools: string[];
|
|
3287
|
+
upstreamModel: string;
|
|
3288
|
+
ok: boolean;
|
|
3289
|
+
status: number;
|
|
3290
|
+
}>;
|
|
3291
|
+
}> {
|
|
3292
|
+
const account = this.accounts.find((a) => a.config.email === email);
|
|
3293
|
+
if (!account) {
|
|
3294
|
+
return { ok: false, error: "account not found", results: [] };
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
const idlePools = account.quota.filter((q) => this.isQuotaIdleForKickstart(q));
|
|
3298
|
+
if (idlePools.length === 0) {
|
|
3299
|
+
return { ok: true, results: [] };
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
// Deduplicate: group quota pool keys by their upstream model
|
|
3303
|
+
const upstreamToQuotaPools = new Map<string, string[]>();
|
|
3304
|
+
for (const q of idlePools) {
|
|
3305
|
+
const upstream = KICKSTART_MODEL_FOR_QUOTA_POOL[q.modelKey] ?? q.modelKey;
|
|
3306
|
+
const list = upstreamToQuotaPools.get(upstream) ?? [];
|
|
3307
|
+
list.push(q.modelKey);
|
|
3308
|
+
upstreamToQuotaPools.set(upstream, list);
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
const results: Array<{
|
|
3312
|
+
quotaPools: string[];
|
|
3313
|
+
upstreamModel: string;
|
|
3314
|
+
ok: boolean;
|
|
3315
|
+
status: number;
|
|
3316
|
+
}> = [];
|
|
3317
|
+
|
|
3318
|
+
for (const [upstreamModel, quotaPools] of upstreamToQuotaPools) {
|
|
3319
|
+
// Use the primary quota pool key for this upstream (for recordRequest/markExhausted)
|
|
3320
|
+
const primaryQuotaKey =
|
|
3321
|
+
QUOTA_POOL_FOR_KICKSTART_MODEL[upstreamModel] ?? quotaPools[0];
|
|
3322
|
+
const result = await this.kickstartTimerForAccount(email, primaryQuotaKey);
|
|
3323
|
+
results.push({ quotaPools, upstreamModel, ok: result.ok, status: result.status });
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
const allOk = results.every((r) => r.ok);
|
|
3327
|
+
return { ok: allOk, results };
|
|
3328
|
+
}
|
|
3064
3329
|
}
|