pi-antigravity-rotator 1.0.0
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/LICENSE +21 -0
- package/README.md +227 -0
- package/bin/cli.mjs +5 -0
- package/bin/pi-antigravity-rotator.js +5 -0
- package/package.json +41 -0
- package/src/cli.ts +55 -0
- package/src/dashboard.ts +522 -0
- package/src/index.ts +61 -0
- package/src/login.ts +364 -0
- package/src/paths.ts +36 -0
- package/src/proxy.ts +358 -0
- package/src/rotator.ts +589 -0
- package/src/types.ts +172 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastián Real (tuxevil)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Pi Antigravity Rotator
|
|
2
|
+
|
|
3
|
+
Multi-account rotation proxy for Google Antigravity. Distributes API usage across multiple Google accounts with per-model routing, real-time quota tracking, automatic token management, and infringement detection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Per-model routing** -- Each model (Gemini Pro, Flash, Claude) routes to its own active account independently. Multiple agents using different models won't interfere with each other.
|
|
8
|
+
- **Real-time quota monitoring** -- Polls Google's quota API every 5 minutes to track remaining usage per model per account
|
|
9
|
+
- **Per-model timer tracking** -- Timer priority (fresh/7d/5h) is evaluated per model using each model's actual `resetTime` from the quota API, not a per-account estimate
|
|
10
|
+
- **Smart rotation** -- Rotates only the specific model whose quota dropped, leaving other models on their current accounts
|
|
11
|
+
- **Infringement detection** -- On 403 with infringement/abuse/suspension keywords, the account is immediately flagged and excluded from routing
|
|
12
|
+
- **Automatic failover** -- On 429 rate limits, instantly switches the affected model to the next available account
|
|
13
|
+
- **Token auto-refresh** -- Tokens are refreshed automatically before expiry; no manual management
|
|
14
|
+
- **Endpoint cascade** -- Tries daily, autopush, and prod API endpoints for resilience
|
|
15
|
+
- **Web dashboard** -- Real-time view of model routing table, per-account quota bars with per-model timers, and flagged account alerts
|
|
16
|
+
- **State persistence** -- Survives restarts; routing assignments, cooldowns, and flags are saved to disk
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Option A: Install from npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g pi-antigravity-rotator
|
|
24
|
+
|
|
25
|
+
# Add your first account
|
|
26
|
+
pi-antigravity-rotator login
|
|
27
|
+
|
|
28
|
+
# Start the proxy
|
|
29
|
+
pi-antigravity-rotator start
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Option B: Clone from source
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/tuxevil/pi-antigravity-rotator.git
|
|
36
|
+
cd pi-antigravity-rotator
|
|
37
|
+
npm install
|
|
38
|
+
|
|
39
|
+
# Add your first account
|
|
40
|
+
npm run login
|
|
41
|
+
|
|
42
|
+
# Start the proxy
|
|
43
|
+
npm start
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Adding Accounts
|
|
47
|
+
|
|
48
|
+
Run `npm run login` once per Google account:
|
|
49
|
+
|
|
50
|
+
1. A Google OAuth URL is printed to the terminal -- open it in your browser
|
|
51
|
+
2. Complete the sign-in and grant permissions
|
|
52
|
+
3. The browser redirects to a `localhost` URL that won't load -- this is expected
|
|
53
|
+
4. Copy the **full URL** from the browser's address bar and paste it into the terminal
|
|
54
|
+
|
|
55
|
+
The tool automatically:
|
|
56
|
+
|
|
57
|
+
- Creates or updates `accounts.json` with the account credentials
|
|
58
|
+
- Configures `~/.pi/agent/models.json` to point `google-antigravity` at the proxy
|
|
59
|
+
- Configures `~/.pi/agent/auth.json` with proxy-managed credentials
|
|
60
|
+
|
|
61
|
+
Re-running with the same email updates the existing entry.
|
|
62
|
+
|
|
63
|
+
## Dashboard
|
|
64
|
+
|
|
65
|
+
After starting the proxy, open `http://localhost:51200/dashboard` or `http://<your-server-ip>:51200/dashboard` from any machine on the same network (the proxy binds to `0.0.0.0`).
|
|
66
|
+
|
|
67
|
+
The dashboard shows:
|
|
68
|
+
|
|
69
|
+
- **Model Routing table** -- Which account each model (Gemini Pro, Flash, Claude) is currently routed to
|
|
70
|
+
- **Account cards** with:
|
|
71
|
+
- Status badge: `active`, `ready`, `cooldown`, `flagged`, `disabled`, or `error`
|
|
72
|
+
- Model badges: which models this account is currently serving
|
|
73
|
+
- Per-model quota bars with timer type (`fresh`/`7d`/`5h`) and reset countdown
|
|
74
|
+
- Request counts, last used time, token status
|
|
75
|
+
- Error messages for flagged/errored accounts
|
|
76
|
+
- Re-enable button for flagged or disabled accounts
|
|
77
|
+
|
|
78
|
+

