pi-antigravity-rotator 1.1.0 → 1.2.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/package.json +1 -1
- package/src/dashboard.ts +53 -5
- package/src/proxy.ts +52 -85
- package/src/rotator.ts +15 -1
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-antigravity-rotator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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/dashboard.ts
CHANGED
|
@@ -254,6 +254,17 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
254
254
|
word-break: break-all;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
.card-hint {
|
|
258
|
+
margin-top: 6px;
|
|
259
|
+
padding: 8px 10px;
|
|
260
|
+
background: rgba(250, 204, 21, 0.08);
|
|
261
|
+
border-radius: 8px;
|
|
262
|
+
border-left: 3px solid var(--yellow);
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
color: var(--yellow);
|
|
265
|
+
line-height: 1.4;
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
.card-actions { margin-top: 10px; display: flex; gap: 8px; }
|
|
258
269
|
|
|
259
270
|
.btn-enable {
|
|
@@ -364,7 +375,8 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
364
375
|
<div class="header-stats">
|
|
365
376
|
Uptime: <span id="uptime">--</span> |
|
|
366
377
|
Port: <span id="port">--</span> |
|
|
367
|
-
Rotation: <span id="rotation">--</span> reqs
|
|
378
|
+
Rotation: <span id="rotation">--</span> reqs |
|
|
379
|
+
<button id="maskBtn" onclick="toggleMask()" style="background:none;border:1px solid var(--border);color:var(--text-dim);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:12px;font-family:inherit;">PII: Visible</button>
|
|
368
380
|
</div>
|
|
369
381
|
</div>
|
|
370
382
|
|
|
@@ -443,7 +455,7 @@ function renderModelRouting(activeAccounts) {
|
|
|
443
455
|
return '<div class="model-route">' +
|
|
444
456
|
'<span class="model-name">' + e[0] + '</span>' +
|
|
445
457
|
'<span class="route-arrow">-></span>' +
|
|
446
|
-
'<span class="account-name">' + e[1] + '</span>' +
|
|
458
|
+
'<span class="account-name">' + maskText(e[1]) + '</span>' +
|
|
447
459
|
'</div>';
|
|
448
460
|
}).join('');
|
|
449
461
|
container.innerHTML = '<div class="model-routing-title">Model Routing</div>' + rows;
|
|
@@ -487,13 +499,13 @@ function renderAccounts(data) {
|
|
|
487
499
|
|
|
488
500
|
return '<div class="account-card ' + a.status + '">' +
|
|
489
501
|
'<div class="card-header">' +
|
|
490
|
-
'<div class="card-label">' + a.label + '</div>' +
|
|
502
|
+
'<div class="card-label">' + maskText(a.label) + '</div>' +
|
|
491
503
|
'<div class="card-badges">' +
|
|
492
504
|
'<span class="badge badge-' + a.status + (isActive ? ' pulse' : '') + '">' + a.status + '</span>' +
|
|
493
505
|
modelBadges +
|
|
494
506
|
'</div>' +
|
|
495
507
|
'</div>' +
|
|
496
|
-
'<div class="card-email">' + a.email + '</div>' +
|
|
508
|
+
'<div class="card-email">' + maskEmail(a.email) + '</div>' +
|
|
497
509
|
(a.quota && a.quota.length > 0 ? renderQuotaBars(a.quota) : '') +
|
|
498
510
|
'<div class="card-stats">' +
|
|
499
511
|
'<div class="card-stat"><div class="stat-label">Requests</div><div class="stat-value">' +
|
|
@@ -506,7 +518,12 @@ function renderAccounts(data) {
|
|
|
506
518
|
(a.hasValidToken ? 'var(--green)' : 'var(--text-dim)') + '">' +
|
|
507
519
|
(a.hasValidToken ? 'Valid' : 'Expired') + '</div></div>' +
|
|
508
520
|
'</div>' +
|
|
509
|
-
(a.lastError ? '<div class="card-error">' + a.lastError.slice(0, 150) + '</div>'
|
|
521
|
+
(a.lastError ? '<div class="card-error">' + a.lastError.slice(0, 150) + '</div>' +
|
|
522
|
+
(a.lastError.toLowerCase().includes('verif') ?
|
|
523
|
+
'<div class="card-hint">Fix: Open Antigravity IDE, sign in with this account, and complete the verification prompt. Then click Re-enable.</div>' :
|
|
524
|
+
a.lastError.toLowerCase().includes('terms of service') ?
|
|
525
|
+
'<div class="card-hint">This account was suspended by Google. Submit an appeal at <a href="https://support.google.com/accounts/troubleshooter/2402620" target="_blank" style="color:var(--cyan)">Google Account Recovery</a>, then Re-enable.</div>' :
|
|
526
|
+
'') : '') +
|
|
510
527
|
(isDisabled ? '<div class="card-actions"><button class="btn-enable" onclick="enableAccount(\\'' +
|
|
511
528
|
a.email + '\\')">Re-enable</button></div>' : '') +
|
|
512
529
|
(isCooldown && cooldownPercent > 0 ? '<div class="cooldown-bar" style="width:' + cooldownPercent + '%"></div>' : '') +
|
|
@@ -514,6 +531,25 @@ function renderAccounts(data) {
|
|
|
514
531
|
}).join('');
|
|
515
532
|
}
|
|
516
533
|
|
|
534
|
+
var MASK_MODE = new URLSearchParams(window.location.search).has('mask');
|
|
535
|
+
var maskCounter = 0;
|
|
536
|
+
var maskMap = {};
|
|
537
|
+
|
|
538
|
+
function maskText(text) {
|
|
539
|
+
if (!MASK_MODE) return text;
|
|
540
|
+
if (!maskMap[text]) {
|
|
541
|
+
maskCounter++;
|
|
542
|
+
maskMap[text] = 'Account ' + maskCounter;
|
|
543
|
+
}
|
|
544
|
+
return maskMap[text];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function maskEmail(email) {
|
|
548
|
+
if (!MASK_MODE) return email;
|
|
549
|
+
var masked = maskText(email.split('@')[0]);
|
|
550
|
+
return masked.toLowerCase().replace(/ /g, '-') + '@***.com';
|
|
551
|
+
}
|
|
552
|
+
|
|
517
553
|
async function enableAccount(email) {
|
|
518
554
|
await fetch('/api/enable/' + encodeURIComponent(email), { method: 'POST' });
|
|
519
555
|
refresh();
|
|
@@ -524,11 +560,23 @@ async function refresh() {
|
|
|
524
560
|
var res = await fetch('/api/status');
|
|
525
561
|
var data = await res.json();
|
|
526
562
|
renderAccounts(data);
|
|
563
|
+
var btn = document.getElementById('maskBtn');
|
|
564
|
+
if (btn) btn.textContent = MASK_MODE ? 'PII: Hidden' : 'PII: Visible';
|
|
527
565
|
} catch (err) {
|
|
528
566
|
console.error('Status fetch failed:', err);
|
|
529
567
|
}
|
|
530
568
|
}
|
|
531
569
|
|
|
570
|
+
function toggleMask() {
|
|
571
|
+
var url = new URL(window.location);
|
|
572
|
+
if (MASK_MODE) {
|
|
573
|
+
url.searchParams.delete('mask');
|
|
574
|
+
} else {
|
|
575
|
+
url.searchParams.set('mask', '1');
|
|
576
|
+
}
|
|
577
|
+
window.location.href = url.toString();
|
|
578
|
+
}
|
|
579
|
+
|
|
532
580
|
refresh();
|
|
533
581
|
setInterval(refresh, 3000);
|
|
534
582
|
</script>
|
package/src/proxy.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// HTTP reverse proxy - forwards requests to Antigravity with credential rotation
|
|
2
2
|
|
|
3
3
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
4
5
|
import { ANTIGRAVITY_ENDPOINTS } from "./types.js";
|
|
5
6
|
import type { AccountRuntime } from "./types.js";
|
|
6
7
|
import type { AccountRotator } from "./rotator.js";
|
|
@@ -92,14 +93,12 @@ function readBody(req: IncomingMessage): Promise<Buffer> {
|
|
|
92
93
|
|
|
93
94
|
/**
|
|
94
95
|
* Forward a request to the real Antigravity endpoint with credential swapping.
|
|
95
|
-
* Handles endpoint cascade (daily -> autopush -> prod) and 429 retry.
|
|
96
96
|
*/
|
|
97
97
|
async function forwardRequest(
|
|
98
98
|
account: AccountRuntime,
|
|
99
99
|
body: RequestBody,
|
|
100
100
|
originalHeaders: Record<string, string>,
|
|
101
|
-
|
|
102
|
-
): Promise<{ status: number; headers: Headers; body: ReadableStream<Uint8Array> | null }> {
|
|
101
|
+
): Promise<Response> {
|
|
103
102
|
// Swap credentials
|
|
104
103
|
body.project = account.config.projectId;
|
|
105
104
|
const requestBody = JSON.stringify(body);
|
|
@@ -129,11 +128,9 @@ async function forwardRequest(
|
|
|
129
128
|
const isProd = endpointIdx === ANTIGRAVITY_ENDPOINTS.length - 1;
|
|
130
129
|
|
|
131
130
|
try {
|
|
132
|
-
// Timeout non-prod endpoints after 10s to avoid long hangs
|
|
133
131
|
const controller = !isProd ? new AbortController() : undefined;
|
|
134
132
|
const timeout = controller ? setTimeout(() => controller.abort(), 10_000) : undefined;
|
|
135
133
|
|
|
136
|
-
const fetchStart = Date.now();
|
|
137
134
|
const response = await fetch(url, {
|
|
138
135
|
method: "POST",
|
|
139
136
|
headers: forwardHeaders,
|
|
@@ -142,23 +139,13 @@ async function forwardRequest(
|
|
|
142
139
|
});
|
|
143
140
|
if (timeout) clearTimeout(timeout);
|
|
144
141
|
|
|
145
|
-
const fetchMs = Date.now() - fetchStart;
|
|
146
|
-
|
|
147
|
-
// On 401/403/404, try next endpoint
|
|
148
142
|
if ((response.status === 401 || response.status === 403 || response.status === 404) && endpointIdx < ANTIGRAVITY_ENDPOINTS.length - 1) {
|
|
149
|
-
log(`Endpoint ${endpoint} returned ${response.status}
|
|
143
|
+
log(`Endpoint ${endpoint} returned ${response.status}, cascading...`);
|
|
144
|
+
response.text().catch(() => {});
|
|
150
145
|
continue;
|
|
151
146
|
}
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
log(`Using endpoint ${endpoint} (${fetchMs}ms)`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
status: response.status,
|
|
159
|
-
headers: response.headers,
|
|
160
|
-
body: response.body,
|
|
161
|
-
};
|
|
148
|
+
return response;
|
|
162
149
|
} catch (err) {
|
|
163
150
|
if (endpointIdx < ANTIGRAVITY_ENDPOINTS.length - 1) {
|
|
164
151
|
log(`Endpoint ${endpoint} failed: ${err instanceof Error ? err.message : err}, cascading...`);
|
|
@@ -194,7 +181,6 @@ async function handleProxyRequest(
|
|
|
194
181
|
return;
|
|
195
182
|
}
|
|
196
183
|
|
|
197
|
-
// Try up to MAX_ENDPOINT_RETRIES accounts on 429
|
|
198
184
|
for (let attempt = 0; attempt < MAX_ENDPOINT_RETRIES; attempt++) {
|
|
199
185
|
const account = await rotator.getActiveAccount(body.model);
|
|
200
186
|
if (!account) {
|
|
@@ -207,31 +193,29 @@ async function handleProxyRequest(
|
|
|
207
193
|
log(`[${label}] Forwarding ${body.model} request (attempt ${attempt + 1})`);
|
|
208
194
|
|
|
209
195
|
try {
|
|
210
|
-
const
|
|
196
|
+
const response = await forwardRequest(account, { ...body }, flattenHeaders(req.headers));
|
|
211
197
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const cooldownMs = capCooldown(extractRetryDelay(errorText, upstream.headers));
|
|
198
|
+
if (response.status === 429) {
|
|
199
|
+
const errorText = await response.text().catch(() => "");
|
|
200
|
+
const cooldownMs = capCooldown(extractRetryDelay(errorText, response.headers));
|
|
216
201
|
log(`[${label}] 429 rate limited, cooldown ${Math.ceil(cooldownMs / 1000)}s`);
|
|
217
202
|
rotator.markExhausted(account, cooldownMs);
|
|
218
203
|
await rotator.rotateToNext(body.model);
|
|
219
204
|
continue;
|
|
220
205
|
}
|
|
221
206
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
const errorText = upstream.body ? await streamToString(upstream.body) : "";
|
|
207
|
+
if (response.status === 401) {
|
|
208
|
+
const errorText = await response.text().catch(() => "");
|
|
225
209
|
log(`[${label}] BLOCKED (401): ${errorText.slice(0, 200)}`);
|
|
226
210
|
rotator.markFlagged(account, `Account blocked (401): ${errorText.slice(0, 300)}`);
|
|
227
211
|
await rotator.rotateToNext(body.model);
|
|
228
212
|
continue;
|
|
229
213
|
}
|
|
230
214
|
|
|
231
|
-
if (
|
|
232
|
-
const errorText =
|
|
215
|
+
if (response.status === 403) {
|
|
216
|
+
const errorText = await response.text().catch(() => "");
|
|
233
217
|
const lower = errorText.toLowerCase();
|
|
234
|
-
const flagPatterns = ["infring", "suspend", "abus", "terminat", "violat", "banned", "policy", "forbidden"];
|
|
218
|
+
const flagPatterns = ["infring", "suspend", "abus", "terminat", "violat", "banned", "policy", "forbidden", "verif"];
|
|
235
219
|
const isFlagged = flagPatterns.some((p) => lower.includes(p));
|
|
236
220
|
|
|
237
221
|
if (isFlagged) {
|
|
@@ -240,54 +224,56 @@ async function handleProxyRequest(
|
|
|
240
224
|
await rotator.rotateToNext(body.model);
|
|
241
225
|
continue;
|
|
242
226
|
}
|
|
243
|
-
// Non-
|
|
227
|
+
// Non-flagging 403: return to client
|
|
228
|
+
log(`[${label}] 403: ${errorText.slice(0, 200)}`);
|
|
229
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
230
|
+
res.end(errorText || JSON.stringify({ error: "Forbidden" }));
|
|
231
|
+
return;
|
|
244
232
|
}
|
|
245
233
|
|
|
246
|
-
if (
|
|
247
|
-
const errorText =
|
|
248
|
-
log(`[${label}] Server error ${
|
|
249
|
-
|
|
250
|
-
// Return the error to the agent so it can handle retries/backoff
|
|
251
|
-
if (upstream.status === 503) {
|
|
234
|
+
if (response.status >= 500) {
|
|
235
|
+
const errorText = await response.text().catch(() => "");
|
|
236
|
+
log(`[${label}] Server error ${response.status}: ${errorText.slice(0, 200)}`);
|
|
237
|
+
if (response.status === 503) {
|
|
252
238
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
253
239
|
res.end(errorText || JSON.stringify({ error: "Server unavailable" }));
|
|
254
240
|
return;
|
|
255
241
|
}
|
|
256
|
-
rotator.markError(account, `${
|
|
242
|
+
rotator.markError(account, `${response.status}: ${errorText.slice(0, 200)}`);
|
|
257
243
|
await rotator.rotateToNext(body.model);
|
|
258
244
|
continue;
|
|
259
245
|
}
|
|
260
246
|
|
|
261
|
-
// Success or
|
|
247
|
+
// Success or non-error client response
|
|
262
248
|
const shouldRotate = rotator.recordRequest(account);
|
|
263
249
|
|
|
264
|
-
// Copy response headers
|
|
265
250
|
const responseHeaders: Record<string, string> = {};
|
|
266
|
-
|
|
267
|
-
// Skip hop-by-hop headers
|
|
251
|
+
response.headers.forEach((value, key) => {
|
|
268
252
|
if (key.toLowerCase() !== "transfer-encoding" && key.toLowerCase() !== "connection") {
|
|
269
253
|
responseHeaders[key] = value;
|
|
270
254
|
}
|
|
271
255
|
});
|
|
272
256
|
|
|
273
|
-
res.writeHead(
|
|
257
|
+
res.writeHead(response.status, responseHeaders);
|
|
274
258
|
|
|
275
|
-
// Stream
|
|
276
|
-
if (
|
|
277
|
-
const reader = upstream.body.getReader();
|
|
259
|
+
// Stream body using Node.js Readable (avoids ReadableStream locking issues)
|
|
260
|
+
if (response.body) {
|
|
278
261
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
262
|
+
const nodeStream = Readable.fromWeb(response.body as import("node:stream/web").ReadableStream);
|
|
263
|
+
await new Promise<void>((resolve) => {
|
|
264
|
+
nodeStream.on("data", (chunk: Buffer) => res.write(chunk));
|
|
265
|
+
nodeStream.on("end", resolve);
|
|
266
|
+
nodeStream.on("error", (err) => {
|
|
267
|
+
log(`[${label}] Stream error: ${err}`);
|
|
268
|
+
resolve();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
284
271
|
} catch (err) {
|
|
285
|
-
log(`[${label}] Stream error: ${err}`);
|
|
272
|
+
log(`[${label}] Stream setup error: ${err}`);
|
|
286
273
|
}
|
|
287
274
|
}
|
|
288
275
|
res.end();
|
|
289
276
|
|
|
290
|
-
// Rotate after response completes if threshold was hit
|
|
291
277
|
if (shouldRotate) {
|
|
292
278
|
await rotator.rotateToNext(body.model);
|
|
293
279
|
}
|
|
@@ -295,13 +281,18 @@ async function handleProxyRequest(
|
|
|
295
281
|
} catch (err) {
|
|
296
282
|
log(`[${label}] Request failed: ${err}`);
|
|
297
283
|
rotator.markError(account, err instanceof Error ? err.message : String(err));
|
|
284
|
+
if (res.headersSent) {
|
|
285
|
+
res.end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
298
288
|
await rotator.rotateToNext(body.model);
|
|
299
289
|
continue;
|
|
300
290
|
}
|
|
301
291
|
}
|
|
302
292
|
|
|
303
|
-
|
|
304
|
-
|
|
293
|
+
if (!res.headersSent) {
|
|
294
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
295
|
+
}
|
|
305
296
|
res.end(JSON.stringify({ error: "All retry attempts failed" }));
|
|
306
297
|
}
|
|
307
298
|
|
|
@@ -315,25 +306,13 @@ function flattenHeaders(headers: IncomingMessage["headers"]): Record<string, str
|
|
|
315
306
|
return flat;
|
|
316
307
|
}
|
|
317
308
|
|
|
318
|
-
async function streamToString(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
319
|
-
const reader = stream.getReader();
|
|
320
|
-
const decoder = new TextDecoder();
|
|
321
|
-
let result = "";
|
|
322
|
-
while (true) {
|
|
323
|
-
const { done, value } = await reader.read();
|
|
324
|
-
if (done) break;
|
|
325
|
-
result += decoder.decode(value, { stream: true });
|
|
326
|
-
}
|
|
327
|
-
return result;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
309
|
export function startProxy(rotator: AccountRotator, port: number): void {
|
|
331
310
|
const server = createServer((req, res) => {
|
|
332
|
-
const
|
|
333
|
-
const
|
|
311
|
+
const method = req.method?.toUpperCase();
|
|
312
|
+
const url = req.url || "";
|
|
313
|
+
const pathname = url.split("?")[0];
|
|
334
314
|
|
|
335
|
-
|
|
336
|
-
if (method === "GET" && (url === "/" || url === "/dashboard")) {
|
|
315
|
+
if (method === "GET" && (pathname === "/" || pathname === "/dashboard")) {
|
|
337
316
|
serveDashboard(res);
|
|
338
317
|
return;
|
|
339
318
|
}
|
|
@@ -366,24 +345,12 @@ export function startProxy(rotator: AccountRotator, port: number): void {
|
|
|
366
345
|
return;
|
|
367
346
|
}
|
|
368
347
|
|
|
369
|
-
// 404
|
|
370
348
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
371
349
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
372
350
|
});
|
|
373
351
|
|
|
374
352
|
server.listen(port, "0.0.0.0", () => {
|
|
375
|
-
log(`
|
|
376
|
-
log(`Dashboard: http://localhost:${port}/dashboard`);
|
|
377
|
-
log(`API endpoint: http://localhost:${port}/v1internal:streamGenerateContent?alt=sse`);
|
|
353
|
+
console.log(`[proxy] Listening on 0.0.0.0:${port}`);
|
|
354
|
+
console.log(`[proxy] Dashboard: http://localhost:${port}/dashboard`);
|
|
378
355
|
});
|
|
379
|
-
|
|
380
|
-
// Graceful shutdown
|
|
381
|
-
const shutdown = () => {
|
|
382
|
-
log("Shutting down...");
|
|
383
|
-
rotator.saveState();
|
|
384
|
-
server.close();
|
|
385
|
-
process.exit(0);
|
|
386
|
-
};
|
|
387
|
-
process.on("SIGINT", shutdown);
|
|
388
|
-
process.on("SIGTERM", shutdown);
|
|
389
356
|
}
|
package/src/rotator.ts
CHANGED
|
@@ -310,6 +310,16 @@ export class AccountRotator {
|
|
|
310
310
|
|
|
311
311
|
const current = this.accounts[idx];
|
|
312
312
|
if (current && this.isAvailable(current, now)) {
|
|
313
|
+
// Check if this account has quota for the requested model
|
|
314
|
+
if (modelKey) {
|
|
315
|
+
const quota = this.getModelQuota(current, modelKey);
|
|
316
|
+
if (quota === 0) {
|
|
317
|
+
this.log(
|
|
318
|
+
`${current.config.label || current.config.email} [${modelKey}]: 0% quota, skipping`,
|
|
319
|
+
);
|
|
320
|
+
return this.rotateModel(modelKey);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
313
323
|
await this.ensureValidToken(current);
|
|
314
324
|
return current;
|
|
315
325
|
}
|
|
@@ -333,9 +343,13 @@ export class AccountRotator {
|
|
|
333
343
|
const account = this.accounts[i];
|
|
334
344
|
|
|
335
345
|
if (this.isAvailable(account, now)) {
|
|
336
|
-
const priority = this.getModelTimerPriority(account, modelKey);
|
|
337
346
|
const quota = this.getModelQuota(account, modelKey);
|
|
338
347
|
|
|
348
|
+
// Skip accounts with 0% quota for this model (they will 429)
|
|
349
|
+
if (quota === 0) continue;
|
|
350
|
+
|
|
351
|
+
const priority = this.getModelTimerPriority(account, modelKey);
|
|
352
|
+
|
|
339
353
|
if (priority < bestPriority || (priority === bestPriority && quota > bestQuota)) {
|
|
340
354
|
best = account;
|
|
341
355
|
bestPriority = priority;
|
package/src/types.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const QUOTA_MODEL_KEYS: Record<string, { key: string; altKeys: string[];
|
|
|
59
59
|
},
|
|
60
60
|
claude: {
|
|
61
61
|
key: "claude-opus-4-6-thinking",
|
|
62
|
-
altKeys: ["claude-opus-4-5-thinking", "claude-opus-4-5", "claude-sonnet-4-6", "claude-sonnet-4-5"],
|
|
62
|
+
altKeys: ["claude-opus-4-5-thinking", "claude-opus-4-5", "claude-sonnet-4-6-thinking", "claude-sonnet-4-6", "claude-sonnet-4-5-thinking", "claude-sonnet-4-5"],
|
|
63
63
|
display: "Claude",
|
|
64
64
|
},
|
|
65
65
|
};
|