pi-antigravity-rotator 1.3.6 → 1.3.7
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 +121 -0
- package/package.json +8 -7
- package/src/proxy.ts +90 -15
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.3.7] - 2026-04-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Release in-flight account reservations when a streaming response closes early, the client disconnects, or the upstream stream goes idle, preventing accounts from getting stuck as busy indefinitely.
|
|
9
|
+
|
|
10
|
+
## [1.3.6] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Treat Node `fetch failed` transport errors as transient upstream/network failures instead of account health errors, avoiding false account disables during stalled requests.
|
|
14
|
+
|
|
15
|
+
## [1.3.5] - 2026-04-24
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Make request-count rotation deterministic by counting per-model account assignments before the next request is forwarded, instead of rotating only after a successful response completes.
|
|
19
|
+
|
|
20
|
+
## [1.3.4] - 2026-04-24
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Rotate fairly among accounts that tie on model timer priority and remaining quota instead of repeatedly selecting the first matching candidate.
|
|
24
|
+
|
|
25
|
+
## [1.3.3] - 2026-04-23
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Hosted Antigravity login flow so operators can complete Google account linking from a browser and feed the callback URL back into the rotator workflow.
|
|
29
|
+
- Global fresh-window operator control plus per-account override so dormant quota windows can be blocked pool-wide and selectively re-enabled account by account.
|
|
30
|
+
- Header modal launchers for Attention Needed and Pro Family Advisor to keep operator actions available without taking permanent dashboard space.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Reworked the dashboard layout to prioritize the account grid above the fold: request totals moved into the header, bulky summary widgets were removed, and Recent Events now sits at the bottom.
|
|
34
|
+
- Simplified the header by moving the PII visibility toggle next to the title and removing the inline model-routing pills.
|
|
35
|
+
- Tightened the routing health strip with denser pills, single-line counters, and clearer spacing between major dashboard sections.
|
|
36
|
+
|
|
37
|
+
## [1.3.2] - 2026-04-23
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Routing health panel in the dashboard with current state, stop reason, retry window, and pool blocker counts.
|
|
41
|
+
- Attention Needed summary panel for flagged, cooling, disabled, and error accounts.
|
|
42
|
+
- Recent Events feed showing the latest rotator and proxy incidents that led to the current state.
|
|
43
|
+
- In-memory event buffer exposed through the status API for dashboard diagnostics.
|
|
44
|
+
- Conservative concurrency guardrail to cap each account to one in-flight request by default.
|
|
45
|
+
- Protective pause after serious provider ToS/abuse-style flags to stop the rest of the pool from being burned.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Dashboard now focuses on operator visibility so the service can be monitored without relying on `journalctl`.
|
|
49
|
+
- Request-count rotation is now only used when quota data is still unknown, reducing unnecessary account churn.
|
|
50
|
+
- Flagged accounts remain quarantined until the provider explicitly restores access.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
- Fixed the exhausted fallback path so cooled-down accounts are no longer selected again when all accounts are exhausted.
|
|
54
|
+
- Fixed proxy retry behavior so it returns `503` immediately when no healthy replacement account exists instead of continuing to hammer the pool.
|
|
55
|
+
- Fixed quota polling so flagged accounts are no longer re-polled every cycle after a provider `403`.
|
|
56
|
+
- Fixed bursty same-account pressure by reserving accounts during selection and request handling.
|
|
57
|
+
|
|
58
|
+
## [1.3.1] - 2026-04-22
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- Prioritize Pro 5h accounts in rotation. Accounts with active 5h timers are now drained first to maximize the +40% recharge benefit when the timer expires. Previously they were saved for last, wasting quota.
|
|
62
|
+
|
|
63
|
+
## [1.3.0] - 2026-04-22
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- Pro Family Sharing Advisor: dashboard panel suggests when to add/remove accounts from Pro family sharing.
|
|
67
|
+
- Pro/Free/Family Manager badges on account cards (auto-detected from 5h/7d timer type).
|
|
68
|
+
- `familyManager` config flag for the account that owns the family plan.
|
|
69
|
+
- `proSlots` config option for max simultaneous Pro accounts (default 6).
|
|
70
|
+
- Advisor prioritizes accounts by longest reset time when suggesting Pro upgrades.
|
|
71
|
+
- Only G3Pro and Claude quotas considered for remove-pro decisions (Flash ignored).
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## [1.2.0] - 2026-04-22
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
- PII masking mode for dashboard (`?mask` URL param or toggle button). Masks emails and labels for screen recordings.
|
|
78
|
+
- Contextual help hints for flagged accounts (verification instructions, Google Account Recovery link).
|
|
79
|
+
- Model-aware quota rotation: accounts with 0% quota for the requested model are skipped instead of wasting requests.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
- Fixed `ReadableStream is locked` crash by using `Response.text()` and `Readable.fromWeb()` instead of raw ReadableStream API.
|
|
83
|
+
- Fixed `ERR_HTTP_HEADERS_SENT` crash when retrying after response headers were already sent.
|
|
84
|
+
- Fixed 403 fallthrough bug: non-flagging 403 responses consumed the body then fell through to streaming, causing locked stream errors.
|
|
85
|
+
- Accounts needing verification (`Verify your account`) are now flagged immediately instead of retried.
|
|
86
|
+
- Dashboard URL routing now handles query parameters correctly.
|
|
87
|
+
|
|
88
|
+
## [1.1.0] - 2026-04-22
|
|
89
|
+
|
|
90
|
+
### Changed
|
|
91
|
+
- Use prod endpoint only (`cloudcode-pa.googleapis.com`). Removed daily/autopush endpoints that caused multi-minute hangs.
|
|
92
|
+
- 503 errors (no capacity) are now returned directly to the agent for its own retry/backoff instead of burning through all accounts.
|
|
93
|
+
- Quota-based rotation only triggers if a healthy account is available. The proxy won't rotate away from a working account if there's no better alternative.
|
|
94
|
+
- Dashboard accounts are sorted by total quota (highest first), flagged/disabled last.
|
|
95
|
+
- Config files now default to `~/.pi-antigravity-rotator/` (overridable via `PI_ROTATOR_DIR` env or `--config-dir` flag).
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
- `POST /api/reset-cooldowns` endpoint to clear all cooldowns at once.
|
|
99
|
+
- CLI entry point with `start`, `login`, and `status` commands.
|
|
100
|
+
- 30-minute max cooldown cap on all exhaustions (prevents multi-day cooldowns).
|
|
101
|
+
- Stale cooldowns from `state.json` are capped to 30 minutes on startup.
|
|
102
|
+
- Case-insensitive authorization header handling (fixes duplicate header bug with pi agent).
|
|
103
|
+
- MIT License.
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
- Fixed duplicate `Authorization` header causing 401 on all accounts. Pi sends lowercase `authorization`; the proxy was keeping both the original and the new one.
|
|
107
|
+
- Fixed infinite retry loop when all accounts are exhausted or 503 (no capacity).
|
|
108
|
+
- Fixed quota rotation moving away from the only working account when no alternatives are available.
|
|
109
|
+
|
|
110
|
+
## [1.0.0] - 2026-04-22
|
|
111
|
+
|
|
112
|
+
### Added
|
|
113
|
+
- Initial release.
|
|
114
|
+
- Per-model routing (Gemini Pro, Flash, Claude).
|
|
115
|
+
- Quota-based rotation with configurable drop threshold.
|
|
116
|
+
- Request-count-based rotation (fallback).
|
|
117
|
+
- 429 failover with automatic cooldown.
|
|
118
|
+
- Account protection: quota API 403, API 401, API 403 keyword detection.
|
|
119
|
+
- Real-time dashboard with account cards, quota bars, and model routing table.
|
|
120
|
+
- OAuth login helper with automatic pi agent configuration.
|
|
121
|
+
- State persistence across restarts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
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",
|
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
"start": "tsx src/cli.ts start",
|
|
12
12
|
"login": "tsx src/cli.ts login"
|
|
13
13
|
},
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
20
21
|
"keywords": [
|
|
21
22
|
"pi-package",
|
|
22
23
|
"pi",
|
package/src/proxy.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { handleHostedCallback, serveLoginLanding, startHostedLogin } from "./onb
|
|
|
16
16
|
|
|
17
17
|
const MAX_ENDPOINT_RETRIES = 3;
|
|
18
18
|
const MAX_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes max cooldown
|
|
19
|
+
const STREAM_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // Release account if a stream goes silent.
|
|
19
20
|
|
|
20
21
|
interface RequestBody {
|
|
21
22
|
project: string;
|
|
@@ -103,6 +104,91 @@ function isFetchTransportError(err: unknown): boolean {
|
|
|
103
104
|
return err instanceof TypeError && err.message === "fetch failed";
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
async function streamResponseBody(
|
|
108
|
+
body: Response["body"],
|
|
109
|
+
req: IncomingMessage,
|
|
110
|
+
res: ServerResponse,
|
|
111
|
+
label: string,
|
|
112
|
+
proxyLog: (msg: string, level?: "info" | "warn" | "error") => void,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
if (!body) return;
|
|
115
|
+
|
|
116
|
+
const nodeStream = Readable.fromWeb(body as import("node:stream/web").ReadableStream);
|
|
117
|
+
|
|
118
|
+
await new Promise<void>((resolve) => {
|
|
119
|
+
let settled = false;
|
|
120
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
121
|
+
|
|
122
|
+
const cleanup = (): void => {
|
|
123
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
124
|
+
nodeStream.off("data", onData);
|
|
125
|
+
nodeStream.off("end", onEnd);
|
|
126
|
+
nodeStream.off("error", onError);
|
|
127
|
+
nodeStream.off("close", onClose);
|
|
128
|
+
req.off("aborted", onClientAbort);
|
|
129
|
+
req.off("close", onClientClose);
|
|
130
|
+
res.off("close", onResponseClose);
|
|
131
|
+
res.off("error", onResponseError);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const finish = (reason?: string): void => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
if (reason) proxyLog(`[${label}] Stream closed: ${reason}`, "warn");
|
|
138
|
+
cleanup();
|
|
139
|
+
resolve();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const resetIdleTimer = (): void => {
|
|
143
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
144
|
+
idleTimer = setTimeout(() => {
|
|
145
|
+
nodeStream.destroy(new Error(`stream idle for ${Math.round(STREAM_IDLE_TIMEOUT_MS / 1000)}s`));
|
|
146
|
+
finish(`idle timeout after ${Math.round(STREAM_IDLE_TIMEOUT_MS / 1000)}s`);
|
|
147
|
+
}, STREAM_IDLE_TIMEOUT_MS);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const onData = (chunk: Buffer): void => {
|
|
151
|
+
resetIdleTimer();
|
|
152
|
+
if (!res.destroyed && !res.writableEnded) {
|
|
153
|
+
res.write(chunk);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const onEnd = (): void => finish();
|
|
157
|
+
const onError = (err: Error): void => finish(String(err));
|
|
158
|
+
const onClose = (): void => finish();
|
|
159
|
+
const onClientAbort = (): void => {
|
|
160
|
+
nodeStream.destroy();
|
|
161
|
+
finish("client aborted");
|
|
162
|
+
};
|
|
163
|
+
const onClientClose = (): void => {
|
|
164
|
+
if (!res.writableEnded) {
|
|
165
|
+
nodeStream.destroy();
|
|
166
|
+
finish("client closed connection");
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const onResponseClose = (): void => {
|
|
170
|
+
if (!res.writableEnded) {
|
|
171
|
+
nodeStream.destroy();
|
|
172
|
+
finish("response closed before completion");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const onResponseError = (err: Error): void => {
|
|
176
|
+
nodeStream.destroy(err);
|
|
177
|
+
finish(String(err));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
nodeStream.on("data", onData);
|
|
181
|
+
nodeStream.once("end", onEnd);
|
|
182
|
+
nodeStream.once("error", onError);
|
|
183
|
+
nodeStream.once("close", onClose);
|
|
184
|
+
req.once("aborted", onClientAbort);
|
|
185
|
+
req.once("close", onClientClose);
|
|
186
|
+
res.once("close", onResponseClose);
|
|
187
|
+
res.once("error", onResponseError);
|
|
188
|
+
resetIdleTimer();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
106
192
|
/**
|
|
107
193
|
* Read the full request body from an IncomingMessage.
|
|
108
194
|
*/
|
|
@@ -313,23 +399,12 @@ async function handleProxyRequest(
|
|
|
313
399
|
|
|
314
400
|
res.writeHead(response.status, responseHeaders);
|
|
315
401
|
|
|
316
|
-
// Stream body using Node.js Readable (avoids ReadableStream locking issues)
|
|
317
|
-
if (response.body) {
|
|
318
402
|
try {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
nodeStream.on("end", resolve);
|
|
323
|
-
nodeStream.on("error", (err) => {
|
|
324
|
-
proxyLog(`[${label}] Stream error: ${err}`, "warn");
|
|
325
|
-
resolve();
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
} catch (err) {
|
|
329
|
-
proxyLog(`[${label}] Stream setup error: ${err}`, "warn");
|
|
330
|
-
}
|
|
403
|
+
await streamResponseBody(response.body, req, res, label, proxyLog);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
proxyLog(`[${label}] Stream setup error: ${err}`, "warn");
|
|
331
406
|
}
|
|
332
|
-
|
|
407
|
+
res.end();
|
|
333
408
|
|
|
334
409
|
if (shouldRotate) {
|
|
335
410
|
await rotateAndRelease();
|