pi-antigravity-rotator 1.3.4 → 1.3.6

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 CHANGED
@@ -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) -- After `requestsPerRotation` successful requests (default: 5), the rotator asks for a rotation on the model that served that request. By default this fallback is only used when quota data for that model is still unknown.
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.4",
3
+ "version": "1.3.6",
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/proxy.ts CHANGED
@@ -86,6 +86,23 @@ function capCooldown(ms: number): number {
86
86
  return Math.min(ms, MAX_COOLDOWN_MS);
87
87
  }
88
88
 
89
+ function formatError(err: unknown): string {
90
+ if (!(err instanceof Error)) return String(err);
91
+ const cause = err.cause;
92
+ if (cause && typeof cause === "object") {
93
+ const code = "code" in cause ? String(cause.code) : null;
94
+ const message = "message" in cause ? String(cause.message) : null;
95
+ if (code || message) {
96
+ return `${err.name}: ${err.message} (${[code, message].filter(Boolean).join(": ")})`;
97
+ }
98
+ }
99
+ return `${err.name}: ${err.message}`;
100
+ }
101
+
102
+ function isFetchTransportError(err: unknown): boolean {
103
+ return err instanceof TypeError && err.message === "fetch failed";
104
+ }
105
+
89
106
  /**
90
107
  * Read the full request body from an IncomingMessage.
91
108
  */
@@ -319,8 +336,11 @@ async function handleProxyRequest(
319
336
  }
320
337
  return;
321
338
  } catch (err) {
322
- proxyLog(`[${label}] Request failed: ${err}`, "error");
323
- rotator.markError(account, err instanceof Error ? err.message : String(err));
339
+ const formattedError = formatError(err);
340
+ proxyLog(`[${label}] Request failed: ${formattedError}`, isFetchTransportError(err) ? "warn" : "error");
341
+ if (!isFetchTransportError(err)) {
342
+ rotator.markError(account, formattedError);
343
+ }
324
344
  if (res.headersSent) {
325
345
  res.end();
326
346
  return;
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
  }
@@ -369,6 +370,29 @@ export class AccountRotator {
369
370
  return best;
370
371
  }
371
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
+
372
396
  // =========================================================================
373
397
  // Account Selection (per-model)
374
398
  // =========================================================================
@@ -388,12 +412,26 @@ export class AccountRotator {
388
412
  if (current && this.isAvailable(current, now)) {
389
413
  // Check if this account has quota for the requested model
390
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
+ }
391
429
  const quota = this.getModelQuota(current, modelKey);
392
430
  if (quota === 0) {
393
431
  this.log(
394
432
  `${current.config.label || current.config.email} [${modelKey}]: 0% quota, skipping`,
395
433
  );
396
- return this.rotateModel(modelKey);
434
+ return this.rotateModelForRequest(modelKey);
397
435
  }
398
436
  if (!this.isFreshWindowAllowed(current, modelKey)) {
399
437
  const label = current.config.label || current.config.email;
@@ -403,12 +441,13 @@ export class AccountRotator {
403
441
  : `${label} [${modelKey}]: fresh window blocked by operator toggle`,
404
442
  "warn",
405
443
  );
406
- return this.rotateModel(modelKey);
444
+ return this.rotateModelForRequest(modelKey);
407
445
  }
408
446
  }
409
447
  this.startRequest(current);
410
448
  try {
411
449
  await this.ensureValidToken(current);
450
+ if (modelKey) this.countModelAssignment(modelKey);
412
451
  return current;
413
452
  } catch (err) {
414
453
  this.finishRequest(current);
@@ -418,7 +457,7 @@ export class AccountRotator {
418
457
 
419
458
  // Current unavailable, or no per-model assignment yet
420
459
  if (modelKey) {
421
- return this.rotateModel(modelKey, now, state ? idx : -1);
460
+ return this.rotateModelForRequest(modelKey, now, state ? idx : -1);
422
461
  }
423
462
  return this.rotateDefault();
424
463
  }
@@ -438,6 +477,7 @@ export class AccountRotator {
438
477
  this.modelState.set(modelKey, {
439
478
  activeAccountIndex: newIdx,
440
479
  quotaAtRotationStart: quota,
480
+ requestsOnActiveAccount: 0,
441
481
  });
442
482
  this.log(
443
483
  `[${modelKey}] Rotated to ${best.config.label || best.config.email} [${timerType}] (quota: ${quota >= 0 ? quota + "%" : "unknown"})`,
@@ -540,17 +580,8 @@ export class AccountRotator {
540
580
  account.consecutiveErrors = 0;
541
581
  account.lastError = null;
542
582
 
543
- const shouldRotate =
544
- this.shouldUseRequestCountRotation(account, model) &&
545
- account.requestsSinceRotation >= this.config.requestsPerRotation;
546
- if (shouldRotate) {
547
- account.requestsSinceRotation = 0;
548
- this.log(
549
- `${account.config.label || account.config.email}: hit rotation threshold (${this.config.requestsPerRotation})`,
550
- );
551
- }
552
583
  this.saveState();
553
- return shouldRotate;
584
+ return false;
554
585
  }
555
586
 
556
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