pi-antigravity-rotator 2.2.2 → 2.3.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/CHANGELOG.md +11 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/account-store.ts +109 -168
- package/src/admin-auth.ts +74 -83
- package/src/cli.ts +62 -46
- package/src/config-defaults.ts +64 -0
- package/src/config-storage.ts +21 -0
- package/src/dashboard.ts +310 -3193
- package/src/db-store.ts +155 -0
- package/src/doctor.ts +96 -78
- package/src/index.ts +200 -149
- package/src/onboarding.ts +352 -113
- package/src/paths.ts +34 -31
- package/src/proxy.ts +1751 -1270
- package/src/responses-store.ts +150 -178
- package/src/rotator.ts +3041 -2310
- package/src/settings-repository.ts +314 -0
- package/src/static/dashboard.css +1224 -0
- package/src/static/dashboard.js +1834 -0
- package/src/types.ts +501 -397
package/src/index.ts
CHANGED
|
@@ -7,35 +7,40 @@ import { AccountRotator } from "./rotator.js";
|
|
|
7
7
|
import { startProxy } from "./proxy.js";
|
|
8
8
|
import { getConfigDir } from "./paths.js";
|
|
9
9
|
import { TelemetryReporter, setActiveReporter } from "./telemetry.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { loadConfig as loadConfigFromStore } from "./account-store.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureAdminToken,
|
|
13
|
+
getConfiguredAdminToken,
|
|
14
|
+
setPersistedAdminToken,
|
|
15
|
+
} from "./admin-auth.js";
|
|
12
16
|
import { warnIfUsingFallbackOAuthCreds } from "./oauth.js";
|
|
13
17
|
import { warnIfInsecureTelemetryEndpoint } from "./telemetry.js";
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
setModelSpecsOverride,
|
|
20
|
+
loadResponsesStore,
|
|
21
|
+
flushResponsesStoreSync,
|
|
22
|
+
} from "./compat.js";
|
|
15
23
|
import { setModelAliasesOverride } from "./types.js";
|
|
16
24
|
import { writeTextFileAtomic } from "./storage.js";
|
|
25
|
+
import { initDb } from "./db-store.js";
|
|
26
|
+
import { stopPendingSessionReaper } from "./onboarding.js";
|
|
17
27
|
|
|
18
28
|
function loadConfig(): Config {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return config;
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error(`Failed to parse ${configPath}: ${err}`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
29
|
+
try {
|
|
30
|
+
const config = loadConfigFromStore();
|
|
31
|
+
|
|
32
|
+
if (!config.accounts || config.accounts.length === 0) {
|
|
33
|
+
console.error(
|
|
34
|
+
"No accounts configured. Run 'pi-antigravity-rotator login' to add one.",
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return config;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`Failed to load config: ${err}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
@@ -46,145 +51,191 @@ const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
|
|
|
46
51
|
* then writes .star-prompted so it never appears again.
|
|
47
52
|
*/
|
|
48
53
|
function maybeShowStarNudge(): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
const dir = getConfigDir();
|
|
55
|
+
const promptedPath = join(dir, ".star-prompted");
|
|
56
|
+
if (existsSync(promptedPath)) return; // already shown, done forever
|
|
57
|
+
|
|
58
|
+
const firstBootPath = join(dir, ".first-boot");
|
|
59
|
+
let firstBootMs: number;
|
|
60
|
+
|
|
61
|
+
if (existsSync(firstBootPath)) {
|
|
62
|
+
try {
|
|
63
|
+
firstBootMs = parseInt(readFileSync(firstBootPath, "utf-8").trim(), 10);
|
|
64
|
+
if (Number.isNaN(firstBootMs)) return;
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// First ever boot — record timestamp
|
|
70
|
+
firstBootMs = Date.now();
|
|
71
|
+
try {
|
|
72
|
+
writeTextFileAtomic(firstBootPath, String(firstBootMs));
|
|
73
|
+
} catch {
|
|
74
|
+
/* best effort */
|
|
75
|
+
}
|
|
76
|
+
return; // too early, come back after 24h
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Date.now() - firstBootMs < TWENTY_FOUR_HOURS_MS) return; // not yet
|
|
80
|
+
|
|
81
|
+
// Show it once
|
|
82
|
+
console.log(" ╭──────────────────────────────────────────────────────────╮");
|
|
83
|
+
console.log(" │ ⭐ Enjoying pi-antigravity-rotator? │");
|
|
84
|
+
console.log(" │ github.com/tuxevil/pi-antigravity-rotator │");
|
|
85
|
+
console.log(" │ A star helps others find it. Thanks! │");
|
|
86
|
+
console.log(" ╰──────────────────────────────────────────────────────────╯");
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
writeTextFileAtomic(promptedPath, String(Date.now()));
|
|
91
|
+
} catch {
|
|
92
|
+
/* best effort */
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
95
|
|
|
81
96
|
/**
|
|
82
97
|
* Resolve the effective admin token at startup. If no PI_ROTATOR_ADMIN_TOKEN
|
|
83
|
-
* env var is set and no
|
|
84
|
-
*
|
|
98
|
+
* env var is set and no persisted token exists, a new token is generated,
|
|
99
|
+
* saved to the repository, and printed to the operator once. This ensures
|
|
85
100
|
* admin routes are protected by default on first run.
|
|
86
101
|
*/
|
|
87
|
-
function bootstrapAdminToken(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
function bootstrapAdminToken(): void {
|
|
103
|
+
const resolved = ensureAdminToken();
|
|
104
|
+
setPersistedAdminToken(resolved.token);
|
|
105
|
+
if (resolved.source === "generated") {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(
|
|
108
|
+
" ╭──────────────────────────────────────────────────────────╮",
|
|
109
|
+
);
|
|
110
|
+
console.log(
|
|
111
|
+
" │ Generated admin token (saved to repository): │",
|
|
112
|
+
);
|
|
113
|
+
const tokenPreview =
|
|
114
|
+
resolved.token.length > 12
|
|
115
|
+
? `${resolved.token.slice(0, 8)}…${resolved.token.slice(-4)}`
|
|
116
|
+
: resolved.token;
|
|
117
|
+
console.log(` │ ${tokenPreview} │`);
|
|
118
|
+
console.log(
|
|
119
|
+
" │ │",
|
|
120
|
+
);
|
|
121
|
+
console.log(
|
|
122
|
+
" │ Header: x-rotator-admin-token: <token> │",
|
|
123
|
+
);
|
|
124
|
+
console.log(
|
|
125
|
+
" │ Bearer: Authorization: Bearer <token> │",
|
|
126
|
+
);
|
|
127
|
+
console.log(
|
|
128
|
+
" │ URL: <url>?token=<token> │",
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
" ╰──────────────────────────────────────────────────────────╯",
|
|
132
|
+
);
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
function maybeWarnAboutAdminExposure(config: Config): void {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
if (getConfiguredAdminToken()) return;
|
|
139
|
+
console.warn("WARNING: PI_ROTATOR_ADMIN_TOKEN is not configured.");
|
|
140
|
+
console.warn(
|
|
141
|
+
`WARNING: Dashboard and /api/* routes are open on ${config.bindHost}:${config.proxyPort}.`,
|
|
142
|
+
);
|
|
143
|
+
console.warn(
|
|
144
|
+
"WARNING: For local-only use, prefer bindHost=127.0.0.1 or set PI_ROTATOR_ADMIN_TOKEN.",
|
|
145
|
+
);
|
|
146
|
+
console.warn();
|
|
114
147
|
}
|
|
115
148
|
|
|
116
|
-
export function main(): void {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
149
|
+
export async function main(): Promise<void> {
|
|
150
|
+
console.log("=== Pi Antigravity Rotator ===");
|
|
151
|
+
console.log();
|
|
152
|
+
|
|
153
|
+
await initDb();
|
|
154
|
+
|
|
155
|
+
const config = loadConfig();
|
|
156
|
+
console.log(`Loaded ${config.accounts.length} accounts`);
|
|
157
|
+
console.log(
|
|
158
|
+
`Rotation: ${config.requestsPerRotation} requests / ${config.rotateOnQuotaDrop}% quota drop`,
|
|
159
|
+
);
|
|
160
|
+
console.log(
|
|
161
|
+
`Quota poll: every ${Math.round((config.quotaPollIntervalMs || 300000) / 1000)}s`,
|
|
162
|
+
);
|
|
163
|
+
console.log(
|
|
164
|
+
`Concurrency cap: ${config.maxConcurrentRequestsPerAccount} request/account, ${config.maxConcurrentRequestsPerProjectModel} request/project+model`,
|
|
165
|
+
);
|
|
166
|
+
console.log(`Bind host: ${config.bindHost}`);
|
|
167
|
+
console.log(`Routing policy: ${config.routingPolicy}`);
|
|
168
|
+
console.log(
|
|
169
|
+
`Safety breaker: ${config.projectCircuitBreaker429Threshold} provider 429s / ${Math.round((config.projectCircuitBreakerWindowMs || 0) / 60000)}m pauses project+model for ${Math.round((config.projectCircuitBreakerCooldownMs || 0) / 60000)}m`,
|
|
170
|
+
);
|
|
171
|
+
console.log(
|
|
172
|
+
`Protective pause: ${Math.round((config.protectivePauseMs || 0) / 3600000)}h after serious flag`,
|
|
173
|
+
);
|
|
174
|
+
console.log();
|
|
175
|
+
|
|
176
|
+
for (const account of config.accounts) {
|
|
177
|
+
console.log(` ${account.label || account.email} (${account.email})`);
|
|
178
|
+
}
|
|
179
|
+
console.log();
|
|
180
|
+
|
|
181
|
+
maybeShowStarNudge();
|
|
182
|
+
bootstrapAdminToken();
|
|
183
|
+
maybeWarnAboutAdminExposure(config);
|
|
184
|
+
warnIfUsingFallbackOAuthCreds();
|
|
185
|
+
warnIfInsecureTelemetryEndpoint();
|
|
186
|
+
setModelSpecsOverride(config.modelSpecs ?? null);
|
|
187
|
+
setModelAliasesOverride(config.modelAliases ?? null);
|
|
188
|
+
void loadResponsesStore();
|
|
189
|
+
|
|
190
|
+
const rotator = new AccountRotator(config);
|
|
191
|
+
|
|
192
|
+
// ── Telemetry (anonymous, opt-out via PI_ROTATOR_TELEMETRY=off) ──
|
|
193
|
+
const telemetry = new TelemetryReporter(() => {
|
|
194
|
+
const status = rotator.getStatus();
|
|
195
|
+
|
|
196
|
+
// getTokenUsage() deduplicates rolled-up buckets and exposes tokensByModel
|
|
197
|
+
const tu = rotator.getTokenUsage();
|
|
198
|
+
const tokensByModel = tu.tokensByModel;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
accountCount: status.accounts.length,
|
|
202
|
+
modelsUsed: Object.keys(status.activeAccounts),
|
|
203
|
+
totalRequests: status.totalRequestsAllAccounts,
|
|
204
|
+
uptimeSeconds: Math.round(status.uptime / 1000),
|
|
205
|
+
routingHealthState: status.routingHealth.state,
|
|
206
|
+
flaggedCount: status.routingHealth.flaggedCount,
|
|
207
|
+
disabledCount: status.routingHealth.disabledCount,
|
|
208
|
+
proCount: status.accounts.filter((a) => a.proDetected).length,
|
|
209
|
+
freeCount: status.accounts.filter((a) => !a.proDetected).length,
|
|
210
|
+
tokensByModel,
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
setActiveReporter(telemetry);
|
|
214
|
+
void telemetry.start();
|
|
215
|
+
|
|
216
|
+
// ── Graceful shutdown ──
|
|
217
|
+
const shutdown = async (): Promise<void> => {
|
|
218
|
+
console.log("\nShutting down...");
|
|
219
|
+
flushResponsesStoreSync();
|
|
220
|
+
rotator.flushPendingStateSaveSync();
|
|
221
|
+
rotator.flushPendingTokenUsageSaveSync();
|
|
222
|
+
await telemetry.shutdown();
|
|
223
|
+
rotator.stopQuotaPolling();
|
|
224
|
+
stopPendingSessionReaper();
|
|
225
|
+
const { closeDb } = await import("./db-store.js");
|
|
226
|
+
await closeDb();
|
|
227
|
+
process.exit(0);
|
|
228
|
+
};
|
|
229
|
+
process.on("SIGINT", () => void shutdown());
|
|
230
|
+
process.on("SIGTERM", () => void shutdown());
|
|
231
|
+
|
|
232
|
+
startProxy(rotator, config.proxyPort, config.bindHost || "0.0.0.0");
|
|
185
233
|
}
|
|
186
234
|
|
|
187
235
|
// Direct execution
|
|
188
236
|
if (process.argv[1]?.includes("index")) {
|
|
189
|
-
|
|
237
|
+
main().catch((err) => {
|
|
238
|
+
console.error("Fatal startup error:", err);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
190
241
|
}
|