pi-antigravity-rotator 1.3.3 → 1.3.5
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/README.md +2 -2
- package/package.json +1 -1
- package/src/rotator.ts +53 -14
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ Each model maintains its own active account. When the proxy needs to rotate a mo
|
|
|
134
134
|
| 2 | `7d` | Long reset window is already active for this model | Already ticking, so it is still worth using |
|
|
135
135
|
| 3 (last) | `fresh` | No active reset window is known for this model yet | Save untouched quota for later if other timed pools exist |
|
|
136
136
|
|
|
137
|
-
Within the same priority tier, the account with the most remaining quota for that model wins.
|
|
137
|
+
Within the same priority tier, the account with the most remaining quota for that model wins. If multiple accounts tie on priority and quota, rotation advances circularly from the current account so equal candidates share traffic instead of always favoring the first configured match.
|
|
138
138
|
|
|
139
139
|
Timer meanings:
|
|
140
140
|
|
|
@@ -148,7 +148,7 @@ Three mechanisms trigger rotation, scoped to the specific model:
|
|
|
148
148
|
|
|
149
149
|
1. **Quota-based** (primary) -- Polls the Google quota API every 5 minutes. When a model's remaining quota drops by `rotateOnQuotaDrop` percentage points (default: 20%), that model rotates to the next account. Other models stay on their current accounts.
|
|
150
150
|
|
|
151
|
-
2. **Request-count** (fallback) --
|
|
151
|
+
2. **Request-count** (fallback) -- Before forwarding a request, the rotator checks how many requests the current account has already served for that specific model and rotates once it reaches `requestsPerRotation` (default: 5). By default this fallback is only used when quota data for that model is still unknown.
|
|
152
152
|
|
|
153
153
|
3. **429 failover** (reactive) -- On rate limit, the account is marked exhausted with a parsed retry cooldown and the affected model immediately switches.
|
|
154
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/rotator.ts
CHANGED
|
@@ -78,6 +78,7 @@ export class AccountRotator {
|
|
|
78
78
|
this.modelState.set(model, {
|
|
79
79
|
activeAccountIndex: Math.min(idx, this.accounts.length - 1),
|
|
80
80
|
quotaAtRotationStart: -1,
|
|
81
|
+
requestsOnActiveAccount: 0,
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -340,6 +341,7 @@ export class AccountRotator {
|
|
|
340
341
|
let best: AccountRuntime | null = null;
|
|
341
342
|
let bestPriority = Infinity;
|
|
342
343
|
let bestQuota = -2;
|
|
344
|
+
let bestDistance = Infinity;
|
|
343
345
|
|
|
344
346
|
for (let i = 0; i < this.accounts.length; i++) {
|
|
345
347
|
if (i === excludeIdx) continue;
|
|
@@ -351,16 +353,46 @@ export class AccountRotator {
|
|
|
351
353
|
if (!this.isFreshWindowAllowed(account, modelKey)) continue;
|
|
352
354
|
|
|
353
355
|
const priority = this.getModelTimerPriority(account, modelKey);
|
|
354
|
-
|
|
356
|
+
const distance =
|
|
357
|
+
excludeIdx >= 0 ? (i - excludeIdx + this.accounts.length) % this.accounts.length : i + 1;
|
|
358
|
+
if (
|
|
359
|
+
priority < bestPriority ||
|
|
360
|
+
(priority === bestPriority && quota > bestQuota) ||
|
|
361
|
+
(priority === bestPriority && quota === bestQuota && distance < bestDistance)
|
|
362
|
+
) {
|
|
355
363
|
best = account;
|
|
356
364
|
bestPriority = priority;
|
|
357
365
|
bestQuota = quota;
|
|
366
|
+
bestDistance = distance;
|
|
358
367
|
}
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
return best;
|
|
362
371
|
}
|
|
363
372
|
|
|
373
|
+
private countModelAssignment(modelKey: string): void {
|
|
374
|
+
const state = this.modelState.get(modelKey);
|
|
375
|
+
if (state) {
|
|
376
|
+
state.requestsOnActiveAccount++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private shouldRotateBeforeRequest(account: AccountRuntime, modelKey: string, state: ModelRotationState | null): boolean {
|
|
381
|
+
return (
|
|
382
|
+
!!state &&
|
|
383
|
+
this.shouldUseRequestCountRotation(account, modelKey) &&
|
|
384
|
+
state.requestsOnActiveAccount >= this.config.requestsPerRotation
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async rotateModelForRequest(modelKey: string, now: number = Date.now(), excludeIdx?: number): Promise<AccountRuntime | null> {
|
|
389
|
+
const account = await this.rotateModel(modelKey, now, excludeIdx);
|
|
390
|
+
if (account) {
|
|
391
|
+
this.countModelAssignment(modelKey);
|
|
392
|
+
}
|
|
393
|
+
return account;
|
|
394
|
+
}
|
|
395
|
+
|
|
364
396
|
// =========================================================================
|
|
365
397
|
// Account Selection (per-model)
|
|
366
398
|
// =========================================================================
|
|
@@ -380,12 +412,26 @@ export class AccountRotator {
|
|
|
380
412
|
if (current && this.isAvailable(current, now)) {
|
|
381
413
|
// Check if this account has quota for the requested model
|
|
382
414
|
if (modelKey) {
|
|
415
|
+
if (this.shouldRotateBeforeRequest(current, modelKey, state ?? null)) {
|
|
416
|
+
this.log(
|
|
417
|
+
`${current.config.label || current.config.email} [${modelKey}]: hit rotation threshold (${this.config.requestsPerRotation})`,
|
|
418
|
+
);
|
|
419
|
+
const rotated = await this.rotateModelForRequest(modelKey, now, idx);
|
|
420
|
+
if (rotated) {
|
|
421
|
+
current.requestsSinceRotation = 0;
|
|
422
|
+
return rotated;
|
|
423
|
+
}
|
|
424
|
+
this.log(
|
|
425
|
+
`${current.config.label || current.config.email} [${modelKey}]: threshold reached but no replacement is available, staying`,
|
|
426
|
+
"warn",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
383
429
|
const quota = this.getModelQuota(current, modelKey);
|
|
384
430
|
if (quota === 0) {
|
|
385
431
|
this.log(
|
|
386
432
|
`${current.config.label || current.config.email} [${modelKey}]: 0% quota, skipping`,
|
|
387
433
|
);
|
|
388
|
-
return this.
|
|
434
|
+
return this.rotateModelForRequest(modelKey);
|
|
389
435
|
}
|
|
390
436
|
if (!this.isFreshWindowAllowed(current, modelKey)) {
|
|
391
437
|
const label = current.config.label || current.config.email;
|
|
@@ -395,12 +441,13 @@ export class AccountRotator {
|
|
|
395
441
|
: `${label} [${modelKey}]: fresh window blocked by operator toggle`,
|
|
396
442
|
"warn",
|
|
397
443
|
);
|
|
398
|
-
return this.
|
|
444
|
+
return this.rotateModelForRequest(modelKey);
|
|
399
445
|
}
|
|
400
446
|
}
|
|
401
447
|
this.startRequest(current);
|
|
402
448
|
try {
|
|
403
449
|
await this.ensureValidToken(current);
|
|
450
|
+
if (modelKey) this.countModelAssignment(modelKey);
|
|
404
451
|
return current;
|
|
405
452
|
} catch (err) {
|
|
406
453
|
this.finishRequest(current);
|
|
@@ -410,7 +457,7 @@ export class AccountRotator {
|
|
|
410
457
|
|
|
411
458
|
// Current unavailable, or no per-model assignment yet
|
|
412
459
|
if (modelKey) {
|
|
413
|
-
return this.
|
|
460
|
+
return this.rotateModelForRequest(modelKey, now, state ? idx : -1);
|
|
414
461
|
}
|
|
415
462
|
return this.rotateDefault();
|
|
416
463
|
}
|
|
@@ -430,6 +477,7 @@ export class AccountRotator {
|
|
|
430
477
|
this.modelState.set(modelKey, {
|
|
431
478
|
activeAccountIndex: newIdx,
|
|
432
479
|
quotaAtRotationStart: quota,
|
|
480
|
+
requestsOnActiveAccount: 0,
|
|
433
481
|
});
|
|
434
482
|
this.log(
|
|
435
483
|
`[${modelKey}] Rotated to ${best.config.label || best.config.email} [${timerType}] (quota: ${quota >= 0 ? quota + "%" : "unknown"})`,
|
|
@@ -532,17 +580,8 @@ export class AccountRotator {
|
|
|
532
580
|
account.consecutiveErrors = 0;
|
|
533
581
|
account.lastError = null;
|
|
534
582
|
|
|
535
|
-
const shouldRotate =
|
|
536
|
-
this.shouldUseRequestCountRotation(account, model) &&
|
|
537
|
-
account.requestsSinceRotation >= this.config.requestsPerRotation;
|
|
538
|
-
if (shouldRotate) {
|
|
539
|
-
account.requestsSinceRotation = 0;
|
|
540
|
-
this.log(
|
|
541
|
-
`${account.config.label || account.config.email}: hit rotation threshold (${this.config.requestsPerRotation})`,
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
583
|
this.saveState();
|
|
545
|
-
return
|
|
584
|
+
return false;
|
|
546
585
|
}
|
|
547
586
|
|
|
548
587
|
// Mark an account as exhausted (429 or quota exceeded)
|
package/src/types.ts
CHANGED
|
@@ -117,6 +117,7 @@ export interface AccountRuntime {
|
|
|
117
117
|
export interface ModelRotationState {
|
|
118
118
|
activeAccountIndex: number;
|
|
119
119
|
quotaAtRotationStart: number; // quota % when this account became active for this model
|
|
120
|
+
requestsOnActiveAccount: number;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// Persisted state across restarts
|