|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
### Proxying
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Pi Agent 1 (Gemini Pro) ---> localhost:51200 ---> Account A
|
|
86
|
+
Pi Agent 2 (Claude) ---> localhost:51200 ---> Account C
|
|
87
|
+
Pi Agent 3 (Flash) ---> localhost:51200 ---> Account A
|
|
88
|
+
(this proxy) (per-model routing)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
1. Pi sends a request to `localhost:51200` with a model name in the body
|
|
92
|
+
2. The proxy resolves the model to a quota key (e.g., `gemini-3.1-pro`)
|
|
93
|
+
3. The best available account for that specific model is selected
|
|
94
|
+
4. The `Authorization` header and `project` field are swapped with real credentials
|
|
95
|
+
5. The request is forwarded (trying daily, autopush, then prod endpoints)
|
|
96
|
+
6. The SSE response streams back to pi transparently
|
|
97
|
+
|
|
98
|
+
### Per-Model Account Selection
|
|
99
|
+
|
|
100
|
+
Each model maintains its own active account. When the proxy needs to rotate a model, it picks the next account using a priority system:
|
|
101
|
+
|
|
102
|
+
| Priority | Badge | Condition | Rationale |
|
|
103
|
+
|----------|-------|-----------|-----------|
|
|
104
|
+
| 1 (first) | `fresh` | No active timer for this model | Start the 7-day clock ASAP so it resets sooner |
|
|
105
|
+
| 2 | `7d` | 7-day timer running for this model | Already ticking, keep using it |
|
|
106
|
+
| 3 (last) | `5h` | 5-hour timer running for this model | Short-lived; wasted if not fully consumed |
|
|
107
|
+
|
|
108
|
+
Within the same priority tier, the account with the most remaining quota for that model wins.
|
|
109
|
+
|
|
110
|
+
### Rotation Triggers
|
|
111
|
+
|
|
112
|
+
Three mechanisms trigger rotation, scoped to the specific model:
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
2. **Request-count** (fallback) -- After `requestsPerRotation` requests (default: 5), all models on that account rotate. Safety net for when quota data isn't available yet.
|
|
117
|
+
|
|
118
|
+
3. **429 failover** (reactive) -- On rate limit or 5xx, the account is marked exhausted with a cooldown and the affected model immediately switches.
|
|
119
|
+
|
|
120
|
+
### Account Protection
|
|
121
|
+
|
|
122
|
+
The proxy detects blocked/suspended accounts at three levels:
|
|
123
|
+
|
|
124
|
+
1. **Quota API check** (on startup + every poll) -- If the quota API returns `403 PERMISSION_DENIED` with "violation of Terms of Service", the account is immediately flagged. This catches suspended accounts before any request is wasted.
|
|
125
|
+
|
|
126
|
+
2. **API endpoint 401** (on request) -- If all 3 endpoints (daily, autopush, prod) reject the token with `401 UNAUTHENTICATED`, the account is flagged. The proxy cascades through all endpoints before giving up.
|
|
127
|
+
|
|
128
|
+
3. **API endpoint 403** (on request) -- If the response body contains infringement keywords (`infring`, `suspend`, `abus`, `terminat`, `violat`, `banned`, `policy`, `forbidden`), the account is flagged.
|
|
129
|
+
|
|
130
|
+
Flagged accounts are **immediately excluded** from all model routing. The dashboard shows a red `FLAGGED` badge with the error message. Use the Re-enable button or `POST /api/enable/<email>` to clear the flag after resolving the issue with Google.
|
|
131
|
+
|
|
132
|
+
### Endpoint Cascade
|
|
133
|
+
|
|
134
|
+
The proxy tries three Google API endpoints in order for each request:
|
|
135
|
+
|
|
136
|
+
1. `daily-cloudcode-pa.sandbox.googleapis.com`
|
|
137
|
+
2. `autopush-cloudcode-pa.sandbox.googleapis.com`
|
|
138
|
+
3. `cloudcode-pa.googleapis.com` (prod)
|
|
139
|
+
|
|
140
|
+
On `401`, `403`, or `404`, it cascades to the next endpoint. Only the final endpoint's response is used for flagging decisions.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
All configuration is in `accounts.json`, created automatically by `npm run login`:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"proxyPort": 51200,
|
|
149
|
+
"requestsPerRotation": 5,
|
|
150
|
+
"rotateOnQuotaDrop": 20,
|
|
151
|
+
"quotaPollIntervalMs": 300000,
|
|
152
|
+
"accounts": [
|
|
153
|
+
{
|
|
154
|
+
"email": "user@gmail.com",
|
|
155
|
+
"refreshToken": "1//...",
|
|
156
|
+
"projectId": "project-abc123",
|
|
157
|
+
"label": "user"
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Options
|
|
164
|
+
|
|
165
|
+
| Field | Default | Description |
|
|
166
|
+
|-------|---------|-------------|
|
|
167
|
+
| `proxyPort` | `51200` | Port the proxy listens on |
|
|
168
|
+
| `requestsPerRotation` | `5` | Max requests before rotating (fallback trigger) |
|
|
169
|
+
| `rotateOnQuotaDrop` | `20` | Rotate when a model's quota drops this many %. Set to `0` to disable |
|
|
170
|
+
| `quotaPollIntervalMs` | `300000` | Quota poll interval in ms (5 minutes) |
|
|
171
|
+
|
|
172
|
+
### Account Fields
|
|
173
|
+
|
|
174
|
+
| Field | Description |
|
|
175
|
+
|-------|-------------|
|
|
176
|
+
| `email` | Google account email (auto-filled by login) |
|
|
177
|
+
| `refreshToken` | OAuth refresh token (auto-filled by login) |
|
|
178
|
+
| `projectId` | Cloud project ID (auto-discovered during login) |
|
|
179
|
+
| `label` | Display name on the dashboard (auto-filled, defaults to email username) |
|
|
180
|
+
|
|
181
|
+
## API Endpoints
|
|
182
|
+
|
|
183
|
+
| Method | Path | Description |
|
|
184
|
+
|--------|------|-------------|
|
|
185
|
+
| `GET` | `/dashboard` | Web dashboard |
|
|
186
|
+
| `GET` | `/api/status` | JSON status: accounts, quotas, model routing, flags |
|
|
187
|
+
| `POST` | `/api/enable/<email>` | Clear flagged/disabled state and re-enable an account |
|
|
188
|
+
| `POST` | `/v1internal:streamGenerateContent` | Proxy endpoint (used by pi) |
|
|
189
|
+
|
|
190
|
+
## Running as a Service
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Using nohup
|
|
194
|
+
nohup npm start > rotator.log 2>&1 &
|
|
195
|
+
|
|
196
|
+
# Or with systemd (create /etc/systemd/system/pi-antigravity-rotator.service)
|
|
197
|
+
[Unit]
|
|
198
|
+
Description=Pi Antigravity Rotator
|
|
199
|
+
After=network.target
|
|
200
|
+
|
|
201
|
+
[Service]
|
|
202
|
+
Type=simple
|
|
203
|
+
WorkingDirectory=/path/to/pi-antigravity-rotator
|
|
204
|
+
ExecStart=/usr/bin/npm start
|
|
205
|
+
Restart=always
|
|
206
|
+
RestartSec=5
|
|
207
|
+
|
|
208
|
+
[Install]
|
|
209
|
+
WantedBy=multi-user.target
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
**Account shows `flagged` status**
|
|
215
|
+
Google detected potential abuse. Review the error message on the dashboard. After resolving with Google, click Re-enable or `POST /api/enable/<email>`.
|
|
216
|
+
|
|
217
|
+
**Account keeps getting disabled after 5 errors**
|
|
218
|
+
Check the error message. Common causes: revoked OAuth consent, expired refresh token (re-run `npm run login`), or Google account suspension.
|
|
219
|
+
|
|
220
|
+
**Quota bars not showing**
|
|
221
|
+
Quota data appears after the first poll cycle (up to 5 minutes). Ensure accounts have valid tokens.
|
|
222
|
+
|
|
223
|
+
**All accounts exhausted**
|
|
224
|
+
The proxy uses the account with the shortest remaining cooldown. Add more accounts or increase `requestsPerRotation`.
|
|
225
|
+
|
|
226
|
+
**Multiple agents on different models**
|
|
227
|
+
This is fully supported. Each model routes independently. Agent 1 using Gemini Pro and Agent 2 using Claude will each have their own active account and won't interfere with each other's rotation.
|
package/bin/cli.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-antigravity-rotator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-account rotation proxy for Google Antigravity with per-model routing, real-time quota tracking, and infringement detection",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pi-antigravity-rotator": "bin/pi-antigravity-rotator.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "tsx src/cli.ts start",
|
|
12
|
+
"login": "tsx src/cli.ts login"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"src/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"pi",
|
|
22
|
+
"antigravity",
|
|
23
|
+
"proxy",
|
|
24
|
+
"rotation",
|
|
25
|
+
"google",
|
|
26
|
+
"quota",
|
|
27
|
+
"gemini"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/tuxevil/pi-antigravity-rotator.git"
|
|
32
|
+
},
|
|
33
|
+
"author": "tuxevil",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"tsx": "^4.19.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CLI entry point for pi-antigravity-rotator
|
|
4
|
+
// Usage:
|
|
5
|
+
// pi-antigravity-rotator start Start the proxy
|
|
6
|
+
// pi-antigravity-rotator login Add a new account
|
|
7
|
+
// pi-antigravity-rotator status Show account status
|
|
8
|
+
|
|
9
|
+
import { getConfigDir } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith("--config-dir") && a !== process.argv[process.argv.indexOf("--config-dir") + 1]);
|
|
12
|
+
const command = args[0] || "start";
|
|
13
|
+
|
|
14
|
+
console.log(`Config dir: ${getConfigDir()}`);
|
|
15
|
+
console.log();
|
|
16
|
+
|
|
17
|
+
switch (command) {
|
|
18
|
+
case "start": {
|
|
19
|
+
// Dynamic import to avoid loading everything for help
|
|
20
|
+
const { main } = await import("./index.js");
|
|
21
|
+
main();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "login": {
|
|
25
|
+
const { runLogin } = await import("./login.js");
|
|
26
|
+
runLogin();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "status": {
|
|
30
|
+
try {
|
|
31
|
+
const port = 51200;
|
|
32
|
+
const res = await fetch(`http://localhost:${port}/api/status`);
|
|
33
|
+
const data = await res.json();
|
|
34
|
+
console.log(JSON.stringify(data, null, 2));
|
|
35
|
+
} catch {
|
|
36
|
+
console.error("Rotator is not running or unreachable on port 51200");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
console.log("Pi Antigravity Rotator");
|
|
43
|
+
console.log();
|
|
44
|
+
console.log("Usage:");
|
|
45
|
+
console.log(" pi-antigravity-rotator start Start the proxy (default)");
|
|
46
|
+
console.log(" pi-antigravity-rotator login Add a new Google account");
|
|
47
|
+
console.log(" pi-antigravity-rotator status Show account status (JSON)");
|
|
48
|
+
console.log();
|
|
49
|
+
console.log("Options:");
|
|
50
|
+
console.log(" --config-dir <path> Config directory (default: ~/.pi-antigravity-rotator/)");
|
|
51
|
+
console.log();
|
|
52
|
+
console.log("Environment:");
|
|
53
|
+
console.log(" PI_ROTATOR_DIR Config directory override");
|
|
54
|
+
process.exit(command === "help" || command === "--help" ? 0 : 1);
|
|
55
|
+
}
|